sphica 0.6.33 → 0.6.35
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/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/README.md +1 -1
- package/dist/capture.js +271 -61
- package/dist/cli.js +338 -62
- package/dist/deliver.js +205 -65
- package/dist/mcp-record.js +203 -65
- package/dist/mcp.js +454 -198
- package/package.json +1 -1
- package/skills/review/SKILL.md +2 -2
- package/skills/review/reviewers/precedent.md +18 -4
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://anthropic.com/claude-code/plugin.schema.json",
|
|
3
3
|
"name": "sphica",
|
|
4
|
-
"version": "0.6.
|
|
4
|
+
"version": "0.6.35",
|
|
5
5
|
"description": "Records Claude Code and Codex sessions on your machine and keeps past implementation and decisions, with their sources, for your agent to find.",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "iroha924",
|
package/README.md
CHANGED
|
@@ -144,7 +144,7 @@ The agent searches with Sphica's `search` and opens full records with `read`. `s
|
|
|
144
144
|
- Showing a record does not make the agent follow it.
|
|
145
145
|
- Whether the agent decided something itself is judged from what its reply says, so it can be wrong.
|
|
146
146
|
- An AI's decision is adopted only when the trace runs in an interactive Claude Code session. In headless and SDK runs and in Codex it stays a candidate. The automatic trace also runs only in Claude Code; Codex tells you when sessions wait.
|
|
147
|
-
- When Sphica cannot tell which turn ran one of its record tools
|
|
147
|
+
- When Sphica cannot tell which turn ran one of its record tools, every reply Claude Code's agent writes in that project from then on stays a candidate. If the hook only timed out, it keeps what it saw and the next recording sends it; from then on, replies of other turns can be adopted again (when the hook knew the turn), while the turn that ran the tool stays out. If the hook was turned off, the host sent no tool call id, or the call came before Sphica was installed, nothing can be sent later and the stop stays for good. Records already saved as candidates are not adopted afterwards. `sphica doctor` lists the calls behind a stop and since when. Your decisions are not affected.
|
|
148
148
|
- The automatic trace uses your subscription's usage.
|
|
149
149
|
- The agent never withdraws an AI's decision. When a later record contradicts it, the AI's decision is held back from being shown until you settle it, and so is the later record unless it is your decision.
|
|
150
150
|
- A code location in a record is checked against your working tree when it is read. Finding the code name (a function name, say) the record points to does not mean the decision still holds.
|
package/dist/capture.js
CHANGED
|
@@ -11927,6 +11927,8 @@ var spoolDir = () => path6.join(sphicaHome(), "spool");
|
|
|
11927
11927
|
var stateFile = () => path6.join(sphicaHome(), "capture.json");
|
|
11928
11928
|
var rejectedDir = () => path6.join(spoolDir(), "rejected");
|
|
11929
11929
|
var unregisteredDir = () => path6.join(spoolDir(), "unregistered");
|
|
11930
|
+
var callsDir = () => path6.join(spoolDir(), "calls");
|
|
11931
|
+
var callsRejectedDir = () => path6.join(callsDir(), "rejected");
|
|
11930
11932
|
var HOLD_DAYS = 30;
|
|
11931
11933
|
var HOLD_MAX = 1000;
|
|
11932
11934
|
var baselineDir = () => path6.join(sphicaHome(), "worktree");
|
|
@@ -11975,7 +11977,7 @@ ${z}`,
|
|
|
11975
11977
|
originalBytes: all
|
|
11976
11978
|
};
|
|
11977
11979
|
}
|
|
11978
|
-
function prune(held) {
|
|
11980
|
+
function prune(held, removed = () => {}) {
|
|
11979
11981
|
let files;
|
|
11980
11982
|
try {
|
|
11981
11983
|
files = fs5.readdirSync(held).filter((f) => f.endsWith(".json") && !f.startsWith(".")).sort();
|
|
@@ -11985,16 +11987,21 @@ function prune(held) {
|
|
|
11985
11987
|
const cutoff = Date.now() - HOLD_DAYS * 24 * 60 * 60 * 1000;
|
|
11986
11988
|
const stale = files.filter((f) => Number(f.split("-")[0]) < cutoff);
|
|
11987
11989
|
const over = files.slice(0, Math.max(0, files.length - HOLD_MAX));
|
|
11988
|
-
for (const f of new Set([...stale, ...over]))
|
|
11990
|
+
for (const f of new Set([...stale, ...over])) {
|
|
11989
11991
|
fs5.rmSync(path6.join(held, f), { force: true });
|
|
11992
|
+
removed();
|
|
11993
|
+
}
|
|
11990
11994
|
}
|
|
11991
|
-
function
|
|
11992
|
-
const dir = spoolDir();
|
|
11995
|
+
function put(dir, record) {
|
|
11993
11996
|
fs5.mkdirSync(dir, { recursive: true, mode: 448 });
|
|
11994
11997
|
const name = `${Date.now()}-${process.pid}-${Math.random().toString(36).slice(2, 10)}.json`;
|
|
11995
11998
|
const tmp = path6.join(dir, `.${name}`);
|
|
11996
11999
|
fs5.writeFileSync(tmp, JSON.stringify(record), { mode: 384 });
|
|
11997
12000
|
fs5.renameSync(tmp, path6.join(dir, name));
|
|
12001
|
+
return path6.join(dir, name);
|
|
12002
|
+
}
|
|
12003
|
+
function spool(record) {
|
|
12004
|
+
put(spoolDir(), record);
|
|
11998
12005
|
}
|
|
11999
12006
|
var branchOf = (root) => {
|
|
12000
12007
|
try {
|
|
@@ -12049,9 +12056,11 @@ function captureNotice(file = dbFile()) {
|
|
|
12049
12056
|
if (!fs5.existsSync(file))
|
|
12050
12057
|
return panel("sphica: no database, so conversations are not recorded", [file], "Create it with sphica init");
|
|
12051
12058
|
const s = readState();
|
|
12059
|
+
if (s.unreadable)
|
|
12060
|
+
return panel("sphica: cannot read the recording queue", [spoolDir(), s.unreadable], "Check with sphica doctor");
|
|
12052
12061
|
if (s.stuck)
|
|
12053
12062
|
return panel("sphica: cannot send recordings", [`${s.pending} pending / failed: ${plain2(s.stuck.slice(0, 120))}`], "Check with sphica doctor");
|
|
12054
|
-
if (s.rejected
|
|
12063
|
+
if (s.rejected)
|
|
12055
12064
|
return panel(`sphica: the database rejected ${plural(s.rejected, "record")}`, [rejectedDir(), `Move them back to ${spoolDir()} to resend`], "Fix them first, then check with sphica doctor");
|
|
12056
12065
|
return null;
|
|
12057
12066
|
}
|
|
@@ -12242,37 +12251,116 @@ function onHook(host, input) {
|
|
|
12242
12251
|
}
|
|
12243
12252
|
return { flush: false };
|
|
12244
12253
|
}
|
|
12254
|
+
function readStateFile() {
|
|
12255
|
+
let raw = {};
|
|
12256
|
+
try {
|
|
12257
|
+
const parsed = JSON.parse(fs5.readFileSync(stateFile(), "utf8"));
|
|
12258
|
+
if (parsed && typeof parsed === "object")
|
|
12259
|
+
raw = parsed;
|
|
12260
|
+
} catch {}
|
|
12261
|
+
const p = raw.pruned;
|
|
12262
|
+
return {
|
|
12263
|
+
flushedAt: typeof raw.flushedAt === "string" ? raw.flushedAt : undefined,
|
|
12264
|
+
error: typeof raw.error === "string" ? raw.error || "unknown failure" : null,
|
|
12265
|
+
deferred: typeof raw.deferred === "number" ? raw.deferred : undefined,
|
|
12266
|
+
pruned: p && typeof p.at === "string" && typeof p.count === "number" ? { at: p.at, count: p.count } : undefined
|
|
12267
|
+
};
|
|
12268
|
+
}
|
|
12245
12269
|
function writeState(s) {
|
|
12246
12270
|
try {
|
|
12247
|
-
fs5.writeFileSync(stateFile(), JSON.stringify(s));
|
|
12271
|
+
fs5.writeFileSync(stateFile(), JSON.stringify({ ...s, pruned: s.pruned ?? readStateFile().pruned }));
|
|
12248
12272
|
} catch {}
|
|
12249
12273
|
}
|
|
12250
12274
|
function readState() {
|
|
12251
|
-
const
|
|
12275
|
+
const queue = listQueue(spoolDir());
|
|
12276
|
+
const counts = {
|
|
12277
|
+
pending: queue.files?.length ?? null,
|
|
12278
|
+
rejected: listQueue(rejectedDir()).files?.length ?? null,
|
|
12279
|
+
unregistered: listQueue(unregisteredDir()).files?.length ?? null
|
|
12280
|
+
};
|
|
12281
|
+
const state = readStateFile();
|
|
12282
|
+
const stuck = state.error && (counts.pending ?? 0) > 0 ? state.error : null;
|
|
12283
|
+
return { ...state, ...counts, unreadable: queue.code, stuck };
|
|
12284
|
+
}
|
|
12285
|
+
function listQueue(dir) {
|
|
12286
|
+
try {
|
|
12287
|
+
const names = fs5.readdirSync(dir);
|
|
12288
|
+
return {
|
|
12289
|
+
files: names.filter((f) => f.endsWith(".json") && !f.startsWith(".")),
|
|
12290
|
+
temp: names.filter((f) => f.endsWith(".json") && f.startsWith(".")),
|
|
12291
|
+
code: null
|
|
12292
|
+
};
|
|
12293
|
+
} catch (e) {
|
|
12294
|
+
const code = e.code ?? "unknown";
|
|
12295
|
+
return code === "ENOENT" ? { files: [], temp: [], code: null } : { files: null, temp: [], code };
|
|
12296
|
+
}
|
|
12297
|
+
}
|
|
12298
|
+
var TEMP_AGE_MS = 60000;
|
|
12299
|
+
var REASON = /^[a-z]+(-[a-z]+)?(:[A-Z]+)?$/;
|
|
12300
|
+
function queueReport(now = Date.now()) {
|
|
12301
|
+
const dirs = {
|
|
12302
|
+
queue: spoolDir(),
|
|
12303
|
+
calls: callsDir(),
|
|
12304
|
+
rejected: rejectedDir(),
|
|
12305
|
+
unregistered: unregisteredDir(),
|
|
12306
|
+
callsRejected: callsRejectedDir()
|
|
12307
|
+
};
|
|
12308
|
+
const listed = Object.fromEntries(Object.entries(dirs).map(([k, d]) => [k, listQueue(d)]));
|
|
12309
|
+
const report = (k) => {
|
|
12310
|
+
const temp = { count: 0, oldestMs: 0, bytes: 0 };
|
|
12311
|
+
for (const name of listed[k].temp)
|
|
12312
|
+
try {
|
|
12313
|
+
const st = fs5.statSync(path6.join(dirs[k], name));
|
|
12314
|
+
const age = now - st.mtimeMs;
|
|
12315
|
+
if (age < TEMP_AGE_MS)
|
|
12316
|
+
continue;
|
|
12317
|
+
temp.count++;
|
|
12318
|
+
temp.bytes += st.size;
|
|
12319
|
+
temp.oldestMs = Math.max(temp.oldestMs, age);
|
|
12320
|
+
} catch {}
|
|
12321
|
+
return { files: listed[k].files?.length ?? null, code: listed[k].code, temp };
|
|
12322
|
+
};
|
|
12323
|
+
const reasons = (k) => {
|
|
12324
|
+
const out = {};
|
|
12325
|
+
for (const name of listed[k].files ?? []) {
|
|
12326
|
+
let why = "unknown";
|
|
12327
|
+
try {
|
|
12328
|
+
const text = fs5.readFileSync(path6.join(dirs[k], `${name}.reason`), "utf8");
|
|
12329
|
+
if (REASON.test(text))
|
|
12330
|
+
why = text;
|
|
12331
|
+
} catch {}
|
|
12332
|
+
out[why] = (out[why] ?? 0) + 1;
|
|
12333
|
+
}
|
|
12334
|
+
return out;
|
|
12335
|
+
};
|
|
12336
|
+
const read = (dir, name) => {
|
|
12252
12337
|
try {
|
|
12253
|
-
return fs5.
|
|
12338
|
+
return JSON.parse(fs5.readFileSync(path6.join(dir, name), "utf8"));
|
|
12254
12339
|
} catch {
|
|
12255
|
-
return
|
|
12340
|
+
return null;
|
|
12256
12341
|
}
|
|
12257
12342
|
};
|
|
12258
|
-
const
|
|
12259
|
-
|
|
12260
|
-
|
|
12261
|
-
|
|
12262
|
-
|
|
12263
|
-
|
|
12264
|
-
|
|
12265
|
-
|
|
12266
|
-
|
|
12267
|
-
|
|
12268
|
-
|
|
12269
|
-
|
|
12343
|
+
const held = new Map;
|
|
12344
|
+
for (const name of listed.unregistered.files ?? []) {
|
|
12345
|
+
const key = read(dirs.unregistered, name)?.project;
|
|
12346
|
+
const project = typeof key === "string" ? key : "unreadable";
|
|
12347
|
+
const age = Math.max(0, now - (Number(name.split("-")[0]) || now));
|
|
12348
|
+
const h = held.get(project) ?? { count: 0, oldestMs: 0 };
|
|
12349
|
+
held.set(project, { count: h.count + 1, oldestMs: Math.max(h.oldestMs, age) });
|
|
12350
|
+
}
|
|
12351
|
+
const waiting = new Set;
|
|
12352
|
+
for (const name of listed.calls.files ?? []) {
|
|
12353
|
+
const o = observation(read(dirs.calls, name));
|
|
12354
|
+
if (o)
|
|
12355
|
+
waiting.add(o.toolUse);
|
|
12356
|
+
}
|
|
12270
12357
|
return {
|
|
12271
|
-
|
|
12272
|
-
|
|
12273
|
-
|
|
12274
|
-
...
|
|
12275
|
-
|
|
12358
|
+
dirs: Object.fromEntries(Object.keys(dirs).map((k) => [k, report(k)])),
|
|
12359
|
+
rejected: reasons("rejected"),
|
|
12360
|
+
callsRejected: reasons("callsRejected"),
|
|
12361
|
+
held: [...held].map(([project, h]) => ({ project, ...h })).sort((a, b) => a.project.localeCompare(b.project)),
|
|
12362
|
+
waiting,
|
|
12363
|
+
pruned: readStateFile().pruned
|
|
12276
12364
|
};
|
|
12277
12365
|
}
|
|
12278
12366
|
function lock() {
|
|
@@ -12412,16 +12500,36 @@ var queued = (from) => {
|
|
|
12412
12500
|
return [];
|
|
12413
12501
|
}
|
|
12414
12502
|
};
|
|
12503
|
+
var CODE_NAMES = { 18: "TOOBIG", 19: "CONSTRAINT", 20: "MISMATCH", 25: "RANGE" };
|
|
12504
|
+
function setAside(from, name, dir, reason2) {
|
|
12505
|
+
fs5.mkdirSync(dir, { recursive: true, mode: 448 });
|
|
12506
|
+
try {
|
|
12507
|
+
fs5.renameSync(path6.join(from, name), path6.join(dir, name));
|
|
12508
|
+
} catch (e) {
|
|
12509
|
+
if (e.code === "ENOENT")
|
|
12510
|
+
return;
|
|
12511
|
+
throw e;
|
|
12512
|
+
}
|
|
12513
|
+
try {
|
|
12514
|
+
fs5.writeFileSync(path6.join(dir, `${name}.reason`), reason2, { mode: 384 });
|
|
12515
|
+
} catch {}
|
|
12516
|
+
}
|
|
12415
12517
|
async function sendBatch(db, names) {
|
|
12416
12518
|
const held = unregisteredDir();
|
|
12417
12519
|
const records = [];
|
|
12418
12520
|
for (const { name, from } of names) {
|
|
12521
|
+
let raw;
|
|
12419
12522
|
let r;
|
|
12420
12523
|
try {
|
|
12421
|
-
|
|
12524
|
+
raw = JSON.parse(fs5.readFileSync(path6.join(from, name), "utf8"));
|
|
12422
12525
|
} catch {
|
|
12423
|
-
|
|
12424
|
-
|
|
12526
|
+
setAside(from, name, rejectedDir(), "unreadable");
|
|
12527
|
+
continue;
|
|
12528
|
+
}
|
|
12529
|
+
try {
|
|
12530
|
+
r = current(raw);
|
|
12531
|
+
} catch {
|
|
12532
|
+
setAside(from, name, rejectedDir(), typeof raw?.project === "string" ? "version" : "no-project");
|
|
12425
12533
|
continue;
|
|
12426
12534
|
}
|
|
12427
12535
|
if (r)
|
|
@@ -12448,21 +12556,12 @@ async function sendBatch(db, names) {
|
|
|
12448
12556
|
} catch (e2) {
|
|
12449
12557
|
if (!rejected(e2))
|
|
12450
12558
|
throw e2;
|
|
12451
|
-
bad.push(x);
|
|
12452
|
-
}
|
|
12453
|
-
}
|
|
12454
|
-
}
|
|
12455
|
-
if (bad.length) {
|
|
12456
|
-
fs5.mkdirSync(rejectedDir(), { recursive: true, mode: 448 });
|
|
12457
|
-
for (const x of bad) {
|
|
12458
|
-
try {
|
|
12459
|
-
fs5.renameSync(path6.join(x.from, x.name), path6.join(rejectedDir(), x.name));
|
|
12460
|
-
} catch (e) {
|
|
12461
|
-
if (e.code !== "ENOENT")
|
|
12462
|
-
throw e;
|
|
12559
|
+
bad.push({ ...x, why: `sqlite:${CODE_NAMES[sqliteCode(e2) ?? -1] ?? "unknown"}` });
|
|
12463
12560
|
}
|
|
12464
12561
|
}
|
|
12465
12562
|
}
|
|
12563
|
+
for (const x of bad)
|
|
12564
|
+
setAside(x.from, x.name, rejectedDir(), x.why);
|
|
12466
12565
|
if (strayed.length) {
|
|
12467
12566
|
fs5.mkdirSync(held, { recursive: true, mode: 448 });
|
|
12468
12567
|
for (const x of strayed) {
|
|
@@ -12482,6 +12581,60 @@ async function sendBatch(db, names) {
|
|
|
12482
12581
|
fs5.rmSync(path6.join(x.from, x.name), { force: true });
|
|
12483
12582
|
return { sent, rejected: bad.length };
|
|
12484
12583
|
}
|
|
12584
|
+
async function sendObservations(db, names) {
|
|
12585
|
+
const aside = (name, why) => {
|
|
12586
|
+
try {
|
|
12587
|
+
setAside(callsDir(), name, callsRejectedDir(), why);
|
|
12588
|
+
} catch {}
|
|
12589
|
+
};
|
|
12590
|
+
const rows = [];
|
|
12591
|
+
for (const { name, from } of names) {
|
|
12592
|
+
let text;
|
|
12593
|
+
try {
|
|
12594
|
+
text = fs5.readFileSync(path6.join(from, name), "utf8");
|
|
12595
|
+
} catch {
|
|
12596
|
+
continue;
|
|
12597
|
+
}
|
|
12598
|
+
let o = null;
|
|
12599
|
+
let why = "unreadable";
|
|
12600
|
+
try {
|
|
12601
|
+
o = observation(JSON.parse(text));
|
|
12602
|
+
why = "shape";
|
|
12603
|
+
} catch {}
|
|
12604
|
+
if (o)
|
|
12605
|
+
rows.push({ name, o });
|
|
12606
|
+
else
|
|
12607
|
+
aside(name, why);
|
|
12608
|
+
}
|
|
12609
|
+
if (!rows.length)
|
|
12610
|
+
return true;
|
|
12611
|
+
const insert = (part) => db.insertInto("capture_tool_call").values(part.map((x) => observationRow(x.o))).execute();
|
|
12612
|
+
const bad = new Map;
|
|
12613
|
+
try {
|
|
12614
|
+
await insert(rows);
|
|
12615
|
+
} catch (e) {
|
|
12616
|
+
if (!rejected(e))
|
|
12617
|
+
return false;
|
|
12618
|
+
for (const x of rows)
|
|
12619
|
+
try {
|
|
12620
|
+
await insert([x]);
|
|
12621
|
+
} catch (e2) {
|
|
12622
|
+
if (!rejected(e2))
|
|
12623
|
+
return false;
|
|
12624
|
+
bad.set(x.name, `sqlite:${CODE_NAMES[sqliteCode(e2) ?? -1] ?? "unknown"}`);
|
|
12625
|
+
}
|
|
12626
|
+
}
|
|
12627
|
+
for (const x of rows) {
|
|
12628
|
+
const why = bad.get(x.name);
|
|
12629
|
+
if (why)
|
|
12630
|
+
aside(x.name, why);
|
|
12631
|
+
else
|
|
12632
|
+
try {
|
|
12633
|
+
fs5.rmSync(path6.join(callsDir(), x.name), { force: true });
|
|
12634
|
+
} catch {}
|
|
12635
|
+
}
|
|
12636
|
+
return true;
|
|
12637
|
+
}
|
|
12485
12638
|
async function flush(file = dbFile(), budgetMs = FLUSH_BUDGET_MS) {
|
|
12486
12639
|
const deadline = Date.now() + budgetMs;
|
|
12487
12640
|
const dir = spoolDir();
|
|
@@ -12489,6 +12642,8 @@ async function flush(file = dbFile(), budgetMs = FLUSH_BUDGET_MS) {
|
|
|
12489
12642
|
const total = { sent: 0, deferred: 0, rejected: 0 };
|
|
12490
12643
|
let batches = 0;
|
|
12491
12644
|
let queueBatches = 0;
|
|
12645
|
+
let removed = 0;
|
|
12646
|
+
const pruned = () => removed ? { at: new Date().toISOString(), count: removed } : undefined;
|
|
12492
12647
|
for (let first = true;; first = false) {
|
|
12493
12648
|
let unlock = lock();
|
|
12494
12649
|
while (!unlock && Date.now() < deadline) {
|
|
@@ -12510,12 +12665,20 @@ async function flush(file = dbFile(), budgetMs = FLUSH_BUDGET_MS) {
|
|
|
12510
12665
|
const sentBefore = batches;
|
|
12511
12666
|
try {
|
|
12512
12667
|
if (first) {
|
|
12513
|
-
prune(held);
|
|
12668
|
+
prune(held, () => removed++);
|
|
12514
12669
|
for (const part of chunks(queued(held), BATCH)) {
|
|
12515
12670
|
if (late())
|
|
12516
12671
|
break;
|
|
12517
12672
|
await send(part);
|
|
12518
12673
|
}
|
|
12674
|
+
for (const part of chunks(queued(callsDir()), BATCH)) {
|
|
12675
|
+
if (late())
|
|
12676
|
+
break;
|
|
12677
|
+
client ??= openWriter("capture", file);
|
|
12678
|
+
if (!await sendObservations(client, part))
|
|
12679
|
+
break;
|
|
12680
|
+
batches++;
|
|
12681
|
+
}
|
|
12519
12682
|
}
|
|
12520
12683
|
while (queueBatches === 0 || !late()) {
|
|
12521
12684
|
const part = queued(dir).slice(0, BATCH);
|
|
@@ -12524,12 +12687,19 @@ async function flush(file = dbFile(), budgetMs = FLUSH_BUDGET_MS) {
|
|
|
12524
12687
|
await send(part);
|
|
12525
12688
|
queueBatches++;
|
|
12526
12689
|
}
|
|
12527
|
-
prune(held);
|
|
12690
|
+
prune(held, () => removed++);
|
|
12528
12691
|
total.deferred = queued(held).length;
|
|
12529
12692
|
if (batches > sentBefore)
|
|
12530
|
-
writeState({
|
|
12693
|
+
writeState({
|
|
12694
|
+
flushedAt: new Date().toISOString(),
|
|
12695
|
+
error: null,
|
|
12696
|
+
deferred: total.deferred,
|
|
12697
|
+
pruned: pruned()
|
|
12698
|
+
});
|
|
12699
|
+
else if (removed)
|
|
12700
|
+
writeState({ ...readStateFile(), pruned: pruned() });
|
|
12531
12701
|
} catch (e) {
|
|
12532
|
-
writeState({ flushedAt: new Date().toISOString(), error: reason(e).slice(0, 300) });
|
|
12702
|
+
writeState({ flushedAt: new Date().toISOString(), error: reason(e).slice(0, 300), pruned: pruned() });
|
|
12533
12703
|
throw e;
|
|
12534
12704
|
} finally {
|
|
12535
12705
|
await client?.destroy().catch(() => {});
|
|
@@ -12548,27 +12718,63 @@ async function readInput(stream) {
|
|
|
12548
12718
|
}
|
|
12549
12719
|
var RECORD_TOOLS = "mcp__plugin_sphica_record__";
|
|
12550
12720
|
var OBSERVE_WAIT_MS = 5000;
|
|
12551
|
-
var hostId = (v) => v && /^[\x21-\x7e]{1,200}$/.test(v) ? v : null;
|
|
12552
|
-
|
|
12553
|
-
|
|
12554
|
-
|
|
12555
|
-
|
|
12556
|
-
|
|
12721
|
+
var hostId = (v) => typeof v === "string" && /^[\x21-\x7e]{1,200}$/.test(v) ? v : null;
|
|
12722
|
+
var OBSERVATION_KEYS = ["at", "host", "owner", "session", "tool", "toolUse", "turn", "v"].join();
|
|
12723
|
+
function observation(raw) {
|
|
12724
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw))
|
|
12725
|
+
return null;
|
|
12726
|
+
const o = raw;
|
|
12727
|
+
if (Object.keys(o).sort().join() !== OBSERVATION_KEYS)
|
|
12728
|
+
return null;
|
|
12729
|
+
if (o.v !== 1 || o.host !== "claude-code" || !hostId(o.session) || !hostId(o.toolUse))
|
|
12730
|
+
return null;
|
|
12731
|
+
if (!hostId(o.tool)?.startsWith(RECORD_TOOLS))
|
|
12732
|
+
return null;
|
|
12733
|
+
if (o.turn !== null && !hostId(o.turn))
|
|
12734
|
+
return null;
|
|
12735
|
+
if (o.owner !== 0 && o.owner !== 1)
|
|
12736
|
+
return null;
|
|
12737
|
+
const at = typeof o.at === "string" && /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/.test(o.at) ? o.at : null;
|
|
12738
|
+
if (!at || Number.isNaN(Date.parse(at)) || new Date(at).toISOString() !== at)
|
|
12739
|
+
return null;
|
|
12740
|
+
return o;
|
|
12741
|
+
}
|
|
12742
|
+
var observationRow = (o) => ({
|
|
12743
|
+
host: o.host,
|
|
12744
|
+
session_external: o.session,
|
|
12745
|
+
turn_id: o.turn,
|
|
12746
|
+
tool_use_id: o.toolUse,
|
|
12747
|
+
tool_name: o.tool,
|
|
12748
|
+
owner_turn: o.owner,
|
|
12749
|
+
observed_at: o.at
|
|
12750
|
+
});
|
|
12751
|
+
async function observeRecordCall(file, input, owner, waitMs = OBSERVE_WAIT_MS) {
|
|
12752
|
+
const o = observation({
|
|
12753
|
+
v: 1,
|
|
12754
|
+
host: "claude-code",
|
|
12755
|
+
session: hostId(input.session_id),
|
|
12756
|
+
turn: hostId(input.prompt_id),
|
|
12757
|
+
toolUse: hostId(input.tool_use_id),
|
|
12758
|
+
tool: hostId(input.tool_name),
|
|
12759
|
+
owner: owner ? 1 : 0,
|
|
12760
|
+
at: iso(Date.now())
|
|
12761
|
+
});
|
|
12762
|
+
if (!o)
|
|
12557
12763
|
return;
|
|
12558
|
-
|
|
12764
|
+
let kept = null;
|
|
12559
12765
|
try {
|
|
12560
|
-
|
|
12561
|
-
|
|
12562
|
-
|
|
12563
|
-
|
|
12564
|
-
|
|
12565
|
-
tool_name: tool,
|
|
12566
|
-
owner_turn: owner ? 1 : 0,
|
|
12567
|
-
observed_at: iso(Date.now())
|
|
12568
|
-
}).execute();
|
|
12766
|
+
kept = put(callsDir(), o);
|
|
12767
|
+
} catch {}
|
|
12768
|
+
const cap = openWriter("capture", file, waitMs);
|
|
12769
|
+
try {
|
|
12770
|
+
await cap.insertInto("capture_tool_call").values(observationRow(o)).execute();
|
|
12569
12771
|
} finally {
|
|
12570
12772
|
await cap.destroy().catch(() => {});
|
|
12571
12773
|
}
|
|
12774
|
+
if (kept)
|
|
12775
|
+
try {
|
|
12776
|
+
fs5.rmSync(kept, { force: true });
|
|
12777
|
+
} catch {}
|
|
12572
12778
|
}
|
|
12573
12779
|
async function main() {
|
|
12574
12780
|
if (process.argv[2] === "--flush") {
|
|
@@ -12603,15 +12809,19 @@ export {
|
|
|
12603
12809
|
TOOL_FLUSH_BUDGET_MS,
|
|
12604
12810
|
answersOf,
|
|
12605
12811
|
branchOf,
|
|
12812
|
+
callsDir,
|
|
12813
|
+
callsRejectedDir,
|
|
12606
12814
|
captureNotice,
|
|
12607
12815
|
closeTurn,
|
|
12608
12816
|
current,
|
|
12609
12817
|
fit,
|
|
12610
12818
|
flush,
|
|
12611
12819
|
isOwnerTurn,
|
|
12820
|
+
observation,
|
|
12612
12821
|
observeRecordCall,
|
|
12613
12822
|
onHook,
|
|
12614
12823
|
openTurn,
|
|
12824
|
+
queueReport,
|
|
12615
12825
|
readInput,
|
|
12616
12826
|
readState,
|
|
12617
12827
|
rejectedDir,
|