Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 11 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
# rses

**Cross-resume between Claude Code, Codex CLI, and OpenCode.**
Pick up where one AI coding agent left off in another.
**Cross-resume between Claude Code, Codex CLI, OpenCode, and Droid.**
Pick up where one AI coding agent left off - in another.

```
rses claude with codex --last
```

That's it. Claude launches with full context from your last Codex session: the original task, git diff, conversation history, and a pointer to the session file for deep-dive.

Works in all 6 directions between Claude Code, Codex CLI, and OpenCode.
Works in all 12 directions between Claude Code, Codex CLI, OpenCode, and Droid.

## Install

Expand All @@ -31,6 +31,10 @@ rses codex with claude --last
# OpenCode works too — any combination.
rses opencode with codex --last
rses claude with opencode --last

# Droid sessions are supported too.
rses codex with droid --last
rses droid with claude --last
```

## What it does
Expand All @@ -53,6 +57,7 @@ rses <target> with <source> [session-id] [flags]
rses claude with codex --last # most recent Codex session
rses codex with claude ses_46f04b499ffe... # specific Claude session
rses opencode with codex # interactive picker
rses droid with codex --last # most recent Droid session
rses claude with codex --last --dry-run # print handoff, don't launch
```

Expand Down Expand Up @@ -82,6 +87,7 @@ Power-user shorthand — type less, ship faster:
| `cc`, `cl`, `c` | `claude` |
| `cdx`, `cx`, `x` | `codex` |
| `oc`, `o` | `opencode` |
| `d`, `dr` | `droid` |
| `w` | `with` |

```bash
Expand Down Expand Up @@ -114,13 +120,14 @@ rses opencode with claude --last --provider anthropic
| Claude Code | `~/.claude/transcripts/ses_*.jsonl` | Reads only `user`/`assistant` types |
| Codex CLI | `~/.codex/state_*.sqlite` (auto-discovers version) + JSONL fallback | Handles both 2025 and 2026 schemas |
| OpenCode | `~/.local/share/opencode/opencode.db` | Single JOIN query, reads stable columns only |
| Droid | `~/.factory/sessions/**/*.jsonl` | Reads session metadata plus user/assistant turns, strips system reminders |

All parsers are read-only and wrapped in try/catch — if a tool changes its format, rses degrades gracefully instead of crashing.

## Requirements

- **Node.js 22+** (for built-in `node:sqlite`)
- At least one of: [Claude Code](https://docs.anthropic.com/en/docs/claude-code), [Codex CLI](https://github.com/openai/codex), [OpenCode](https://github.com/opencode-ai/opencode)
- At least one of: [Claude Code](https://docs.anthropic.com/en/docs/claude-code), [Codex CLI](https://github.com/openai/codex), [OpenCode](https://github.com/opencode-ai/opencode), [Droid](https://docs.factory.ai/cli/getting-started/overview)
- macOS or Linux (Windows support planned)

## License
Expand Down
70 changes: 60 additions & 10 deletions bin/rses.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,29 +18,35 @@ import {
parseOpenCodeSession, findOpenCodeSessionById, getLastOpenCodeSession,
queryOpenCodeSessions
} from '../src/parse-opencode.js'
import {
parseDroidSession, findDroidSessionById, getLastDroidSession,
queryDroidSessions
} from '../src/parse-droid.js'
import { buildHandoff } from '../src/build-handoff.js'
import { launchWithHandoff } from '../src/launch.js'
import { lsSessions } from '../src/ls.js'
import { pick } from '../src/picker.js'

const VALID_TOOLS = new Set(['claude', 'codex', 'opencode'])
const VALID_TOOLS = new Set(['claude', 'codex', 'opencode', 'droid'])
const ALIASES = {
cc: 'claude', cl: 'claude', c: 'claude',
cdx: 'codex', cx: 'codex', x: 'codex',
oc: 'opencode', o: 'opencode',
d: 'droid', dr: 'droid',
w: 'with',
}
function resolve_alias(s) { return ALIASES[s] || s }
const RESUME_HINTS = {
claude: ' claude --resume',
codex: ' codex resume',
opencode: ' opencode (select session from built-in picker)',
droid: ' droid --resume',
}

program
.name('rses')
.version('0.1.0')
.description('Cross-resume between Claude Code, Codex CLI, and OpenCode sessions')
.description('Cross-resume between Claude Code, Codex CLI, OpenCode, and Droid sessions')

program
.command('export <source> <id>')
Expand All @@ -49,7 +55,7 @@ program
.action((rawSource, id, opts) => {
const source = resolve_alias(rawSource)
if (!VALID_TOOLS.has(source)) {
console.error(`Unknown source: ${source}. Use 'claude', 'codex', or 'opencode'.`)
console.error(`Unknown source: ${source}. Use 'claude', 'codex', 'opencode', or 'droid'.`)
process.exit(1)
}
const handoff = resolveAndBuild(source, id, { ...opts, last: false })
Expand All @@ -58,14 +64,14 @@ program

program
.command('ls [tool]')
.description('List recent sessions (claude, codex, opencode, or all)')
.description('List recent sessions (claude, codex, opencode, droid, or all)')
.option('--dir <path>', 'Filter by working directory')
.action((rawTool, opts) => {
const tool = rawTool ? resolve_alias(rawTool) : null
const tools = tool ? [tool] : ['codex', 'claude', 'opencode']
const tools = tool ? [tool] : ['codex', 'claude', 'opencode', 'droid']
for (const t of tools) {
if (!VALID_TOOLS.has(t)) {
console.error(`Unknown tool: ${t}. Use 'claude', 'codex', or 'opencode'.`)
console.error(`Unknown tool: ${t}. Use 'claude', 'codex', 'opencode', or 'droid'.`)
process.exit(1)
}
lsSessions(t, opts.dir ? expandPath(opts.dir) : null)
Expand Down Expand Up @@ -181,6 +187,21 @@ async function pickSession(source, filterDir) {

return pick(items, 'Select an OpenCode session:')

} else if (source === 'droid') {
const rows = queryDroidSessions({ limit: 30, filterDir }) || []
if (!rows.length) return null

const items = rows.map(r => {
const date = new Date(r.updatedAt).toISOString().slice(0, 16).replace('T', ' ')
const cwd = shorten(r.cwd)
return {
display: makeDisplay(date, cwd, r.title || basename(r.path, '.jsonl')),
value: r.path,
}
})

return pick(items, 'Select a Droid session:')

} else {
const files = findClaudeSessions(filterDir)
if (!files.length) return null
Expand Down Expand Up @@ -285,8 +306,32 @@ async function resolveAndBuildAsync(source, id, opts) {
process.exit(1)
}

} else if (source === 'droid') {
if (opts.last) {
filePath = getLastDroidSession(filterDir)
if (!filePath) {
console.error('No Droid sessions found' + (filterDir ? ` in ${filterDir}` : '') + '.')
process.exit(1)
}
} else if (id) {
filePath = findDroidSessionById(id)
if (!filePath) {
console.error(`Droid session not found: ${id}`)
console.error('Tip: run `rses ls droid` to list sessions.')
process.exit(1)
}
} else {
filePath = await pickSession('droid', filterDir)
if (!filePath) { console.error('Cancelled.'); process.exit(0) }
}

try { parsed = parseDroidSession(filePath); parsed.filePath = filePath } catch (e) {
console.error(`Failed to parse Droid session: ${e.message}`)
process.exit(1)
}

} else {
console.error(`Unknown source tool: ${source}. Use 'claude', 'codex', or 'opencode'.`)
console.error(`Unknown source tool: ${source}. Use 'claude', 'codex', 'opencode', or 'droid'.`)
process.exit(1)
}

Expand Down Expand Up @@ -315,8 +360,13 @@ function resolveAndBuild(source, id, opts) {
const sessionId = opts.last ? getLastOpenCodeSession(filterDir) : findOpenCodeSessionById(id)
if (!sessionId) { console.error(`OpenCode session not found: ${id}`); process.exit(1) }
parsed = parseOpenCodeSession(sessionId)
} else if (source === 'droid') {
if (!id && !opts.last) { console.error('export requires a session ID.'); process.exit(1) }
filePath = opts.last ? getLastDroidSession(filterDir) : findDroidSessionById(id)
if (!filePath) { console.error(`Droid session not found: ${id}`); process.exit(1) }
parsed = parseDroidSession(filePath); parsed.filePath = filePath
} else {
console.error(`Unknown source: ${source}. Use 'claude', 'codex', or 'opencode'.`)
console.error(`Unknown source: ${source}. Use 'claude', 'codex', 'opencode', or 'droid'.`)
process.exit(1)
}

Expand All @@ -326,10 +376,10 @@ function resolveAndBuild(source, id, opts) {

async function runHandoff(target, source, id, opts) {
if (!VALID_TOOLS.has(target)) {
console.error(`Unknown target: ${target}. Use 'claude', 'codex', or 'opencode'.`); process.exit(1)
console.error(`Unknown target: ${target}. Use 'claude', 'codex', 'opencode', or 'droid'.`); process.exit(1)
}
if (!VALID_TOOLS.has(source)) {
console.error(`Unknown source: ${source}. Use 'claude', 'codex', or 'opencode'.`); process.exit(1)
console.error(`Unknown source: ${source}. Use 'claude', 'codex', 'opencode', or 'droid'.`); process.exit(1)
}
if (target === source) {
console.error(`Same source and target (${target}). Use the native resume:`)
Expand Down
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "rses-cli",
"version": "0.1.0",
"description": "Cross-resume between Claude Code, Codex CLI, and OpenCode sessions",
"description": "Cross-resume between Claude Code, Codex CLI, OpenCode, and Droid sessions",
"bin": {
"rses": "./bin/rses.js"
},
Expand All @@ -23,6 +23,6 @@
"engines": {
"node": ">=22"
},
"keywords": ["claude", "codex", "opencode", "ai", "cli", "resume", "session", "handoff"],
"keywords": ["claude", "codex", "opencode", "droid", "ai", "cli", "resume", "session", "handoff"],
"license": "MIT"
}
2 changes: 1 addition & 1 deletion src/build-handoff.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ function trunc(str, max) {
export function buildHandoff(source, parsed) {
const { cwd, uuid, sessionId, startCommit, branch: parsedBranch, task, turns, filePath } = parsed
const id = uuid || sessionId || 'unknown'
const TOOL_NAMES = { codex: 'Codex', claude: 'Claude', opencode: 'OpenCode' }
const TOOL_NAMES = { codex: 'Codex', claude: 'Claude', opencode: 'OpenCode', droid: 'Droid' }
const toolName = TOOL_NAMES[source] || source

const git = cwd ? getGitContext(cwd, startCommit) : null
Expand Down
3 changes: 2 additions & 1 deletion src/launch.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { spawn } from 'child_process'

export function launchWithHandoff(tool, handoff, cwd, passthroughArgs = []) {
// opencode uses `opencode run <message>`, claude/codex accept prompt as bare arg
// opencode uses `opencode run <message>`, claude/codex/droid accept prompt as bare arg
const args = tool === 'opencode'
? ['run', ...passthroughArgs, handoff]
: [...passthroughArgs, handoff]
Expand All @@ -20,6 +20,7 @@ export function launchWithHandoff(tool, handoff, cwd, passthroughArgs = []) {
claude: ' Install: npm i -g @anthropic-ai/claude-code',
codex: ' Install: npm i -g @openai/codex',
opencode: ' Install: see https://github.com/opencode-ai/opencode',
droid: ' Install: see https://docs.factory.ai/cli/getting-started/overview',
}
console.error(`\nError: '${tool}' not found on PATH. Is it installed?`)
console.error(installHints[tool] || ` Install ${tool} and ensure it's on your PATH.`)
Expand Down
17 changes: 15 additions & 2 deletions src/ls.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { findCodexSessions, queryCodexSessions } from './parse-codex.js'
import { findClaudeSessions } from './parse-claude.js'
import { queryOpenCodeSessions } from './parse-opencode.js'
import { queryDroidSessions } from './parse-droid.js'
import { readFileSync } from 'fs'
import { basename } from 'path'

Expand Down Expand Up @@ -106,9 +107,21 @@ export function lsSessions(tool, filterDir = null) {
}
}

if (tool === 'droid') {
const dbRows = queryDroidSessions({ limit: 20, filterDir })
if (dbRows?.length) {
rows = dbRows.map(r => ({
id: r.id,
date: formatDate(r.updatedAt),
cwd: r.cwd || '—',
task: (r.title || '(no title)').slice(0, 70),
}))
}
}

if (!rows) {
if (tool === 'opencode') {
console.log('No opencode sessions found.')
if (tool === 'opencode' || tool === 'droid') {
console.log(`No ${tool} sessions found.`)
return
}

Expand Down
Loading