0rrery 0.1.1 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,17 +1,17 @@
1
1
  # 0rrery
2
2
 
3
- Trace-first, local-first observability for AI agent workflows. Watch what your Claude Code sessions actually did every tool call, subagent, LLM call, and permission decision as live traces in a local dashboard. One process, one SQLite file, no cloud.
3
+ Trace-first, local-first observability for AI agent workflows. Watch what your Claude Code sessions actually did (every tool call, subagent, LLM call, and permission decision) as live traces in a local dashboard. One process, one SQLite file, no cloud. Ingests Claude Code and OpenAI Codex CLI sessions.
4
4
 
5
5
  ## Quickstart
6
6
 
7
7
  ```
8
- bun install -g 0rrery
8
+ npm install -g 0rrery # or: bun install -g 0rrery
9
9
  0rrery init
10
10
  ```
11
11
 
12
12
  `init` does three things (each skippable): installs Claude Code hooks (`--no-hooks`), sets up a user service so 0rrery runs persistently (`--no-service`), and imports your existing session history (`--no-import`). Then open **http://localhost:7317**.
13
13
 
14
- Requires [Bun](https://bun.sh) ≥ 1.1. Claude Code hooks require the global install (the hook command is `0rrery hook`); `bunx 0rrery serve` works for a look around without installing.
14
+ Requires [Bun](https://bun.sh) ≥ 1.1 at runtime: the CLI runs on Bun even when installed via npm, so `npm install` succeeds without Bun, but the `0rrery` command exits with an install-Bun message until Bun is on your PATH. Claude Code hooks require the global install (the hook command is `0rrery hook`); `bunx 0rrery serve` works for a look around without installing.
15
15
 
16
16
  ## Commands
17
17
 
@@ -52,7 +52,7 @@ Versions are published by CI via npm Trusted Publishing: bump the version in `sc
52
52
  ## Upgrade
53
53
 
54
54
  ```
55
- bun install -g 0rrery@latest
55
+ npm install -g 0rrery@latest # or: bun install -g 0rrery@latest
56
56
  0rrery service uninstall && 0rrery service install # regenerate if the bin path changed
57
57
  ```
58
58
 
@@ -62,7 +62,7 @@ After upgrading (or when developing from the repo), rebuild and force a reinstal
62
62
 
63
63
  ```
64
64
  0rrery service uninstall
65
- bun remove -g 0rrery
65
+ npm uninstall -g 0rrery # or: bun remove -g 0rrery
66
66
  rm -rf ~/.0rrery
67
67
  ```
68
68
  Hook entries: re-run a Claude session or remove entries whose command is `0rrery hook` from `~/.claude/settings.json`.
@@ -78,3 +78,5 @@ bun run build:pkg # stage the npm package in dist-pkg/
78
78
  ```
79
79
 
80
80
  Specs live in `docs/superpowers/specs/`.
81
+
82
+ Want 0rrery to ingest another agent CLI? See [ADAPTERS.md](./ADAPTERS.md).
package/index.js CHANGED
@@ -1,4 +1,6 @@
1
- #!/usr/bin/env bun
1
+ #!/bin/sh
2
+ ':' //; command -v bun >/dev/null 2>&1 || { echo "0rrery runs on Bun (>= 1.1), and bun was not found on your PATH." >&2; echo "Install it from https://bun.sh then re-run this command." >&2; exit 1; }
3
+ ':' //; exec bun "$0" "$@"
2
4
  // @bun
3
5
  var __defProp = Object.defineProperty;
4
6
  var __returnValue = (v) => v;
@@ -16,9 +18,9 @@ var __export = (target, all) => {
16
18
  };
17
19
 
18
20
  // packages/cli/src/index.ts
19
- import { existsSync as existsSync6 } from "fs";
21
+ import { existsSync as existsSync7, openSync as openSync2, readSync as readSync2, closeSync as closeSync2 } from "fs";
20
22
  import { homedir as homedir3 } from "os";
21
- import { join as join8, resolve as resolve2 } from "path";
23
+ import { join as join9, resolve as resolve2 } from "path";
22
24
 
23
25
  // packages/server/src/server.ts
24
26
  import { appendFileSync, mkdirSync } from "fs";
@@ -4009,7 +4011,7 @@ var ts = exports_external.number().int().nonnegative();
4009
4011
  var SessionStartSchema = exports_external.object({
4010
4012
  op: exports_external.literal("session.start"),
4011
4013
  sessionId: exports_external.string().min(1),
4012
- source: exports_external.enum(["claude-code", "api"]),
4014
+ source: exports_external.enum(["claude-code", "api", "codex"]),
4013
4015
  project: exports_external.string().optional(),
4014
4016
  cwd: exports_external.string().optional(),
4015
4017
  gitBranch: exports_external.string().optional(),
@@ -5017,7 +5019,8 @@ function parseTranscriptLine(raw, state) {
5017
5019
  // packages/claude-code/src/importer.ts
5018
5020
  import { openSync, readSync, fstatSync, closeSync, readdirSync } from "fs";
5019
5021
  import { join as join3, dirname, basename } from "path";
5020
- async function importTranscript(path, url, fromByte = 0, state = newTranscriptState(), finalize = false) {
5022
+ var claudeParser = { parse: parseTranscriptLine };
5023
+ async function importTranscript(path, url, fromByte = 0, state = newTranscriptState(), finalize = false, parser = claudeParser) {
5021
5024
  const fd = openSync(path, "r");
5022
5025
  let text;
5023
5026
  try {
@@ -5036,19 +5039,24 @@ async function importTranscript(path, url, fromByte = 0, state = newTranscriptSt
5036
5039
  return { ops: 0, emitted: true, bytesRead: fromByte };
5037
5040
  const complete = text.slice(0, lastNewline);
5038
5041
  const consumedBytes = Buffer.byteLength(text.slice(0, lastNewline + 1));
5039
- const snapshot = { ...state, agentToolUseIds: new Set(state.agentToolUseIds) };
5042
+ const snapshot = { ...state };
5043
+ for (const k of Object.keys(snapshot))
5044
+ if (snapshot[k] instanceof Set)
5045
+ snapshot[k] = new Set(snapshot[k]);
5040
5046
  const ops = complete.split(`
5041
- `).filter(Boolean).flatMap((l) => parseTranscriptLine(l, state));
5047
+ `).filter(Boolean).flatMap((l) => parser.parse(l, state));
5042
5048
  if (ops.length > 0) {
5043
5049
  const maxTs = ops.reduce((max, o) => o.ts > max ? o.ts : max, 0);
5044
- if (state.agentId) {
5050
+ if ("agentId" in state && state.agentId && ops.length) {
5045
5051
  ops.push({ op: "span.end", id: `agent:${state.agentId}`, ts: maxTs, status: "ok" });
5046
5052
  }
5047
- if (finalize && !state.agentId) {
5053
+ if (finalize && !(("agentId" in state) && state.agentId)) {
5048
5054
  const sessionId = ops.find((o) => ("sessionId" in o))?.sessionId;
5049
5055
  if (sessionId)
5050
5056
  ops.push({ op: "session.end", sessionId, ts: maxTs });
5051
5057
  }
5058
+ if (finalize && parser.finalize)
5059
+ ops.push(...parser.finalize(state, maxTs));
5052
5060
  }
5053
5061
  const emitted = await emitOps(url, ops, 5000);
5054
5062
  if (!emitted) {
@@ -5058,23 +5066,27 @@ async function importTranscript(path, url, fromByte = 0, state = newTranscriptSt
5058
5066
  return { ops: ops.length, emitted, bytesRead: fromByte + consumedBytes };
5059
5067
  }
5060
5068
  async function importSession(path, url, opts = {}) {
5069
+ const newState = opts.newState ?? newTranscriptState;
5070
+ const parser = opts.parser ?? claudeParser;
5061
5071
  let files = 0, ops = 0, emitted = true;
5062
- const main = await importTranscript(path, url, 0, newTranscriptState(), opts.finalize ?? false);
5072
+ const main = await importTranscript(path, url, 0, newState(), opts.finalize ?? false, parser);
5063
5073
  files++;
5064
5074
  ops += main.ops;
5065
5075
  emitted = emitted && main.emitted;
5066
5076
  if (!main.emitted)
5067
5077
  return { files, ops, emitted: false };
5068
- const subDir = join3(dirname(path), basename(path, ".jsonl"), "subagents");
5069
- let subs = [];
5070
- try {
5071
- subs = readdirSync(subDir).filter((f) => f.endsWith(".jsonl"));
5072
- } catch {}
5073
- for (const f of subs) {
5074
- const r = await importTranscript(join3(subDir, f), url, 0, newTranscriptState());
5075
- files++;
5076
- ops += r.ops;
5077
- emitted = emitted && r.emitted;
5078
+ if (opts.parser === undefined) {
5079
+ const subDir = join3(dirname(path), basename(path, ".jsonl"), "subagents");
5080
+ let subs = [];
5081
+ try {
5082
+ subs = readdirSync(subDir).filter((f) => f.endsWith(".jsonl"));
5083
+ } catch {}
5084
+ for (const f of subs) {
5085
+ const r = await importTranscript(join3(subDir, f), url, 0, newTranscriptState());
5086
+ files++;
5087
+ ops += r.ops;
5088
+ emitted = emitted && r.emitted;
5089
+ }
5078
5090
  }
5079
5091
  return { files, ops, emitted };
5080
5092
  }
@@ -5099,7 +5111,7 @@ function reviveState(json2) {
5099
5111
  agentToolUseIds: Array.isArray(j.agentToolUseIds) ? new Set(j.agentToolUseIds.filter((x) => typeof x === "string")) : fresh.agentToolUseIds
5100
5112
  };
5101
5113
  }
5102
- function loadOffsets(path) {
5114
+ function loadOffsets(path, revive) {
5103
5115
  const out = new Map;
5104
5116
  let raw;
5105
5117
  try {
@@ -5115,7 +5127,7 @@ function loadOffsets(path) {
5115
5127
  if (!existsSync2(file))
5116
5128
  continue;
5117
5129
  const offset = Number.isInteger(entry?.offset) && entry.offset >= 0 ? entry.offset : 0;
5118
- out.set(file, { offset, state: reviveState(entry?.state) });
5130
+ out.set(file, { offset, state: revive(entry?.state) });
5119
5131
  }
5120
5132
  } catch (e) {
5121
5133
  console.error("0rrery: tailer offsets snapshot corrupt, starting fresh", e);
@@ -5127,7 +5139,11 @@ function saveOffsets(path, files) {
5127
5139
  try {
5128
5140
  const filesJson = {};
5129
5141
  for (const [file, { offset, state }] of files) {
5130
- filesJson[file] = { offset, state: { ...state, agentToolUseIds: [...state.agentToolUseIds] } };
5142
+ const serialized = { ...state };
5143
+ for (const k of Object.keys(serialized))
5144
+ if (serialized[k] instanceof Set)
5145
+ serialized[k] = [...serialized[k]];
5146
+ filesJson[file] = { offset, state: serialized };
5131
5147
  }
5132
5148
  writeFileSync(path + ".tmp", JSON.stringify({ version: VERSION, files: filesJson }));
5133
5149
  renameSync(path + ".tmp", path);
@@ -5138,7 +5154,7 @@ function saveOffsets(path, files) {
5138
5154
 
5139
5155
  // packages/claude-code/src/tailer.ts
5140
5156
  function startTailer(projectsDir, url, pollMs = 2000, offsetsPath) {
5141
- const files = offsetsPath ? loadOffsets(offsetsPath) : new Map;
5157
+ const files = offsetsPath ? loadOffsets(offsetsPath, reviveState) : new Map;
5142
5158
  let stopped = false;
5143
5159
  let dirty = false;
5144
5160
  async function scanFile(path) {
@@ -5209,14 +5225,246 @@ function startTailer(projectsDir, url, pollMs = 2000, offsetsPath) {
5209
5225
  stopped = true;
5210
5226
  } };
5211
5227
  }
5228
+ // packages/codex/src/codex.ts
5229
+ function newCodexState() {
5230
+ return { sessionId: null, project: null, model: null, openTurnId: null, turnIn: 0, turnOut: 0, threadId: null };
5231
+ }
5232
+ function reviveCodexState(json2) {
5233
+ const fresh = newCodexState();
5234
+ if (typeof json2 !== "object" || json2 === null)
5235
+ return fresh;
5236
+ const j = json2;
5237
+ const str = (v) => typeof v === "string" ? v : null;
5238
+ const num = (v) => typeof v === "number" && Number.isFinite(v) ? v : 0;
5239
+ if (j.sessionId !== null && typeof j.sessionId !== "string")
5240
+ return fresh;
5241
+ return {
5242
+ sessionId: str(j.sessionId),
5243
+ project: str(j.project),
5244
+ model: str(j.model),
5245
+ openTurnId: str(j.openTurnId),
5246
+ turnIn: num(j.turnIn),
5247
+ turnOut: num(j.turnOut),
5248
+ threadId: str(j.threadId)
5249
+ };
5250
+ }
5251
+ function idSalt(state) {
5252
+ return state.threadId !== null && state.threadId !== state.sessionId ? `:${state.threadId}` : "";
5253
+ }
5254
+ var preview = (s) => s.slice(0, 200);
5255
+ function messageText(content) {
5256
+ if (!Array.isArray(content))
5257
+ return "";
5258
+ return content.map((c) => c?.text ?? "").join("").trim();
5259
+ }
5260
+ function parseCodexLine(raw, state) {
5261
+ let line;
5262
+ try {
5263
+ line = JSON.parse(raw);
5264
+ } catch {
5265
+ return [];
5266
+ }
5267
+ if (typeof line !== "object" || line === null)
5268
+ return [];
5269
+ const parsed = Date.parse(line.timestamp);
5270
+ const ts2 = Number.isNaN(parsed) ? Date.now() : parsed;
5271
+ const p = line.payload;
5272
+ if (typeof p !== "object" || p === null)
5273
+ return [];
5274
+ if (line.type === "session_meta") {
5275
+ state.sessionId = typeof p.session_id === "string" ? p.session_id : typeof p.id === "string" ? p.id : null;
5276
+ if (!state.sessionId)
5277
+ return [];
5278
+ state.threadId = typeof p.id === "string" ? p.id : state.sessionId;
5279
+ state.project = typeof p.cwd === "string" ? p.cwd.split("/").pop() ?? null : null;
5280
+ state.model = typeof p.model_provider === "string" ? p.model_provider : null;
5281
+ return [{
5282
+ op: "session.start",
5283
+ sessionId: state.sessionId,
5284
+ source: "codex",
5285
+ ts: ts2,
5286
+ project: state.project ?? undefined,
5287
+ cwd: typeof p.cwd === "string" ? p.cwd : undefined,
5288
+ meta: { model_provider: p.model_provider, cli_version: p.cli_version, originator: p.originator }
5289
+ }];
5290
+ }
5291
+ const sid = state.sessionId;
5292
+ if (!sid)
5293
+ return [];
5294
+ const ops = [];
5295
+ const closeTurn = (endTs) => {
5296
+ if (!state.openTurnId)
5297
+ return;
5298
+ ops.push({ op: "span.end", id: `llm:${state.openTurnId}`, ts: endTs, status: "ok" });
5299
+ state.openTurnId = null;
5300
+ state.turnIn = 0;
5301
+ state.turnOut = 0;
5302
+ };
5303
+ if (line.type === "turn_context") {
5304
+ closeTurn(ts2);
5305
+ const turnId = typeof p.turn_id === "string" ? p.turn_id : null;
5306
+ if (typeof p.model === "string")
5307
+ state.model = p.model;
5308
+ if (turnId) {
5309
+ state.openTurnId = turnId;
5310
+ ops.push({
5311
+ op: "span.start",
5312
+ id: `llm:${turnId}`,
5313
+ sessionId: sid,
5314
+ parentId: null,
5315
+ kind: "llm",
5316
+ name: state.model ?? "(model)",
5317
+ ts: ts2,
5318
+ attrs: {}
5319
+ });
5320
+ ops.push({ op: "event", id: `evt:turn:${turnId}`, sessionId: sid, type: "turn.context", ts: ts2, attrs: {} });
5321
+ }
5322
+ return ops;
5323
+ }
5324
+ if (line.type === "event_msg") {
5325
+ if (p.type === "task_complete") {
5326
+ closeTurn(ts2);
5327
+ ops.push({ op: "event", id: `evt:stop:${sid}${idSalt(state)}:${ts2}`, sessionId: sid, type: "turn.stop", ts: ts2, attrs: {} });
5328
+ return ops;
5329
+ }
5330
+ if (p.type === "token_count" && p.info && typeof p.info === "object" && state.openTurnId) {
5331
+ const u = p.info.last_token_usage;
5332
+ if (u && typeof u === "object") {
5333
+ state.turnIn += u.input_tokens ?? 0;
5334
+ state.turnOut += u.output_tokens ?? 0;
5335
+ ops.push({
5336
+ op: "span.start",
5337
+ id: `llm:${state.openTurnId}`,
5338
+ sessionId: sid,
5339
+ parentId: null,
5340
+ kind: "llm",
5341
+ name: state.model ?? "(model)",
5342
+ ts: ts2,
5343
+ attrs: { input_tokens: state.turnIn, output_tokens: state.turnOut }
5344
+ });
5345
+ }
5346
+ return ops;
5347
+ }
5348
+ return [];
5349
+ }
5350
+ if (line.type === "response_item") {
5351
+ if (p.type === "function_call" && typeof p.call_id === "string") {
5352
+ let input = p.arguments;
5353
+ try {
5354
+ input = JSON.parse(p.arguments);
5355
+ } catch {}
5356
+ const name = typeof p.name === "string" ? p.name : "(tool)";
5357
+ return [{
5358
+ op: "span.start",
5359
+ id: `tool:${p.call_id}`,
5360
+ sessionId: sid,
5361
+ parentId: state.openTurnId ? `llm:${state.openTurnId}` : null,
5362
+ kind: isMcpTool(name) ? "mcp" : "tool",
5363
+ name,
5364
+ ts: ts2,
5365
+ attrs: { input }
5366
+ }];
5367
+ }
5368
+ if (p.type === "function_call_output" && typeof p.call_id === "string") {
5369
+ const out = typeof p.output === "string" ? p.output : "";
5370
+ const status = /exited with code [1-9]/.test(out) ? "error" : "ok";
5371
+ return [{ op: "span.end", id: `tool:${p.call_id}`, ts: ts2, status, attrs: {} }];
5372
+ }
5373
+ if (p.type === "web_search_call" && typeof p.id === "string") {
5374
+ return [
5375
+ {
5376
+ op: "span.start",
5377
+ id: `tool:${p.id}`,
5378
+ sessionId: sid,
5379
+ parentId: state.openTurnId ? `llm:${state.openTurnId}` : null,
5380
+ kind: "tool",
5381
+ name: "web_search",
5382
+ ts: ts2,
5383
+ attrs: { input: { query: p.action?.query ?? "" } }
5384
+ },
5385
+ { op: "span.end", id: `tool:${p.id}`, ts: ts2, status: "ok", attrs: {} }
5386
+ ];
5387
+ }
5388
+ if (p.type === "message" && (p.role === "user" || p.role === "assistant")) {
5389
+ const text = messageText(p.content);
5390
+ if (!text)
5391
+ return [];
5392
+ return [{
5393
+ op: "event",
5394
+ id: `evt:msg:${sid}${idSalt(state)}:${ts2}:${p.role}`,
5395
+ sessionId: sid,
5396
+ type: p.role === "user" ? "message.user" : "message.assistant",
5397
+ ts: ts2,
5398
+ attrs: { preview: preview(text) }
5399
+ }];
5400
+ }
5401
+ return [];
5402
+ }
5403
+ return [];
5404
+ }
5405
+ var codexParser = {
5406
+ parse: parseCodexLine,
5407
+ finalize: (state, maxTs) => state.openTurnId ? [{ op: "span.end", id: `llm:${state.openTurnId}`, ts: maxTs, status: "ok" }] : []
5408
+ };
5409
+ // packages/codex/src/tailer.ts
5410
+ import { readdirSync as readdirSync3, statSync as statSync2 } from "fs";
5411
+ import { join as join5 } from "path";
5412
+ function startCodexTailer(rootDir, url, pollMs = 2000, offsetsPath) {
5413
+ const files = offsetsPath ? loadOffsets(offsetsPath, reviveCodexState) : new Map;
5414
+ let stopped = false;
5415
+ const pass = async () => {
5416
+ let dirty = false;
5417
+ let entries = [];
5418
+ try {
5419
+ entries = readdirSync3(rootDir, { recursive: true }).filter((e) => e.endsWith(".jsonl"));
5420
+ } catch {
5421
+ return;
5422
+ }
5423
+ for (const rel of entries) {
5424
+ const path = join5(rootDir, String(rel));
5425
+ try {
5426
+ let fs = files.get(path);
5427
+ if (!fs) {
5428
+ fs = { offset: 0, state: newCodexState() };
5429
+ files.set(path, fs);
5430
+ }
5431
+ const size = statSync2(path).size;
5432
+ if (size < fs.offset) {
5433
+ fs.offset = 0;
5434
+ fs.state = newCodexState();
5435
+ dirty = true;
5436
+ }
5437
+ if (size > fs.offset) {
5438
+ const r = await importTranscript(path, url, fs.offset, fs.state, false, codexParser);
5439
+ if (r.bytesRead !== fs.offset) {
5440
+ fs.offset = r.bytesRead;
5441
+ dirty = true;
5442
+ }
5443
+ }
5444
+ } catch {}
5445
+ }
5446
+ if (dirty && offsetsPath)
5447
+ saveOffsets(offsetsPath, files);
5448
+ };
5449
+ const loop = async () => {
5450
+ while (!stopped) {
5451
+ await pass();
5452
+ await Bun.sleep(pollMs);
5453
+ }
5454
+ };
5455
+ loop();
5456
+ return { stop() {
5457
+ stopped = true;
5458
+ } };
5459
+ }
5212
5460
  // packages/cli/src/install.ts
5213
5461
  import { existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
5214
- import { join as join5 } from "path";
5462
+ import { join as join6 } from "path";
5215
5463
  var HOOK_EVENTS = ["SessionStart", "SessionEnd", "PreToolUse", "PostToolUse", "Notification", "Stop", "SubagentStop", "PermissionRequest", "PermissionDenied"];
5216
5464
  var NEEDS_MATCHER = new Set(["PreToolUse", "PostToolUse"]);
5217
5465
  function installHooks(claudeDir, hookCommand) {
5218
5466
  mkdirSync2(claudeDir, { recursive: true });
5219
- const settingsPath = join5(claudeDir, "settings.json");
5467
+ const settingsPath = join6(claudeDir, "settings.json");
5220
5468
  let settings = {};
5221
5469
  if (existsSync3(settingsPath)) {
5222
5470
  try {
@@ -5248,34 +5496,58 @@ function installHooks(claudeDir, hookCommand) {
5248
5496
  }
5249
5497
 
5250
5498
  // packages/cli/src/sweep.ts
5251
- async function importAll(projectsDir, url) {
5252
- const files = Array.from(new Bun.Glob("*/*.jsonl").scanSync({ cwd: projectsDir, absolute: true })).sort();
5499
+ import { existsSync as existsSync4 } from "fs";
5500
+ async function importOne(path, url, opts, label) {
5501
+ try {
5502
+ const r = await importSession(path, url, opts);
5503
+ if (r.emitted) {
5504
+ console.log(` ${label}: ${r.ops} ops`);
5505
+ return "ok";
5506
+ }
5507
+ console.error(` ${label}: server unreachable at ${url} \u2014 aborting sweep`);
5508
+ return "unreachable";
5509
+ } catch (err) {
5510
+ console.error(` ${label}: ${err instanceof Error ? err.message : String(err)}`);
5511
+ return "failed";
5512
+ }
5513
+ }
5514
+ async function importAll(projectsDir, url, codexRoot) {
5515
+ const claudeFiles = existsSync4(projectsDir) ? Array.from(new Bun.Glob("*/*.jsonl").scanSync({ cwd: projectsDir, absolute: true })).sort() : [];
5253
5516
  let ok = 0;
5254
5517
  let failed = 0;
5255
- for (const f of files) {
5518
+ let aborted = false;
5519
+ for (const f of claudeFiles) {
5256
5520
  const name = f.split("/").slice(-2).join("/");
5257
- try {
5258
- const r = await importSession(f, url, { finalize: true });
5259
- if (r.emitted) {
5260
- ok++;
5261
- console.log(` ${name}: ${r.ops} ops`);
5262
- } else {
5263
- failed++;
5264
- console.error(` ${name}: server unreachable at ${url} \u2014 aborting sweep`);
5521
+ const result = await importOne(f, url, { finalize: true }, name);
5522
+ if (result === "ok") {
5523
+ ok++;
5524
+ } else {
5525
+ failed++;
5526
+ if (result === "unreachable") {
5527
+ aborted = true;
5265
5528
  break;
5266
5529
  }
5267
- } catch (err) {
5530
+ }
5531
+ }
5532
+ const codexFiles = !aborted && codexRoot && existsSync4(codexRoot) ? Array.from(new Bun.Glob("**/*.jsonl").scanSync({ cwd: codexRoot, absolute: true })).sort() : [];
5533
+ for (const f of codexFiles) {
5534
+ const name = f.split("/").slice(-2).join("/");
5535
+ const result = await importOne(f, url, { finalize: true, parser: codexParser, newState: newCodexState }, name);
5536
+ if (result === "ok") {
5537
+ ok++;
5538
+ } else {
5268
5539
  failed++;
5269
- console.error(` ${name}: ${err instanceof Error ? err.message : String(err)}`);
5540
+ if (result === "unreachable")
5541
+ break;
5270
5542
  }
5271
5543
  }
5272
- return { ok, failed, total: files.length };
5544
+ return { ok, failed, total: claudeFiles.length + codexFiles.length };
5273
5545
  }
5274
5546
 
5275
5547
  // packages/cli/src/service.ts
5276
- import { existsSync as existsSync4, mkdirSync as mkdirSync3, realpathSync, rmSync, writeFileSync as writeFileSync3 } from "fs";
5548
+ import { existsSync as existsSync5, mkdirSync as mkdirSync3, realpathSync, rmSync, writeFileSync as writeFileSync3 } from "fs";
5277
5549
  import { homedir as homedir2 } from "os";
5278
- import { dirname as dirname2, join as join6 } from "path";
5550
+ import { dirname as dirname2, join as join7 } from "path";
5279
5551
  function systemdUnit(execStart) {
5280
5552
  return `[Unit]
5281
5553
  Description=0rrery - trace-first observability for AI agent workflows
@@ -5309,9 +5581,9 @@ ${items}
5309
5581
  }
5310
5582
  function servicePath(platform = process.platform) {
5311
5583
  if (platform === "linux")
5312
- return join6(homedir2(), ".config/systemd/user/0rrery.service");
5584
+ return join7(homedir2(), ".config/systemd/user/0rrery.service");
5313
5585
  if (platform === "darwin")
5314
- return join6(homedir2(), "Library/LaunchAgents/com.0pon.0rrery.plist");
5586
+ return join7(homedir2(), "Library/LaunchAgents/com.0pon.0rrery.plist");
5315
5587
  return null;
5316
5588
  }
5317
5589
  function resolveBin() {
@@ -5331,7 +5603,7 @@ function runService(sub, platform = process.platform, exec = run, file = service
5331
5603
  const linux = platform === "linux";
5332
5604
  if (sub === "install") {
5333
5605
  const bin = resolveBin();
5334
- if (!linux && existsSync4(file))
5606
+ if (!linux && existsSync5(file))
5335
5607
  exec(["launchctl", "unload", "-w", file]);
5336
5608
  mkdirSync3(dirname2(file), { recursive: true });
5337
5609
  writeFileSync3(file, linux ? systemdUnit([...bin, "serve"].join(" ")) : launchdPlist([...bin, "serve"]));
@@ -5340,7 +5612,7 @@ function runService(sub, platform = process.platform, exec = run, file = service
5340
5612
  return ok;
5341
5613
  }
5342
5614
  if (sub === "uninstall") {
5343
- const existed = existsSync4(file);
5615
+ const existed = existsSync5(file);
5344
5616
  const ok = linux ? exec(["systemctl", "--user", "disable", "--now", "0rrery"]) : exec(["launchctl", "unload", "-w", file]);
5345
5617
  if (!existed) {
5346
5618
  console.log("no service installed");
@@ -5362,14 +5634,14 @@ function runService(sub, platform = process.platform, exec = run, file = service
5362
5634
  }
5363
5635
 
5364
5636
  // packages/cli/src/skill.ts
5365
- import { cpSync, existsSync as existsSync5, mkdirSync as mkdirSync4 } from "fs";
5366
- import { join as join7 } from "path";
5637
+ import { cpSync, existsSync as existsSync6, mkdirSync as mkdirSync4 } from "fs";
5638
+ import { join as join8 } from "path";
5367
5639
  function skillSourceDir() {
5368
- const candidates = [join7(import.meta.dir, "skill"), join7(import.meta.dir, "../skill")];
5369
- return candidates.find(existsSync5) ?? null;
5640
+ const candidates = [join8(import.meta.dir, "skill"), join8(import.meta.dir, "../skill")];
5641
+ return candidates.find(existsSync6) ?? null;
5370
5642
  }
5371
5643
  function installSkill(claudeDir, srcDir) {
5372
- const dest = join7(claudeDir, "skills", "0rrery");
5644
+ const dest = join8(claudeDir, "skills", "0rrery");
5373
5645
  mkdirSync4(dest, { recursive: true });
5374
5646
  cpSync(srcDir, dest, { recursive: true });
5375
5647
  return dest;
@@ -5378,9 +5650,20 @@ function installSkill(claudeDir, srcDir) {
5378
5650
  // packages/cli/src/index.ts
5379
5651
  var [cmd, arg] = process.argv.slice(2);
5380
5652
  var url = process.env.ORRERY_URL ?? "http://localhost:7317";
5381
- var claudeDir = () => process.env.ORRERY_CLAUDE_DIR ?? join8(homedir3(), ".claude");
5653
+ var claudeDir = () => process.env.ORRERY_CLAUDE_DIR ?? join9(homedir3(), ".claude");
5654
+ var codexDir = () => process.env.ORRERY_CODEX_DIR ?? join9(homedir3(), ".codex", "sessions");
5655
+ function sniffHead(path) {
5656
+ const fd = openSync2(path, "r");
5657
+ try {
5658
+ const buf = Buffer.alloc(400);
5659
+ const n = readSync2(fd, buf, 0, buf.length, 0);
5660
+ return buf.subarray(0, n).toString("utf8");
5661
+ } finally {
5662
+ closeSync2(fd);
5663
+ }
5664
+ }
5382
5665
  function runInstall() {
5383
- if (!existsSync6(claudeDir())) {
5666
+ if (!existsSync7(claudeDir())) {
5384
5667
  console.warn(`${claudeDir()} not found \u2014 Claude Code not present, skipping hooks`);
5385
5668
  return false;
5386
5669
  }
@@ -5397,12 +5680,16 @@ switch (cmd) {
5397
5680
  case "serve": {
5398
5681
  const config = loadConfig();
5399
5682
  const srv = startServer(config);
5400
- const projectsDir = join8(claudeDir(), "projects");
5401
- const tailer = startTailer(projectsDir, srv.url, 2000, join8(config.dataDir, "tailer-offsets.json"));
5683
+ const projectsDir = join9(claudeDir(), "projects");
5684
+ const tailer = startTailer(projectsDir, srv.url, 2000, join9(config.dataDir, "tailer-offsets.json"));
5685
+ const cx = existsSync7(codexDir()) ? startCodexTailer(codexDir(), srv.url, 2000, join9(config.dataDir, "codex-offsets.json")) : null;
5402
5686
  console.log(`0rrery serving on ${srv.url} (db: ${config.dbPath})`);
5403
5687
  console.log(`tailing ${projectsDir}`);
5688
+ if (cx)
5689
+ console.log(`tailing ${codexDir()} (codex)`);
5404
5690
  process.on("SIGINT", () => {
5405
5691
  tailer.stop();
5692
+ cx?.stop();
5406
5693
  srv.stop();
5407
5694
  process.exit(0);
5408
5695
  });
@@ -5431,15 +5718,18 @@ switch (cmd) {
5431
5718
  process.exit(1);
5432
5719
  }
5433
5720
  if (arg === "--all") {
5434
- const projectsDir = join8(claudeDir(), "projects");
5435
- const r2 = await importAll(projectsDir, url);
5721
+ const projectsDir = join9(claudeDir(), "projects");
5722
+ const cxRoot = existsSync7(codexDir()) ? codexDir() : undefined;
5723
+ const r2 = await importAll(projectsDir, url, cxRoot);
5436
5724
  console.log(r2.total ? `imported ${r2.ok}/${r2.total} transcript(s)` : `no transcripts found under ${projectsDir}`);
5437
5725
  process.exit(r2.failed && !r2.ok ? 1 : 0);
5438
5726
  break;
5439
5727
  }
5440
5728
  let r;
5441
5729
  try {
5442
- r = await importSession(resolve2(arg), url, { finalize: true });
5730
+ const path = resolve2(arg);
5731
+ const isCodex = /"type"\s*:\s*"session_meta"/.test(sniffHead(path));
5732
+ r = isCodex ? await importSession(path, url, { finalize: true, parser: codexParser, newState: newCodexState }) : await importSession(path, url, { finalize: true });
5443
5733
  } catch (err) {
5444
5734
  console.error(`0rrery import: cannot read ${arg}: ${err instanceof Error ? err.message : String(err)}`);
5445
5735
  process.exit(1);
@@ -5472,7 +5762,7 @@ switch (cmd) {
5472
5762
  if (!flags.has("--no-skill")) {
5473
5763
  console.log("\u203A skill");
5474
5764
  const src = skillSourceDir();
5475
- if (!existsSync6(claudeDir()))
5765
+ if (!existsSync7(claudeDir()))
5476
5766
  console.log(` ${claudeDir()} not found \u2014 skipping skill`);
5477
5767
  else if (!src)
5478
5768
  console.log(" skill assets not found \u2014 skipping");
@@ -5486,14 +5776,15 @@ switch (cmd) {
5486
5776
  }
5487
5777
  if (!flags.has("--no-import")) {
5488
5778
  console.log("\u203A importing history");
5489
- const projectsDir = join8(claudeDir(), "projects");
5490
- if (existsSync6(projectsDir)) {
5779
+ const projectsDir = join9(claudeDir(), "projects");
5780
+ const cxRoot = existsSync7(codexDir()) ? codexDir() : undefined;
5781
+ if (existsSync7(projectsDir) || cxRoot) {
5491
5782
  for (let i = 0;i < 30; i++) {
5492
5783
  if (await fetch(`${url}/api/sessions`).then((x) => x.ok).catch(() => false))
5493
5784
  break;
5494
5785
  await Bun.sleep(200);
5495
5786
  }
5496
- const r = await importAll(projectsDir, url);
5787
+ const r = await importAll(projectsDir, url, cxRoot);
5497
5788
  console.log(r.total ? ` imported ${r.ok}/${r.total} transcript(s)` : " no transcripts found");
5498
5789
  } else {
5499
5790
  console.log(` ${projectsDir} not found \u2014 nothing to import`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "0rrery",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
4
4
  "description": "Trace-first, local-first observability for AI agent workflows",
5
5
  "license": "MIT",
6
6
  "repository": {
package/skill/SKILL.md CHANGED
@@ -5,7 +5,7 @@ description: Use when the user asks about past agent sessions, AI spend/cost, to
5
5
 
6
6
  # 0rrery — query your own trace history
7
7
 
8
- 0rrery records every Claude Code session on this machine (tools, subagents, LLM calls, permissions) in a local SQLite DB behind a localhost HTTP API. Use it to answer questions about past sessions, spend, failures, and agent activity.
8
+ 0rrery records every Claude Code and Codex CLI session on this machine (tools, subagents, LLM calls, permissions) in a local SQLite DB behind a localhost HTTP API. Use it to answer questions about past sessions, spend, failures, and agent activity.
9
9
 
10
10
  ## Before anything: is it running?
11
11