Metadata-Version: 2.4
Name: pyjor
Version: 0.1.1
Summary: AI session discovery, conversion, and cross-tool continuation
Project-URL: Homepage, https://github.com/folusoogunlana/jor
Project-URL: Repository, https://github.com/folusoogunlana/jor
Project-URL: Issues, https://github.com/folusoogunlana/jor/issues
Author: Jor Contributors
License: MIT
License-File: LICENSE
Keywords: ai,claude,codex,developer-tools,sessions
Classifier: Development Status :: 3 - Alpha
Classifier: Environment :: Console
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Requires-Python: >=3.11
Requires-Dist: click>=8.0
Requires-Dist: pydantic>=2.0
Description-Content-Type: text/markdown

# 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

```bash
pip install pyjor
```

## Usage

```bash
# 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:

```json
{
  "$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.

```python
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`:

```python
class TestMyToolSchema(BaseSchemaTest):
    connector = "my_tool"
    fixture = "my_tool_session.jsonl"
```

### 4. Register

Add your connector to `cli.py`:

```python
from jor.connectors.my_tool.connector import MyToolConnector

CONNECTORS = {
    "claude": ClaudeConnector,
    "codex": CodexConnector,
    "my_tool": MyToolConnector,
}
```

## License

MIT
