AI session discovery, conversion, and cross-tool continuation
Project description
jor
Transfer AI sessions between tools. Start a conversation in Claude Code, continue it in Codex — or vice versa.
Jor finds AI sessions across tools on your machine and lets you resume any session in any supported tool.
Supported Tools
- Claude Code — reads and writes
.jsonlsessions - Codex — reads and writes
.jsonlsessions
Install
pip install pyjor
Usage
# List sessions (auto-discovers new ones)
jor list
jor list --codex # only Codex sessions
jor list --claude # only Claude Code sessions
jor list -q "auth refactor" # search titles
jor list --path /code/myapp # filter by project
# Open a session (resume in its original tool, or cross-tool)
jor open <session-id> # resume in original tool
jor open <session-id> --codex # open in Codex
jor open <session-id> --claude # open in Claude Code
How It Works
jor listscans known session directories, indexes what it finds, and shows a tablejor opentranslates the session to the target tool's native format and launches it
Sessions are portable — file paths are stored relative, and source provenance is preserved.
Adding a Connector
Each connector is one class + one schema. That's it.
src/jor/connectors/my_tool/
├── __init__.py
├── schema.json # format contract — what one JSONL line looks like
└── connector.py # one class: reads, writes, and launches sessions
1. schema.json
A JSON Schema that validates one line of the tool's native session file. This catches format drift — if the tool changes its format, schema validation fails in tests before the parser silently produces garbage.
Be specific about the message structure, not just top-level fields:
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"description": "One line of a MyTool session file",
"type": "object",
"required": ["type", "message"],
"properties": {
"type": { "type": "string", "enum": ["user", "assistant"] },
"message": {
"type": "object",
"required": ["role", "content"],
"properties": {
"role": { "type": "string" },
"content": { "type": "string" }
}
}
}
}
2. connector.py
Subclass BaseConnector and implement these methods:
| Method | Purpose |
|---|---|
extract_metadata(records, session_path) |
Pull title, project, timestamps from raw records |
from_record(record, source_id) |
Convert one native record → JorMessage (reading) |
to_record(msg, session_id) |
Convert one JorMessage → native record (writing) |
write(messages, target) |
Write a session file, return (session_id, path) |
resume_command(session_file) |
Shell command to resume (e.g. "mytool resume {id}") |
write_session(messages, project) |
Write + return (session_id, resume_cmd, path) |
The base class handles all boilerplate: JSONL scanning, JSON parsing, index creation, launching.
import uuid
from pathlib import Path
from jor.connectors.base import BaseConnector
from jor.core.schema import JorMessage
class MyToolConnector(BaseConnector):
TOOL_NAME = "my_tool"
GLOB_PATTERN = "sessions/*.jsonl" # where to find session files
DETECT_PATH = "sessions" # dir to check in detect()
DEFAULT_HOME = Path.home() / ".my_tool" # tool's home directory
STRICT_JSON = False # True = abort entire file on bad line
RESUME_CMD = "mytool resume {session_id}"
def __init__(self, my_tool_home=None):
super().__init__(home_path=my_tool_home)
# --- Reading ---
def extract_metadata(self, records, session_path):
return {
"source_id": session_path.stem,
"started_at": records[0].get("timestamp", "") if records else "",
"project": records[0].get("cwd", "") if records else "",
"title": "", # falls back to first user message
}
def from_record(self, record, source_id):
"""Native record → JorMessage. Return None to skip."""
if record.get("type") == "user":
return JorMessage(
id=str(uuid.uuid4()),
role="user",
content=record["message"]["content"],
source_tool="my_tool",
source_id=source_id,
)
return None
# --- Writing ---
def to_record(self, msg, session_id):
"""JorMessage → native record."""
return {"type": msg.role, "message": {"role": msg.role, "content": msg.content}}
def write(self, messages, target_dir):
target_dir.mkdir(parents=True, exist_ok=True)
sid = str(uuid.uuid4())
path = target_dir / f"{sid}.jsonl"
self.write_jsonl(messages, path, sid)
return sid, path
def resume_command(self, session_file):
return f"mytool resume {session_file.stem}"
def write_session(self, messages, project):
sid, path = self.write(messages, self._home / "sessions")
return sid, self.resume_command(path), path
3. Testing
Create a fixture at tests/fixtures/my_tool_session.jsonl with real session data (copy from the actual tool, don't invent it — see RALPH.md ground truth rules).
Then add tests at tests/connectors/my_tool/:
- test_parser.py — unit tests for
from_record(),to_record(), andextract_metadata() - test_connector.py — integration tests that scan a fixture and verify IndexEntry output
Schema validation is automatic — add a test class to tests/test_schemas.py:
class TestMyToolSchema(BaseSchemaTest):
connector = "my_tool"
fixture = "my_tool_session.jsonl"
4. Register
Add your connector to cli.py:
from jor.connectors.my_tool.connector import MyToolConnector
CONNECTORS = {
"claude": ClaudeConnector,
"codex": CodexConnector,
"my_tool": MyToolConnector,
}
License
MIT
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file pyjor-0.1.1.tar.gz.
File metadata
- Download URL: pyjor-0.1.1.tar.gz
- Upload date:
- Size: 24.4 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
9f78fd0856b4d77fc3bafcd298d5abb7af9581a315912a64f8c8f067388c4f3c
|
|
| MD5 |
90fe287fc1edeb858459709f26477cb5
|
|
| BLAKE2b-256 |
18ba12f1db24b6f3c57e9048bd2d5dd434b0923f96016d19319a630c40614172
|
Provenance
The following attestation bundles were made for pyjor-0.1.1.tar.gz:
Publisher:
publish.yml on foogunlana/jor
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
pyjor-0.1.1.tar.gz -
Subject digest:
9f78fd0856b4d77fc3bafcd298d5abb7af9581a315912a64f8c8f067388c4f3c - Sigstore transparency entry: 1674269505
- Sigstore integration time:
-
Permalink:
foogunlana/jor@192308e64539c58a64c6fef2df5b73ccf5a23b44 -
Branch / Tag:
refs/tags/v0.1.1 - Owner: https://github.com/foogunlana
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@192308e64539c58a64c6fef2df5b73ccf5a23b44 -
Trigger Event:
release
-
Statement type:
File details
Details for the file pyjor-0.1.1-py3-none-any.whl.
File metadata
- Download URL: pyjor-0.1.1-py3-none-any.whl
- Upload date:
- Size: 32.5 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
a0a572d73278ed57a1c856223972a67a6b8592ee4af2c18a05481636fafb1956
|
|
| MD5 |
b3f67edfb864b2357cdf3b88bb1f9083
|
|
| BLAKE2b-256 |
b2379e8d2628eb3ff00471b29fafddd52d79aa8d7ae3f6cb7aa42e45aaf71c84
|
Provenance
The following attestation bundles were made for pyjor-0.1.1-py3-none-any.whl:
Publisher:
publish.yml on foogunlana/jor
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
pyjor-0.1.1-py3-none-any.whl -
Subject digest:
a0a572d73278ed57a1c856223972a67a6b8592ee4af2c18a05481636fafb1956 - Sigstore transparency entry: 1674269552
- Sigstore integration time:
-
Permalink:
foogunlana/jor@192308e64539c58a64c6fef2df5b73ccf5a23b44 -
Branch / Tag:
refs/tags/v0.1.1 - Owner: https://github.com/foogunlana
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@192308e64539c58a64c6fef2df5b73ccf5a23b44 -
Trigger Event:
release
-
Statement type: