Skip to content
Closed
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
23 changes: 15 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Codbash

Control room for AI coding sessions. Search, replay, and resume Claude Code, Codex, Qwen, Cursor, OpenCode, Kiro, Kilo, and Copilot Chat sessions without digging through scattered logs.
Control room for AI coding sessions. Search, replay, and resume Claude Code, Codex, Qwen, Pi, Oh My Pi, Cursor, OpenCode, Kiro, Kilo, and Copilot Chat sessions without digging through scattered logs.

[Russian / Русский](docs/README_RU.md) | [Chinese / 中文](docs/README_ZH.md)

Expand All @@ -21,6 +21,8 @@ codbash run
|-------|----------|---------|--------|-------------|---------|---------|--------|
| Claude Code | JSONL | Yes | Yes | Yes | Yes | Yes | Terminal / cmux |
| Codex CLI | JSONL | Yes | Yes | Yes | Yes | Yes | Terminal |
| Pi | JSONL | Yes | Yes | Yes | - | Yes | Terminal |
| Oh My Pi | JSONL | Yes | Yes | Yes | - | Yes | Terminal |
| Cursor | JSONL | Yes | Yes | Yes | - | Yes | Open in Cursor |
| OpenCode | SQLite | Yes | Yes | Yes | - | Yes | Terminal |
| Kiro CLI | SQLite | Yes | Yes | Yes | - | Yes | Terminal |
Expand All @@ -32,7 +34,7 @@ Also detects Claude Code running inside Cursor (via `claude-vscode` entrypoint).

**Browser Dashboard**
- Grid and List view with project grouping
- Trigram fuzzy search + full-text deep search across all messages
- Trigram fuzzy search + full-text deep search across all supported message logs
- Filter by agent, tags, date range
- Star/pin sessions, tag with labels
- GitHub-style SVG activity heatmap with streak stats
Expand All @@ -41,29 +43,29 @@ Also detects Claude Code running inside Cursor (via `claude-vscode` entrypoint).
- Themes: Dark, Light, System

**Live Monitoring**
- LIVE/WAITING badges on all agent types
- LIVE/WAITING badges for terminal-launched agents when their processes can be matched
- Animated border on active session cards
- Running view with CPU, Memory, PID, Uptime
- Focus Terminal / Open in Cursor buttons
- Polling every 5 seconds

**Cost Analytics**
- Real cost from actual token usage (input, output, cache)
- Real cost from actual token usage when agents record usage (input, output, cache)
- Per-model pricing: Opus, Sonnet, Haiku, Codex, GPT-5
- Daily cost chart, cost by project, most expensive sessions

**Cross-Agent**
- Convert sessions between Claude Code and Codex
- Handoff: generate context document to continue in any agent
- Install Agents: one-click install commands for all agents
- Convert sessions between Claude Code, Codex, and Qwen formats
- Handoff: generate context document from any session with readable messages
- Install Agents: one-click install commands for supported agent CLIs

**CLI**
```bash
codbash run [--port=N] [--no-browser]
codbash search <query>
codbash show <session-id>
codbash handoff <id> [target] [--verbosity=full] [--out=file.md]
codbash convert <id> claude|codex
codbash convert <id> claude|codex|qwen
codbash list [limit]
codbash stats
codbash export [file.tar.gz]
Expand All @@ -81,6 +83,8 @@ codbash stop
~/.claude/ Claude Code sessions + PID tracking
~/.codex/ Codex CLI sessions
~/.cursor/projects/*/agent-transcripts/ Cursor agent sessions
~/.pi/agent/sessions/**/*.jsonl Pi coding-agent sessions
~/.omp/agent/sessions/**/*.jsonl Oh My Pi coding-agent sessions
~/.local/share/opencode/opencode.db OpenCode (SQLite)
~/Library/Application Support/kiro-cli/ Kiro CLI (SQLite)
<vscode-user-data>/workspaceStorage/ Copilot Chat (JSON/JSONL)
Expand All @@ -97,6 +101,9 @@ Zero dependencies. Everything runs on `localhost`.
curl -fsSL https://claude.ai/install.sh | bash # Claude Code
npm i -g @openai/codex # Codex CLI
curl -fsSL https://cli.kiro.dev/install | bash # Kiro CLI
npm i -g @earendil-works/pi-coding-agent # Pi
curl -fsSL https://omp.sh/install | sh # Oh My Pi
bun install -g @oh-my-pi/pi-coding-agent # Oh My Pi (Bun)
curl -fsSL https://opencode.ai/install | bash # OpenCode
```

Expand Down
16 changes: 14 additions & 2 deletions bin/cli.js
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ const TOOL_LABELS = {
'claude-ext': { label: 'claude-ext', ansi: '\x1b[34mclaude-ext\x1b[0m' },
codex: { label: 'codex', ansi: '\x1b[36mcodex\x1b[0m' },
qwen: { label: 'qwen', ansi: '\x1b[33mqwen\x1b[0m' },
pi: { label: 'pi', ansi: '\x1b[95mpi\x1b[0m' },
cursor: { label: 'cursor', ansi: '\x1b[35mcursor\x1b[0m' },
opencode: { label: 'opencode', ansi: '\x1b[95mopencode\x1b[0m' },
kiro: { label: 'kiro', ansi: '\x1b[91mkiro\x1b[0m' },
Expand All @@ -65,9 +66,19 @@ function getToolDisplay(tool) {
return TOOL_LABELS[tool] || { label: tool || 'unknown', ansi: tool || 'unknown' };
}

function getResumeCommand(tool, sessionId) {
function quoteShellArg(value) {
return "'" + String(value).replace(/'/g, "'\\''") + "'";
}

function getResumeCommand(session) {
const tool = session && session.tool;
const sessionId = session && session.id;
if (tool === 'codex') return `codex resume ${sessionId}`;
if (tool === 'qwen') return `qwen -r ${sessionId}`;
if (tool === 'pi') {
const target = session.resume_target || sessionId;
return `${session.agent_variant === 'ohmypi' ? 'omp' : 'pi'} --resume ${quoteShellArg(target)}`;
}
if (tool === 'cursor') return 'cursor';
return `claude --resume ${sessionId}`;
}
Expand All @@ -76,6 +87,7 @@ const STATS_TOOL_ROWS = [
{ label: 'Claude sessions', match: (s) => s.tool === 'claude' || s.tool === 'claude-ext' },
{ label: 'Codex sessions', match: (s) => s.tool === 'codex' },
{ label: 'Qwen sessions', match: (s) => s.tool === 'qwen' },
{ label: 'Pi/OhMyPi sessions', match: (s) => s.tool === 'pi' },
{ label: 'Cursor sessions', match: (s) => s.tool === 'cursor' },
{ label: 'OpenCode sessions', match: (s) => s.tool === 'opencode' },
{ label: 'Kiro sessions', match: (s) => s.tool === 'kiro' },
Expand Down Expand Up @@ -206,7 +218,7 @@ switch (command) {
console.log('');
}

console.log(` Resume: \x1b[2m${getResumeCommand(session.tool, session.id)}\x1b[0m`);
console.log(` Resume: \x1b[2m${getResumeCommand(session)}\x1b[0m`);
console.log('');
break;
}
Expand Down
42 changes: 30 additions & 12 deletions docs/ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ Browser (localhost:3847) Node.js Server
~/.claude/ ~/.codex/ ~/.cursor/
~/.local/share/opencode/opencode.db
~/Library/Application Support/kiro-cli/data.sqlite3
~/.pi/agent/sessions/*/*.jsonl
~/.omp/agent/sessions/*/*.jsonl
~/.config/Code/User/workspaceStorage/*/chatSessions/
```

Expand Down Expand Up @@ -112,6 +114,7 @@ Detection logic in `data.js`:
| History index | `~/.codex/history.jsonl` |
| Session data | `~/.codex/sessions/<YYYY>/<MM>/<DD>/rollout-<TIMESTAMP>-<UUID>.jsonl` |


**history.jsonl**:
```json
{"session_id": "uuid", "ts": 1712345678, "text": "user prompt", "display": "...", "project": "/path", "cwd": "/path"}
Expand All @@ -127,7 +130,20 @@ Note: `ts` is in **seconds** (not milliseconds like Claude).

Session ID extracted from filename: `rollout-20260406-<UUID>.jsonl` → UUID part.

### 4. Cursor (Agent Mode)

### 4. OhMyPi / Pi

| File | Purpose |
|------|---------|
| Pi session data | `~/.pi/agent/sessions/--<cwd-encoded>--/<timestamp>_<sessionId>.jsonl` |
| OhMyPi session data | `~/.omp/agent/sessions/--<cwd-encoded>--/<timestamp>_<sessionId>.jsonl` |

**JSONL format**:
- First line is a session header: `{ type: "session", id, timestamp, cwd, title }`.
- Conversation rows use `{ type: "message", message: { role, content, usage } }`.
- Token usage maps `usage.input`, `usage.output`, `usage.cacheRead`, `usage.cacheWrite`, and optional `usage.cost.total` into codbash analytics.

### 5. Cursor (Agent Mode)

| Item | Location |
|------|----------|
Expand All @@ -146,7 +162,7 @@ Session ID extracted from filename: `rollout-20260406-<UUID>.jsonl` → UUID par

User messages wrapped in `<user_query>...</user_query>` tags — stripped during parsing.

### 5. OpenCode
### 6. OpenCode

| Item | Location |
|------|----------|
Expand All @@ -171,7 +187,7 @@ GROUP BY m.id ORDER BY m.time_created

Tables: `session`, `message`, `part`. Message `data` is JSON with `{role, tokens, model}`. Part `data` is JSON with `{type, text}`.

### 6. Kiro CLI
### 7. Kiro CLI

| Item | Location |
|------|----------|
Expand All @@ -198,7 +214,7 @@ FROM conversations_v2 ORDER BY updated_at DESC
}
```

### 7. Copilot (VS Code Extension)
### 8. Copilot (VS Code Extension)

| Item | Location |
|------|----------|
Expand Down Expand Up @@ -241,15 +257,16 @@ FROM conversations_v2 ORDER BY updated_at DESC
```
1. Read ~/.claude/history.jsonl → sessions{} keyed by sessionId (tool: "claude")
2. scanCodexSessions() → merge into sessions{} (tool: "codex")
3. scanOpenCodeSessions() → merge (tool: "opencode")
4. scanCursorSessions() → merge (tool: "cursor")
5. scanKiroSessions() → merge (tool: "kiro")
5a. scanCopilotSessions() → merge (tool: "copilot-chat")
6. Enrich Claude sessions with detail files:
3. scanPiSessions() → merge (tool: "pi")
4. scanOpenCodeSessions() → merge (tool: "opencode")
5. scanCursorSessions() → merge (tool: "cursor")
6. scanKiroSessions() → merge (tool: "kiro")
7. scanCopilotSessions() → merge (tool: "copilot-chat")
8. Enrich Claude sessions with detail files:
- Count messages, get file size
- Check entrypoint → change tool to "claude-ext" if not "cli"
7. Scan orphan sessions from ~/.claude/projects/ (Claude Extension)
8. Sort by last_ts DESC, format dates
9. Scan orphan sessions from ~/.claude/projects/ (Claude Extension)
10. Sort by last_ts DESC, format dates
```

### Search Index
Expand All @@ -271,12 +288,13 @@ cost = input_tokens * input_price

Model pricing in `MODEL_PRICING` object (per-token rates for opus, sonnet, haiku, codex-mini, gpt-5).
Codex fallback: estimate from file size (~4 bytes per token).
OhMyPi / Pi uses `usage.cost.total` when present; otherwise it uses mapped token counts only when the model has known pricing.

### Active Session Detection

```
1. Read ~/.claude/sessions/*.json → PID-to-session map
2. ps aux | grep "claude|codex|opencode|kiro-cli|cursor-agent"
2. ps aux | grep "claude|codex|qwen|omp|opencode|kiro-cli|cursor-agent|kilo"
3. For each process: parse PID, CPU%, memory, state
4. Status: "active" (CPU >= 1%) or "waiting" (sleeping/stopped)
5. Map PID → sessionId via PID files
Expand Down
22 changes: 21 additions & 1 deletion src/agents-detect.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ const AGENT_DEFS = Object.freeze([
{ id: 'codex', label: 'Codex', bin: 'codex' },
{ id: 'cursor', label: 'Cursor', bin: 'cursor-agent', appBundle: 'Cursor.app' },
{ id: 'qwen', label: 'Qwen Code', bin: 'qwen' },
{ id: 'pi', label: 'OhMyPi', customCheck: 'piPath' },
{ id: 'kilo', label: 'Kilo', bin: 'kilo' },
{ id: 'kiro', label: 'Kiro CLI', bin: 'kiro-cli' },
{ id: 'opencode', label: 'OpenCode', bin: 'opencode' },
Expand Down Expand Up @@ -75,9 +76,21 @@ function vscodeCopilotChatExtension() {
return null;
}

function piPath(ctx) {
const which = ctx && ctx.which ? ctx.which : realWhich;
const pi = which('pi');
const omp = which('omp');
if (!pi && !omp) return null;
const commands = [];
if (pi) commands.push('pi');
if (omp) commands.push('omp');
return { ok: true, detectedVia: 'path', binPath: pi || omp, command: pi ? 'pi' : 'omp', commands };
}

var CUSTOM_CHECKS = {
ghCopilotExtension: ghCopilotExtension,
vscodeCopilotChatExtension: vscodeCopilotChatExtension,
piPath: piPath,
};

function realWhich(bin) {
Expand Down Expand Up @@ -128,6 +141,8 @@ async function detect(ctx) {
for (const def of AGENT_DEFS) {
let detectedVia = null;
let binPath;
let detectedCommand;
let detectedCommands;
if (def.bin) {
const found = which(def.bin);
if (found) {
Expand All @@ -142,14 +157,19 @@ async function detect(ctx) {
}
if (!detectedVia && def.customCheck) {
const fn = customChecks[def.customCheck];
const result = typeof fn === 'function' ? fn() : null;
const result = typeof fn === 'function' ? fn({ which, platform, appBundleExists }) : null;
if (result && result.ok) {
detectedVia = result.detectedVia || 'custom';
if (result.binPath) binPath = result.binPath;
if (result.command) detectedCommand = result.command;
if (Array.isArray(result.commands) && result.commands.length) detectedCommands = result.commands.slice();
}
}
if (detectedVia) {
const entry = { id: def.id, label: def.label, detectedVia };
if (binPath) entry.binPath = binPath;
if (detectedCommand) entry.command = detectedCommand;
if (detectedCommands) entry.commands = detectedCommands;
out.push(entry);
}
}
Expand Down
Loading