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

git clone https://github.com/foogunlana/jor.git
cd jor
pip install -e .

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.0.tar.gz (1.3 MB 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.0-py3-none-any.whl (32.5 kB view details)

Uploaded Python 3

File details

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

File metadata

  • Download URL: pyjor-0.1.0.tar.gz
  • Upload date:
  • Size: 1.3 MB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.6.11

File hashes

Hashes for pyjor-0.1.0.tar.gz
Algorithm Hash digest
SHA256 dbbab43b8b4c09e29d766e8a0173ff214574147a42c31430e1a34a65f834161a
MD5 b56e4e136ee63eaa67fd0b2d9de3e7eb
BLAKE2b-256 d9c2ab2aaf87a159aca61f969a926a4d85135a44f69bdf1217f91bb471042e3e

See more details on using hashes here.

File details

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

File metadata

  • Download URL: pyjor-0.1.0-py3-none-any.whl
  • Upload date:
  • Size: 32.5 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.6.11

File hashes

Hashes for pyjor-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 10ac8b51ac10c0330b20fb70ad01d991ddab7862502f390c7cabcdb953336c92
MD5 ed3ec2ecf1be6629fe64aa2df8540c43
BLAKE2b-256 180bcd03366ad533cdda6b1e69d50258ab676499940004260a444b46e457b07b

See more details on using hashes here.

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