0rrery 0.1.0 → 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 +11 -5
- package/index.js +383 -75
- package/package.json +1 -1
- package/public/assets/index-sGLSeJmJ.js +40 -0
- package/public/assets/index-yRmv5nEJ.css +1 -0
- package/public/index.html +2 -2
- package/skill/SKILL.md +2 -2
- package/public/assets/index-5F_iDwQK.css +0 -1
- package/public/assets/index-gNN2_ieI.js +0 -40
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(),
|
|
@@ -4291,7 +4293,15 @@ function spendSeries(db, f) {
|
|
|
4291
4293
|
FROM spans sp JOIN sessions se ON se.id = sp.session_id
|
|
4292
4294
|
WHERE ${where}
|
|
4293
4295
|
GROUP BY day, model, project ORDER BY day, model`).all(...params);
|
|
4294
|
-
return rows.map((r) => ({
|
|
4296
|
+
return rows.map((r) => ({
|
|
4297
|
+
day: r.day,
|
|
4298
|
+
model: r.model,
|
|
4299
|
+
project: r.project,
|
|
4300
|
+
tokens_in: r.tokens_in,
|
|
4301
|
+
tokens_out: r.tokens_out,
|
|
4302
|
+
calls: r.calls,
|
|
4303
|
+
est_cost: estCost(r.model, r.tokens_in, r.tokens_out)
|
|
4304
|
+
}));
|
|
4295
4305
|
}
|
|
4296
4306
|
function toolHealth(db, f) {
|
|
4297
4307
|
const { where, params } = spanWhere(f, "sp.kind IN ('tool', 'mcp')");
|
|
@@ -4350,10 +4360,11 @@ function searchSessions(db, f, opts) {
|
|
|
4350
4360
|
const conds = ["1 = 1"];
|
|
4351
4361
|
const params = [];
|
|
4352
4362
|
if (f.q) {
|
|
4353
|
-
|
|
4363
|
+
const q = f.q.replaceAll(/[%_\\]/g, (m) => "\\" + m);
|
|
4364
|
+
conds.push(`(project LIKE '%' || ? || '%' ESCAPE '\\' OR EXISTS(
|
|
4354
4365
|
SELECT 1 FROM events ev WHERE ev.session_id = sessions.id
|
|
4355
|
-
AND ev.type = 'message.user' AND json_extract(ev.attrs, '$.preview') LIKE '%' || ? || '%'))`);
|
|
4356
|
-
params.push(
|
|
4366
|
+
AND ev.type = 'message.user' AND json_extract(ev.attrs, '$.preview') LIKE '%' || ? || '%' ESCAPE '\\'))`);
|
|
4367
|
+
params.push(q, q);
|
|
4357
4368
|
}
|
|
4358
4369
|
if (f.project) {
|
|
4359
4370
|
conds.push("project = ?");
|
|
@@ -4498,7 +4509,7 @@ function sessionSummary(db, id) {
|
|
|
4498
4509
|
SUM(COALESCE(json_extract(attrs, '$.input_tokens'), 0)) tin,
|
|
4499
4510
|
SUM(COALESCE(json_extract(attrs, '$.output_tokens'), 0)) tout
|
|
4500
4511
|
FROM spans WHERE session_id = ? AND kind = 'llm'
|
|
4501
|
-
GROUP BY name ORDER BY calls DESC, model`).all(id);
|
|
4512
|
+
GROUP BY name ORDER BY calls DESC, model LIMIT 20`).all(id);
|
|
4502
4513
|
const top_tools = db.query(`SELECT name, kind, COUNT(*) calls, SUM(status = 'error') errors
|
|
4503
4514
|
FROM spans WHERE session_id = ? AND kind IN ('tool', 'mcp')
|
|
4504
4515
|
GROUP BY name, kind ORDER BY calls DESC LIMIT 10`).all(id);
|
|
@@ -4625,7 +4636,7 @@ class LiveBus {
|
|
|
4625
4636
|
// packages/server/src/server.ts
|
|
4626
4637
|
var json = (data, status = 200) => new Response(JSON.stringify(data), { status, headers: { "Content-Type": "application/json" } });
|
|
4627
4638
|
var numParam = (raw) => {
|
|
4628
|
-
if (raw === null)
|
|
4639
|
+
if (raw === null || raw === "")
|
|
4629
4640
|
return;
|
|
4630
4641
|
const n = Number(raw);
|
|
4631
4642
|
return Number.isInteger(n) && n >= 0 ? n : undefined;
|
|
@@ -4685,9 +4696,9 @@ function startServer(config) {
|
|
|
4685
4696
|
const bad = (name) => json({ error: `invalid ${name}` }, 400);
|
|
4686
4697
|
const rawFrom = url.searchParams.get("from"), rawTo = url.searchParams.get("to");
|
|
4687
4698
|
const from = numParam(rawFrom), to = numParam(rawTo);
|
|
4688
|
-
if (rawFrom !== null && from === undefined)
|
|
4699
|
+
if (rawFrom !== null && rawFrom !== "" && from === undefined)
|
|
4689
4700
|
return bad("from");
|
|
4690
|
-
if (rawTo !== null && to === undefined)
|
|
4701
|
+
if (rawTo !== null && rawTo !== "" && to === undefined)
|
|
4691
4702
|
return bad("to");
|
|
4692
4703
|
const f = { project: url.searchParams.get("project") ?? undefined, from, to };
|
|
4693
4704
|
switch (insightsMatch[1]) {
|
|
@@ -4708,9 +4719,9 @@ function startServer(config) {
|
|
|
4708
4719
|
if (path === "/api/sessions" && req.method === "GET") {
|
|
4709
4720
|
const rawFrom = url.searchParams.get("from"), rawTo = url.searchParams.get("to");
|
|
4710
4721
|
const from = numParam(rawFrom), to = numParam(rawTo);
|
|
4711
|
-
if (rawFrom !== null && from === undefined)
|
|
4722
|
+
if (rawFrom !== null && rawFrom !== "" && from === undefined)
|
|
4712
4723
|
return json({ error: "invalid from" }, 400);
|
|
4713
|
-
if (rawTo !== null && to === undefined)
|
|
4724
|
+
if (rawTo !== null && rawTo !== "" && to === undefined)
|
|
4714
4725
|
return json({ error: "invalid to" }, 400);
|
|
4715
4726
|
const q = url.searchParams.get("q") ?? undefined;
|
|
4716
4727
|
const project = url.searchParams.get("project") ?? undefined;
|
|
@@ -5008,7 +5019,8 @@ function parseTranscriptLine(raw, state) {
|
|
|
5008
5019
|
// packages/claude-code/src/importer.ts
|
|
5009
5020
|
import { openSync, readSync, fstatSync, closeSync, readdirSync } from "fs";
|
|
5010
5021
|
import { join as join3, dirname, basename } from "path";
|
|
5011
|
-
|
|
5022
|
+
var claudeParser = { parse: parseTranscriptLine };
|
|
5023
|
+
async function importTranscript(path, url, fromByte = 0, state = newTranscriptState(), finalize = false, parser = claudeParser) {
|
|
5012
5024
|
const fd = openSync(path, "r");
|
|
5013
5025
|
let text;
|
|
5014
5026
|
try {
|
|
@@ -5027,19 +5039,24 @@ async function importTranscript(path, url, fromByte = 0, state = newTranscriptSt
|
|
|
5027
5039
|
return { ops: 0, emitted: true, bytesRead: fromByte };
|
|
5028
5040
|
const complete = text.slice(0, lastNewline);
|
|
5029
5041
|
const consumedBytes = Buffer.byteLength(text.slice(0, lastNewline + 1));
|
|
5030
|
-
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]);
|
|
5031
5046
|
const ops = complete.split(`
|
|
5032
|
-
`).filter(Boolean).flatMap((l) =>
|
|
5047
|
+
`).filter(Boolean).flatMap((l) => parser.parse(l, state));
|
|
5033
5048
|
if (ops.length > 0) {
|
|
5034
5049
|
const maxTs = ops.reduce((max, o) => o.ts > max ? o.ts : max, 0);
|
|
5035
|
-
if (state.agentId) {
|
|
5050
|
+
if ("agentId" in state && state.agentId && ops.length) {
|
|
5036
5051
|
ops.push({ op: "span.end", id: `agent:${state.agentId}`, ts: maxTs, status: "ok" });
|
|
5037
5052
|
}
|
|
5038
|
-
if (finalize && !state.agentId) {
|
|
5053
|
+
if (finalize && !(("agentId" in state) && state.agentId)) {
|
|
5039
5054
|
const sessionId = ops.find((o) => ("sessionId" in o))?.sessionId;
|
|
5040
5055
|
if (sessionId)
|
|
5041
5056
|
ops.push({ op: "session.end", sessionId, ts: maxTs });
|
|
5042
5057
|
}
|
|
5058
|
+
if (finalize && parser.finalize)
|
|
5059
|
+
ops.push(...parser.finalize(state, maxTs));
|
|
5043
5060
|
}
|
|
5044
5061
|
const emitted = await emitOps(url, ops, 5000);
|
|
5045
5062
|
if (!emitted) {
|
|
@@ -5049,23 +5066,27 @@ async function importTranscript(path, url, fromByte = 0, state = newTranscriptSt
|
|
|
5049
5066
|
return { ops: ops.length, emitted, bytesRead: fromByte + consumedBytes };
|
|
5050
5067
|
}
|
|
5051
5068
|
async function importSession(path, url, opts = {}) {
|
|
5069
|
+
const newState = opts.newState ?? newTranscriptState;
|
|
5070
|
+
const parser = opts.parser ?? claudeParser;
|
|
5052
5071
|
let files = 0, ops = 0, emitted = true;
|
|
5053
|
-
const main = await importTranscript(path, url, 0,
|
|
5072
|
+
const main = await importTranscript(path, url, 0, newState(), opts.finalize ?? false, parser);
|
|
5054
5073
|
files++;
|
|
5055
5074
|
ops += main.ops;
|
|
5056
5075
|
emitted = emitted && main.emitted;
|
|
5057
5076
|
if (!main.emitted)
|
|
5058
5077
|
return { files, ops, emitted: false };
|
|
5059
|
-
|
|
5060
|
-
|
|
5061
|
-
|
|
5062
|
-
|
|
5063
|
-
|
|
5064
|
-
|
|
5065
|
-
|
|
5066
|
-
|
|
5067
|
-
|
|
5068
|
-
|
|
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
|
+
}
|
|
5069
5090
|
}
|
|
5070
5091
|
return { files, ops, emitted };
|
|
5071
5092
|
}
|
|
@@ -5090,7 +5111,7 @@ function reviveState(json2) {
|
|
|
5090
5111
|
agentToolUseIds: Array.isArray(j.agentToolUseIds) ? new Set(j.agentToolUseIds.filter((x) => typeof x === "string")) : fresh.agentToolUseIds
|
|
5091
5112
|
};
|
|
5092
5113
|
}
|
|
5093
|
-
function loadOffsets(path) {
|
|
5114
|
+
function loadOffsets(path, revive) {
|
|
5094
5115
|
const out = new Map;
|
|
5095
5116
|
let raw;
|
|
5096
5117
|
try {
|
|
@@ -5106,7 +5127,7 @@ function loadOffsets(path) {
|
|
|
5106
5127
|
if (!existsSync2(file))
|
|
5107
5128
|
continue;
|
|
5108
5129
|
const offset = Number.isInteger(entry?.offset) && entry.offset >= 0 ? entry.offset : 0;
|
|
5109
|
-
out.set(file, { offset, state:
|
|
5130
|
+
out.set(file, { offset, state: revive(entry?.state) });
|
|
5110
5131
|
}
|
|
5111
5132
|
} catch (e) {
|
|
5112
5133
|
console.error("0rrery: tailer offsets snapshot corrupt, starting fresh", e);
|
|
@@ -5118,7 +5139,11 @@ function saveOffsets(path, files) {
|
|
|
5118
5139
|
try {
|
|
5119
5140
|
const filesJson = {};
|
|
5120
5141
|
for (const [file, { offset, state }] of files) {
|
|
5121
|
-
|
|
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 };
|
|
5122
5147
|
}
|
|
5123
5148
|
writeFileSync(path + ".tmp", JSON.stringify({ version: VERSION, files: filesJson }));
|
|
5124
5149
|
renameSync(path + ".tmp", path);
|
|
@@ -5129,7 +5154,7 @@ function saveOffsets(path, files) {
|
|
|
5129
5154
|
|
|
5130
5155
|
// packages/claude-code/src/tailer.ts
|
|
5131
5156
|
function startTailer(projectsDir, url, pollMs = 2000, offsetsPath) {
|
|
5132
|
-
const files = offsetsPath ? loadOffsets(offsetsPath) : new Map;
|
|
5157
|
+
const files = offsetsPath ? loadOffsets(offsetsPath, reviveState) : new Map;
|
|
5133
5158
|
let stopped = false;
|
|
5134
5159
|
let dirty = false;
|
|
5135
5160
|
async function scanFile(path) {
|
|
@@ -5200,14 +5225,246 @@ function startTailer(projectsDir, url, pollMs = 2000, offsetsPath) {
|
|
|
5200
5225
|
stopped = true;
|
|
5201
5226
|
} };
|
|
5202
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
|
+
}
|
|
5203
5460
|
// packages/cli/src/install.ts
|
|
5204
5461
|
import { existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
|
|
5205
|
-
import { join as
|
|
5462
|
+
import { join as join6 } from "path";
|
|
5206
5463
|
var HOOK_EVENTS = ["SessionStart", "SessionEnd", "PreToolUse", "PostToolUse", "Notification", "Stop", "SubagentStop", "PermissionRequest", "PermissionDenied"];
|
|
5207
5464
|
var NEEDS_MATCHER = new Set(["PreToolUse", "PostToolUse"]);
|
|
5208
5465
|
function installHooks(claudeDir, hookCommand) {
|
|
5209
5466
|
mkdirSync2(claudeDir, { recursive: true });
|
|
5210
|
-
const settingsPath =
|
|
5467
|
+
const settingsPath = join6(claudeDir, "settings.json");
|
|
5211
5468
|
let settings = {};
|
|
5212
5469
|
if (existsSync3(settingsPath)) {
|
|
5213
5470
|
try {
|
|
@@ -5239,34 +5496,58 @@ function installHooks(claudeDir, hookCommand) {
|
|
|
5239
5496
|
}
|
|
5240
5497
|
|
|
5241
5498
|
// packages/cli/src/sweep.ts
|
|
5242
|
-
|
|
5243
|
-
|
|
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() : [];
|
|
5244
5516
|
let ok = 0;
|
|
5245
5517
|
let failed = 0;
|
|
5246
|
-
|
|
5518
|
+
let aborted = false;
|
|
5519
|
+
for (const f of claudeFiles) {
|
|
5247
5520
|
const name = f.split("/").slice(-2).join("/");
|
|
5248
|
-
|
|
5249
|
-
|
|
5250
|
-
|
|
5251
|
-
|
|
5252
|
-
|
|
5253
|
-
|
|
5254
|
-
|
|
5255
|
-
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;
|
|
5256
5528
|
break;
|
|
5257
5529
|
}
|
|
5258
|
-
}
|
|
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 {
|
|
5259
5539
|
failed++;
|
|
5260
|
-
|
|
5540
|
+
if (result === "unreachable")
|
|
5541
|
+
break;
|
|
5261
5542
|
}
|
|
5262
5543
|
}
|
|
5263
|
-
return { ok, failed, total:
|
|
5544
|
+
return { ok, failed, total: claudeFiles.length + codexFiles.length };
|
|
5264
5545
|
}
|
|
5265
5546
|
|
|
5266
5547
|
// packages/cli/src/service.ts
|
|
5267
|
-
import { existsSync as
|
|
5548
|
+
import { existsSync as existsSync5, mkdirSync as mkdirSync3, realpathSync, rmSync, writeFileSync as writeFileSync3 } from "fs";
|
|
5268
5549
|
import { homedir as homedir2 } from "os";
|
|
5269
|
-
import { dirname as dirname2, join as
|
|
5550
|
+
import { dirname as dirname2, join as join7 } from "path";
|
|
5270
5551
|
function systemdUnit(execStart) {
|
|
5271
5552
|
return `[Unit]
|
|
5272
5553
|
Description=0rrery - trace-first observability for AI agent workflows
|
|
@@ -5300,9 +5581,9 @@ ${items}
|
|
|
5300
5581
|
}
|
|
5301
5582
|
function servicePath(platform = process.platform) {
|
|
5302
5583
|
if (platform === "linux")
|
|
5303
|
-
return
|
|
5584
|
+
return join7(homedir2(), ".config/systemd/user/0rrery.service");
|
|
5304
5585
|
if (platform === "darwin")
|
|
5305
|
-
return
|
|
5586
|
+
return join7(homedir2(), "Library/LaunchAgents/com.0pon.0rrery.plist");
|
|
5306
5587
|
return null;
|
|
5307
5588
|
}
|
|
5308
5589
|
function resolveBin() {
|
|
@@ -5322,7 +5603,7 @@ function runService(sub, platform = process.platform, exec = run, file = service
|
|
|
5322
5603
|
const linux = platform === "linux";
|
|
5323
5604
|
if (sub === "install") {
|
|
5324
5605
|
const bin = resolveBin();
|
|
5325
|
-
if (!linux &&
|
|
5606
|
+
if (!linux && existsSync5(file))
|
|
5326
5607
|
exec(["launchctl", "unload", "-w", file]);
|
|
5327
5608
|
mkdirSync3(dirname2(file), { recursive: true });
|
|
5328
5609
|
writeFileSync3(file, linux ? systemdUnit([...bin, "serve"].join(" ")) : launchdPlist([...bin, "serve"]));
|
|
@@ -5331,11 +5612,12 @@ function runService(sub, platform = process.platform, exec = run, file = service
|
|
|
5331
5612
|
return ok;
|
|
5332
5613
|
}
|
|
5333
5614
|
if (sub === "uninstall") {
|
|
5334
|
-
|
|
5615
|
+
const existed = existsSync5(file);
|
|
5616
|
+
const ok = linux ? exec(["systemctl", "--user", "disable", "--now", "0rrery"]) : exec(["launchctl", "unload", "-w", file]);
|
|
5617
|
+
if (!existed) {
|
|
5335
5618
|
console.log("no service installed");
|
|
5336
5619
|
return true;
|
|
5337
5620
|
}
|
|
5338
|
-
const ok = linux ? exec(["systemctl", "--user", "disable", "--now", "0rrery"]) : exec(["launchctl", "unload", "-w", file]);
|
|
5339
5621
|
rmSync(file, { force: true });
|
|
5340
5622
|
if (linux)
|
|
5341
5623
|
exec(["systemctl", "--user", "daemon-reload"]);
|
|
@@ -5343,21 +5625,23 @@ function runService(sub, platform = process.platform, exec = run, file = service
|
|
|
5343
5625
|
return ok;
|
|
5344
5626
|
}
|
|
5345
5627
|
if (sub === "status") {
|
|
5346
|
-
|
|
5628
|
+
const ok = linux ? exec(["systemctl", "--user", "status", "0rrery", "--no-pager"]) : exec(["launchctl", "list", "com.0pon.0rrery"]);
|
|
5629
|
+
console.log(`dashboard: http://localhost:${process.env.ORRERY_PORT ?? 7317}`);
|
|
5630
|
+
return ok;
|
|
5347
5631
|
}
|
|
5348
5632
|
console.error("usage: 0rrery service <install|uninstall|status>");
|
|
5349
5633
|
return false;
|
|
5350
5634
|
}
|
|
5351
5635
|
|
|
5352
5636
|
// packages/cli/src/skill.ts
|
|
5353
|
-
import { cpSync, existsSync as
|
|
5354
|
-
import { join as
|
|
5637
|
+
import { cpSync, existsSync as existsSync6, mkdirSync as mkdirSync4 } from "fs";
|
|
5638
|
+
import { join as join8 } from "path";
|
|
5355
5639
|
function skillSourceDir() {
|
|
5356
|
-
const candidates = [
|
|
5357
|
-
return candidates.find(
|
|
5640
|
+
const candidates = [join8(import.meta.dir, "skill"), join8(import.meta.dir, "../skill")];
|
|
5641
|
+
return candidates.find(existsSync6) ?? null;
|
|
5358
5642
|
}
|
|
5359
5643
|
function installSkill(claudeDir, srcDir) {
|
|
5360
|
-
const dest =
|
|
5644
|
+
const dest = join8(claudeDir, "skills", "0rrery");
|
|
5361
5645
|
mkdirSync4(dest, { recursive: true });
|
|
5362
5646
|
cpSync(srcDir, dest, { recursive: true });
|
|
5363
5647
|
return dest;
|
|
@@ -5366,9 +5650,20 @@ function installSkill(claudeDir, srcDir) {
|
|
|
5366
5650
|
// packages/cli/src/index.ts
|
|
5367
5651
|
var [cmd, arg] = process.argv.slice(2);
|
|
5368
5652
|
var url = process.env.ORRERY_URL ?? "http://localhost:7317";
|
|
5369
|
-
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
|
+
}
|
|
5370
5665
|
function runInstall() {
|
|
5371
|
-
if (!
|
|
5666
|
+
if (!existsSync7(claudeDir())) {
|
|
5372
5667
|
console.warn(`${claudeDir()} not found \u2014 Claude Code not present, skipping hooks`);
|
|
5373
5668
|
return false;
|
|
5374
5669
|
}
|
|
@@ -5385,12 +5680,16 @@ switch (cmd) {
|
|
|
5385
5680
|
case "serve": {
|
|
5386
5681
|
const config = loadConfig();
|
|
5387
5682
|
const srv = startServer(config);
|
|
5388
|
-
const projectsDir =
|
|
5389
|
-
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;
|
|
5390
5686
|
console.log(`0rrery serving on ${srv.url} (db: ${config.dbPath})`);
|
|
5391
5687
|
console.log(`tailing ${projectsDir}`);
|
|
5688
|
+
if (cx)
|
|
5689
|
+
console.log(`tailing ${codexDir()} (codex)`);
|
|
5392
5690
|
process.on("SIGINT", () => {
|
|
5393
5691
|
tailer.stop();
|
|
5692
|
+
cx?.stop();
|
|
5394
5693
|
srv.stop();
|
|
5395
5694
|
process.exit(0);
|
|
5396
5695
|
});
|
|
@@ -5419,15 +5718,18 @@ switch (cmd) {
|
|
|
5419
5718
|
process.exit(1);
|
|
5420
5719
|
}
|
|
5421
5720
|
if (arg === "--all") {
|
|
5422
|
-
const projectsDir =
|
|
5423
|
-
const
|
|
5721
|
+
const projectsDir = join9(claudeDir(), "projects");
|
|
5722
|
+
const cxRoot = existsSync7(codexDir()) ? codexDir() : undefined;
|
|
5723
|
+
const r2 = await importAll(projectsDir, url, cxRoot);
|
|
5424
5724
|
console.log(r2.total ? `imported ${r2.ok}/${r2.total} transcript(s)` : `no transcripts found under ${projectsDir}`);
|
|
5425
5725
|
process.exit(r2.failed && !r2.ok ? 1 : 0);
|
|
5426
5726
|
break;
|
|
5427
5727
|
}
|
|
5428
5728
|
let r;
|
|
5429
5729
|
try {
|
|
5430
|
-
|
|
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 });
|
|
5431
5733
|
} catch (err) {
|
|
5432
5734
|
console.error(`0rrery import: cannot read ${arg}: ${err instanceof Error ? err.message : String(err)}`);
|
|
5433
5735
|
process.exit(1);
|
|
@@ -5441,7 +5743,12 @@ switch (cmd) {
|
|
|
5441
5743
|
break;
|
|
5442
5744
|
}
|
|
5443
5745
|
case "init": {
|
|
5444
|
-
const
|
|
5746
|
+
const KNOWN_INIT_FLAGS = new Set(["--no-hooks", "--no-service", "--no-import", "--no-skill"]);
|
|
5747
|
+
const argv = process.argv.slice(3);
|
|
5748
|
+
const flags = new Set(argv);
|
|
5749
|
+
for (const f of argv)
|
|
5750
|
+
if (!KNOWN_INIT_FLAGS.has(f))
|
|
5751
|
+
console.warn(`unknown flag: ${f}`);
|
|
5445
5752
|
let failed = false;
|
|
5446
5753
|
if (!flags.has("--no-hooks")) {
|
|
5447
5754
|
console.log("\u203A hooks");
|
|
@@ -5455,7 +5762,7 @@ switch (cmd) {
|
|
|
5455
5762
|
if (!flags.has("--no-skill")) {
|
|
5456
5763
|
console.log("\u203A skill");
|
|
5457
5764
|
const src = skillSourceDir();
|
|
5458
|
-
if (!
|
|
5765
|
+
if (!existsSync7(claudeDir()))
|
|
5459
5766
|
console.log(` ${claudeDir()} not found \u2014 skipping skill`);
|
|
5460
5767
|
else if (!src)
|
|
5461
5768
|
console.log(" skill assets not found \u2014 skipping");
|
|
@@ -5469,14 +5776,15 @@ switch (cmd) {
|
|
|
5469
5776
|
}
|
|
5470
5777
|
if (!flags.has("--no-import")) {
|
|
5471
5778
|
console.log("\u203A importing history");
|
|
5472
|
-
const projectsDir =
|
|
5473
|
-
|
|
5779
|
+
const projectsDir = join9(claudeDir(), "projects");
|
|
5780
|
+
const cxRoot = existsSync7(codexDir()) ? codexDir() : undefined;
|
|
5781
|
+
if (existsSync7(projectsDir) || cxRoot) {
|
|
5474
5782
|
for (let i = 0;i < 30; i++) {
|
|
5475
5783
|
if (await fetch(`${url}/api/sessions`).then((x) => x.ok).catch(() => false))
|
|
5476
5784
|
break;
|
|
5477
5785
|
await Bun.sleep(200);
|
|
5478
5786
|
}
|
|
5479
|
-
const r = await importAll(projectsDir, url);
|
|
5787
|
+
const r = await importAll(projectsDir, url, cxRoot);
|
|
5480
5788
|
console.log(r.total ? ` imported ${r.ok}/${r.total} transcript(s)` : " no transcripts found");
|
|
5481
5789
|
} else {
|
|
5482
5790
|
console.log(` ${projectsDir} not found \u2014 nothing to import`);
|