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 +7 -5
- package/index.js +353 -62
- package/package.json +1 -1
- package/skill/SKILL.md +1 -1
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
|
|
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
|
-
#!/
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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) =>
|
|
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,
|
|
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
|
-
|
|
5069
|
-
|
|
5070
|
-
|
|
5071
|
-
|
|
5072
|
-
|
|
5073
|
-
|
|
5074
|
-
|
|
5075
|
-
|
|
5076
|
-
|
|
5077
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
|
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 =
|
|
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
|
-
|
|
5252
|
-
|
|
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
|
-
|
|
5518
|
+
let aborted = false;
|
|
5519
|
+
for (const f of claudeFiles) {
|
|
5256
5520
|
const name = f.split("/").slice(-2).join("/");
|
|
5257
|
-
|
|
5258
|
-
|
|
5259
|
-
|
|
5260
|
-
|
|
5261
|
-
|
|
5262
|
-
|
|
5263
|
-
|
|
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
|
-
}
|
|
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
|
-
|
|
5540
|
+
if (result === "unreachable")
|
|
5541
|
+
break;
|
|
5270
5542
|
}
|
|
5271
5543
|
}
|
|
5272
|
-
return { ok, failed, total:
|
|
5544
|
+
return { ok, failed, total: claudeFiles.length + codexFiles.length };
|
|
5273
5545
|
}
|
|
5274
5546
|
|
|
5275
5547
|
// packages/cli/src/service.ts
|
|
5276
|
-
import { existsSync as
|
|
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
|
|
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
|
|
5584
|
+
return join7(homedir2(), ".config/systemd/user/0rrery.service");
|
|
5313
5585
|
if (platform === "darwin")
|
|
5314
|
-
return
|
|
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 &&
|
|
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 =
|
|
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
|
|
5366
|
-
import { join as
|
|
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 = [
|
|
5369
|
-
return candidates.find(
|
|
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 =
|
|
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 ??
|
|
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 (!
|
|
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 =
|
|
5401
|
-
const tailer = startTailer(projectsDir, srv.url, 2000,
|
|
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 =
|
|
5435
|
-
const
|
|
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
|
-
|
|
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 (!
|
|
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 =
|
|
5490
|
-
|
|
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
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
|
|