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.1.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.1-py3-none-any.whl (32.5 kB view details)

Uploaded Python 3

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

Hashes for pyjor-0.1.1.tar.gz
Algorithm Hash digest
SHA256 9f78fd0856b4d77fc3bafcd298d5abb7af9581a315912a64f8c8f067388c4f3c
MD5 90fe287fc1edeb858459709f26477cb5
BLAKE2b-256 18ba12f1db24b6f3c57e9048bd2d5dd434b0923f96016d19319a630c40614172

See more details on using hashes here.

Provenance

The following attestation bundles were made for pyjor-0.1.1.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.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

Hashes for pyjor-0.1.1-py3-none-any.whl
Algorithm Hash digest
SHA256 a0a572d73278ed57a1c856223972a67a6b8592ee4af2c18a05481636fafb1956
MD5 b3f67edfb864b2357cdf3b88bb1f9083
BLAKE2b-256 b2379e8d2628eb3ff00471b29fafddd52d79aa8d7ae3f6cb7aa42e45aaf71c84

See more details on using hashes here.

Provenance

The following attestation bundles were made for pyjor-0.1.1-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