Skip to main content

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 .jsonl sessions
  • Codex — reads and writes .jsonl sessions

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

  1. jor list scans known session directories, indexes what it finds, and shows a table
  2. jor open translates 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(), and extract_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


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

pyjor-0.1.2.tar.gz (24.4 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

pyjor-0.1.2-py3-none-any.whl (32.5 kB view details)

Uploaded Python 3

File details

Details for the file pyjor-0.1.2.tar.gz.

File metadata

  • Download URL: pyjor-0.1.2.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

Hashes for pyjor-0.1.2.tar.gz
Algorithm Hash digest
SHA256 d61bb6cc11dd2a5347a71046a6393942ac981ef4c360817ffce6e38bfdf88cd0
MD5 56038b7ea5e301cc4bad3f9c7275f27c
BLAKE2b-256 63c575a756b863c49b5d85ff9babed068912c58caf24f57d34a7d25b3075b477

See more details on using hashes here.

Provenance

The following attestation bundles were made for pyjor-0.1.2.tar.gz:

Publisher: publish.yml on foogunlana/jor

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file pyjor-0.1.2-py3-none-any.whl.

File metadata

  • Download URL: pyjor-0.1.2-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

Hashes for pyjor-0.1.2-py3-none-any.whl
Algorithm Hash digest
SHA256 8e433328f93421b5eb11061459640a226154b89597571fdc09e8b433dc411fde
MD5 9b1f854b6ee5e86aef5b4c10b2485069
BLAKE2b-256 1ef938d15f04e6f68a54a0c56315bbc298f66d415188e0ff8713cca2015e32bc

See more details on using hashes here.

Provenance

The following attestation bundles were made for pyjor-0.1.2-py3-none-any.whl:

Publisher: publish.yml on foogunlana/jor

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page