sessionpipe-core 0.13.0 → 0.13.1
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/dist/control.js +1 -1
- package/dist/index.js +280 -105
- package/dist/index.js.map +3 -3
- package/dist/schema/v1.d.ts +12 -0
- package/dist/schema/v1.d.ts.map +1 -1
- package/dist/schema.js +10 -2
- package/dist/schema.js.map +2 -2
- package/dist/service.js +1 -1
- package/dist/src/adapters/antigravity.d.ts.map +1 -1
- package/dist/src/adapters/claude-code.d.ts.map +1 -1
- package/dist/src/adapters/claude-shaped.d.ts +10 -0
- package/dist/src/adapters/claude-shaped.d.ts.map +1 -1
- package/dist/src/adapters/codex.d.ts.map +1 -1
- package/dist/src/adapters/devin.d.ts +1 -1
- package/dist/src/adapters/devin.d.ts.map +1 -1
- package/dist/src/adapters/types.d.ts +2 -1
- package/dist/src/adapters/types.d.ts.map +1 -1
- package/dist/src/config.d.ts +15 -1
- package/dist/src/config.d.ts.map +1 -1
- package/dist/src/index.d.ts +2 -2
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/outbox.d.ts +35 -2
- package/dist/src/outbox.d.ts.map +1 -1
- package/dist/src/privacy/pii.d.ts.map +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// sessionpipe-core 0.13.
|
|
1
|
+
// sessionpipe-core 0.13.1 · Apache-2.0 · https://sessionpipe.org
|
|
2
2
|
var __defProp = Object.defineProperty;
|
|
3
3
|
var __export = (target, all) => {
|
|
4
4
|
for (var name in all)
|
|
@@ -59,7 +59,7 @@ var safeId = (s) => String(s).replace(/[^A-Za-z0-9_.-]/g, "_").slice(0, 160);
|
|
|
59
59
|
// src/adapters/claude-code.ts
|
|
60
60
|
import { execFileSync as execFileSync2 } from "node:child_process";
|
|
61
61
|
import { createHash } from "node:crypto";
|
|
62
|
-
import { existsSync as existsSync2, readdirSync as readdirSync2, readFileSync as readFileSync2, statSync as statSync2
|
|
62
|
+
import { existsSync as existsSync2, readdirSync as readdirSync2, readFileSync as readFileSync2, statSync as statSync2 } from "node:fs";
|
|
63
63
|
import { userInfo } from "node:os";
|
|
64
64
|
import path4 from "node:path";
|
|
65
65
|
|
|
@@ -417,6 +417,41 @@ function readJson(file) {
|
|
|
417
417
|
const f = readJsonFile(file);
|
|
418
418
|
return f.ok ? f.value : {};
|
|
419
419
|
}
|
|
420
|
+
function refused(file, e, what, existed) {
|
|
421
|
+
const { code, syscall } = e;
|
|
422
|
+
const why = `can't ${what} it (${code ?? String(e)})`;
|
|
423
|
+
if (what === "remove" || syscall === "open" || syscall === "mkdir")
|
|
424
|
+
return { file, changed: false, skipped: true, note: `${why}; not touched` };
|
|
425
|
+
return {
|
|
426
|
+
file,
|
|
427
|
+
changed: true,
|
|
428
|
+
skipped: true,
|
|
429
|
+
...existed ? {} : { created: true },
|
|
430
|
+
note: `${why} part way; check the file before the harness reads it`
|
|
431
|
+
};
|
|
432
|
+
}
|
|
433
|
+
function writeBack(file, text, existed = true) {
|
|
434
|
+
try {
|
|
435
|
+
mkdirSync(path3.dirname(file), { recursive: true });
|
|
436
|
+
writeFileSync(file, text);
|
|
437
|
+
return null;
|
|
438
|
+
} catch (e) {
|
|
439
|
+
return refused(file, e, "write", existed);
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
function removeEmptied(file, empty) {
|
|
443
|
+
try {
|
|
444
|
+
unlinkSync(file);
|
|
445
|
+
return { file, changed: true, note: "removed (sessionpipe created it)" };
|
|
446
|
+
} catch (e) {
|
|
447
|
+
const bad = refused(file, e, "remove", true);
|
|
448
|
+
return writeBack(file, empty) ?? {
|
|
449
|
+
file,
|
|
450
|
+
changed: true,
|
|
451
|
+
note: `emptied; ${bad.note.replace(/; not touched$/, "")}, so the empty file stays`
|
|
452
|
+
};
|
|
453
|
+
}
|
|
454
|
+
}
|
|
420
455
|
function entryFor(event, harness, cmd, o) {
|
|
421
456
|
const matcher = o.matcherFor(event);
|
|
422
457
|
return {
|
|
@@ -458,8 +493,8 @@ function installClaudeShaped(file, harness, events, cmd, o) {
|
|
|
458
493
|
}
|
|
459
494
|
if (changed) {
|
|
460
495
|
settings2.hooks = hooks;
|
|
461
|
-
|
|
462
|
-
|
|
496
|
+
const bad = writeBack(file, formatJson(settings2, f), f.existed);
|
|
497
|
+
if (bad) return bad;
|
|
463
498
|
}
|
|
464
499
|
return { file, changed, created: !f.existed && changed };
|
|
465
500
|
}
|
|
@@ -482,12 +517,8 @@ function uninstallClaudeShaped(file, events, opts = {}) {
|
|
|
482
517
|
}
|
|
483
518
|
if (!changed) return { file, changed: false };
|
|
484
519
|
if (!Object.keys(hooks).length) delete settings2.hooks;
|
|
485
|
-
if (opts.created && !Object.keys(settings2).length)
|
|
486
|
-
|
|
487
|
-
return { file, changed: true, note: "removed (sessionpipe created it)" };
|
|
488
|
-
}
|
|
489
|
-
writeFileSync(file, formatJson(settings2, f));
|
|
490
|
-
return { file, changed: true };
|
|
520
|
+
if (opts.created && !Object.keys(settings2).length) return removeEmptied(file, formatJson(settings2, f));
|
|
521
|
+
return writeBack(file, formatJson(settings2, f)) ?? { file, changed: true };
|
|
491
522
|
}
|
|
492
523
|
function installedClaudeShaped(file, harness, events, cmd, o) {
|
|
493
524
|
const f = readJsonFile(file);
|
|
@@ -566,7 +597,7 @@ var claudeCode = {
|
|
|
566
597
|
configFiles: (env = process.env) => claudeDirs(env).map(settingsIn),
|
|
567
598
|
install: (cmd, env = process.env, opts = {}) => claudeDirs(env).flatMap((d) => {
|
|
568
599
|
const hooks = installClaudeShaped(settingsIn(d), NAME, eventsFor(opts.lean), cmd, OPTS);
|
|
569
|
-
const line = opts.statusLine === void 0 ? null : statusLine(settingsIn(d), opts.statusLine, cmd);
|
|
600
|
+
const line = opts.statusLine === void 0 || hooks.skipped ? null : statusLine(settingsIn(d), opts.statusLine, cmd);
|
|
570
601
|
return line ? [hooks, line] : [hooks];
|
|
571
602
|
}),
|
|
572
603
|
uninstall: (env = process.env, opts = {}) => claudeDirs(env).map((d) => {
|
|
@@ -574,7 +605,9 @@ var claudeCode = {
|
|
|
574
605
|
const hooks = uninstallClaudeShaped(settingsIn(d), CLAUDE_EVENTS, {
|
|
575
606
|
created: opts.created?.includes(settingsIn(d))
|
|
576
607
|
});
|
|
577
|
-
|
|
608
|
+
if (line?.changed && !line.skipped && hooks.skipped)
|
|
609
|
+
return { ...hooks, note: `status line removed, hooks not: ${hooks.note?.replace(/; not touched$/, "")}` };
|
|
610
|
+
return line && !hooks.changed && !hooks.skipped ? line : hooks;
|
|
578
611
|
}),
|
|
579
612
|
installed: (cmd, env = process.env, opts = {}) => claudeDirs(env).map((d) => ({
|
|
580
613
|
file: settingsIn(d),
|
|
@@ -669,7 +702,8 @@ var claudeCode = {
|
|
|
669
702
|
attention_id: call_id ?? permissionId(),
|
|
670
703
|
kind: "permission",
|
|
671
704
|
tool,
|
|
672
|
-
message: tool ? `${tool}${describe(s.tool_input)}` : void 0
|
|
705
|
+
message: tool ? `${tool}${describe(s.tool_input)}` : void 0,
|
|
706
|
+
agent_id
|
|
673
707
|
})
|
|
674
708
|
);
|
|
675
709
|
break;
|
|
@@ -678,7 +712,8 @@ var claudeCode = {
|
|
|
678
712
|
ev("attention.needed", {
|
|
679
713
|
attention_id: `notif-${Date.now()}`,
|
|
680
714
|
kind: notifKind(s.notification_type),
|
|
681
|
-
message: typeof s.message === "string" ? s.message.slice(0, 200) : void 0
|
|
715
|
+
message: typeof s.message === "string" ? s.message.slice(0, 200) : void 0,
|
|
716
|
+
agent_id
|
|
682
717
|
})
|
|
683
718
|
);
|
|
684
719
|
break;
|
|
@@ -803,8 +838,11 @@ function statusLine(file, want, cmd) {
|
|
|
803
838
|
if (want ? ours && cur?.command === command && cur?.refreshInterval === LINE_REFRESH_S : !ours) return null;
|
|
804
839
|
if (want) f.value.statusLine = { type: "command", command, refreshInterval: LINE_REFRESH_S };
|
|
805
840
|
else delete f.value.statusLine;
|
|
806
|
-
|
|
807
|
-
|
|
841
|
+
return writeBack(file, formatJson(f.value, f), f.existed) ?? {
|
|
842
|
+
file,
|
|
843
|
+
changed: true,
|
|
844
|
+
note: want ? "status line written (account limits)" : "status line removed"
|
|
845
|
+
};
|
|
808
846
|
}
|
|
809
847
|
var epochIso = (t) => typeof t === "number" && t > 0 && t < 1e11 ? new Date(t * 1e3).toISOString() : void 0;
|
|
810
848
|
var WINDOW_MINUTES = { five_hour: 300, seven_day: 10080 };
|
|
@@ -2166,18 +2204,18 @@ function startAcp(o) {
|
|
|
2166
2204
|
);
|
|
2167
2205
|
const stop = str(res?.stopReason);
|
|
2168
2206
|
const words = t.text.trim() || `(${label} finished without a written reply.)`;
|
|
2169
|
-
const
|
|
2170
|
-
if (stop === "end_turn" || stop === "max_tokens") return { reply: words +
|
|
2207
|
+
const refused2 = deniedNote(t.denied, t.count);
|
|
2208
|
+
if (stop === "end_turn" || stop === "max_tokens") return { reply: words + refused2 };
|
|
2171
2209
|
if (stop === "refusal") return { reply: `${words}
|
|
2172
2210
|
|
|
2173
|
-
(${label} refused the request.)${
|
|
2211
|
+
(${label} refused the request.)${refused2}` };
|
|
2174
2212
|
if (stop === "cancelled")
|
|
2175
2213
|
return timedOut ? {
|
|
2176
2214
|
reply: "",
|
|
2177
2215
|
error: `${label} was still working after ${Math.round(limit / 6e4)} minutes, so it was stopped`
|
|
2178
2216
|
} : { reply: `${words}
|
|
2179
2217
|
|
|
2180
|
-
(The turn was cancelled.)${
|
|
2218
|
+
(The turn was cancelled.)${refused2}` };
|
|
2181
2219
|
return { reply: "", error: `${label} ended the turn with ${stop || "no stop reason"}` };
|
|
2182
2220
|
} catch (e) {
|
|
2183
2221
|
return { reply: "", error: e.message };
|
|
@@ -2238,16 +2276,7 @@ function startAcp(o) {
|
|
|
2238
2276
|
}
|
|
2239
2277
|
|
|
2240
2278
|
// src/adapters/antigravity.ts
|
|
2241
|
-
import {
|
|
2242
|
-
existsSync as existsSync3,
|
|
2243
|
-
mkdirSync as mkdirSync2,
|
|
2244
|
-
readdirSync as readdirSync3,
|
|
2245
|
-
readFileSync as readFileSync5,
|
|
2246
|
-
realpathSync,
|
|
2247
|
-
statSync as statSync4,
|
|
2248
|
-
unlinkSync as unlinkSync2,
|
|
2249
|
-
writeFileSync as writeFileSync3
|
|
2250
|
-
} from "node:fs";
|
|
2279
|
+
import { existsSync as existsSync3, readdirSync as readdirSync3, readFileSync as readFileSync5, realpathSync, statSync as statSync4 } from "node:fs";
|
|
2251
2280
|
import path8 from "node:path";
|
|
2252
2281
|
var NAME2 = "antigravity";
|
|
2253
2282
|
var HOOK_NAME = "sessionpipe";
|
|
@@ -2279,10 +2308,8 @@ var antigravity = {
|
|
|
2279
2308
|
const e = entry(cmd);
|
|
2280
2309
|
if (JSON.stringify(hooks[HOOK_NAME]) === JSON.stringify(e)) return [{ file, changed: false }];
|
|
2281
2310
|
hooks[HOOK_NAME] = e;
|
|
2282
|
-
mkdirSync2(path8.dirname(file), { recursive: true });
|
|
2283
|
-
writeFileSync3(file, formatJson(hooks, f));
|
|
2284
2311
|
return [
|
|
2285
|
-
{
|
|
2312
|
+
writeBack(file, formatJson(hooks, f), f.existed) ?? {
|
|
2286
2313
|
file,
|
|
2287
2314
|
changed: true,
|
|
2288
2315
|
created: !f.existed,
|
|
@@ -2298,12 +2325,8 @@ var antigravity = {
|
|
|
2298
2325
|
const hooks = f.value;
|
|
2299
2326
|
if (!(HOOK_NAME in hooks)) return [{ file, changed: false }];
|
|
2300
2327
|
delete hooks[HOOK_NAME];
|
|
2301
|
-
if (opts.created?.includes(file) && !Object.keys(hooks).length)
|
|
2302
|
-
|
|
2303
|
-
return [{ file, changed: true, note: "removed (sessionpipe created it)" }];
|
|
2304
|
-
}
|
|
2305
|
-
writeFileSync3(file, formatJson(hooks, f));
|
|
2306
|
-
return [{ file, changed: true }];
|
|
2328
|
+
if (opts.created?.includes(file) && !Object.keys(hooks).length) return [removeEmptied(file, formatJson(hooks, f))];
|
|
2329
|
+
return [writeBack(file, formatJson(hooks, f)) ?? { file, changed: true }];
|
|
2307
2330
|
},
|
|
2308
2331
|
installed(cmd, env = process.env) {
|
|
2309
2332
|
const file = hooksFile(env);
|
|
@@ -3174,7 +3197,7 @@ function parseResult(stdout) {
|
|
|
3174
3197
|
var isUuid = (s) => typeof s === "string" && UUID_RE2.test(s);
|
|
3175
3198
|
|
|
3176
3199
|
// src/adapters/codex.ts
|
|
3177
|
-
import { existsSync as existsSync4,
|
|
3200
|
+
import { existsSync as existsSync4, readdirSync as readdirSync5, readFileSync as readFileSync7 } from "node:fs";
|
|
3178
3201
|
import path11 from "node:path";
|
|
3179
3202
|
|
|
3180
3203
|
// src/adapters/codex-config.ts
|
|
@@ -3327,29 +3350,30 @@ var configFile2 = (env) => path11.join(codexHome(env), "config.toml");
|
|
|
3327
3350
|
var sessionsDir = (env) => path11.join(codexHome(env), "sessions");
|
|
3328
3351
|
var validTool3 = (n) => typeof n === "string" && /^[A-Za-z0-9_.:-]{1,120}$/.test(n);
|
|
3329
3352
|
var endReason2 = (r) => r === "clear" || r === "logout" || r === "exit" ? r : "other";
|
|
3353
|
+
var UNTIL_APPROVED = "Codex will report nothing until you approve the hooks: start `codex` and accept the hook review";
|
|
3330
3354
|
function installNotify(cmd, env) {
|
|
3331
3355
|
const file = configFile2(env);
|
|
3332
3356
|
let text = "";
|
|
3357
|
+
let existed = true;
|
|
3333
3358
|
try {
|
|
3334
3359
|
text = readFileSync7(file, "utf8");
|
|
3335
|
-
} catch {
|
|
3360
|
+
} catch (e) {
|
|
3361
|
+
existed = e.code !== "ENOENT";
|
|
3336
3362
|
}
|
|
3337
3363
|
const argv = cmd([NAME3, "notify"]).argv;
|
|
3338
3364
|
const line = `notify = ${JSON.stringify(argv)}`;
|
|
3339
3365
|
const n = readNotify(text);
|
|
3340
|
-
const write = (next) => {
|
|
3341
|
-
|
|
3342
|
-
|
|
3366
|
+
const write = (next, note) => {
|
|
3367
|
+
const bad = writeBack(file, next, existed);
|
|
3368
|
+
return bad ? { ...bad, note: `${bad.note}. ${UNTIL_APPROVED}` } : { file, changed: true, note };
|
|
3343
3369
|
};
|
|
3344
3370
|
if (n && !n.other) {
|
|
3345
3371
|
if (!n.misplaced && JSON.stringify(n.argv) === JSON.stringify(argv))
|
|
3346
3372
|
return { file, changed: false, note: "notify current" };
|
|
3347
|
-
write(
|
|
3348
|
-
|
|
3349
|
-
|
|
3350
|
-
|
|
3351
|
-
note: n.misplaced ? "notify moved to the top level (it sat inside a [table], where Codex never reads it)" : "notify updated"
|
|
3352
|
-
};
|
|
3373
|
+
return write(
|
|
3374
|
+
withNotify(text, line),
|
|
3375
|
+
n.misplaced ? "notify moved to the top level (it sat inside a [table], where Codex never reads it)" : "notify updated"
|
|
3376
|
+
);
|
|
3353
3377
|
}
|
|
3354
3378
|
if (n?.other) {
|
|
3355
3379
|
const prev = previousNotify(n.argv);
|
|
@@ -3358,20 +3382,20 @@ function installNotify(cmd, env) {
|
|
|
3358
3382
|
if (JSON.stringify(prev) === JSON.stringify(argv))
|
|
3359
3383
|
return { file, changed: false, note: "notify current (after Codex Computer Use)" };
|
|
3360
3384
|
}
|
|
3361
|
-
if (!prev || OURS2.test(JSON.stringify(prev)))
|
|
3362
|
-
write(
|
|
3363
|
-
|
|
3364
|
-
|
|
3385
|
+
if (!prev || OURS2.test(JSON.stringify(prev)))
|
|
3386
|
+
return write(
|
|
3387
|
+
replaceNotify(text, n, `notify = ${withPreviousRaw(n.raw, argv)}`),
|
|
3388
|
+
"chained after Codex Computer Use (--previous-notify)"
|
|
3389
|
+
);
|
|
3365
3390
|
}
|
|
3366
3391
|
return {
|
|
3367
3392
|
file,
|
|
3368
3393
|
changed: false,
|
|
3369
3394
|
skipped: true,
|
|
3370
|
-
note: `notify belongs to ${(n.argv?.[0] ?? n.raw).replace(/^.*\//, "").slice(0, 40)}; left alone.
|
|
3395
|
+
note: `notify belongs to ${(n.argv?.[0] ?? n.raw).replace(/^.*\//, "").slice(0, 40)}; left alone. ${UNTIL_APPROVED}`
|
|
3371
3396
|
};
|
|
3372
3397
|
}
|
|
3373
|
-
write(withNotify(text, line));
|
|
3374
|
-
return { file, changed: true, note: "notify set at the top level" };
|
|
3398
|
+
return write(withNotify(text, line), "notify set at the top level");
|
|
3375
3399
|
}
|
|
3376
3400
|
function uninstallNotify(env) {
|
|
3377
3401
|
const file = configFile2(env);
|
|
@@ -3383,15 +3407,13 @@ function uninstallNotify(env) {
|
|
|
3383
3407
|
}
|
|
3384
3408
|
const n = readNotify(text);
|
|
3385
3409
|
if (!n) return { file, changed: false };
|
|
3386
|
-
if (!n.other) {
|
|
3387
|
-
writeFileSync4(file, withoutNotify(text));
|
|
3388
|
-
return { file, changed: true };
|
|
3389
|
-
}
|
|
3410
|
+
if (!n.other) return writeBack(file, withoutNotify(text)) ?? { file, changed: true };
|
|
3390
3411
|
const prev = previousNotify(n.argv);
|
|
3391
|
-
if (isComputerUse(n.argv) && prev && OURS2.test(JSON.stringify(prev)))
|
|
3392
|
-
|
|
3393
|
-
|
|
3394
|
-
|
|
3412
|
+
if (isComputerUse(n.argv) && prev && OURS2.test(JSON.stringify(prev)))
|
|
3413
|
+
return writeBack(file, replaceNotify(text, n, `notify = ${stripPreviousRaw(n.raw)}`)) ?? {
|
|
3414
|
+
file,
|
|
3415
|
+
changed: true
|
|
3416
|
+
};
|
|
3395
3417
|
return { file, changed: false };
|
|
3396
3418
|
}
|
|
3397
3419
|
function findRollout(threadId, env = process.env) {
|
|
@@ -3423,8 +3445,12 @@ function rolloutMeta(file) {
|
|
|
3423
3445
|
return scanLines(file, (line) => parseLines([line])[0] ?? {});
|
|
3424
3446
|
}
|
|
3425
3447
|
function ofAnotherThread(file, id) {
|
|
3448
|
+
const other = threadOf(file);
|
|
3449
|
+
return !!other && other !== id.toLowerCase();
|
|
3450
|
+
}
|
|
3451
|
+
function threadOf(file) {
|
|
3426
3452
|
const m = /-([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})\.jsonl(?:\.zst)?$/i.exec(file);
|
|
3427
|
-
return
|
|
3453
|
+
return m?.[1] ? m[1].toLowerCase() : null;
|
|
3428
3454
|
}
|
|
3429
3455
|
var codex = {
|
|
3430
3456
|
name: NAME3,
|
|
@@ -3503,6 +3529,8 @@ var codex = {
|
|
|
3503
3529
|
const turn_id = typeof s.turn_id === "string" ? s.turn_id : void 0;
|
|
3504
3530
|
const tool = validTool3(s.tool_name) ? s.tool_name : void 0;
|
|
3505
3531
|
const call_id = typeof s.tool_use_id === "string" ? s.tool_use_id : void 0;
|
|
3532
|
+
const thread = typeof s.transcript_path === "string" ? threadOf(s.transcript_path) : null;
|
|
3533
|
+
const agent_id = typeof s.agent_id === "string" && s.agent_id ? s.agent_id : typeof s.subagent_id === "string" && s.subagent_id ? s.subagent_id : thread && thread !== String(id).toLowerCase() ? thread : void 0;
|
|
3506
3534
|
const ev = (type, data) => ({ type, data: prune3(data), harnessEvent: event });
|
|
3507
3535
|
const events = [];
|
|
3508
3536
|
switch (event) {
|
|
@@ -3522,17 +3550,30 @@ var codex = {
|
|
|
3522
3550
|
);
|
|
3523
3551
|
break;
|
|
3524
3552
|
case "PreToolUse":
|
|
3525
|
-
if (tool) events.push(ev("tool.started", { tool, call_id, turn_id, input: s.tool_input }));
|
|
3553
|
+
if (tool) events.push(ev("tool.started", { tool, call_id, turn_id, agent_id, input: s.tool_input }));
|
|
3526
3554
|
break;
|
|
3527
3555
|
case "PostToolUse":
|
|
3528
3556
|
if (tool)
|
|
3529
3557
|
events.push(
|
|
3530
|
-
ev("tool.ended", {
|
|
3558
|
+
ev("tool.ended", {
|
|
3559
|
+
tool,
|
|
3560
|
+
call_id,
|
|
3561
|
+
turn_id,
|
|
3562
|
+
agent_id,
|
|
3563
|
+
ok: true,
|
|
3564
|
+
input: s.tool_input,
|
|
3565
|
+
output: s.tool_response
|
|
3566
|
+
})
|
|
3531
3567
|
);
|
|
3532
3568
|
break;
|
|
3533
3569
|
case "PermissionRequest":
|
|
3534
3570
|
events.push(
|
|
3535
|
-
ev("attention.needed", {
|
|
3571
|
+
ev("attention.needed", {
|
|
3572
|
+
attention_id: call_id ?? `perm-${Date.now()}`,
|
|
3573
|
+
kind: "permission",
|
|
3574
|
+
tool,
|
|
3575
|
+
agent_id
|
|
3576
|
+
})
|
|
3536
3577
|
);
|
|
3537
3578
|
break;
|
|
3538
3579
|
case "Stop":
|
|
@@ -4190,7 +4231,7 @@ var CHAIN_LIMIT = 4e3;
|
|
|
4190
4231
|
var CHAIN_SQL = `WITH RECURSIVE chain(node_id, parent_node_id, chat_message, created_at) AS (
|
|
4191
4232
|
SELECT node_id, parent_node_id, chat_message, created_at FROM message_nodes
|
|
4192
4233
|
WHERE session_id = ? AND node_id = (SELECT main_chain_id FROM sessions WHERE id = ?)
|
|
4193
|
-
UNION
|
|
4234
|
+
UNION
|
|
4194
4235
|
SELECT m.node_id, m.parent_node_id, m.chat_message, m.created_at FROM message_nodes m
|
|
4195
4236
|
JOIN chain c ON m.node_id = c.parent_node_id AND m.session_id = ? AND m.node_id >= ?
|
|
4196
4237
|
)
|
|
@@ -4468,7 +4509,7 @@ function drivableHarnesses(env = process.env) {
|
|
|
4468
4509
|
}
|
|
4469
4510
|
|
|
4470
4511
|
// src/config.ts
|
|
4471
|
-
import { chmodSync, existsSync as existsSync8, mkdirSync as
|
|
4512
|
+
import { chmodSync, existsSync as existsSync8, mkdirSync as mkdirSync2, readFileSync as readFileSync10, writeFileSync as writeFileSync2 } from "node:fs";
|
|
4472
4513
|
import path15 from "node:path";
|
|
4473
4514
|
var DEFAULT_CONFIG = { sinks: [], harnesses: {}, update_check: true, keep_days: 30 };
|
|
4474
4515
|
function readConfig(file = configFile()) {
|
|
@@ -4484,9 +4525,36 @@ function readConfig(file = configFile()) {
|
|
|
4484
4525
|
return { ...DEFAULT_CONFIG, sinks: [], harnesses: {} };
|
|
4485
4526
|
}
|
|
4486
4527
|
}
|
|
4528
|
+
function configProblem(file) {
|
|
4529
|
+
let text;
|
|
4530
|
+
try {
|
|
4531
|
+
text = readFileSync10(file, "utf8");
|
|
4532
|
+
} catch (e) {
|
|
4533
|
+
const code = e.code;
|
|
4534
|
+
return code === "ENOENT" ? void 0 : `could not be read (${code})`;
|
|
4535
|
+
}
|
|
4536
|
+
if (!text.trim()) return void 0;
|
|
4537
|
+
if (text.charCodeAt(0) === 65279) return "starts with a byte-order mark (save it as UTF-8 without one)";
|
|
4538
|
+
try {
|
|
4539
|
+
const j = JSON.parse(text);
|
|
4540
|
+
return j && typeof j === "object" && !Array.isArray(j) ? void 0 : "is not a JSON object";
|
|
4541
|
+
} catch (e) {
|
|
4542
|
+
const m = e.message;
|
|
4543
|
+
const at = (/line \d+ column \d+/.exec(m) ?? /position \d+/.exec(m))?.[0];
|
|
4544
|
+
return `is not valid JSON${at ? ` at ${at}` : ""}`;
|
|
4545
|
+
}
|
|
4546
|
+
}
|
|
4547
|
+
function assertConfigWritable(file = configFile()) {
|
|
4548
|
+
const problem = configProblem(file);
|
|
4549
|
+
if (problem)
|
|
4550
|
+
throw new Error(
|
|
4551
|
+
`${tilde(file)} ${problem}, so it was not written over: that would lose its machine name and sinks. Fix it, or move it aside to start again, then run this again.`
|
|
4552
|
+
);
|
|
4553
|
+
}
|
|
4487
4554
|
function writeConfig(c, file = configFile()) {
|
|
4488
|
-
|
|
4489
|
-
|
|
4555
|
+
assertConfigWritable(file);
|
|
4556
|
+
mkdirSync2(path15.dirname(file), { recursive: true, mode: 448 });
|
|
4557
|
+
writeFileSync2(file, `${JSON.stringify(c, null, 2)}
|
|
4490
4558
|
`, { mode: 384 });
|
|
4491
4559
|
if (process.platform !== "win32" && existsSync8(file)) {
|
|
4492
4560
|
try {
|
|
@@ -4551,19 +4619,20 @@ function makeEvent(a, ctx) {
|
|
|
4551
4619
|
}
|
|
4552
4620
|
|
|
4553
4621
|
// src/outbox.ts
|
|
4622
|
+
import { randomBytes as randomBytes3 } from "node:crypto";
|
|
4554
4623
|
import {
|
|
4555
4624
|
appendFileSync,
|
|
4556
4625
|
closeSync as closeSync3,
|
|
4557
4626
|
existsSync as existsSync9,
|
|
4558
|
-
mkdirSync as
|
|
4627
|
+
mkdirSync as mkdirSync3,
|
|
4559
4628
|
openSync as openSync3,
|
|
4560
4629
|
readdirSync as readdirSync6,
|
|
4561
4630
|
readFileSync as readFileSync11,
|
|
4562
4631
|
readSync as readSync3,
|
|
4563
4632
|
renameSync,
|
|
4564
4633
|
statSync as statSync6,
|
|
4565
|
-
unlinkSync as
|
|
4566
|
-
writeFileSync as
|
|
4634
|
+
unlinkSync as unlinkSync2,
|
|
4635
|
+
writeFileSync as writeFileSync3
|
|
4567
4636
|
} from "node:fs";
|
|
4568
4637
|
import path16 from "node:path";
|
|
4569
4638
|
var READ_CHUNK = 1024 * 1024;
|
|
@@ -4593,8 +4662,8 @@ var Outbox = class {
|
|
|
4593
4662
|
} catch {
|
|
4594
4663
|
}
|
|
4595
4664
|
if (n < 0) n = this.lastSeqInFile(ref) + 1;
|
|
4596
|
-
|
|
4597
|
-
|
|
4665
|
+
mkdirSync3(path16.dirname(f), { recursive: true, mode: 448 });
|
|
4666
|
+
writeFileSync3(f, String(n + 1), { mode: 384 });
|
|
4598
4667
|
return n;
|
|
4599
4668
|
}
|
|
4600
4669
|
lastSeqInFile(ref) {
|
|
@@ -4612,7 +4681,7 @@ var Outbox = class {
|
|
|
4612
4681
|
}
|
|
4613
4682
|
append(ref, events) {
|
|
4614
4683
|
if (!events.length) return;
|
|
4615
|
-
|
|
4684
|
+
mkdirSync3(this.dir(ref.harness), { recursive: true, mode: 448 });
|
|
4616
4685
|
appendFileSync(this.file(ref), `${events.map((e) => JSON.stringify(e)).join("\n")}
|
|
4617
4686
|
`, { mode: 384 });
|
|
4618
4687
|
}
|
|
@@ -4682,34 +4751,106 @@ var Outbox = class {
|
|
|
4682
4751
|
closeSync3(fd);
|
|
4683
4752
|
}
|
|
4684
4753
|
}
|
|
4685
|
-
/** Files older than `days` are pruned
|
|
4754
|
+
/** Files older than `days` are pruned, and each sink's cursor into them. The seq
|
|
4755
|
+
* counter stays: a session resumed after the window counts on from where it was,
|
|
4756
|
+
* so a receiver's (harness, session, seq) dedup doesn't take its new events for
|
|
4757
|
+
* ones it already has, and the sinks read the new file from its start.
|
|
4758
|
+
* It all happens under the drain's `flush` lock, since a drain holds the whole
|
|
4759
|
+
* cursor map and writes it back: a file and its cursor keys go together, so a
|
|
4760
|
+
* session resumed in between can't keep a key past its new file's end. While a
|
|
4761
|
+
* drain runs, nothing is pruned (0); the next prune does it. It never throws: it
|
|
4762
|
+
* is a cleanup, and a hook's drain comes after it. */
|
|
4686
4763
|
prune(days, now = Date.now()) {
|
|
4764
|
+
let release = null;
|
|
4765
|
+
try {
|
|
4766
|
+
release = takeNamedLock(this.root, "flush");
|
|
4767
|
+
} catch {
|
|
4768
|
+
}
|
|
4769
|
+
if (!release) return 0;
|
|
4687
4770
|
let n = 0;
|
|
4688
|
-
|
|
4689
|
-
|
|
4690
|
-
const
|
|
4691
|
-
|
|
4692
|
-
|
|
4693
|
-
|
|
4694
|
-
|
|
4695
|
-
|
|
4696
|
-
|
|
4771
|
+
try {
|
|
4772
|
+
const cutoff = now - days * 864e5;
|
|
4773
|
+
const aged = this.sessions().filter((ref) => {
|
|
4774
|
+
try {
|
|
4775
|
+
return statSync6(this.file(ref)).mtimeMs < cutoff;
|
|
4776
|
+
} catch {
|
|
4777
|
+
return false;
|
|
4778
|
+
}
|
|
4779
|
+
});
|
|
4780
|
+
this.dropStaleCursors(new Set(aged.map((ref) => `${ref.harness}/${ref.session}`)));
|
|
4781
|
+
for (const ref of aged) {
|
|
4782
|
+
const f = this.file(ref);
|
|
4783
|
+
try {
|
|
4784
|
+
if (statSync6(f).mtimeMs < cutoff) {
|
|
4785
|
+
unlinkSync2(f);
|
|
4786
|
+
n++;
|
|
4697
4787
|
}
|
|
4698
|
-
|
|
4788
|
+
} catch {
|
|
4699
4789
|
}
|
|
4700
|
-
} catch {
|
|
4701
4790
|
}
|
|
4791
|
+
} catch {
|
|
4792
|
+
} finally {
|
|
4793
|
+
release();
|
|
4702
4794
|
}
|
|
4703
4795
|
return n;
|
|
4704
4796
|
}
|
|
4797
|
+
/** A sink's cursor into a session file about to be pruned (`gone`), or one already
|
|
4798
|
+
* missing (an earlier prune, an older release, by hand) is removed. The caller
|
|
4799
|
+
* holds the `flush` lock. */
|
|
4800
|
+
dropStaleCursors(gone) {
|
|
4801
|
+
let names = [];
|
|
4802
|
+
try {
|
|
4803
|
+
names = readdirSync6(path16.join(this.root, "cursors")).filter((n) => n.endsWith(".json"));
|
|
4804
|
+
} catch {
|
|
4805
|
+
}
|
|
4806
|
+
const stale = (c) => Object.keys(c).filter((k) => {
|
|
4807
|
+
if (gone.has(k)) return true;
|
|
4808
|
+
const i = k.indexOf("/");
|
|
4809
|
+
return !existsSync9(this.file({ harness: k.slice(0, i), session: k.slice(i + 1) }));
|
|
4810
|
+
});
|
|
4811
|
+
for (const n of names) {
|
|
4812
|
+
const s = new Cursors(this.root, n.slice(0, -5));
|
|
4813
|
+
const c = s.read();
|
|
4814
|
+
const keys = stale(c);
|
|
4815
|
+
if (!keys.length) continue;
|
|
4816
|
+
for (const k of keys) delete c[k];
|
|
4817
|
+
s.write(c);
|
|
4818
|
+
}
|
|
4819
|
+
}
|
|
4705
4820
|
delete(ref) {
|
|
4821
|
+
this.dropCursors(ref);
|
|
4706
4822
|
for (const f of [this.file(ref), this.seqFile(ref)]) {
|
|
4707
4823
|
try {
|
|
4708
|
-
|
|
4824
|
+
unlinkSync2(f);
|
|
4709
4825
|
} catch {
|
|
4710
4826
|
}
|
|
4711
4827
|
}
|
|
4712
4828
|
}
|
|
4829
|
+
/** The key every sink's cursor holds for a session is removed with the session's
|
|
4830
|
+
* file: the key alone pointed into the old file, and a session that went on (still
|
|
4831
|
+
* running, or resumed later) had its new file's first events unsent until it grew
|
|
4832
|
+
* past that offset. Every file under <state>/cursors is taken (a sink since taken
|
|
4833
|
+
* out of the config still has one; one that doesn't parse reads as empty and is
|
|
4834
|
+
* left), by the key a drain writes (the file's own name, `safeId`) and by the ref
|
|
4835
|
+
* as given. The caller holds the drain's `flush` lock: a drain holds the whole
|
|
4836
|
+
* cursor map and writes it back, so a key dropped under it would come back. */
|
|
4837
|
+
dropCursors(ref) {
|
|
4838
|
+
const keys = /* @__PURE__ */ new Set([`${ref.harness}/${ref.session}`, `${safeId(ref.harness)}/${safeId(ref.session)}`]);
|
|
4839
|
+
let names = [];
|
|
4840
|
+
try {
|
|
4841
|
+
names = readdirSync6(path16.join(this.root, "cursors")).filter((n) => n.endsWith(".json"));
|
|
4842
|
+
} catch {
|
|
4843
|
+
return;
|
|
4844
|
+
}
|
|
4845
|
+
for (const n of names) {
|
|
4846
|
+
const s = new Cursors(this.root, n.slice(0, -5));
|
|
4847
|
+
const c = s.read();
|
|
4848
|
+
const had = Object.keys(c).filter((k) => keys.has(k));
|
|
4849
|
+
if (!had.length) continue;
|
|
4850
|
+
for (const k of had) delete c[k];
|
|
4851
|
+
s.write(c);
|
|
4852
|
+
}
|
|
4853
|
+
}
|
|
4713
4854
|
};
|
|
4714
4855
|
var Cursors = class {
|
|
4715
4856
|
constructor(root, sink) {
|
|
@@ -4721,18 +4862,21 @@ var Cursors = class {
|
|
|
4721
4862
|
file() {
|
|
4722
4863
|
return path16.join(this.root, "cursors", `${safeId(this.sink)}.json`);
|
|
4723
4864
|
}
|
|
4865
|
+
/** The map; empty when the file is missing, cut off mid-write, or holds anything
|
|
4866
|
+
* but an object (`null`, a list), so a drain never indexes into those. */
|
|
4724
4867
|
read() {
|
|
4725
4868
|
try {
|
|
4726
|
-
|
|
4869
|
+
const c = JSON.parse(readFileSync11(this.file(), "utf8"));
|
|
4870
|
+
return c && typeof c === "object" && !Array.isArray(c) ? c : {};
|
|
4727
4871
|
} catch {
|
|
4728
4872
|
return {};
|
|
4729
4873
|
}
|
|
4730
4874
|
}
|
|
4731
4875
|
write(c) {
|
|
4732
4876
|
const f = this.file();
|
|
4733
|
-
|
|
4877
|
+
mkdirSync3(path16.dirname(f), { recursive: true, mode: 448 });
|
|
4734
4878
|
const tmp = `${f}.${process.pid}.tmp`;
|
|
4735
|
-
|
|
4879
|
+
writeFileSync3(tmp, JSON.stringify(c), { mode: 384 });
|
|
4736
4880
|
renameSync(tmp, f);
|
|
4737
4881
|
}
|
|
4738
4882
|
key(ref) {
|
|
@@ -4744,25 +4888,54 @@ function takeLock(root, ref, staleMs = 5 * 6e4) {
|
|
|
4744
4888
|
}
|
|
4745
4889
|
function takeNamedLock(root, name, staleMs = 5 * 6e4) {
|
|
4746
4890
|
const dir = path16.join(root, "locks");
|
|
4747
|
-
|
|
4891
|
+
mkdirSync3(dir, { recursive: true, mode: 448 });
|
|
4748
4892
|
const f = path16.join(dir, `${name}.lock`);
|
|
4893
|
+
const nonce = randomBytes3(8).toString("hex");
|
|
4894
|
+
const mine = `${process.pid} ${nonce}`;
|
|
4895
|
+
let body = null;
|
|
4749
4896
|
try {
|
|
4750
|
-
|
|
4751
|
-
|
|
4897
|
+
body = readFileSync11(f, "utf8");
|
|
4898
|
+
const stale = Date.now() - statSync6(f).mtimeMs > staleMs || !processAlive(Number(body.split(" ")[0]));
|
|
4899
|
+
if (!stale) return null;
|
|
4752
4900
|
} catch {
|
|
4901
|
+
body = null;
|
|
4902
|
+
}
|
|
4903
|
+
if (body !== null) {
|
|
4904
|
+
const aside = `${f}.${process.pid}.${nonce}.stale`;
|
|
4905
|
+
try {
|
|
4906
|
+
renameSync(f, aside);
|
|
4907
|
+
} catch {
|
|
4908
|
+
return null;
|
|
4909
|
+
}
|
|
4910
|
+
const moved = readFileSync11(aside, "utf8");
|
|
4911
|
+
unlinkSync2(aside);
|
|
4912
|
+
if (moved !== body) {
|
|
4913
|
+
try {
|
|
4914
|
+
writeFileSync3(f, moved, { flag: "wx" });
|
|
4915
|
+
} catch {
|
|
4916
|
+
}
|
|
4917
|
+
return null;
|
|
4918
|
+
}
|
|
4753
4919
|
}
|
|
4754
4920
|
try {
|
|
4755
|
-
|
|
4921
|
+
writeFileSync3(f, mine, { flag: "wx" });
|
|
4756
4922
|
} catch {
|
|
4757
4923
|
return null;
|
|
4758
4924
|
}
|
|
4759
4925
|
return () => {
|
|
4760
4926
|
try {
|
|
4761
|
-
|
|
4927
|
+
if (readFileSync11(f, "utf8") === mine) unlinkSync2(f);
|
|
4762
4928
|
} catch {
|
|
4763
4929
|
}
|
|
4764
4930
|
};
|
|
4765
4931
|
}
|
|
4932
|
+
async function waitNamedLock(root, name, forMs, everyMs = 100) {
|
|
4933
|
+
for (let waited = 0; ; waited += everyMs) {
|
|
4934
|
+
const release = takeNamedLock(root, name);
|
|
4935
|
+
if (release || waited >= forMs) return release;
|
|
4936
|
+
await new Promise((r) => setTimeout(r, everyMs));
|
|
4937
|
+
}
|
|
4938
|
+
}
|
|
4766
4939
|
function processAlive(pid) {
|
|
4767
4940
|
if (!Number.isInteger(pid) || pid <= 0) return true;
|
|
4768
4941
|
try {
|
|
@@ -4777,7 +4950,7 @@ function processAlive(pid) {
|
|
|
4777
4950
|
import { createHash as createHash4 } from "node:crypto";
|
|
4778
4951
|
var RULESET_PII = "pii@1";
|
|
4779
4952
|
var EMAIL = /\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}\b/g;
|
|
4780
|
-
var PHONE = /(?<![\w.])(?:\+\d{1,3}[ .-]?)?(?:\(\d{2,4}\)[ .-]?)?\d{2,4}(?:[ .-]?\d{2,4}){1,3}(?!\w)/g;
|
|
4953
|
+
var PHONE = /(?<![\w.])(?:(?<!\)[ .-]?|\+\d{1,3}[ .-]?)(\+?(?:19|20)\d{2}-(?:0[1-9]|1[0-2])-(?:0[1-9]|[12]\d|3[01]))(?:(?= (?:[01]?\d|2[0-3]):)|(?!\w|[ .-]\d\d))|(?:\+\d{1,3}[ .-]?)?(?:\(\d{2,4}\)[ .-]?)?\d{2,4}(?:[ .-]?\d{2,4}){1,3}(?!\w))/g;
|
|
4781
4954
|
function looksPhone(s) {
|
|
4782
4955
|
const digits = s.replace(/\D/g, "");
|
|
4783
4956
|
if (digits.length < 7 || digits.length > 15) return false;
|
|
@@ -4788,7 +4961,7 @@ function looksPhone(s) {
|
|
|
4788
4961
|
function reducePii(text, home) {
|
|
4789
4962
|
if (!text) return text;
|
|
4790
4963
|
let out = text.replace(EMAIL, "[email]");
|
|
4791
|
-
out = out.replace(PHONE, (m) => looksPhone(m) ? "[phone]" : m);
|
|
4964
|
+
out = out.replace(PHONE, (m, date) => !date && looksPhone(m) ? "[phone]" : m);
|
|
4792
4965
|
if (home) {
|
|
4793
4966
|
const h = home.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
4794
4967
|
out = out.replace(new RegExp(`(?<![A-Za-z0-9_])${h}(?=/|$|\\s|["'\`])`, "g"), "~");
|
|
@@ -5005,7 +5178,7 @@ var DATA_ALLOW = {
|
|
|
5005
5178
|
"tool.started": { 1: ["tool", "call_id", "turn_id", "agent_id"], 3: ["input"] },
|
|
5006
5179
|
"tool.ended": { 1: ["tool", "call_id", "turn_id", "agent_id", "ms", "ok", "error"], 3: ["input", "output"] },
|
|
5007
5180
|
"files.changed": { 1: ["paths"] },
|
|
5008
|
-
"attention.needed": { 0: ["attention_id", "kind"], 1: ["tool", "message"] },
|
|
5181
|
+
"attention.needed": { 0: ["attention_id", "kind"], 1: ["tool", "message", "agent_id"] },
|
|
5009
5182
|
"attention.cleared": { 0: ["attention_id", "how"] },
|
|
5010
5183
|
"subagent.started": { 1: ["agent_id", "agent_type"] },
|
|
5011
5184
|
"subagent.ended": { 1: ["agent_id", "agent_type"] },
|
|
@@ -5096,7 +5269,7 @@ function shortId(harness, id) {
|
|
|
5096
5269
|
}
|
|
5097
5270
|
|
|
5098
5271
|
// src/sinks/file.ts
|
|
5099
|
-
import { appendFileSync as appendFileSync2, mkdirSync as
|
|
5272
|
+
import { appendFileSync as appendFileSync2, mkdirSync as mkdirSync4 } from "node:fs";
|
|
5100
5273
|
import path17 from "node:path";
|
|
5101
5274
|
var FileSink = class {
|
|
5102
5275
|
constructor(name, file, tier, pii) {
|
|
@@ -5111,7 +5284,7 @@ var FileSink = class {
|
|
|
5111
5284
|
pii;
|
|
5112
5285
|
async send(events) {
|
|
5113
5286
|
try {
|
|
5114
|
-
|
|
5287
|
+
mkdirSync4(path17.dirname(this.file), { recursive: true });
|
|
5115
5288
|
appendFileSync2(this.file, `${events.map((e) => JSON.stringify(e)).join("\n")}
|
|
5116
5289
|
`);
|
|
5117
5290
|
return { ok: true, acknowledged: events.length, accepted: events.length, duplicates: 0, rejected: [] };
|
|
@@ -5171,6 +5344,7 @@ export {
|
|
|
5171
5344
|
antigravity,
|
|
5172
5345
|
antigravityAnswer,
|
|
5173
5346
|
antigravity_control_exports as antigravityControl,
|
|
5347
|
+
assertConfigWritable,
|
|
5174
5348
|
capTail,
|
|
5175
5349
|
claudeAccounts,
|
|
5176
5350
|
claudeCode,
|
|
@@ -5224,6 +5398,7 @@ export {
|
|
|
5224
5398
|
tilde,
|
|
5225
5399
|
ulid,
|
|
5226
5400
|
untilde,
|
|
5401
|
+
waitNamedLock,
|
|
5227
5402
|
withoutLineage,
|
|
5228
5403
|
writeConfig
|
|
5229
5404
|
};
|