sessionpipe 0.7.0 → 0.9.0
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 +58 -4
- package/dist/cli.d.ts +11 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +2055 -691
- package/dist/cli.js.map +4 -4
- package/dist/connect.d.ts.map +1 -1
- package/dist/control/aliases.d.ts.map +1 -1
- package/dist/control/autoupdate.d.ts.map +1 -1
- package/dist/control/cli.d.ts +8 -1
- package/dist/control/cli.d.ts.map +1 -1
- package/dist/control/daemon.d.ts +16 -1
- package/dist/control/daemon.d.ts.map +1 -1
- package/dist/control/files.d.ts +33 -0
- package/dist/control/files.d.ts.map +1 -0
- package/dist/control/pair.d.ts +7 -0
- package/dist/control/pair.d.ts.map +1 -1
- package/dist/control/route.d.ts.map +1 -1
- package/dist/control/store.d.ts.map +1 -1
- package/dist/control/stream.d.ts.map +1 -1
- package/dist/help.d.ts +3 -0
- package/dist/help.d.ts.map +1 -0
- package/dist/hook.d.ts +9 -1
- package/dist/hook.d.ts.map +1 -1
- package/dist/hook.js +184 -158
- package/dist/hook.js.map +3 -3
- package/dist/http-sink.d.ts +3 -0
- package/dist/http-sink.d.ts.map +1 -1
- package/dist/index.js +893 -198
- package/dist/index.js.map +4 -4
- package/dist/launcher.d.ts.map +1 -1
- package/dist/run.d.ts +70 -8
- package/dist/run.d.ts.map +1 -1
- package/dist/runtime.d.ts +5 -1
- package/dist/runtime.d.ts.map +1 -1
- package/dist/secrets.d.ts.map +1 -1
- package/dist/update.d.ts +3 -2
- package/dist/update.d.ts.map +1 -1
- package/dist/worker.d.ts +3 -1
- package/dist/worker.d.ts.map +1 -1
- package/dist/worker.js +983 -249
- package/dist/worker.js.map +4 -4
- package/package.json +5 -4
package/dist/cli.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
// sessionpipe 0.
|
|
2
|
+
// sessionpipe 0.9.0 · Apache-2.0 · https://sessionpipe.org
|
|
3
3
|
var __defProp = Object.defineProperty;
|
|
4
4
|
var __export = (target, all) => {
|
|
5
5
|
for (var name in all)
|
|
@@ -7,10 +7,19 @@ var __export = (target, all) => {
|
|
|
7
7
|
};
|
|
8
8
|
|
|
9
9
|
// src/cli.ts
|
|
10
|
-
import { execFileSync as
|
|
11
|
-
import {
|
|
10
|
+
import { execFileSync as execFileSync8, spawnSync as spawnSync2 } from "node:child_process";
|
|
11
|
+
import {
|
|
12
|
+
existsSync as existsSync17,
|
|
13
|
+
mkdtempSync,
|
|
14
|
+
readdirSync as readdirSync8,
|
|
15
|
+
readFileSync as readFileSync16,
|
|
16
|
+
realpathSync as realpathSync7,
|
|
17
|
+
rmSync as rmSync3,
|
|
18
|
+
statSync as statSync11,
|
|
19
|
+
unlinkSync as unlinkSync7
|
|
20
|
+
} from "node:fs";
|
|
12
21
|
import os11 from "node:os";
|
|
13
|
-
import
|
|
22
|
+
import path29 from "node:path";
|
|
14
23
|
import { fileURLToPath } from "node:url";
|
|
15
24
|
|
|
16
25
|
// ../core/src/adapters/antigravity.ts
|
|
@@ -29,20 +38,20 @@ import path4 from "node:path";
|
|
|
29
38
|
// ../core/src/paths.ts
|
|
30
39
|
import os from "node:os";
|
|
31
40
|
import path from "node:path";
|
|
32
|
-
var
|
|
41
|
+
var homeDir = () => os.homedir();
|
|
33
42
|
function configFile(env = process.env) {
|
|
34
43
|
if (env.SESSIONPIPE_CONFIG) return path.resolve(env.SESSIONPIPE_CONFIG);
|
|
35
44
|
if (process.platform === "win32")
|
|
36
|
-
return path.join(env.APPDATA || path.join(
|
|
37
|
-
return path.join(env.XDG_CONFIG_HOME || path.join(
|
|
45
|
+
return path.join(env.APPDATA || path.join(homeDir(), "AppData", "Roaming"), "sessionpipe", "config.json");
|
|
46
|
+
return path.join(env.XDG_CONFIG_HOME || path.join(homeDir(), ".config"), "sessionpipe", "config.json");
|
|
38
47
|
}
|
|
39
48
|
function stateDir(env = process.env) {
|
|
40
49
|
if (env.SESSIONPIPE_STATE) return path.resolve(env.SESSIONPIPE_STATE);
|
|
41
50
|
if (process.platform === "win32")
|
|
42
|
-
return path.join(env.LOCALAPPDATA || path.join(
|
|
43
|
-
return path.join(env.XDG_STATE_HOME || path.join(
|
|
51
|
+
return path.join(env.LOCALAPPDATA || path.join(homeDir(), "AppData", "Local"), "sessionpipe");
|
|
52
|
+
return path.join(env.XDG_STATE_HOME || path.join(homeDir(), ".local", "state"), "sessionpipe");
|
|
44
53
|
}
|
|
45
|
-
function tilde(p, home =
|
|
54
|
+
function tilde(p, home = homeDir()) {
|
|
46
55
|
if (!p) return p;
|
|
47
56
|
if (p === home) return "~";
|
|
48
57
|
return p.startsWith(home + path.sep) || p.startsWith(`${home}/`) ? `~${p.slice(home.length)}` : p;
|
|
@@ -139,6 +148,19 @@ function recentFiles(root, depth, sinceMs, match) {
|
|
|
139
148
|
return out2;
|
|
140
149
|
}
|
|
141
150
|
|
|
151
|
+
// ../core/src/adapters/codex-rollout.ts
|
|
152
|
+
function codexSubagent(meta) {
|
|
153
|
+
if (meta?.type !== "session_meta") return false;
|
|
154
|
+
const p = meta.payload;
|
|
155
|
+
return !!p?.source?.subagent;
|
|
156
|
+
}
|
|
157
|
+
function codexReplayBoundary(meta) {
|
|
158
|
+
if (meta?.type !== "session_meta") return void 0;
|
|
159
|
+
const p = meta.payload;
|
|
160
|
+
if (!p || !p.forked_from_id && !p.source?.subagent) return void 0;
|
|
161
|
+
return (record) => typeof p.id === "string" && !!p.id && record?.type === "event_msg" && record.payload?.thread_id === p.id;
|
|
162
|
+
}
|
|
163
|
+
|
|
142
164
|
// ../core/src/readers/transcripts.ts
|
|
143
165
|
function pairTurns(msgs, endLine) {
|
|
144
166
|
const turns = [];
|
|
@@ -161,10 +183,14 @@ ${m.text}`;
|
|
|
161
183
|
}
|
|
162
184
|
if (cur?.assistant) turns.push(cur);
|
|
163
185
|
const line = cur && !cur.assistant ? cur.startLine : endLine;
|
|
164
|
-
|
|
186
|
+
const out2 = {
|
|
187
|
+
turns: turns.map((t) => ({ user: t.user, assistant: t.assistant, at: t.at, endLine: t.endLine })),
|
|
188
|
+
line
|
|
189
|
+
};
|
|
190
|
+
if (cur && !cur.assistant && cur.user) out2.open = { user: cur.user, at: cur.at };
|
|
191
|
+
return out2;
|
|
165
192
|
}
|
|
166
|
-
var isInjected = (text) => /^<[a-z_-]
|
|
167
|
-
var endOf = (lines) => lines[lines.length - 1] === "" ? lines.length - 1 : lines.length;
|
|
193
|
+
var isInjected = (text) => /^<[a-z_-]+(?:\s[^>]*)?>/i.test(text) && /<\/[a-z_-]+>\s*$/i.test(text) || /^<!--\s/.test(text) || /^<command-/.test(text);
|
|
168
194
|
var parse = (raw) => {
|
|
169
195
|
try {
|
|
170
196
|
return JSON.parse(raw);
|
|
@@ -172,6 +198,11 @@ var parse = (raw) => {
|
|
|
172
198
|
return null;
|
|
173
199
|
}
|
|
174
200
|
};
|
|
201
|
+
var endOf = (lines) => {
|
|
202
|
+
const last = lines[lines.length - 1];
|
|
203
|
+
if (last === void 0) return 0;
|
|
204
|
+
return last === "" || parse(last) === null ? lines.length - 1 : lines.length;
|
|
205
|
+
};
|
|
175
206
|
var textParts = (c, kinds) => {
|
|
176
207
|
if (typeof c === "string") return c;
|
|
177
208
|
if (!Array.isArray(c)) return "";
|
|
@@ -179,13 +210,21 @@ var textParts = (c, kinds) => {
|
|
|
179
210
|
(b) => b && typeof b === "object" && kinds.includes(b.type ?? "") && typeof b.text === "string"
|
|
180
211
|
).map((b) => b.text).join("\n");
|
|
181
212
|
};
|
|
213
|
+
var queuedPrompt = (a) => {
|
|
214
|
+
if (!a || typeof a !== "object") return "";
|
|
215
|
+
const q = a;
|
|
216
|
+
if (q.type !== "queued_command" || (q.commandMode ?? "prompt") !== "prompt") return "";
|
|
217
|
+
if (q.origin?.kind && q.origin.kind !== "human") return "";
|
|
218
|
+
return textParts(q.prompt, ["text"]).trim();
|
|
219
|
+
};
|
|
182
220
|
function readClaude(lines, from) {
|
|
183
221
|
const msgs = [];
|
|
184
222
|
for (let i = from; i < lines.length; i++) {
|
|
185
223
|
const j = lines[i] ? parse(lines[i]) : null;
|
|
186
224
|
if (!j || j.isSidechain) continue;
|
|
187
225
|
const origin = j.origin;
|
|
188
|
-
if (j.type === "user" && (j.isMeta || origin?.kind && origin.kind !== "human"))
|
|
226
|
+
if (j.type === "user" && (j.isMeta || j.isCompactSummary || j.repeatsCompactSummary || origin?.kind && origin.kind !== "human"))
|
|
227
|
+
continue;
|
|
189
228
|
const at = Date.parse(String(j.timestamp ?? "")) || Date.now();
|
|
190
229
|
const c = j.message?.content;
|
|
191
230
|
if (j.type === "user") {
|
|
@@ -195,6 +234,9 @@ function readClaude(lines, from) {
|
|
|
195
234
|
} else if (j.type === "assistant" && Array.isArray(c)) {
|
|
196
235
|
const text = textParts(c, ["text"]).trim();
|
|
197
236
|
if (text) msgs.push({ role: "assistant", text, at, line: i });
|
|
237
|
+
} else if (j.type === "attachment") {
|
|
238
|
+
const text = queuedPrompt(j.attachment);
|
|
239
|
+
if (text && !isInjected(text)) msgs.push({ role: "user", text, at, line: i });
|
|
198
240
|
}
|
|
199
241
|
}
|
|
200
242
|
return pairTurns(msgs, endOf(lines));
|
|
@@ -204,7 +246,10 @@ function codexInjected(text) {
|
|
|
204
246
|
}
|
|
205
247
|
function readCodex(lines, from) {
|
|
206
248
|
const msgs = [];
|
|
207
|
-
|
|
249
|
+
const boundary = codexReplayBoundary(parse(lines.find((line) => line.trim()) ?? ""));
|
|
250
|
+
const start = boundary ? lines.findIndex((line) => boundary(parse(line))) : 0;
|
|
251
|
+
if (start < 0) return pairTurns(msgs, endOf(lines));
|
|
252
|
+
for (let i = Math.max(from, start); i < lines.length; i++) {
|
|
208
253
|
const j = lines[i] ? parse(lines[i]) : null;
|
|
209
254
|
if (!j || j.type !== "response_item") continue;
|
|
210
255
|
const p = j.payload;
|
|
@@ -242,7 +287,7 @@ function readAntigravity(lines, from) {
|
|
|
242
287
|
const at = Date.parse(String(j.created_at ?? j.timestamp ?? "")) || Date.now();
|
|
243
288
|
if (j.type === "USER_INPUT") {
|
|
244
289
|
const m = /<USER_REQUEST>([\s\S]*?)(?:<\/USER_REQUEST>|$)/.exec(j.content);
|
|
245
|
-
const text = (m ? m[1]
|
|
290
|
+
const text = (m ? m[1] : j.content).trim();
|
|
246
291
|
if (text) msgs.push({ role: "user", text, at, line: i });
|
|
247
292
|
} else if (j.type === "PLANNER_RESPONSE") {
|
|
248
293
|
const text = j.content.trim();
|
|
@@ -290,14 +335,18 @@ function formatJson(value, f) {
|
|
|
290
335
|
const text = JSON.stringify(value, null, f.indent).replace(/\n/g, f.eol);
|
|
291
336
|
return f.finalNewline ? text + f.eol : text;
|
|
292
337
|
}
|
|
293
|
-
function
|
|
338
|
+
function readJson(file) {
|
|
339
|
+
const f = readJsonFile(file);
|
|
340
|
+
return f.ok ? f.value : {};
|
|
341
|
+
}
|
|
342
|
+
function entryFor(event, harness, cmd, o) {
|
|
294
343
|
const matcher = o.matcherFor(event);
|
|
295
344
|
return {
|
|
296
345
|
...matcher !== void 0 ? { matcher } : {},
|
|
297
|
-
hooks: [{ type: "command", command:
|
|
346
|
+
hooks: [{ type: "command", command: cmd([harness, event]).command, timeout: o.timeoutFor?.(event) ?? o.timeout }]
|
|
298
347
|
};
|
|
299
348
|
}
|
|
300
|
-
function installClaudeShaped(file, harness, events,
|
|
349
|
+
function installClaudeShaped(file, harness, events, cmd, o) {
|
|
301
350
|
const f = readJsonFile(file);
|
|
302
351
|
if (!f.ok) return { file, changed: false, skipped: true, note: `${f.reason}; not touched` };
|
|
303
352
|
const settings2 = f.value;
|
|
@@ -316,7 +365,7 @@ function installClaudeShaped(file, harness, events, cmd2, o) {
|
|
|
316
365
|
}
|
|
317
366
|
for (const ev of events) {
|
|
318
367
|
const list = Array.isArray(hooks[ev]) ? hooks[ev] : [];
|
|
319
|
-
const entry2 = entryFor(ev, harness,
|
|
368
|
+
const entry2 = entryFor(ev, harness, cmd, o);
|
|
320
369
|
const mine = list.filter((h) => isOurs(h));
|
|
321
370
|
if (mine.length === 1 && JSON.stringify(mine[0]) === JSON.stringify(entry2)) continue;
|
|
322
371
|
hooks[ev] = list.filter((h) => !isOurs(h)).concat([entry2]);
|
|
@@ -359,11 +408,10 @@ function uninstallClaudeShaped(file, events, opts = {}) {
|
|
|
359
408
|
unlinkSync(file);
|
|
360
409
|
return { file, changed: true, note: "removed (sessionpipe created it)" };
|
|
361
410
|
}
|
|
362
|
-
|
|
363
|
-
if (next !== f.raw) writeFileSync(file, next);
|
|
411
|
+
writeFileSync(file, formatJson(settings2, f));
|
|
364
412
|
return { file, changed: true };
|
|
365
413
|
}
|
|
366
|
-
function installedClaudeShaped(file, harness, events,
|
|
414
|
+
function installedClaudeShaped(file, harness, events, cmd, o) {
|
|
367
415
|
const f = readJsonFile(file);
|
|
368
416
|
const hooks = f.ok ? f.value.hooks ?? {} : {};
|
|
369
417
|
let present = 0;
|
|
@@ -372,7 +420,7 @@ function installedClaudeShaped(file, harness, events, cmd2, o) {
|
|
|
372
420
|
const list = Array.isArray(hooks[ev]) ? hooks[ev] : [];
|
|
373
421
|
const mine = list.filter((h) => isOurs(h));
|
|
374
422
|
if (mine.length) present++;
|
|
375
|
-
if (mine.length === 1 && JSON.stringify(mine[0]) === JSON.stringify(entryFor(ev, harness,
|
|
423
|
+
if (mine.length === 1 && JSON.stringify(mine[0]) === JSON.stringify(entryFor(ev, harness, cmd, o))) current++;
|
|
376
424
|
}
|
|
377
425
|
for (const ev of Object.keys(hooks))
|
|
378
426
|
if (!events.includes(ev) && (Array.isArray(hooks[ev]) ? hooks[ev] : []).some((h) => isOurs(h)))
|
|
@@ -387,14 +435,14 @@ var HOOK_NAME = "sessionpipe";
|
|
|
387
435
|
var AG_EVENTS = ["PreInvocation", "PostInvocation", "PostToolUse", "Stop"];
|
|
388
436
|
var AG_STALE_PRETOOL = '{"decision":"ask","reason":"sessionpipe: an outdated PreToolUse hook is still loaded. Restart this conversation (or run sessionpipe install) to drop it."}\n';
|
|
389
437
|
var antigravityAnswer = (event) => event === "PreToolUse" ? AG_STALE_PRETOOL : "{}\n";
|
|
390
|
-
var gemini = (env) => env.GEMINI_CLI_HOME || path4.join(
|
|
438
|
+
var gemini = (env) => env.GEMINI_CLI_HOME || path4.join(homeDir(), ".gemini");
|
|
391
439
|
var hooksFile = (env) => path4.join(gemini(env), "config", "hooks.json");
|
|
392
440
|
var projectsDir = (env) => path4.join(gemini(env), "config", "projects");
|
|
393
441
|
var dataDirs = (env) => ["antigravity", "antigravity-cli", "antigravity-ide"].map((d) => path4.join(gemini(env), d));
|
|
394
442
|
var ID_RE = /^[A-Za-z0-9_-]{1,128}$/;
|
|
395
443
|
var validTool = (n) => typeof n === "string" && /^[A-Za-z0-9_.:-]{1,120}$/.test(n);
|
|
396
|
-
function entry(
|
|
397
|
-
const handler = (e) => ({ type: "command", command:
|
|
444
|
+
function entry(cmd) {
|
|
445
|
+
const handler = (e) => ({ type: "command", command: cmd([NAME, e]).command, timeout: 5 });
|
|
398
446
|
const out2 = { enabled: true };
|
|
399
447
|
for (const e of AG_EVENTS) out2[e] = /Tool/.test(e) ? [{ matcher: "*", hooks: [handler(e)] }] : [handler(e)];
|
|
400
448
|
return out2;
|
|
@@ -404,12 +452,12 @@ var antigravity = {
|
|
|
404
452
|
events: AG_EVENTS,
|
|
405
453
|
detect: (env = process.env) => existsSync2(path4.join(gemini(env), "config")) || dataDirs(env).some((d) => existsSync2(d)),
|
|
406
454
|
configFiles: (env = process.env) => [hooksFile(env)],
|
|
407
|
-
install(
|
|
455
|
+
install(cmd, env = process.env) {
|
|
408
456
|
const file = hooksFile(env);
|
|
409
457
|
const f = readJsonFile(file);
|
|
410
458
|
if (!f.ok) return [{ file, changed: false, skipped: true, note: `${f.reason}; not touched` }];
|
|
411
459
|
const hooks = f.value;
|
|
412
|
-
const e = entry(
|
|
460
|
+
const e = entry(cmd);
|
|
413
461
|
if (JSON.stringify(hooks[HOOK_NAME]) === JSON.stringify(e)) return [{ file, changed: false }];
|
|
414
462
|
hooks[HOOK_NAME] = e;
|
|
415
463
|
mkdirSync2(path4.dirname(file), { recursive: true });
|
|
@@ -438,14 +486,14 @@ var antigravity = {
|
|
|
438
486
|
writeFileSync2(file, formatJson(hooks, f));
|
|
439
487
|
return [{ file, changed: true }];
|
|
440
488
|
},
|
|
441
|
-
installed(
|
|
489
|
+
installed(cmd, env = process.env) {
|
|
442
490
|
const file = hooksFile(env);
|
|
443
491
|
const f = readJsonFile(file);
|
|
444
492
|
const have = f.ok ? f.value[HOOK_NAME] : void 0;
|
|
445
493
|
return [
|
|
446
494
|
{
|
|
447
495
|
file,
|
|
448
|
-
state: !have ? "missing" : JSON.stringify(have) === JSON.stringify(entry(
|
|
496
|
+
state: !have ? "missing" : JSON.stringify(have) === JSON.stringify(entry(cmd)) ? "current" : "stale"
|
|
449
497
|
}
|
|
450
498
|
];
|
|
451
499
|
},
|
|
@@ -453,7 +501,8 @@ var antigravity = {
|
|
|
453
501
|
const event = input.argv[1] ?? "";
|
|
454
502
|
let s = {};
|
|
455
503
|
try {
|
|
456
|
-
|
|
504
|
+
const j = input.stdin ? JSON.parse(input.stdin) : {};
|
|
505
|
+
if (j && typeof j === "object" && !Array.isArray(j)) s = j;
|
|
457
506
|
} catch {
|
|
458
507
|
}
|
|
459
508
|
const id = s.conversationId ?? s.session_id ?? s.sessionId;
|
|
@@ -509,19 +558,16 @@ var antigravity = {
|
|
|
509
558
|
}
|
|
510
559
|
return readAntigravity(text.split("\n"), fromLine);
|
|
511
560
|
},
|
|
512
|
-
facts(session, transcript, hints
|
|
561
|
+
facts(session, transcript, hints) {
|
|
513
562
|
if (!transcript) return {};
|
|
514
563
|
return antigravityFacts(transcript, {
|
|
515
564
|
conversation: session.id,
|
|
516
565
|
workspaces: hints.workspaces ?? (session.cwd ? [session.cwd] : []),
|
|
517
|
-
state: state2,
|
|
518
566
|
env: process.env
|
|
519
567
|
});
|
|
520
568
|
},
|
|
521
569
|
backfill(sinceMs, env = process.env) {
|
|
522
|
-
const
|
|
523
|
-
const noState = { get: () => ({}), save: () => {
|
|
524
|
-
} };
|
|
570
|
+
const rows2 = [];
|
|
525
571
|
for (const data of dataDirs(env)) {
|
|
526
572
|
let ids = [];
|
|
527
573
|
try {
|
|
@@ -535,13 +581,13 @@ var antigravity = {
|
|
|
535
581
|
} catch {
|
|
536
582
|
continue;
|
|
537
583
|
}
|
|
538
|
-
const facts = antigravityFacts(f, { conversation: id, workspaces: [],
|
|
584
|
+
const facts = antigravityFacts(f, { conversation: id, workspaces: [], env });
|
|
539
585
|
if (!facts.started_at || !facts.last_at) continue;
|
|
540
586
|
const { started_at, last_at, first_ask: _fa, harness_version: _hv, ...rest } = facts;
|
|
541
|
-
|
|
587
|
+
rows2.push({ session: { id, ...rest }, started_at, last_at });
|
|
542
588
|
}
|
|
543
589
|
}
|
|
544
|
-
return
|
|
590
|
+
return rows2;
|
|
545
591
|
}
|
|
546
592
|
};
|
|
547
593
|
function prune(o) {
|
|
@@ -591,7 +637,7 @@ function agProjects(dir) {
|
|
|
591
637
|
}
|
|
592
638
|
const folders = [];
|
|
593
639
|
for (const r of j?.projectResources?.resources ?? []) {
|
|
594
|
-
const f = agPath(r?.folderUri);
|
|
640
|
+
const f = agPath(r?.folderUri ?? r?.gitFolder?.folderUri);
|
|
595
641
|
if (f) folders.push(...agVariants(f));
|
|
596
642
|
}
|
|
597
643
|
if (folders.length) out2.push({ id, folders });
|
|
@@ -645,7 +691,7 @@ function antigravityFacts(file, opts) {
|
|
|
645
691
|
for (const j of H) {
|
|
646
692
|
if (j.type !== "USER_INPUT" || typeof j.content !== "string") continue;
|
|
647
693
|
const m = /<USER_REQUEST>([\s\S]*?)(?:<\/USER_REQUEST>|$)/.exec(j.content);
|
|
648
|
-
const text = (m ? m[1]
|
|
694
|
+
const text = (m ? m[1] : j.content).trim();
|
|
649
695
|
const line = (text.split("\n").find((l) => l.trim()) ?? "").trim();
|
|
650
696
|
if (!line || /^<[a-z_-]+>/i.test(line)) continue;
|
|
651
697
|
out2.title = line.length > 80 ? `${line.slice(0, 80).replace(/\s+\S*$/, "")}\u2026` : line;
|
|
@@ -744,7 +790,7 @@ function runHeadless(bin, args2, o) {
|
|
|
744
790
|
if (o.onLine) {
|
|
745
791
|
const chunk = partial + b.toString("utf8");
|
|
746
792
|
const lines = chunk.split("\n");
|
|
747
|
-
partial = lines.pop()
|
|
793
|
+
partial = lines.pop();
|
|
748
794
|
for (const line of lines) {
|
|
749
795
|
if (!line.trim()) continue;
|
|
750
796
|
try {
|
|
@@ -893,7 +939,7 @@ function agyError(stderr) {
|
|
|
893
939
|
for (const k of ["message", "error", "detail"]) if (typeof j[k] === "string" && j[k]) return j[k];
|
|
894
940
|
} catch {
|
|
895
941
|
}
|
|
896
|
-
return raw.split("\n")[0]
|
|
942
|
+
return raw.split("\n")[0].slice(0, 300);
|
|
897
943
|
}
|
|
898
944
|
var AgyReading = class {
|
|
899
945
|
session = null;
|
|
@@ -970,10 +1016,9 @@ function gitFacts(cwd) {
|
|
|
970
1016
|
return out2;
|
|
971
1017
|
}
|
|
972
1018
|
function stripCredentials(url) {
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
return u.replace(/^https?:\/\//, "").replace(/\.git$/, "");
|
|
1019
|
+
const ssh = /^(?:ssh:\/\/)?git@([^:/]+)[:/](.+?)(?:\.git)?$/.exec(url);
|
|
1020
|
+
if (ssh) return `${ssh[1]}/${ssh[2]}`;
|
|
1021
|
+
return url.replace(/\/\/[^/@]*@/, "//").replace(/^https?:\/\//, "").replace(/\.git$/, "");
|
|
977
1022
|
}
|
|
978
1023
|
|
|
979
1024
|
// ../core/src/adapters/claude-code.ts
|
|
@@ -1004,7 +1049,7 @@ var OPTS = {
|
|
|
1004
1049
|
timeoutFor: (e) => e === "PermissionRequest" ? 130 : void 0
|
|
1005
1050
|
};
|
|
1006
1051
|
var DESKTOP_ID = /^local_[A-Za-z0-9-]{1,64}$/;
|
|
1007
|
-
function claudeDirs(env = process.env, home = env.HOME ||
|
|
1052
|
+
function claudeDirs(env = process.env, home = env.HOME || homeDir()) {
|
|
1008
1053
|
const out2 = [];
|
|
1009
1054
|
const add = (d) => {
|
|
1010
1055
|
if (!d) return;
|
|
@@ -1033,23 +1078,25 @@ var claudeCode = {
|
|
|
1033
1078
|
events: CLAUDE_EVENTS,
|
|
1034
1079
|
detect: (env = process.env) => claudeDirs(env).some((d) => existsSync3(d)),
|
|
1035
1080
|
configFiles: (env = process.env) => claudeDirs(env).map(settingsIn),
|
|
1036
|
-
install: (
|
|
1081
|
+
install: (cmd, env = process.env, opts = {}) => claudeDirs(env).map((d) => installClaudeShaped(settingsIn(d), NAME2, eventsFor(opts.lean), cmd, OPTS)),
|
|
1037
1082
|
uninstall: (env = process.env, opts = {}) => claudeDirs(env).map(
|
|
1038
1083
|
(d) => uninstallClaudeShaped(settingsIn(d), CLAUDE_EVENTS, { created: opts.created?.includes(settingsIn(d)) })
|
|
1039
1084
|
),
|
|
1040
|
-
installed: (
|
|
1085
|
+
installed: (cmd, env = process.env, opts = {}) => claudeDirs(env).map((d) => ({
|
|
1041
1086
|
file: settingsIn(d),
|
|
1042
|
-
state: installedClaudeShaped(settingsIn(d), NAME2, eventsFor(opts.lean),
|
|
1087
|
+
state: installedClaudeShaped(settingsIn(d), NAME2, eventsFor(opts.lean), cmd, OPTS)
|
|
1043
1088
|
})),
|
|
1044
1089
|
fromHook(input) {
|
|
1045
1090
|
const event = input.argv[1] ?? "";
|
|
1046
1091
|
let s = {};
|
|
1047
1092
|
try {
|
|
1048
|
-
|
|
1093
|
+
const j = input.stdin ? JSON.parse(input.stdin) : {};
|
|
1094
|
+
if (j && typeof j === "object" && !Array.isArray(j)) s = j;
|
|
1049
1095
|
} catch {
|
|
1050
1096
|
}
|
|
1051
1097
|
const id = s.session_id ?? s.sessionId;
|
|
1052
1098
|
if (!id) return null;
|
|
1099
|
+
if (notClaudeCode(String(id), s)) return null;
|
|
1053
1100
|
const session = { id: String(id), cwd: typeof s.cwd === "string" ? s.cwd : input.cwd };
|
|
1054
1101
|
if (typeof s.model === "string") session.model = s.model;
|
|
1055
1102
|
if (typeof s.agent_id === "string" && event.startsWith("Subagent") === false && s.agent_id)
|
|
@@ -1168,6 +1215,7 @@ var claudeCode = {
|
|
|
1168
1215
|
return null;
|
|
1169
1216
|
}
|
|
1170
1217
|
const hints = {};
|
|
1218
|
+
if (event === "UserPromptSubmit" && typeof s.prompt === "string") hints.prompt = s.prompt;
|
|
1171
1219
|
const host = input.env.CLAUDE_CODE_HOST_SESSION_ID;
|
|
1172
1220
|
if (host && DESKTOP_ID.test(host)) hints.desktop_id = host;
|
|
1173
1221
|
if (event !== "PreToolUse" && event !== "PostToolUse") hints.claude_pid = claudePid(input.env);
|
|
@@ -1193,7 +1241,7 @@ var claudeCode = {
|
|
|
1193
1241
|
return out2;
|
|
1194
1242
|
},
|
|
1195
1243
|
backfill(sinceMs, env = process.env) {
|
|
1196
|
-
const
|
|
1244
|
+
const rows2 = [];
|
|
1197
1245
|
const files = [];
|
|
1198
1246
|
for (const d of claudeDirs(env))
|
|
1199
1247
|
files.push(...recentFiles(path7.join(d, "projects"), 1, sinceMs, (n) => /^[0-9a-f-]{36}\.jsonl$/.test(n)));
|
|
@@ -1205,19 +1253,23 @@ var claudeCode = {
|
|
|
1205
1253
|
if (!facts.started_at || !facts.last_at) continue;
|
|
1206
1254
|
const { started_at, last_at, first_ask: _fa, harness_version: _hv, ...rest } = facts;
|
|
1207
1255
|
const cwd = rest.cwd;
|
|
1208
|
-
|
|
1256
|
+
rows2.push({
|
|
1209
1257
|
session: { id, ...rest, ...gitFacts(cwd), ...rest.branch ? { branch: rest.branch } : {} },
|
|
1210
1258
|
started_at,
|
|
1211
1259
|
last_at
|
|
1212
1260
|
});
|
|
1213
1261
|
}
|
|
1214
|
-
return
|
|
1262
|
+
return rows2;
|
|
1215
1263
|
}
|
|
1216
1264
|
};
|
|
1217
1265
|
function prune2(o) {
|
|
1218
1266
|
for (const k of Object.keys(o)) if (o[k] === void 0) delete o[k];
|
|
1219
1267
|
return o;
|
|
1220
1268
|
}
|
|
1269
|
+
var UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
1270
|
+
function notClaudeCode(id, s) {
|
|
1271
|
+
return typeof s.transcript_path !== "string" && !UUID.test(id);
|
|
1272
|
+
}
|
|
1221
1273
|
function pickSource(v) {
|
|
1222
1274
|
return typeof v === "string" && ["startup", "resume", "clear", "compact", "fork"].includes(v) ? v : "unknown";
|
|
1223
1275
|
}
|
|
@@ -1323,26 +1375,41 @@ function parentOf() {
|
|
|
1323
1375
|
}
|
|
1324
1376
|
return (pid) => table.get(pid) || 0;
|
|
1325
1377
|
}
|
|
1326
|
-
var
|
|
1327
|
-
function
|
|
1328
|
-
const hit = accountCache.get(dir);
|
|
1329
|
-
if (hit !== void 0) return hit;
|
|
1378
|
+
var EMAIL_RE = /^[^@\s]{1,64}@[^@\s]{1,189}$/;
|
|
1379
|
+
function claudeLogin(dir, home = homeDir()) {
|
|
1330
1380
|
const files = [path7.join(dir, ".claude.json")];
|
|
1331
|
-
if (path7.resolve(dir) === path7.join(
|
|
1332
|
-
let acct = null;
|
|
1381
|
+
if (path7.resolve(dir) === path7.join(home, ".claude")) files.unshift(path7.join(home, ".claude.json"));
|
|
1333
1382
|
for (const f of files) {
|
|
1334
1383
|
try {
|
|
1335
1384
|
const a = JSON.parse(readFileSync3(f, "utf8")).oauthAccount;
|
|
1336
|
-
if (a?.accountUuid) {
|
|
1337
|
-
|
|
1338
|
-
|
|
1385
|
+
if (typeof a?.accountUuid === "string" && a.accountUuid) {
|
|
1386
|
+
const email = typeof a.emailAddress === "string" && EMAIL_RE.test(a.emailAddress) ? a.emailAddress : void 0;
|
|
1387
|
+
return { id: a.accountUuid.slice(0, 128), ...email ? { email } : {} };
|
|
1339
1388
|
}
|
|
1340
1389
|
} catch {
|
|
1341
1390
|
}
|
|
1342
1391
|
}
|
|
1392
|
+
return null;
|
|
1393
|
+
}
|
|
1394
|
+
var accountCache = /* @__PURE__ */ new Map();
|
|
1395
|
+
function claudeAccount(dir) {
|
|
1396
|
+
const hit = accountCache.get(dir);
|
|
1397
|
+
if (hit !== void 0) return hit;
|
|
1398
|
+
const acct = claudeLogin(dir)?.id ?? null;
|
|
1343
1399
|
accountCache.set(dir, acct);
|
|
1344
1400
|
return acct;
|
|
1345
1401
|
}
|
|
1402
|
+
function claudeAccounts(dirs, home = homeDir()) {
|
|
1403
|
+
const out2 = [];
|
|
1404
|
+
for (const d of dirs) {
|
|
1405
|
+
const l = claudeLogin(d, home);
|
|
1406
|
+
if (!l) continue;
|
|
1407
|
+
const seen = out2.find((a) => a.id === l.id);
|
|
1408
|
+
if (!seen) out2.push({ harness: NAME2, ...l });
|
|
1409
|
+
else if (!seen.email && l.email) seen.email = l.email;
|
|
1410
|
+
}
|
|
1411
|
+
return out2;
|
|
1412
|
+
}
|
|
1346
1413
|
var LINK_RE = /^https:\/\/claude\.ai\/code\/session_[A-Za-z0-9]+$/;
|
|
1347
1414
|
function transcriptLink(records) {
|
|
1348
1415
|
for (let i = records.length - 1; i >= 0; i--) {
|
|
@@ -1498,7 +1565,8 @@ var claude_code_control_exports = {};
|
|
|
1498
1565
|
__export(claude_code_control_exports, {
|
|
1499
1566
|
JOB_TIMEOUT_MS: () => JOB_TIMEOUT_MS,
|
|
1500
1567
|
claudeEnv: () => claudeEnv,
|
|
1501
|
-
claudeLogin: () =>
|
|
1568
|
+
claudeLogin: () => claudeLogin2,
|
|
1569
|
+
cmpVersion: () => cmpVersion,
|
|
1502
1570
|
detectCaps: () => detectCaps,
|
|
1503
1571
|
findClaude: () => findClaude,
|
|
1504
1572
|
findTranscript: () => findTranscript,
|
|
@@ -1624,7 +1692,7 @@ function parseAuthStatus(stdout) {
|
|
|
1624
1692
|
return { loggedIn: null };
|
|
1625
1693
|
}
|
|
1626
1694
|
}
|
|
1627
|
-
function
|
|
1695
|
+
function claudeLogin2(bin, configDir, env = process.env) {
|
|
1628
1696
|
return new Promise((resolve) => {
|
|
1629
1697
|
execFile(
|
|
1630
1698
|
bin,
|
|
@@ -1875,18 +1943,13 @@ function parseArgv(raw) {
|
|
|
1875
1943
|
function readNotify(text) {
|
|
1876
1944
|
const { head, tail: tail2, at } = split(text);
|
|
1877
1945
|
const m = NOTIFY_LINE.exec(head);
|
|
1878
|
-
if (m)
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
index: m.index,
|
|
1883
|
-
length: m[0].length,
|
|
1884
|
-
misplaced: false,
|
|
1885
|
-
other: !OURS2.test(m[1] ?? "")
|
|
1886
|
-
};
|
|
1946
|
+
if (m) {
|
|
1947
|
+
const raw = m[1];
|
|
1948
|
+
return { raw, argv: parseArgv(raw), index: m.index, length: m[0].length, misplaced: false, other: !OURS2.test(raw) };
|
|
1949
|
+
}
|
|
1887
1950
|
const k = NOTIFY_KEY.exec(head);
|
|
1888
1951
|
if (k) {
|
|
1889
|
-
const rest = head.slice(k.index).split("\n")[0]
|
|
1952
|
+
const rest = head.slice(k.index).split("\n")[0];
|
|
1890
1953
|
return {
|
|
1891
1954
|
raw: rest.replace(NOTIFY_KEY, "").trim(),
|
|
1892
1955
|
argv: null,
|
|
@@ -1897,15 +1960,11 @@ function readNotify(text) {
|
|
|
1897
1960
|
};
|
|
1898
1961
|
}
|
|
1899
1962
|
const t = NOTIFY_LINE.exec(tail2);
|
|
1900
|
-
if (t
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
argv: parseArgv(t[
|
|
1904
|
-
|
|
1905
|
-
length: t[0].length,
|
|
1906
|
-
misplaced: true,
|
|
1907
|
-
other: false
|
|
1908
|
-
};
|
|
1963
|
+
if (t) {
|
|
1964
|
+
const raw = t[1];
|
|
1965
|
+
if (OURS2.test(raw))
|
|
1966
|
+
return { raw, argv: parseArgv(raw), index: at + t.index, length: t[0].length, misplaced: true, other: false };
|
|
1967
|
+
}
|
|
1909
1968
|
return null;
|
|
1910
1969
|
}
|
|
1911
1970
|
function dropLine(text, index, length) {
|
|
@@ -1924,9 +1983,7 @@ function withNotify(text, line) {
|
|
|
1924
1983
|
if (n?.misplaced) return insertTopLevel(dropLine(text, n.index, n.length), line);
|
|
1925
1984
|
return insertTopLevel(text, line);
|
|
1926
1985
|
}
|
|
1927
|
-
function replaceNotify(text, line) {
|
|
1928
|
-
const n = readNotify(text);
|
|
1929
|
-
if (!n || n.misplaced || !n.length) return null;
|
|
1986
|
+
function replaceNotify(text, n, line) {
|
|
1930
1987
|
return text.slice(0, n.index) + line + text.slice(n.index + n.length);
|
|
1931
1988
|
}
|
|
1932
1989
|
function withoutNotify(text) {
|
|
@@ -1967,20 +2024,20 @@ var CODEX_EVENTS = [
|
|
|
1967
2024
|
];
|
|
1968
2025
|
var OPTS2 = { matcherFor: (e) => /Tool|Permission/.test(e) ? "" : void 0, timeout: 5 };
|
|
1969
2026
|
var eventsFor2 = (lean) => lean ? CODEX_EVENTS.filter((e) => e !== "PreToolUse") : [...CODEX_EVENTS];
|
|
1970
|
-
var codexHome = (env) => env.CODEX_HOME || path9.join(
|
|
2027
|
+
var codexHome = (env) => env.CODEX_HOME || path9.join(homeDir(), ".codex");
|
|
1971
2028
|
var hooksFile2 = (env) => path9.join(codexHome(env), "hooks.json");
|
|
1972
2029
|
var configFile2 = (env) => path9.join(codexHome(env), "config.toml");
|
|
1973
2030
|
var sessionsDir = (env) => path9.join(codexHome(env), "sessions");
|
|
1974
2031
|
var validTool3 = (n) => typeof n === "string" && /^[A-Za-z0-9_.:-]{1,120}$/.test(n);
|
|
1975
2032
|
var endReason2 = (r) => r === "clear" || r === "logout" || r === "exit" ? r : "other";
|
|
1976
|
-
function installNotify(
|
|
2033
|
+
function installNotify(cmd, env) {
|
|
1977
2034
|
const file = configFile2(env);
|
|
1978
2035
|
let text = "";
|
|
1979
2036
|
try {
|
|
1980
2037
|
text = readFileSync5(file, "utf8");
|
|
1981
2038
|
} catch {
|
|
1982
2039
|
}
|
|
1983
|
-
const argv =
|
|
2040
|
+
const argv = cmd([NAME3, "notify"]).argv;
|
|
1984
2041
|
const line = `notify = ${JSON.stringify(argv)}`;
|
|
1985
2042
|
const n = readNotify(text);
|
|
1986
2043
|
const write = (next) => {
|
|
@@ -2005,11 +2062,8 @@ function installNotify(cmd2, env) {
|
|
|
2005
2062
|
return { file, changed: false, note: "notify current (after Codex Computer Use)" };
|
|
2006
2063
|
}
|
|
2007
2064
|
if (!prev || OURS2.test(JSON.stringify(prev))) {
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
write(next);
|
|
2011
|
-
return { file, changed: true, note: "chained after Codex Computer Use (--previous-notify)" };
|
|
2012
|
-
}
|
|
2065
|
+
write(replaceNotify(text, n, `notify = ${withPreviousRaw(n.raw, argv)}`));
|
|
2066
|
+
return { file, changed: true, note: "chained after Codex Computer Use (--previous-notify)" };
|
|
2013
2067
|
}
|
|
2014
2068
|
}
|
|
2015
2069
|
return {
|
|
@@ -2038,8 +2092,7 @@ function uninstallNotify(env) {
|
|
|
2038
2092
|
}
|
|
2039
2093
|
const prev = previousNotify(n.argv);
|
|
2040
2094
|
if (isComputerUse(n.argv) && prev && OURS2.test(JSON.stringify(prev))) {
|
|
2041
|
-
|
|
2042
|
-
if (next) writeFileSync3(file, next);
|
|
2095
|
+
writeFileSync3(file, replaceNotify(text, n, `notify = ${stripPreviousRaw(n.raw)}`));
|
|
2043
2096
|
return { file, changed: true };
|
|
2044
2097
|
}
|
|
2045
2098
|
return { file, changed: false };
|
|
@@ -2069,24 +2122,31 @@ function findRollout(threadId, env = process.env) {
|
|
|
2069
2122
|
walk(sessionsDir(env), 0);
|
|
2070
2123
|
return best;
|
|
2071
2124
|
}
|
|
2125
|
+
function rolloutMeta(file) {
|
|
2126
|
+
return scanLines(file, (line) => parseLines([line])[0] ?? {});
|
|
2127
|
+
}
|
|
2128
|
+
function ofAnotherThread(file, id) {
|
|
2129
|
+
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);
|
|
2130
|
+
return !!m?.[1] && m[1].toLowerCase() !== id.toLowerCase();
|
|
2131
|
+
}
|
|
2072
2132
|
var codex = {
|
|
2073
2133
|
name: NAME3,
|
|
2074
2134
|
events: CODEX_EVENTS,
|
|
2075
2135
|
detect: (env = process.env) => existsSync4(codexHome(env)),
|
|
2076
2136
|
configFiles: (env = process.env) => [hooksFile2(env), configFile2(env)],
|
|
2077
|
-
install: (
|
|
2078
|
-
installClaudeShaped(hooksFile2(env), NAME3, eventsFor2(opts.lean),
|
|
2079
|
-
installNotify(
|
|
2137
|
+
install: (cmd, env = process.env, opts = {}) => [
|
|
2138
|
+
installClaudeShaped(hooksFile2(env), NAME3, eventsFor2(opts.lean), cmd, OPTS2),
|
|
2139
|
+
installNotify(cmd, env)
|
|
2080
2140
|
],
|
|
2081
2141
|
uninstall: (env = process.env, opts = {}) => [
|
|
2082
2142
|
uninstallClaudeShaped(hooksFile2(env), CODEX_EVENTS, { created: opts.created?.includes(hooksFile2(env)) }),
|
|
2083
2143
|
uninstallNotify(env)
|
|
2084
2144
|
],
|
|
2085
|
-
installed: (
|
|
2145
|
+
installed: (cmd, env = process.env, opts = {}) => {
|
|
2086
2146
|
const out2 = [
|
|
2087
2147
|
{
|
|
2088
2148
|
file: hooksFile2(env),
|
|
2089
|
-
state: installedClaudeShaped(hooksFile2(env), NAME3, eventsFor2(opts.lean),
|
|
2149
|
+
state: installedClaudeShaped(hooksFile2(env), NAME3, eventsFor2(opts.lean), cmd, OPTS2),
|
|
2090
2150
|
note: "Codex runs non-managed hooks only after the review in `codex` (/hooks); until then nothing is reported unless the notify fallback is ours"
|
|
2091
2151
|
}
|
|
2092
2152
|
];
|
|
@@ -2096,7 +2156,7 @@ var codex = {
|
|
|
2096
2156
|
} catch {
|
|
2097
2157
|
}
|
|
2098
2158
|
const n = readNotify(text);
|
|
2099
|
-
const argv =
|
|
2159
|
+
const argv = cmd([NAME3, "notify"]).argv;
|
|
2100
2160
|
const prev = n ? previousNotify(n.argv) : null;
|
|
2101
2161
|
const ours = n && (!n.other || prev && JSON.stringify(prev) === JSON.stringify(argv));
|
|
2102
2162
|
out2.push({
|
|
@@ -2111,11 +2171,14 @@ var codex = {
|
|
|
2111
2171
|
if (event === "notify") {
|
|
2112
2172
|
const raw = input.argv.find((a) => a.startsWith("{")) ?? input.stdin;
|
|
2113
2173
|
try {
|
|
2114
|
-
|
|
2174
|
+
const j = raw ? JSON.parse(raw) : {};
|
|
2175
|
+
if (j && typeof j === "object" && !Array.isArray(j)) s = j;
|
|
2115
2176
|
} catch {
|
|
2116
2177
|
}
|
|
2117
2178
|
const id2 = s["thread-id"] ?? s.thread_id ?? s["session-id"] ?? s.session_id;
|
|
2118
2179
|
if (!id2) return null;
|
|
2180
|
+
const file = findRollout(String(id2), input.env);
|
|
2181
|
+
if (!file || codexSubagent(rolloutMeta(file))) return null;
|
|
2119
2182
|
const r2 = {
|
|
2120
2183
|
session: { id: String(id2), cwd: typeof s.cwd === "string" ? s.cwd : input.cwd },
|
|
2121
2184
|
events: [
|
|
@@ -2125,18 +2188,19 @@ var codex = {
|
|
|
2125
2188
|
harnessEvent: "notify"
|
|
2126
2189
|
}
|
|
2127
2190
|
],
|
|
2128
|
-
hints: {}
|
|
2191
|
+
hints: {},
|
|
2192
|
+
transcript: file
|
|
2129
2193
|
};
|
|
2130
|
-
const file = findRollout(String(id2), input.env);
|
|
2131
|
-
if (file) r2.transcript = file;
|
|
2132
2194
|
return r2;
|
|
2133
2195
|
}
|
|
2134
2196
|
try {
|
|
2135
|
-
|
|
2197
|
+
const j = input.stdin ? JSON.parse(input.stdin) : {};
|
|
2198
|
+
if (j && typeof j === "object" && !Array.isArray(j)) s = j;
|
|
2136
2199
|
} catch {
|
|
2137
2200
|
}
|
|
2138
2201
|
const id = s.session_id;
|
|
2139
2202
|
if (!id) return null;
|
|
2203
|
+
if (s.transcript_path === null) return null;
|
|
2140
2204
|
const session = { id: String(id), cwd: typeof s.cwd === "string" ? s.cwd : input.cwd };
|
|
2141
2205
|
if (typeof s.model === "string") session.model = s.model;
|
|
2142
2206
|
const turn_id = typeof s.turn_id === "string" ? s.turn_id : void 0;
|
|
@@ -2208,7 +2272,8 @@ var codex = {
|
|
|
2208
2272
|
return null;
|
|
2209
2273
|
}
|
|
2210
2274
|
const r = { session, events, hints: {} };
|
|
2211
|
-
if (typeof s.transcript_path === "string"
|
|
2275
|
+
if (typeof s.transcript_path === "string" && !ofAnotherThread(s.transcript_path, String(id)))
|
|
2276
|
+
r.transcript = s.transcript_path;
|
|
2212
2277
|
return r;
|
|
2213
2278
|
},
|
|
2214
2279
|
readTranscript(file, fromLine) {
|
|
@@ -2226,16 +2291,17 @@ var codex = {
|
|
|
2226
2291
|
return out2;
|
|
2227
2292
|
},
|
|
2228
2293
|
backfill(sinceMs, env = process.env) {
|
|
2229
|
-
const
|
|
2294
|
+
const rows2 = [];
|
|
2230
2295
|
for (const f of recentFiles(sessionsDir(env), 3, sinceMs, (n) => /^rollout-.*\.jsonl$/.test(n))) {
|
|
2231
2296
|
const m = /([0-9a-f]{8}-[0-9a-f-]{27,})\.jsonl$/i.exec(path9.basename(f));
|
|
2232
2297
|
if (!m?.[1]) continue;
|
|
2298
|
+
if (codexSubagent(rolloutMeta(f))) continue;
|
|
2233
2299
|
const facts = codexFacts(f);
|
|
2234
2300
|
if (!facts.started_at || !facts.last_at) continue;
|
|
2235
2301
|
const { started_at, last_at, first_ask: _fa, harness_version: _hv, ...rest } = facts;
|
|
2236
|
-
|
|
2302
|
+
rows2.push({ session: { id: m[1], ...rest }, started_at, last_at });
|
|
2237
2303
|
}
|
|
2238
|
-
return
|
|
2304
|
+
return rows2;
|
|
2239
2305
|
}
|
|
2240
2306
|
};
|
|
2241
2307
|
function prune3(o) {
|
|
@@ -2244,10 +2310,11 @@ function prune3(o) {
|
|
|
2244
2310
|
}
|
|
2245
2311
|
function codexFacts(file) {
|
|
2246
2312
|
const { head, tail: tail2 } = ends(file, 128 * 1024, 64 * 1024);
|
|
2247
|
-
|
|
2248
|
-
|
|
2313
|
+
let H = parseLines(head);
|
|
2314
|
+
let T = parseLines(tail2);
|
|
2249
2315
|
const out2 = {};
|
|
2250
2316
|
const meta = H.find((j) => j.type === "session_meta" && j.payload);
|
|
2317
|
+
const boundary = codexReplayBoundary(meta);
|
|
2251
2318
|
if (meta?.payload) {
|
|
2252
2319
|
const p = meta.payload;
|
|
2253
2320
|
if (p.cwd) out2.cwd = p.cwd;
|
|
@@ -2255,6 +2322,13 @@ function codexFacts(file) {
|
|
|
2255
2322
|
if (p.git?.branch) out2.branch = p.git.branch;
|
|
2256
2323
|
if (p.git?.repository_url) out2.repo = String(p.git.repository_url).replace(/\/\/[^/@]*@/, "//");
|
|
2257
2324
|
}
|
|
2325
|
+
if (boundary) {
|
|
2326
|
+
const h = H.findIndex(boundary);
|
|
2327
|
+
const t = T.findIndex(boundary);
|
|
2328
|
+
H = h < 0 ? [] : H.slice(h);
|
|
2329
|
+
const found = h >= 0 || t >= 0 || scanLines(file, (line) => boundary(parseLines([line])[0] ?? null) ? true : null, Number.POSITIVE_INFINITY);
|
|
2330
|
+
T = t >= 0 ? T.slice(t) : found ? T : [];
|
|
2331
|
+
}
|
|
2258
2332
|
const ctx = H.concat(T).reverse().find((j) => j.type === "turn_context" && j.payload?.model);
|
|
2259
2333
|
if (ctx?.payload?.model) out2.model = ctx.payload.model;
|
|
2260
2334
|
for (const j of H) {
|
|
@@ -2262,12 +2336,12 @@ function codexFacts(file) {
|
|
|
2262
2336
|
if (j.type !== "response_item" || !p || p.type !== "message" || p.role !== "user") continue;
|
|
2263
2337
|
const text = (p.content ?? []).filter((b) => b && b.type === "input_text" && typeof b.text === "string").map((b) => b.text).join("\n").trim();
|
|
2264
2338
|
if (!text || codexInjected(text)) continue;
|
|
2265
|
-
const line = text.split("\n")
|
|
2339
|
+
const line = text.split("\n")[0];
|
|
2266
2340
|
out2.title = line.length > 80 ? `${line.slice(0, 80).replace(/\s+\S*$/, "")}\u2026` : line;
|
|
2267
2341
|
out2.title_source = "first-ask";
|
|
2268
2342
|
break;
|
|
2269
2343
|
}
|
|
2270
|
-
const times = H.concat(T).map((j) => Date.parse(String(j.timestamp ?? ""))).filter((n) => n > 0);
|
|
2344
|
+
const times = (boundary && meta ? [meta, ...H, ...T] : H.concat(T)).map((j) => Date.parse(String(j.timestamp ?? ""))).filter((n) => n > 0);
|
|
2271
2345
|
if (times.length) {
|
|
2272
2346
|
out2.started_at = Math.min(...times);
|
|
2273
2347
|
out2.last_at = Math.max(...times);
|
|
@@ -2409,11 +2483,432 @@ function codexStderr(stderr) {
|
|
|
2409
2483
|
}
|
|
2410
2484
|
var reading2 = () => new CodexReading();
|
|
2411
2485
|
|
|
2412
|
-
// ../core/src/adapters/
|
|
2413
|
-
import {
|
|
2414
|
-
import { existsSync as
|
|
2486
|
+
// ../core/src/adapters/devin.ts
|
|
2487
|
+
import { createHash as createHash2 } from "node:crypto";
|
|
2488
|
+
import { existsSync as existsSync6, readlinkSync } from "node:fs";
|
|
2415
2489
|
import path11 from "node:path";
|
|
2416
|
-
|
|
2490
|
+
|
|
2491
|
+
// ../core/src/readers/sqlite.ts
|
|
2492
|
+
import { execFileSync as execFileSync4 } from "node:child_process";
|
|
2493
|
+
import { existsSync as existsSync5 } from "node:fs";
|
|
2494
|
+
import { createRequire } from "node:module";
|
|
2495
|
+
function quiet(fn) {
|
|
2496
|
+
const original = process.emitWarning;
|
|
2497
|
+
try {
|
|
2498
|
+
process.emitWarning = ((warning, ...rest) => {
|
|
2499
|
+
const kind = typeof rest[0] === "string" ? rest[0] : rest[0]?.type;
|
|
2500
|
+
if (kind === "ExperimentalWarning" && /sqlite/i.test(String(warning))) return;
|
|
2501
|
+
original.call(process, warning, ...rest);
|
|
2502
|
+
});
|
|
2503
|
+
return fn();
|
|
2504
|
+
} finally {
|
|
2505
|
+
process.emitWarning = original;
|
|
2506
|
+
}
|
|
2507
|
+
}
|
|
2508
|
+
var ctorCache;
|
|
2509
|
+
function dbCtor() {
|
|
2510
|
+
if (ctorCache !== void 0) return ctorCache;
|
|
2511
|
+
ctorCache = null;
|
|
2512
|
+
try {
|
|
2513
|
+
const req = createRequire(import.meta.url);
|
|
2514
|
+
const m = quiet(() => req("node:sqlite"));
|
|
2515
|
+
if (typeof m?.DatabaseSync === "function") ctorCache = m.DatabaseSync;
|
|
2516
|
+
} catch {
|
|
2517
|
+
}
|
|
2518
|
+
return ctorCache;
|
|
2519
|
+
}
|
|
2520
|
+
function plain(row) {
|
|
2521
|
+
if (!row || typeof row !== "object") return null;
|
|
2522
|
+
const out2 = {};
|
|
2523
|
+
for (const [k, v] of Object.entries(row)) {
|
|
2524
|
+
if (typeof v === "bigint") out2[k] = Number(v);
|
|
2525
|
+
else if (v instanceof Uint8Array) out2[k] = null;
|
|
2526
|
+
else out2[k] = v;
|
|
2527
|
+
}
|
|
2528
|
+
return out2;
|
|
2529
|
+
}
|
|
2530
|
+
var rows = (v) => {
|
|
2531
|
+
if (!Array.isArray(v)) return null;
|
|
2532
|
+
const out2 = [];
|
|
2533
|
+
for (const r of v) {
|
|
2534
|
+
const p = plain(r);
|
|
2535
|
+
if (p) out2.push(p);
|
|
2536
|
+
}
|
|
2537
|
+
return out2;
|
|
2538
|
+
};
|
|
2539
|
+
function viaNode(file, sql, params, o) {
|
|
2540
|
+
const C = dbCtor();
|
|
2541
|
+
if (!C) return null;
|
|
2542
|
+
let db;
|
|
2543
|
+
try {
|
|
2544
|
+
db = quiet(() => new C(file, { readOnly: true, timeout: o.timeoutMs }));
|
|
2545
|
+
} catch {
|
|
2546
|
+
return null;
|
|
2547
|
+
}
|
|
2548
|
+
try {
|
|
2549
|
+
try {
|
|
2550
|
+
db.exec(`PRAGMA busy_timeout = ${Math.round(o.timeoutMs)}`);
|
|
2551
|
+
} catch {
|
|
2552
|
+
}
|
|
2553
|
+
return quiet(() => rows(db.prepare(sql).all(...params)));
|
|
2554
|
+
} catch {
|
|
2555
|
+
return null;
|
|
2556
|
+
} finally {
|
|
2557
|
+
try {
|
|
2558
|
+
db.close();
|
|
2559
|
+
} catch {
|
|
2560
|
+
}
|
|
2561
|
+
}
|
|
2562
|
+
}
|
|
2563
|
+
var CHILD = `const { DatabaseSync } = require("node:sqlite");
|
|
2564
|
+
const [, file, sql, params, ms] = process.argv;
|
|
2565
|
+
const db = new DatabaseSync(file, { readOnly: true, timeout: Number(ms) });
|
|
2566
|
+
try { db.exec("PRAGMA busy_timeout = " + Number(ms)); } catch {}
|
|
2567
|
+
const out = db.prepare(sql).all(...JSON.parse(params));
|
|
2568
|
+
db.close();
|
|
2569
|
+
process.stdout.write(JSON.stringify(out));`;
|
|
2570
|
+
function viaChild(file, sql, params, o) {
|
|
2571
|
+
try {
|
|
2572
|
+
const out2 = execFileSync4(
|
|
2573
|
+
process.execPath,
|
|
2574
|
+
["--experimental-sqlite", "--no-warnings", "-e", CHILD, file, sql, JSON.stringify(params), String(o.timeoutMs)],
|
|
2575
|
+
{
|
|
2576
|
+
encoding: "utf8",
|
|
2577
|
+
timeout: o.timeoutMs + 3e3,
|
|
2578
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
2579
|
+
maxBuffer: o.maxBuffer,
|
|
2580
|
+
windowsHide: true
|
|
2581
|
+
}
|
|
2582
|
+
);
|
|
2583
|
+
return rows(JSON.parse(out2 || "null"));
|
|
2584
|
+
} catch {
|
|
2585
|
+
return null;
|
|
2586
|
+
}
|
|
2587
|
+
}
|
|
2588
|
+
var SAFE = /^[A-Za-z0-9 ._:@/+-]{0,256}$/;
|
|
2589
|
+
function inlineParams(sql, params) {
|
|
2590
|
+
if (sql.includes("'")) return null;
|
|
2591
|
+
const holes = sql.split("?").length - 1;
|
|
2592
|
+
if (holes !== params.length) return null;
|
|
2593
|
+
let i = 0;
|
|
2594
|
+
let bad2 = false;
|
|
2595
|
+
const out2 = sql.replace(/\?/g, () => {
|
|
2596
|
+
const p = params[i++];
|
|
2597
|
+
if (typeof p === "number") {
|
|
2598
|
+
if (!Number.isFinite(p)) {
|
|
2599
|
+
bad2 = true;
|
|
2600
|
+
return "NULL";
|
|
2601
|
+
}
|
|
2602
|
+
return String(p);
|
|
2603
|
+
}
|
|
2604
|
+
if (typeof p !== "string" || !SAFE.test(p)) {
|
|
2605
|
+
bad2 = true;
|
|
2606
|
+
return "NULL";
|
|
2607
|
+
}
|
|
2608
|
+
return `'${p}'`;
|
|
2609
|
+
});
|
|
2610
|
+
return bad2 ? null : out2;
|
|
2611
|
+
}
|
|
2612
|
+
function viaCli(file, sql, params, o) {
|
|
2613
|
+
const text = inlineParams(sql, params);
|
|
2614
|
+
if (!text) return null;
|
|
2615
|
+
try {
|
|
2616
|
+
const out2 = execFileSync4("sqlite3", ["-readonly", "-json", file, text], {
|
|
2617
|
+
encoding: "utf8",
|
|
2618
|
+
timeout: o.timeoutMs + 3e3,
|
|
2619
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
2620
|
+
maxBuffer: o.maxBuffer,
|
|
2621
|
+
windowsHide: true
|
|
2622
|
+
}).trim();
|
|
2623
|
+
if (!out2) return [];
|
|
2624
|
+
return rows(JSON.parse(out2));
|
|
2625
|
+
} catch {
|
|
2626
|
+
return null;
|
|
2627
|
+
}
|
|
2628
|
+
}
|
|
2629
|
+
function querySqlite(file, sql, params = [], opts = {}) {
|
|
2630
|
+
const o = {
|
|
2631
|
+
timeoutMs: opts.timeoutMs ?? 2e3,
|
|
2632
|
+
engine: opts.engine ?? "auto",
|
|
2633
|
+
maxBuffer: opts.maxBuffer ?? 32 * 1024 * 1024
|
|
2634
|
+
};
|
|
2635
|
+
if (!file) return null;
|
|
2636
|
+
try {
|
|
2637
|
+
if (!existsSync5(file)) return null;
|
|
2638
|
+
} catch {
|
|
2639
|
+
return null;
|
|
2640
|
+
}
|
|
2641
|
+
const ways = o.engine === "auto" ? [viaNode, viaChild, viaCli] : o.engine === "node" ? [viaNode] : o.engine === "child" ? [viaChild] : [viaCli];
|
|
2642
|
+
for (const way of ways) {
|
|
2643
|
+
const r = way(file, sql, params, o);
|
|
2644
|
+
if (r) return r;
|
|
2645
|
+
}
|
|
2646
|
+
return null;
|
|
2647
|
+
}
|
|
2648
|
+
|
|
2649
|
+
// ../core/src/adapters/devin.ts
|
|
2650
|
+
var NAME4 = "devin";
|
|
2651
|
+
var DEVIN_EVENTS = [
|
|
2652
|
+
"SessionStart",
|
|
2653
|
+
"UserPromptSubmit",
|
|
2654
|
+
"PreToolUse",
|
|
2655
|
+
"PostToolUse",
|
|
2656
|
+
"PermissionRequest",
|
|
2657
|
+
"Stop",
|
|
2658
|
+
"PostCompaction",
|
|
2659
|
+
"SessionEnd"
|
|
2660
|
+
];
|
|
2661
|
+
var OPTS3 = { matcherFor: (e) => /Tool|Permission/.test(e) ? "" : void 0, timeout: 5 };
|
|
2662
|
+
var eventsFor3 = (lean) => lean ? DEVIN_EVENTS.filter((e) => e !== "PreToolUse") : [...DEVIN_EVENTS];
|
|
2663
|
+
var INSTALL_NOTE = "Devin drops a whole hooks file on one event name it doesn't know; `/hooks` in `devin` lists what loaded";
|
|
2664
|
+
var CLAUDE_NOTE = "Devin also reads ~/.claude/settings.json hooks (read_config_from.claude); a Claude-only event name there makes it ignore that file whole";
|
|
2665
|
+
var deadFileNote = (bad2) => `Devin loads NO hooks from this file: "${bad2}" is not one of its event names and it rejects the file whole \u2014 remove that key (\`/hooks\` in \`devin\` lists what loaded)`;
|
|
2666
|
+
function foreignEvent(file) {
|
|
2667
|
+
const hooks = readJson(file).hooks;
|
|
2668
|
+
if (!hooks || typeof hooks !== "object" || Array.isArray(hooks)) return void 0;
|
|
2669
|
+
const ok = DEVIN_EVENTS;
|
|
2670
|
+
return Object.keys(hooks).find((k) => !ok.includes(k));
|
|
2671
|
+
}
|
|
2672
|
+
var homeOf = (env) => env.HOME || homeDir();
|
|
2673
|
+
var appData = (env) => env.APPDATA || path11.join(homeOf(env), "AppData", "Roaming");
|
|
2674
|
+
var devinConfigDir = (env = process.env) => process.platform === "win32" ? path11.join(appData(env), "devin") : path11.join(env.XDG_CONFIG_HOME || path11.join(homeOf(env), ".config"), "devin");
|
|
2675
|
+
var configFile3 = (env) => path11.join(devinConfigDir(env), "config.json");
|
|
2676
|
+
var devinDataDir = (env = process.env) => process.platform === "win32" ? path11.join(appData(env), "devin", "cli") : path11.join(env.XDG_DATA_HOME || path11.join(homeOf(env), ".local", "share"), "devin", "cli");
|
|
2677
|
+
var sessionsDb = (env) => path11.join(devinDataDir(env), "sessions.db");
|
|
2678
|
+
var SESSION_ID = /^[A-Za-z0-9._:-]{1,128}$/;
|
|
2679
|
+
var validTool4 = (n) => typeof n === "string" && /^[A-Za-z0-9_.:-]{1,120}$/.test(n);
|
|
2680
|
+
var endReason3 = (r) => r === "clear" || r === "logout" || r === "exit" ? r : "other";
|
|
2681
|
+
var pickSource2 = (v) => typeof v === "string" && ["startup", "resume", "clear", "compact", "fork"].includes(v) ? v : "unknown";
|
|
2682
|
+
function errText2(v) {
|
|
2683
|
+
if (typeof v === "string" && v) return v.slice(0, 2e3);
|
|
2684
|
+
if (v && typeof v === "object") {
|
|
2685
|
+
const e = v;
|
|
2686
|
+
if (typeof e.message === "string" && e.message) return e.message.slice(0, 2e3);
|
|
2687
|
+
if (typeof e.error === "string" && e.error) return e.error.slice(0, 2e3);
|
|
2688
|
+
}
|
|
2689
|
+
return void 0;
|
|
2690
|
+
}
|
|
2691
|
+
var cut = (s, n) => s.length > n ? `${s.slice(0, n).replace(/\s+\S*$/, "")}\u2026` : s;
|
|
2692
|
+
var devin = {
|
|
2693
|
+
name: NAME4,
|
|
2694
|
+
events: DEVIN_EVENTS,
|
|
2695
|
+
detect: (env = process.env) => existsSync6(devinConfigDir(env)) || existsSync6(devinDataDir(env)),
|
|
2696
|
+
configFiles: (env = process.env) => [configFile3(env)],
|
|
2697
|
+
install: (cmd, env = process.env, opts = {}) => {
|
|
2698
|
+
const file = configFile3(env);
|
|
2699
|
+
const r = installClaudeShaped(file, NAME4, eventsFor3(opts.lean), cmd, OPTS3);
|
|
2700
|
+
if (r.skipped) return [r];
|
|
2701
|
+
const bad2 = foreignEvent(file);
|
|
2702
|
+
return [{ ...r, note: bad2 ? deadFileNote(bad2) : INSTALL_NOTE }];
|
|
2703
|
+
},
|
|
2704
|
+
uninstall: (env = process.env, opts = {}) => [
|
|
2705
|
+
uninstallClaudeShaped(configFile3(env), DEVIN_EVENTS, { created: opts.created?.includes(configFile3(env)) })
|
|
2706
|
+
],
|
|
2707
|
+
installed: (cmd, env = process.env, opts = {}) => {
|
|
2708
|
+
const file = configFile3(env);
|
|
2709
|
+
const state2 = installedClaudeShaped(file, NAME4, eventsFor3(opts.lean), cmd, OPTS3);
|
|
2710
|
+
const bad2 = state2 === "missing" ? void 0 : foreignEvent(file);
|
|
2711
|
+
return [{ file, state: bad2 ? "inactive" : state2, note: bad2 ? deadFileNote(bad2) : CLAUDE_NOTE }];
|
|
2712
|
+
},
|
|
2713
|
+
fromHook(input) {
|
|
2714
|
+
const event = input.argv[1] ?? "";
|
|
2715
|
+
let s = {};
|
|
2716
|
+
try {
|
|
2717
|
+
const j = input.stdin ? JSON.parse(input.stdin) : {};
|
|
2718
|
+
if (j && typeof j === "object" && !Array.isArray(j)) s = j;
|
|
2719
|
+
} catch {
|
|
2720
|
+
}
|
|
2721
|
+
const id = typeof s.session_id === "string" ? s.session_id : "";
|
|
2722
|
+
if (!id) return null;
|
|
2723
|
+
const session = { id, cwd: input.env.DEVIN_PROJECT_DIR ?? input.cwd };
|
|
2724
|
+
const turn_id = typeof s.prompt_id === "string" ? s.prompt_id : void 0;
|
|
2725
|
+
const tool = validTool4(s.tool_name) ? s.tool_name : void 0;
|
|
2726
|
+
const call_id = typeof s.tool_use_id === "string" ? s.tool_use_id : void 0;
|
|
2727
|
+
const response = s.tool_response && typeof s.tool_response === "object" && !Array.isArray(s.tool_response) ? s.tool_response : void 0;
|
|
2728
|
+
const permissionId = () => `perm-${createHash2("sha256").update(`${turn_id ?? ""}|${tool ?? ""}|${JSON.stringify(s.tool_input ?? null)}`).digest("hex").slice(0, 16)}`;
|
|
2729
|
+
const ev = (type, data) => ({ type, data: prune4(data), harnessEvent: event });
|
|
2730
|
+
const events = [];
|
|
2731
|
+
switch (event) {
|
|
2732
|
+
case "SessionStart":
|
|
2733
|
+
events.push(ev("session.started", { source: pickSource2(s.source) }));
|
|
2734
|
+
break;
|
|
2735
|
+
case "UserPromptSubmit":
|
|
2736
|
+
events.push(
|
|
2737
|
+
ev("turn.started", {
|
|
2738
|
+
turn_id,
|
|
2739
|
+
prompt_chars: typeof s.prompt === "string" ? [...s.prompt].length : void 0
|
|
2740
|
+
})
|
|
2741
|
+
);
|
|
2742
|
+
break;
|
|
2743
|
+
case "PreToolUse":
|
|
2744
|
+
if (tool) events.push(ev("tool.started", { tool, call_id, turn_id, input: s.tool_input }));
|
|
2745
|
+
break;
|
|
2746
|
+
case "PostToolUse":
|
|
2747
|
+
if (tool)
|
|
2748
|
+
events.push(
|
|
2749
|
+
ev("tool.ended", {
|
|
2750
|
+
tool,
|
|
2751
|
+
call_id,
|
|
2752
|
+
turn_id,
|
|
2753
|
+
// `ok` is required on tool.ended (PROTOCOL.md §events), so a response
|
|
2754
|
+
// that carries no success flag at all reads as "nothing said it failed".
|
|
2755
|
+
ok: response?.success !== false,
|
|
2756
|
+
error: errText2(response?.error),
|
|
2757
|
+
input: s.tool_input,
|
|
2758
|
+
output: response ? response.output : s.tool_response
|
|
2759
|
+
})
|
|
2760
|
+
);
|
|
2761
|
+
break;
|
|
2762
|
+
case "PermissionRequest":
|
|
2763
|
+
events.push(ev("attention.needed", { attention_id: call_id ?? permissionId(), kind: "permission", tool }));
|
|
2764
|
+
break;
|
|
2765
|
+
case "Stop":
|
|
2766
|
+
events.push(ev("turn.ended", { turn_id, reason: "stop" }));
|
|
2767
|
+
break;
|
|
2768
|
+
case "PostCompaction":
|
|
2769
|
+
events.push(ev("context.compacted", { trigger: "auto" }));
|
|
2770
|
+
break;
|
|
2771
|
+
case "SessionEnd":
|
|
2772
|
+
events.push(ev("session.ended", { reason: endReason3(s.reason) }));
|
|
2773
|
+
break;
|
|
2774
|
+
default:
|
|
2775
|
+
return null;
|
|
2776
|
+
}
|
|
2777
|
+
return {
|
|
2778
|
+
session,
|
|
2779
|
+
events,
|
|
2780
|
+
hints: {},
|
|
2781
|
+
transcript: transcriptRef(sessionsDb(input.env), id)
|
|
2782
|
+
};
|
|
2783
|
+
},
|
|
2784
|
+
readTranscript(file, fromLine) {
|
|
2785
|
+
const ref = splitRef(file);
|
|
2786
|
+
if (!ref) return { turns: [], line: fromLine };
|
|
2787
|
+
return readDevinChain(ref.db, ref.session, fromLine);
|
|
2788
|
+
},
|
|
2789
|
+
facts(session, transcript, _hints, _state, env = process.env) {
|
|
2790
|
+
const out2 = {};
|
|
2791
|
+
const version = devinVersion(env);
|
|
2792
|
+
if (version) out2.harness_version = version;
|
|
2793
|
+
const account = devinAccount(env);
|
|
2794
|
+
if (account) out2.account_id = account;
|
|
2795
|
+
const ref = splitRef(transcript) ?? { db: sessionsDb(env), session: session.id };
|
|
2796
|
+
if (!SESSION_ID.test(ref.session)) return out2;
|
|
2797
|
+
const rows2 = querySqlite(ref.db, SESSION_SQL, [ref.session]);
|
|
2798
|
+
const row = rows2?.[0];
|
|
2799
|
+
if (!row) return out2;
|
|
2800
|
+
Object.assign(out2, sessionFacts(row));
|
|
2801
|
+
return out2;
|
|
2802
|
+
},
|
|
2803
|
+
backfill(sinceMs, env = process.env) {
|
|
2804
|
+
const rows2 = querySqlite(sessionsDb(env), BACKFILL_SQL, ["text", Math.floor(sinceMs / 1e3)]);
|
|
2805
|
+
if (!rows2) return [];
|
|
2806
|
+
const out2 = [];
|
|
2807
|
+
for (const row of rows2) {
|
|
2808
|
+
const id = String(row.id);
|
|
2809
|
+
if (!SESSION_ID.test(id)) continue;
|
|
2810
|
+
const { started_at, last_at, ...rest } = sessionFacts(row);
|
|
2811
|
+
if (!started_at || !last_at) continue;
|
|
2812
|
+
out2.push({ session: { id, ...rest }, started_at, last_at });
|
|
2813
|
+
}
|
|
2814
|
+
return out2;
|
|
2815
|
+
}
|
|
2816
|
+
};
|
|
2817
|
+
function prune4(o) {
|
|
2818
|
+
for (const k of Object.keys(o)) if (o[k] === void 0) delete o[k];
|
|
2819
|
+
return o;
|
|
2820
|
+
}
|
|
2821
|
+
var transcriptRef = (db, session) => `${db}#${session}`;
|
|
2822
|
+
function splitRef(ref) {
|
|
2823
|
+
if (!ref) return null;
|
|
2824
|
+
const at = ref.lastIndexOf("#");
|
|
2825
|
+
if (at <= 0) return null;
|
|
2826
|
+
const db = ref.slice(0, at);
|
|
2827
|
+
const session = ref.slice(at + 1);
|
|
2828
|
+
return db && SESSION_ID.test(session) ? { db, session } : null;
|
|
2829
|
+
}
|
|
2830
|
+
var CHAIN_LIMIT = 4e3;
|
|
2831
|
+
var CHAIN_SQL = `WITH RECURSIVE chain(node_id, parent_node_id, chat_message, created_at) AS (
|
|
2832
|
+
SELECT node_id, parent_node_id, chat_message, created_at FROM message_nodes
|
|
2833
|
+
WHERE session_id = ? AND node_id = (SELECT main_chain_id FROM sessions WHERE id = ?)
|
|
2834
|
+
UNION ALL
|
|
2835
|
+
SELECT m.node_id, m.parent_node_id, m.chat_message, m.created_at FROM message_nodes m
|
|
2836
|
+
JOIN chain c ON m.node_id = c.parent_node_id AND m.session_id = ? AND m.node_id >= ?
|
|
2837
|
+
)
|
|
2838
|
+
SELECT node_id, chat_message, created_at FROM chain ORDER BY node_id DESC LIMIT ${CHAIN_LIMIT}`;
|
|
2839
|
+
var SESSION_FIELDS = `s.model, s.working_directory, s.created_at, s.last_activity_at,
|
|
2840
|
+
(SELECT p.content FROM prompt_history p WHERE p.session_id = s.id AND p.is_shell = 0
|
|
2841
|
+
ORDER BY p.timestamp ASC, p.id ASC LIMIT 1) AS first_ask`;
|
|
2842
|
+
var SESSION_SQL = `SELECT ${SESSION_FIELDS} FROM sessions s WHERE s.id = ? LIMIT 1`;
|
|
2843
|
+
var BACKFILL_SQL = `SELECT s.id, ${SESSION_FIELDS} FROM sessions s
|
|
2844
|
+
WHERE s.hidden = 0 AND typeof(s.id) = ? AND s.last_activity_at >= ?
|
|
2845
|
+
ORDER BY s.last_activity_at DESC LIMIT 500`;
|
|
2846
|
+
function sessionFacts(row) {
|
|
2847
|
+
const out2 = {};
|
|
2848
|
+
if (typeof row.model === "string" && row.model) out2.model = row.model;
|
|
2849
|
+
if (typeof row.working_directory === "string" && path11.isAbsolute(row.working_directory))
|
|
2850
|
+
out2.cwd = row.working_directory;
|
|
2851
|
+
const started = Number(row.created_at);
|
|
2852
|
+
const last = Number(row.last_activity_at);
|
|
2853
|
+
if (Number.isFinite(started) && started > 0) out2.started_at = Math.round(started * 1e3);
|
|
2854
|
+
if (Number.isFinite(last) && last > 0) out2.last_at = Math.round(last * 1e3);
|
|
2855
|
+
if (typeof row.first_ask === "string") {
|
|
2856
|
+
const line = row.first_ask.split("\n").find((l) => l.trim()) ?? "";
|
|
2857
|
+
if (line.trim()) {
|
|
2858
|
+
out2.title = cut(line.trim(), 80);
|
|
2859
|
+
out2.title_source = "first-ask";
|
|
2860
|
+
}
|
|
2861
|
+
}
|
|
2862
|
+
return out2;
|
|
2863
|
+
}
|
|
2864
|
+
function readDevinChain(db, session, fromLine) {
|
|
2865
|
+
if (!SESSION_ID.test(session)) return { turns: [], line: fromLine };
|
|
2866
|
+
const floor = Number.isFinite(fromLine) && fromLine > 0 ? Math.floor(fromLine) : 0;
|
|
2867
|
+
const rows2 = querySqlite(db, CHAIN_SQL, [session, session, session, floor]);
|
|
2868
|
+
if (!rows2?.length) return { turns: [], line: fromLine };
|
|
2869
|
+
const msgs = [];
|
|
2870
|
+
let end = fromLine;
|
|
2871
|
+
for (const row of rows2) {
|
|
2872
|
+
const node = Number(row.node_id);
|
|
2873
|
+
if (!Number.isFinite(node)) continue;
|
|
2874
|
+
if (node + 1 > end) end = node + 1;
|
|
2875
|
+
if (node < fromLine) continue;
|
|
2876
|
+
let j;
|
|
2877
|
+
try {
|
|
2878
|
+
j = JSON.parse(String(row.chat_message ?? ""));
|
|
2879
|
+
} catch {
|
|
2880
|
+
continue;
|
|
2881
|
+
}
|
|
2882
|
+
const text = typeof j.content === "string" ? j.content.trim() : "";
|
|
2883
|
+
if (!text) continue;
|
|
2884
|
+
const secs = Number(row.created_at);
|
|
2885
|
+
const at = Date.parse(String(j.metadata?.created_at ?? "")) || (Number.isFinite(secs) && secs > 0 ? Math.round(secs * 1e3) : 0) || Date.now();
|
|
2886
|
+
if (j.role === "user" && j.metadata?.is_user_input === true) msgs.push({ role: "user", text, at, line: node });
|
|
2887
|
+
else if (j.role === "assistant") msgs.push({ role: "assistant", text, at, line: node });
|
|
2888
|
+
}
|
|
2889
|
+
msgs.sort((a, b) => a.line - b.line);
|
|
2890
|
+
return pairTurns(msgs, end);
|
|
2891
|
+
}
|
|
2892
|
+
function devinVersion(env = process.env) {
|
|
2893
|
+
try {
|
|
2894
|
+
const target = readlinkSync(path11.join(devinDataDir(env), "_versions", "current"));
|
|
2895
|
+
const name = path11.basename(target);
|
|
2896
|
+
return /^[0-9][0-9A-Za-z.+-]{0,63}$/.test(name) ? name : void 0;
|
|
2897
|
+
} catch {
|
|
2898
|
+
return void 0;
|
|
2899
|
+
}
|
|
2900
|
+
}
|
|
2901
|
+
function devinAccount(env = process.env) {
|
|
2902
|
+
const d = readJson(configFile3(env)).devin;
|
|
2903
|
+
const id = d && typeof d === "object" ? d.org_id : void 0;
|
|
2904
|
+
return typeof id === "string" && /^[A-Za-z0-9_.:-]{1,128}$/.test(id) ? id : void 0;
|
|
2905
|
+
}
|
|
2906
|
+
|
|
2907
|
+
// ../core/src/adapters/gemini-cli.ts
|
|
2908
|
+
import { execFileSync as execFileSync5 } from "node:child_process";
|
|
2909
|
+
import { existsSync as existsSync7, readFileSync as readFileSync6 } from "node:fs";
|
|
2910
|
+
import path12 from "node:path";
|
|
2911
|
+
var NAME5 = "gemini-cli";
|
|
2417
2912
|
var GEMINI_EVENTS = [
|
|
2418
2913
|
"SessionStart",
|
|
2419
2914
|
"BeforeAgent",
|
|
@@ -2424,11 +2919,11 @@ var GEMINI_EVENTS = [
|
|
|
2424
2919
|
"PreCompress",
|
|
2425
2920
|
"SessionEnd"
|
|
2426
2921
|
];
|
|
2427
|
-
var
|
|
2428
|
-
var geminiDir = (env) => env.GEMINI_CLI_HOME ||
|
|
2429
|
-
var settings = (env) =>
|
|
2430
|
-
var
|
|
2431
|
-
var
|
|
2922
|
+
var OPTS4 = { matcherFor: (e) => /Tool/.test(e) ? ".*" : void 0, timeout: 5e3 };
|
|
2923
|
+
var geminiDir = (env) => env.GEMINI_CLI_HOME || path12.join(homeDir(), ".gemini");
|
|
2924
|
+
var settings = (env) => path12.join(geminiDir(env), "settings.json");
|
|
2925
|
+
var validTool5 = (n) => typeof n === "string" && /^[A-Za-z0-9_.:-]{1,120}$/.test(n);
|
|
2926
|
+
var eventsFor4 = (lean) => lean ? GEMINI_EVENTS.filter((e) => e !== "BeforeTool") : [...GEMINI_EVENTS];
|
|
2432
2927
|
function geminiHooksActive(settingsFile) {
|
|
2433
2928
|
let tools;
|
|
2434
2929
|
try {
|
|
@@ -2440,7 +2935,7 @@ function geminiHooksActive(settingsFile) {
|
|
|
2440
2935
|
return { active: false, note: "inactive: tools.enableHooks is false in settings.json" };
|
|
2441
2936
|
let version = "";
|
|
2442
2937
|
try {
|
|
2443
|
-
version =
|
|
2938
|
+
version = execFileSync5("gemini", ["--version"], {
|
|
2444
2939
|
encoding: "utf8",
|
|
2445
2940
|
timeout: 4e3,
|
|
2446
2941
|
stdio: ["ignore", "pipe", "ignore"]
|
|
@@ -2459,21 +2954,21 @@ function geminiHooksActive(settingsFile) {
|
|
|
2459
2954
|
};
|
|
2460
2955
|
}
|
|
2461
2956
|
var geminiCli = {
|
|
2462
|
-
name:
|
|
2957
|
+
name: NAME5,
|
|
2463
2958
|
events: GEMINI_EVENTS,
|
|
2464
2959
|
// ~/.gemini also belongs to Antigravity; Gemini CLI is here when its settings or its chat logs exist.
|
|
2465
|
-
detect: (env = process.env) =>
|
|
2960
|
+
detect: (env = process.env) => existsSync7(settings(env)) || existsSync7(path12.join(geminiDir(env), "tmp")),
|
|
2466
2961
|
configFiles: (env = process.env) => [settings(env)],
|
|
2467
|
-
install: (
|
|
2468
|
-
const r = installClaudeShaped(settings(env),
|
|
2962
|
+
install: (cmd, env = process.env, opts = {}) => {
|
|
2963
|
+
const r = installClaudeShaped(settings(env), NAME5, eventsFor4(opts.lean), cmd, OPTS4);
|
|
2469
2964
|
const a = geminiHooksActive(settings(env));
|
|
2470
2965
|
return [a.note && !r.skipped ? { ...r, note: [r.note, a.note].filter(Boolean).join("; ") } : r];
|
|
2471
2966
|
},
|
|
2472
2967
|
uninstall: (env = process.env, opts = {}) => [
|
|
2473
2968
|
uninstallClaudeShaped(settings(env), GEMINI_EVENTS, { created: opts.created?.includes(settings(env)) })
|
|
2474
2969
|
],
|
|
2475
|
-
installed: (
|
|
2476
|
-
const state2 = installedClaudeShaped(settings(env),
|
|
2970
|
+
installed: (cmd, env = process.env, opts = {}) => {
|
|
2971
|
+
const state2 = installedClaudeShaped(settings(env), NAME5, eventsFor4(opts.lean), cmd, OPTS4);
|
|
2477
2972
|
const a = geminiHooksActive(settings(env));
|
|
2478
2973
|
return [
|
|
2479
2974
|
{
|
|
@@ -2487,14 +2982,15 @@ var geminiCli = {
|
|
|
2487
2982
|
const event = input.argv[1] ?? "";
|
|
2488
2983
|
let s = {};
|
|
2489
2984
|
try {
|
|
2490
|
-
|
|
2985
|
+
const j = input.stdin ? JSON.parse(input.stdin) : {};
|
|
2986
|
+
if (j && typeof j === "object" && !Array.isArray(j)) s = j;
|
|
2491
2987
|
} catch {
|
|
2492
2988
|
}
|
|
2493
2989
|
const id = s.session_id;
|
|
2494
2990
|
if (!id) return null;
|
|
2495
2991
|
const session = { id: String(id), cwd: typeof s.cwd === "string" ? s.cwd : input.cwd };
|
|
2496
|
-
const tool =
|
|
2497
|
-
const ev = (type, data) => ({ type, data:
|
|
2992
|
+
const tool = validTool5(s.tool_name) ? s.tool_name : void 0;
|
|
2993
|
+
const ev = (type, data) => ({ type, data: prune5(data), harnessEvent: event });
|
|
2498
2994
|
const events = [];
|
|
2499
2995
|
switch (event) {
|
|
2500
2996
|
case "SessionStart":
|
|
@@ -2575,7 +3071,7 @@ var geminiCli = {
|
|
|
2575
3071
|
const raw = first ? first.content ?? first.message ?? first.text : void 0;
|
|
2576
3072
|
const text = typeof raw === "string" ? raw.trim() : "";
|
|
2577
3073
|
if (text) {
|
|
2578
|
-
const line = text.split("\n")
|
|
3074
|
+
const line = text.split("\n")[0];
|
|
2579
3075
|
out2.title = line.length > 80 ? `${line.slice(0, 80).replace(/\s+\S*$/, "")}\u2026` : line;
|
|
2580
3076
|
out2.title_source = "first-ask";
|
|
2581
3077
|
}
|
|
@@ -2589,13 +3085,13 @@ var geminiCli = {
|
|
|
2589
3085
|
backfill: () => []
|
|
2590
3086
|
// chat logs are keyed by a project hash, not a session id we can name; the first hook of a resumed session reports it
|
|
2591
3087
|
};
|
|
2592
|
-
function
|
|
3088
|
+
function prune5(o) {
|
|
2593
3089
|
for (const k of Object.keys(o)) if (o[k] === void 0) delete o[k];
|
|
2594
3090
|
return o;
|
|
2595
3091
|
}
|
|
2596
3092
|
|
|
2597
3093
|
// ../core/src/adapters/index.ts
|
|
2598
|
-
var ADAPTERS = [claudeCode, codex, geminiCli, antigravity];
|
|
3094
|
+
var ADAPTERS = [claudeCode, codex, geminiCli, antigravity, devin];
|
|
2599
3095
|
var adapterByName = (name) => ADAPTERS.find((a) => a.name === name);
|
|
2600
3096
|
function drivableHarnesses(env = process.env) {
|
|
2601
3097
|
return [
|
|
@@ -2606,8 +3102,8 @@ function drivableHarnesses(env = process.env) {
|
|
|
2606
3102
|
}
|
|
2607
3103
|
|
|
2608
3104
|
// ../core/src/config.ts
|
|
2609
|
-
import { chmodSync, existsSync as
|
|
2610
|
-
import
|
|
3105
|
+
import { chmodSync, existsSync as existsSync8, mkdirSync as mkdirSync4, readFileSync as readFileSync7, writeFileSync as writeFileSync4 } from "node:fs";
|
|
3106
|
+
import path13 from "node:path";
|
|
2611
3107
|
var DEFAULT_CONFIG = { sinks: [], harnesses: {}, update_check: true, keep_days: 30 };
|
|
2612
3108
|
function readConfig(file = configFile()) {
|
|
2613
3109
|
try {
|
|
@@ -2623,10 +3119,10 @@ function readConfig(file = configFile()) {
|
|
|
2623
3119
|
}
|
|
2624
3120
|
}
|
|
2625
3121
|
function writeConfig(c, file = configFile()) {
|
|
2626
|
-
mkdirSync4(
|
|
3122
|
+
mkdirSync4(path13.dirname(file), { recursive: true, mode: 448 });
|
|
2627
3123
|
writeFileSync4(file, `${JSON.stringify(c, null, 2)}
|
|
2628
3124
|
`, { mode: 384 });
|
|
2629
|
-
if (process.platform !== "win32" &&
|
|
3125
|
+
if (process.platform !== "win32" && existsSync8(file)) {
|
|
2630
3126
|
try {
|
|
2631
3127
|
chmodSync(file, 384);
|
|
2632
3128
|
} catch {
|
|
@@ -2655,7 +3151,7 @@ function encodeTime(t) {
|
|
|
2655
3151
|
function ulid(now = Date.now()) {
|
|
2656
3152
|
if (now === lastTime) {
|
|
2657
3153
|
for (let i = lastRand.length - 1; i >= 0; i--) {
|
|
2658
|
-
const v =
|
|
3154
|
+
const v = lastRand[i] + 1;
|
|
2659
3155
|
if (v < 32) {
|
|
2660
3156
|
lastRand[i] = v;
|
|
2661
3157
|
break;
|
|
@@ -2664,8 +3160,7 @@ function ulid(now = Date.now()) {
|
|
|
2664
3160
|
}
|
|
2665
3161
|
} else {
|
|
2666
3162
|
lastTime = now;
|
|
2667
|
-
|
|
2668
|
-
lastRand = Array.from({ length: 16 }, (_, i) => (b[i] ?? 0) % 32);
|
|
3163
|
+
lastRand = Array.from(randomBytes(16), (b) => b % 32);
|
|
2669
3164
|
}
|
|
2670
3165
|
return encodeTime(now) + lastRand.map((v) => ALPHABET[v]).join("");
|
|
2671
3166
|
}
|
|
@@ -2692,7 +3187,7 @@ function makeEvent(a, ctx) {
|
|
|
2692
3187
|
import {
|
|
2693
3188
|
appendFileSync,
|
|
2694
3189
|
closeSync as closeSync3,
|
|
2695
|
-
existsSync as
|
|
3190
|
+
existsSync as existsSync9,
|
|
2696
3191
|
mkdirSync as mkdirSync5,
|
|
2697
3192
|
openSync as openSync3,
|
|
2698
3193
|
readdirSync as readdirSync6,
|
|
@@ -2703,42 +3198,45 @@ import {
|
|
|
2703
3198
|
unlinkSync as unlinkSync3,
|
|
2704
3199
|
writeFileSync as writeFileSync5
|
|
2705
3200
|
} from "node:fs";
|
|
2706
|
-
import
|
|
3201
|
+
import path14 from "node:path";
|
|
3202
|
+
var READ_CHUNK = 1024 * 1024;
|
|
3203
|
+
var READ_MAX = 8 * 1024 * 1024;
|
|
2707
3204
|
var Outbox = class {
|
|
2708
3205
|
constructor(root) {
|
|
2709
3206
|
this.root = root;
|
|
2710
3207
|
}
|
|
2711
3208
|
root;
|
|
2712
3209
|
dir(harness) {
|
|
2713
|
-
return
|
|
3210
|
+
return path14.join(this.root, "outbox", safeId(harness));
|
|
2714
3211
|
}
|
|
2715
3212
|
file(ref) {
|
|
2716
|
-
return
|
|
3213
|
+
return path14.join(this.dir(ref.harness), `${safeId(ref.session)}.jsonl`);
|
|
2717
3214
|
}
|
|
2718
3215
|
seqFile(ref) {
|
|
2719
|
-
return
|
|
3216
|
+
return path14.join(this.dir(ref.harness), `${safeId(ref.session)}.seq`);
|
|
2720
3217
|
}
|
|
2721
3218
|
/** The next seq for a session, persisted (survives a hibernating machine and a
|
|
2722
3219
|
* restart; an in-memory counter would restart at 0 and collide). */
|
|
2723
3220
|
nextSeq(ref) {
|
|
2724
3221
|
const f = this.seqFile(ref);
|
|
2725
|
-
let n =
|
|
3222
|
+
let n = -1;
|
|
2726
3223
|
try {
|
|
2727
|
-
|
|
3224
|
+
const t = readFileSync8(f, "utf8").trim();
|
|
3225
|
+
if (/^\d+$/.test(t)) n = Number(t);
|
|
2728
3226
|
} catch {
|
|
2729
|
-
n = this.lastSeqInFile(ref) + 1;
|
|
2730
3227
|
}
|
|
2731
|
-
|
|
3228
|
+
if (n < 0) n = this.lastSeqInFile(ref) + 1;
|
|
3229
|
+
mkdirSync5(path14.dirname(f), { recursive: true, mode: 448 });
|
|
2732
3230
|
writeFileSync5(f, String(n + 1), { mode: 384 });
|
|
2733
3231
|
return n;
|
|
2734
3232
|
}
|
|
2735
3233
|
lastSeqInFile(ref) {
|
|
2736
3234
|
const f = this.file(ref);
|
|
2737
|
-
if (!
|
|
3235
|
+
if (!existsSync9(f)) return -1;
|
|
2738
3236
|
const lines = readFileSync8(f, "utf8").trim().split("\n");
|
|
2739
3237
|
for (let i = lines.length - 1; i >= 0; i--) {
|
|
2740
3238
|
try {
|
|
2741
|
-
const j = JSON.parse(lines[i]
|
|
3239
|
+
const j = JSON.parse(lines[i]);
|
|
2742
3240
|
if (typeof j.session?.seq === "number") return j.session.seq;
|
|
2743
3241
|
} catch {
|
|
2744
3242
|
}
|
|
@@ -2754,10 +3252,10 @@ var Outbox = class {
|
|
|
2754
3252
|
/** Every session file, newest first. */
|
|
2755
3253
|
sessions() {
|
|
2756
3254
|
const out2 = [];
|
|
2757
|
-
const base =
|
|
2758
|
-
if (!
|
|
3255
|
+
const base = path14.join(this.root, "outbox");
|
|
3256
|
+
if (!existsSync9(base)) return [];
|
|
2759
3257
|
for (const h of readdirSync6(base)) {
|
|
2760
|
-
const d =
|
|
3258
|
+
const d = path14.join(base, h);
|
|
2761
3259
|
let names = [];
|
|
2762
3260
|
try {
|
|
2763
3261
|
names = readdirSync6(d).filter((n) => n.endsWith(".jsonl"));
|
|
@@ -2766,7 +3264,7 @@ var Outbox = class {
|
|
|
2766
3264
|
}
|
|
2767
3265
|
for (const n of names) {
|
|
2768
3266
|
try {
|
|
2769
|
-
out2.push({ ref: { harness: h, session: n.slice(0, -6) }, mtime: statSync6(
|
|
3267
|
+
out2.push({ ref: { harness: h, session: n.slice(0, -6) }, mtime: statSync6(path14.join(d, n)).mtimeMs });
|
|
2770
3268
|
} catch {
|
|
2771
3269
|
}
|
|
2772
3270
|
}
|
|
@@ -2774,31 +3272,43 @@ var Outbox = class {
|
|
|
2774
3272
|
return out2.sort((a, b) => b.mtime - a.mtime).map((x) => x.ref);
|
|
2775
3273
|
}
|
|
2776
3274
|
/** Read events after byte offset `from`; returns them and the new offset. Lines
|
|
2777
|
-
* are read whole; a partial trailing line (a write in progress) is left.
|
|
3275
|
+
* are read whole; a partial trailing line (a write in progress) is left. A read
|
|
3276
|
+
* takes READ_CHUNK bytes at a time, not the rest of the file: a session file grows
|
|
3277
|
+
* to tens of MB and a drain takes it in batches of 50. It reads on until the batch
|
|
3278
|
+
* is full or the file ends, so lines that parse to nothing never stop a drain; one
|
|
3279
|
+
* line longer than the chunk gets a larger one, up to READ_MAX. */
|
|
2778
3280
|
read(ref, from, maxEvents = 500) {
|
|
2779
3281
|
const f = this.file(ref);
|
|
2780
|
-
if (!
|
|
3282
|
+
if (!existsSync9(f)) return { events: [], offset: from };
|
|
2781
3283
|
const size = statSync6(f).size;
|
|
2782
3284
|
if (from >= size) return { events: [], offset: from };
|
|
2783
3285
|
const fd = openSync3(f, "r");
|
|
2784
3286
|
try {
|
|
2785
|
-
const buf = Buffer.alloc(Math.min(size - from, 8 * 1024 * 1024));
|
|
2786
|
-
const n = readSync3(fd, buf, 0, buf.length, from);
|
|
2787
|
-
const text = buf.subarray(0, n).toString("utf8");
|
|
2788
3287
|
const events = [];
|
|
2789
3288
|
let offset = from;
|
|
2790
|
-
let
|
|
2791
|
-
while (events.length < maxEvents) {
|
|
2792
|
-
const
|
|
2793
|
-
|
|
2794
|
-
const
|
|
2795
|
-
pos =
|
|
2796
|
-
|
|
2797
|
-
|
|
2798
|
-
|
|
2799
|
-
|
|
2800
|
-
|
|
3289
|
+
let chunk = READ_CHUNK;
|
|
3290
|
+
while (events.length < maxEvents && offset < size) {
|
|
3291
|
+
const buf = Buffer.alloc(Math.min(size - offset, chunk));
|
|
3292
|
+
const n = readSync3(fd, buf, 0, buf.length, offset);
|
|
3293
|
+
const text = buf.subarray(0, n).toString("utf8");
|
|
3294
|
+
let pos = 0;
|
|
3295
|
+
let used = 0;
|
|
3296
|
+
while (events.length < maxEvents) {
|
|
3297
|
+
const nl = text.indexOf("\n", pos);
|
|
3298
|
+
if (nl < 0) break;
|
|
3299
|
+
const line = text.slice(pos, nl);
|
|
3300
|
+
pos = nl + 1;
|
|
3301
|
+
used += Buffer.byteLength(line, "utf8") + 1;
|
|
3302
|
+
if (!line.trim()) continue;
|
|
3303
|
+
try {
|
|
3304
|
+
events.push(JSON.parse(line));
|
|
3305
|
+
} catch {
|
|
3306
|
+
}
|
|
2801
3307
|
}
|
|
3308
|
+
offset += used;
|
|
3309
|
+
if (used) continue;
|
|
3310
|
+
if (offset + n >= size || chunk >= READ_MAX) break;
|
|
3311
|
+
chunk *= 2;
|
|
2802
3312
|
}
|
|
2803
3313
|
return { events, offset };
|
|
2804
3314
|
} finally {
|
|
@@ -2842,7 +3352,7 @@ var Cursors = class {
|
|
|
2842
3352
|
root;
|
|
2843
3353
|
sink;
|
|
2844
3354
|
file() {
|
|
2845
|
-
return
|
|
3355
|
+
return path14.join(this.root, "cursors", `${safeId(this.sink)}.json`);
|
|
2846
3356
|
}
|
|
2847
3357
|
read() {
|
|
2848
3358
|
try {
|
|
@@ -2853,7 +3363,7 @@ var Cursors = class {
|
|
|
2853
3363
|
}
|
|
2854
3364
|
write(c) {
|
|
2855
3365
|
const f = this.file();
|
|
2856
|
-
mkdirSync5(
|
|
3366
|
+
mkdirSync5(path14.dirname(f), { recursive: true, mode: 448 });
|
|
2857
3367
|
const tmp = `${f}.${process.pid}.tmp`;
|
|
2858
3368
|
writeFileSync5(tmp, JSON.stringify(c), { mode: 384 });
|
|
2859
3369
|
renameSync(tmp, f);
|
|
@@ -2863,11 +3373,14 @@ var Cursors = class {
|
|
|
2863
3373
|
}
|
|
2864
3374
|
};
|
|
2865
3375
|
function takeLock(root, ref, staleMs = 5 * 6e4) {
|
|
2866
|
-
|
|
3376
|
+
return takeNamedLock(root, `${safeId(ref.harness)}-${safeId(ref.session)}`, staleMs);
|
|
3377
|
+
}
|
|
3378
|
+
function takeNamedLock(root, name, staleMs = 5 * 6e4) {
|
|
3379
|
+
const dir = path14.join(root, "locks");
|
|
2867
3380
|
mkdirSync5(dir, { recursive: true, mode: 448 });
|
|
2868
|
-
const f =
|
|
3381
|
+
const f = path14.join(dir, `${name}.lock`);
|
|
2869
3382
|
try {
|
|
2870
|
-
if (Date.now() - statSync6(f).mtimeMs > staleMs) unlinkSync3(f);
|
|
3383
|
+
if (Date.now() - statSync6(f).mtimeMs > staleMs || !processAlive(Number(readFileSync8(f, "utf8")))) unlinkSync3(f);
|
|
2871
3384
|
else return null;
|
|
2872
3385
|
} catch {
|
|
2873
3386
|
}
|
|
@@ -2883,9 +3396,18 @@ function takeLock(root, ref, staleMs = 5 * 6e4) {
|
|
|
2883
3396
|
}
|
|
2884
3397
|
};
|
|
2885
3398
|
}
|
|
3399
|
+
function processAlive(pid) {
|
|
3400
|
+
if (!Number.isInteger(pid) || pid <= 0) return true;
|
|
3401
|
+
try {
|
|
3402
|
+
process.kill(pid, 0);
|
|
3403
|
+
return true;
|
|
3404
|
+
} catch (e) {
|
|
3405
|
+
return e.code === "EPERM";
|
|
3406
|
+
}
|
|
3407
|
+
}
|
|
2886
3408
|
|
|
2887
3409
|
// ../core/src/privacy/pii.ts
|
|
2888
|
-
import { createHash as
|
|
3410
|
+
import { createHash as createHash3 } from "node:crypto";
|
|
2889
3411
|
var RULESET_PII = "pii@1";
|
|
2890
3412
|
var EMAIL = /\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}\b/g;
|
|
2891
3413
|
var PHONE = /(?<![\w.])(?:\+\d{1,3}[ .-]?)?(?:\(\d{2,4}\)[ .-]?)?\d{2,4}(?:[ .-]?\d{2,4}){1,3}(?!\w)/g;
|
|
@@ -2907,7 +3429,7 @@ function reducePii(text, home) {
|
|
|
2907
3429
|
return out2;
|
|
2908
3430
|
}
|
|
2909
3431
|
function hashMachine(name) {
|
|
2910
|
-
return
|
|
3432
|
+
return createHash3("sha256").update(name).digest("hex").slice(0, 8);
|
|
2911
3433
|
}
|
|
2912
3434
|
function reducePiiDeep(v, home) {
|
|
2913
3435
|
if (typeof v === "string") return reducePii(v, home);
|
|
@@ -2996,12 +3518,12 @@ function redactAssignments(text) {
|
|
|
2996
3518
|
let last = 0;
|
|
2997
3519
|
NAME_SEP.lastIndex = 0;
|
|
2998
3520
|
for (let m = NAME_SEP.exec(text); m; m = NAME_SEP.exec(text)) {
|
|
2999
|
-
const name = m[1]
|
|
3521
|
+
const name = m[1];
|
|
3000
3522
|
if (!isSecretName(name)) continue;
|
|
3001
3523
|
VALUE.lastIndex = NAME_SEP.lastIndex;
|
|
3002
3524
|
const v = VALUE.exec(text);
|
|
3003
3525
|
if (!v) continue;
|
|
3004
|
-
const value = v[1] ?? v[2] ?? v[3] ?? v[4]
|
|
3526
|
+
const value = v[1] ?? v[2] ?? v[3] ?? v[4];
|
|
3005
3527
|
NAME_SEP.lastIndex = VALUE.lastIndex;
|
|
3006
3528
|
if (isPlaceholder(value) || value.includes(REDACTED)) continue;
|
|
3007
3529
|
if (/tokens?$/i.test(name) && /^\d+(?:[.,]\d+)*[km]?$/i.test(value)) continue;
|
|
@@ -3057,7 +3579,7 @@ function redactSecrets(text) {
|
|
|
3057
3579
|
let out2 = text;
|
|
3058
3580
|
for (const re of TOKEN_PATTERNS) out2 = out2.replace(re, REDACTED);
|
|
3059
3581
|
out2 = out2.replace(URL_CREDENTIALS, (_m, head, _pw, at) => `${head}${REDACTED}${at}`);
|
|
3060
|
-
out2 = out2.replace(AUTH_HEADER, (
|
|
3582
|
+
out2 = out2.replace(AUTH_HEADER, (_m, head) => `${head}${REDACTED}`);
|
|
3061
3583
|
out2 = redactAssignments(out2);
|
|
3062
3584
|
out2 = out2.replace(
|
|
3063
3585
|
PROSE,
|
|
@@ -3086,6 +3608,7 @@ var TYPE_TIER = {
|
|
|
3086
3608
|
"turn.started": 0,
|
|
3087
3609
|
"turn.ended": 0,
|
|
3088
3610
|
"turn.transcript": 2,
|
|
3611
|
+
"turn.progress": 2,
|
|
3089
3612
|
"tool.started": 1,
|
|
3090
3613
|
"tool.ended": 1,
|
|
3091
3614
|
"files.changed": 1,
|
|
@@ -3105,6 +3628,7 @@ var DATA_ALLOW = {
|
|
|
3105
3628
|
"turn.started": { 0: ["turn_id"], 1: ["prompt_chars"] },
|
|
3106
3629
|
"turn.ended": { 0: ["turn_id", "reason", "ms"], 1: ["error"] },
|
|
3107
3630
|
"turn.transcript": { 2: ["turn", "user", "assistant", "at"] },
|
|
3631
|
+
"turn.progress": { 2: ["turn", "user", "assistant", "at"] },
|
|
3108
3632
|
"tool.started": { 1: ["tool", "call_id", "turn_id", "agent_id"], 3: ["input"] },
|
|
3109
3633
|
"tool.ended": { 1: ["tool", "call_id", "turn_id", "agent_id", "ms", "ok", "error"], 3: ["input", "output"] },
|
|
3110
3634
|
"files.changed": { 1: ["paths"] },
|
|
@@ -3145,10 +3669,12 @@ function cap(v, max) {
|
|
|
3145
3669
|
}
|
|
3146
3670
|
return v;
|
|
3147
3671
|
}
|
|
3672
|
+
function capTail(v, max) {
|
|
3673
|
+
return v.length > max ? `\u2026${v.slice(v.length - max + 1)}` : v;
|
|
3674
|
+
}
|
|
3148
3675
|
function filterEvent(event, tier, pii, opts = {}) {
|
|
3149
3676
|
const minTier = TYPE_TIER[event.type];
|
|
3150
3677
|
if (minTier !== void 0 && minTier > tier) return null;
|
|
3151
|
-
if (event.type === "native" && tier < 3) return null;
|
|
3152
3678
|
const session = {};
|
|
3153
3679
|
for (const k of SESSION_ALLOW)
|
|
3154
3680
|
if (event.session[k] !== void 0)
|
|
@@ -3158,7 +3684,8 @@ function filterEvent(event, tier, pii, opts = {}) {
|
|
|
3158
3684
|
if (keys === null) data = { ...event.data };
|
|
3159
3685
|
else for (const k of keys) if (event.data[k] !== void 0) data[k] = event.data[k];
|
|
3160
3686
|
if (typeof data.user === "string") data.user = cap(data.user, MAX_SIDE);
|
|
3161
|
-
if (typeof data.assistant === "string")
|
|
3687
|
+
if (typeof data.assistant === "string")
|
|
3688
|
+
data.assistant = event.type === "turn.progress" ? capTail(data.assistant, MAX_SIDE) : cap(data.assistant, MAX_SIDE);
|
|
3162
3689
|
if (data.input !== void 0) data.input = cap(data.input, MAX_IO);
|
|
3163
3690
|
if (data.output !== void 0) data.output = cap(data.output, MAX_IO);
|
|
3164
3691
|
if (typeof data.message === "string") data.message = cap(data.message, 200);
|
|
@@ -3179,9 +3706,15 @@ function filterEvent(event, tier, pii, opts = {}) {
|
|
|
3179
3706
|
return out2;
|
|
3180
3707
|
}
|
|
3181
3708
|
|
|
3709
|
+
// ../core/src/short-id.ts
|
|
3710
|
+
import { createHash as createHash4 } from "node:crypto";
|
|
3711
|
+
function shortId(harness, id) {
|
|
3712
|
+
return createHash4("sha256").update(`sessionpipe.session:${harness}:${id}`).digest("hex").slice(0, 8);
|
|
3713
|
+
}
|
|
3714
|
+
|
|
3182
3715
|
// ../core/src/sinks/file.ts
|
|
3183
3716
|
import { appendFileSync as appendFileSync2, mkdirSync as mkdirSync6 } from "node:fs";
|
|
3184
|
-
import
|
|
3717
|
+
import path15 from "node:path";
|
|
3185
3718
|
var FileSink = class {
|
|
3186
3719
|
constructor(name, file, tier, pii) {
|
|
3187
3720
|
this.name = name;
|
|
@@ -3195,7 +3728,7 @@ var FileSink = class {
|
|
|
3195
3728
|
pii;
|
|
3196
3729
|
async send(events) {
|
|
3197
3730
|
try {
|
|
3198
|
-
mkdirSync6(
|
|
3731
|
+
mkdirSync6(path15.dirname(this.file), { recursive: true });
|
|
3199
3732
|
appendFileSync2(this.file, `${events.map((e) => JSON.stringify(e)).join("\n")}
|
|
3200
3733
|
`);
|
|
3201
3734
|
return { ok: true, acknowledged: events.length, accepted: events.length, duplicates: 0, rejected: [] };
|
|
@@ -3219,29 +3752,39 @@ var StdoutSink = class {
|
|
|
3219
3752
|
pii;
|
|
3220
3753
|
write;
|
|
3221
3754
|
async send(events) {
|
|
3222
|
-
|
|
3223
|
-
|
|
3755
|
+
try {
|
|
3756
|
+
for (const e of events) this.write(JSON.stringify(e));
|
|
3757
|
+
return { ok: true, acknowledged: events.length, accepted: events.length, duplicates: 0, rejected: [] };
|
|
3758
|
+
} catch (e) {
|
|
3759
|
+
return { ok: false, acknowledged: 0, error: String(e.message ?? e), action: "retry" };
|
|
3760
|
+
}
|
|
3224
3761
|
}
|
|
3225
3762
|
};
|
|
3226
3763
|
|
|
3227
3764
|
// src/connect.ts
|
|
3228
|
-
import
|
|
3765
|
+
import path16 from "node:path";
|
|
3229
3766
|
function isBroad(dir, home) {
|
|
3230
|
-
const d =
|
|
3231
|
-
const h =
|
|
3232
|
-
if (d ===
|
|
3233
|
-
return d === h || h.startsWith(
|
|
3767
|
+
const d = path16.resolve(dir);
|
|
3768
|
+
const h = path16.resolve(home);
|
|
3769
|
+
if (d === path16.parse(d).root) return true;
|
|
3770
|
+
return d === h || h.startsWith(
|
|
3771
|
+
/* v8 ignore next */
|
|
3772
|
+
d.endsWith(path16.sep) ? d : d + path16.sep
|
|
3773
|
+
);
|
|
3234
3774
|
}
|
|
3235
|
-
var inside = (child, parent) => child !== parent && child.startsWith(
|
|
3775
|
+
var inside = (child, parent) => child !== parent && child.startsWith(
|
|
3776
|
+
/* v8 ignore next */
|
|
3777
|
+
parent.endsWith(path16.sep) ? parent : parent + path16.sep
|
|
3778
|
+
);
|
|
3236
3779
|
function connectFolders(o) {
|
|
3237
|
-
if (o.flags.length) return { folders: o.flags.map((d) =>
|
|
3780
|
+
if (o.flags.length) return { folders: o.flags.map((d) => path16.resolve(d)), from: "flags" };
|
|
3238
3781
|
if (o.existing.length) return { folders: [], from: "existing" };
|
|
3239
|
-
if (!isBroad(o.cwd, o.home)) return { folders: [
|
|
3782
|
+
if (!isBroad(o.cwd, o.home)) return { folders: [path16.resolve(o.cwd)], from: "here" };
|
|
3240
3783
|
const exists = o.exists ?? (() => true);
|
|
3241
3784
|
const picked = [];
|
|
3242
3785
|
for (const raw of o.recent) {
|
|
3243
|
-
if (!raw || !
|
|
3244
|
-
const d =
|
|
3786
|
+
if (!raw || !path16.isAbsolute(raw)) continue;
|
|
3787
|
+
const d = path16.resolve(raw);
|
|
3245
3788
|
if (isBroad(d, o.home) || picked.includes(d) || !exists(d)) continue;
|
|
3246
3789
|
if (picked.some((p) => inside(d, p))) continue;
|
|
3247
3790
|
for (let i = picked.length - 1; i >= 0; i--) if (inside(picked[i], d)) picked.splice(i, 1);
|
|
@@ -3258,42 +3801,136 @@ function connectTier(flag2, current) {
|
|
|
3258
3801
|
|
|
3259
3802
|
// src/control/autoupdate.ts
|
|
3260
3803
|
import { spawn as spawn3 } from "node:child_process";
|
|
3261
|
-
import { readFileSync as
|
|
3262
|
-
import
|
|
3804
|
+
import { readFileSync as readFileSync14 } from "node:fs";
|
|
3805
|
+
import path24 from "node:path";
|
|
3263
3806
|
|
|
3264
3807
|
// src/update.ts
|
|
3265
3808
|
import { spawn as spawn2 } from "node:child_process";
|
|
3266
|
-
import { accessSync as
|
|
3267
|
-
import
|
|
3809
|
+
import { accessSync as accessSync3, constants as constants3, existsSync as existsSync11, realpathSync as realpathSync3 } from "node:fs";
|
|
3810
|
+
import path22 from "node:path";
|
|
3268
3811
|
|
|
3269
3812
|
// src/runtime.ts
|
|
3270
|
-
import { execFileSync as
|
|
3271
|
-
import { realpathSync as realpathSync2 } from "node:fs";
|
|
3272
|
-
import
|
|
3273
|
-
import
|
|
3813
|
+
import { execFileSync as execFileSync6 } from "node:child_process";
|
|
3814
|
+
import { readFileSync as readFileSync12, realpathSync as realpathSync2 } from "node:fs";
|
|
3815
|
+
import os8 from "node:os";
|
|
3816
|
+
import path21 from "node:path";
|
|
3817
|
+
|
|
3818
|
+
// src/launcher.ts
|
|
3819
|
+
import { accessSync as accessSync2, chmodSync as chmodSync2, constants as constants2, mkdirSync as mkdirSync7, readFileSync as readFileSync9, renameSync as renameSync2, rmSync, writeFileSync as writeFileSync6 } from "node:fs";
|
|
3820
|
+
import os5 from "node:os";
|
|
3821
|
+
import path17 from "node:path";
|
|
3822
|
+
var LAUNCHER = "sessionpipe-hook";
|
|
3823
|
+
var COPIED = ["worker.js", "hook.js"];
|
|
3824
|
+
function dataDir(env = process.env) {
|
|
3825
|
+
if (env.SESSIONPIPE_DATA) return path17.resolve(env.SESSIONPIPE_DATA);
|
|
3826
|
+
return path17.join(env.XDG_DATA_HOME || path17.join(os5.homedir(), ".local", "share"), "sessionpipe");
|
|
3827
|
+
}
|
|
3828
|
+
var launcherPath = (env = process.env) => path17.join(dataDir(env), LAUNCHER);
|
|
3829
|
+
var usesLauncher = (platform = process.platform) => platform !== "win32";
|
|
3830
|
+
var sq = (s) => `'${s.replace(/'/g, `'\\''`)}'`;
|
|
3831
|
+
function launcherScript(node, hook, version) {
|
|
3832
|
+
return `#!/bin/sh
|
|
3833
|
+
# sessionpipe ${version} hook launcher, written by \`sessionpipe install\`. Harnesses run
|
|
3834
|
+
# this; it runs hook.js beside it with the node sessionpipe was installed with, or with
|
|
3835
|
+
# any node on PATH once that one is gone (an nvm uninstall, a Homebrew upgrade).
|
|
3836
|
+
n=${sq(node)}
|
|
3837
|
+
[ -x "$n" ] || n=$(command -v node) || { [ "$1" = antigravity ] && if [ "$2" = PreToolUse ]; then echo ${sq(AG_STALE_PRETOOL.trim())}; else echo '{}'; fi; exit 0; }
|
|
3838
|
+
exec "$n" ${sq(hook)} "$@"
|
|
3839
|
+
`;
|
|
3840
|
+
}
|
|
3841
|
+
function writeIfChanged(file, body, mode) {
|
|
3842
|
+
try {
|
|
3843
|
+
const cur = readFileSync9(file);
|
|
3844
|
+
if (cur.equals(Buffer.isBuffer(body) ? body : Buffer.from(body))) {
|
|
3845
|
+
chmodSync2(file, mode);
|
|
3846
|
+
return false;
|
|
3847
|
+
}
|
|
3848
|
+
} catch {
|
|
3849
|
+
}
|
|
3850
|
+
const tmp = `${file}.${process.pid}.tmp`;
|
|
3851
|
+
writeFileSync6(tmp, body, { mode });
|
|
3852
|
+
renameSync2(tmp, file);
|
|
3853
|
+
chmodSync2(file, mode);
|
|
3854
|
+
return true;
|
|
3855
|
+
}
|
|
3856
|
+
function installLauncher(o) {
|
|
3857
|
+
const dir = dataDir(o.env);
|
|
3858
|
+
mkdirSync7(dir, { recursive: true, mode: 493 });
|
|
3859
|
+
let changed = false;
|
|
3860
|
+
for (const f of COPIED)
|
|
3861
|
+
changed = writeIfChanged(path17.join(dir, f), readFileSync9(path17.join(o.distDir, f)), 420) || changed;
|
|
3862
|
+
const launcher = path17.join(dir, LAUNCHER);
|
|
3863
|
+
changed = writeIfChanged(launcher, launcherScript(o.node, path17.join(dir, "hook.js"), o.version), 493) || changed;
|
|
3864
|
+
return { launcher, changed };
|
|
3865
|
+
}
|
|
3866
|
+
function removeLauncher(env = process.env) {
|
|
3867
|
+
for (const f of [LAUNCHER, ...COPIED]) rmSync(path17.join(dataDir(env), f), { force: true });
|
|
3868
|
+
}
|
|
3869
|
+
function launcherState(version, env = process.env) {
|
|
3870
|
+
const file = launcherPath(env);
|
|
3871
|
+
let text = "";
|
|
3872
|
+
try {
|
|
3873
|
+
text = readFileSync9(file, "utf8");
|
|
3874
|
+
} catch {
|
|
3875
|
+
return {
|
|
3876
|
+
file,
|
|
3877
|
+
present: false,
|
|
3878
|
+
node: null,
|
|
3879
|
+
nodeOk: false,
|
|
3880
|
+
version: null,
|
|
3881
|
+
problems: ["the hook launcher is missing; run `sessionpipe install` again"]
|
|
3882
|
+
};
|
|
3883
|
+
}
|
|
3884
|
+
const node = /^n='((?:[^']|'\\'')*)'$/m.exec(text)?.[1]?.replace(/'\\''/g, "'") ?? null;
|
|
3885
|
+
const v = /^# sessionpipe (\S+) hook launcher/m.exec(text)?.[1] ?? null;
|
|
3886
|
+
let nodeOk = false;
|
|
3887
|
+
try {
|
|
3888
|
+
if (node) {
|
|
3889
|
+
accessSync2(node, constants2.X_OK);
|
|
3890
|
+
nodeOk = true;
|
|
3891
|
+
}
|
|
3892
|
+
} catch {
|
|
3893
|
+
}
|
|
3894
|
+
const problems = [];
|
|
3895
|
+
if (!nodeOk)
|
|
3896
|
+
problems.push(
|
|
3897
|
+
`the node the hook launcher names (${node ?? "?"}) is gone; it falls back to the node on PATH. \`sessionpipe install\` points it at this one.`
|
|
3898
|
+
);
|
|
3899
|
+
for (const f of COPIED)
|
|
3900
|
+
try {
|
|
3901
|
+
accessSync2(path17.join(path17.dirname(file), f), constants2.R_OK);
|
|
3902
|
+
} catch {
|
|
3903
|
+
problems.push(`${f} is missing beside the hook launcher; run \`sessionpipe install\` again`);
|
|
3904
|
+
}
|
|
3905
|
+
if (v && v !== version)
|
|
3906
|
+
problems.push(
|
|
3907
|
+
`the hooks run sessionpipe ${v}, this is ${version}; run \`sessionpipe install\` to bring them up to date`
|
|
3908
|
+
);
|
|
3909
|
+
return { file, present: true, node, nodeOk, version: v, problems };
|
|
3910
|
+
}
|
|
3274
3911
|
|
|
3275
3912
|
// src/run.ts
|
|
3276
3913
|
import {
|
|
3277
3914
|
appendFileSync as appendFileSync3,
|
|
3278
|
-
existsSync as
|
|
3279
|
-
mkdirSync as
|
|
3915
|
+
existsSync as existsSync10,
|
|
3916
|
+
mkdirSync as mkdirSync9,
|
|
3280
3917
|
readdirSync as readdirSync7,
|
|
3281
|
-
readFileSync as
|
|
3282
|
-
renameSync as
|
|
3918
|
+
readFileSync as readFileSync11,
|
|
3919
|
+
renameSync as renameSync4,
|
|
3283
3920
|
statSync as statSync7,
|
|
3284
3921
|
unlinkSync as unlinkSync4,
|
|
3285
|
-
writeFileSync as
|
|
3922
|
+
writeFileSync as writeFileSync8
|
|
3286
3923
|
} from "node:fs";
|
|
3287
|
-
import
|
|
3288
|
-
import
|
|
3924
|
+
import os7 from "node:os";
|
|
3925
|
+
import path20 from "node:path";
|
|
3289
3926
|
|
|
3290
3927
|
// src/control/aliases.ts
|
|
3291
|
-
import { chmodSync as
|
|
3292
|
-
import
|
|
3293
|
-
var aliasFile = (controlDir) =>
|
|
3928
|
+
import { chmodSync as chmodSync3, mkdirSync as mkdirSync8, readFileSync as readFileSync10, renameSync as renameSync3, writeFileSync as writeFileSync7 } from "node:fs";
|
|
3929
|
+
import path18 from "node:path";
|
|
3930
|
+
var aliasFile = (controlDir) => path18.join(controlDir, "aliases.json");
|
|
3294
3931
|
function readAliases(file) {
|
|
3295
3932
|
try {
|
|
3296
|
-
const j = JSON.parse(
|
|
3933
|
+
const j = JSON.parse(readFileSync10(file, "utf8"));
|
|
3297
3934
|
if (!j || typeof j !== "object" || Array.isArray(j)) return {};
|
|
3298
3935
|
const out2 = {};
|
|
3299
3936
|
for (const [k, v] of Object.entries(j)) {
|
|
@@ -3307,13 +3944,13 @@ function readAliases(file) {
|
|
|
3307
3944
|
}
|
|
3308
3945
|
}
|
|
3309
3946
|
function writeAliases(file, all) {
|
|
3310
|
-
|
|
3947
|
+
mkdirSync8(path18.dirname(file), { recursive: true, mode: 448 });
|
|
3311
3948
|
const tmp = `${file}.${process.pid}.tmp`;
|
|
3312
|
-
|
|
3949
|
+
writeFileSync7(tmp, `${JSON.stringify(all, null, 2)}
|
|
3313
3950
|
`, { mode: 384 });
|
|
3314
|
-
|
|
3951
|
+
renameSync3(tmp, file);
|
|
3315
3952
|
try {
|
|
3316
|
-
|
|
3953
|
+
chmodSync3(file, 384);
|
|
3317
3954
|
} catch {
|
|
3318
3955
|
}
|
|
3319
3956
|
}
|
|
@@ -3325,12 +3962,12 @@ function receiverIdOf(all, harness, id) {
|
|
|
3325
3962
|
|
|
3326
3963
|
// src/control/local.ts
|
|
3327
3964
|
import net from "node:net";
|
|
3328
|
-
import
|
|
3329
|
-
import
|
|
3965
|
+
import os6 from "node:os";
|
|
3966
|
+
import path19 from "node:path";
|
|
3330
3967
|
function socketPath(state2, env = process.env) {
|
|
3331
3968
|
if (env.SESSIONPIPE_SOCKET) return env.SESSIONPIPE_SOCKET;
|
|
3332
|
-
if (process.platform === "win32") return `\\\\.\\pipe\\sessionpipe-control-${
|
|
3333
|
-
return
|
|
3969
|
+
if (process.platform === "win32") return `\\\\.\\pipe\\sessionpipe-control-${os6.userInfo().username}`;
|
|
3970
|
+
return path19.join(state2, "control.sock");
|
|
3334
3971
|
}
|
|
3335
3972
|
function ask(sock, req, timeoutMs) {
|
|
3336
3973
|
return new Promise((resolve) => {
|
|
@@ -3363,10 +4000,17 @@ function ask(sock, req, timeoutMs) {
|
|
|
3363
4000
|
}
|
|
3364
4001
|
|
|
3365
4002
|
// src/http-sink.ts
|
|
3366
|
-
import { createHash as
|
|
4003
|
+
import { createHash as createHash5, createHmac } from "node:crypto";
|
|
3367
4004
|
var TIMEOUT_MS = 15e3;
|
|
3368
4005
|
function webhookId(events) {
|
|
3369
|
-
return `msg_${
|
|
4006
|
+
return `msg_${createHash5("sha256").update(events.map((e) => e.id).join(",")).digest("base64url").slice(0, 32)}`;
|
|
4007
|
+
}
|
|
4008
|
+
function retryAfterMs(header, now = Date.now()) {
|
|
4009
|
+
if (header === null || header.trim() === "") return 6e4;
|
|
4010
|
+
const secs = Number(header);
|
|
4011
|
+
if (Number.isFinite(secs)) return Math.max(0, secs) * 1e3;
|
|
4012
|
+
const at = Date.parse(header);
|
|
4013
|
+
return Number.isFinite(at) ? Math.max(0, at - now) : 6e4;
|
|
3370
4014
|
}
|
|
3371
4015
|
var HttpSink = class {
|
|
3372
4016
|
constructor(cfg, tier) {
|
|
@@ -3439,7 +4083,7 @@ var HttpSink = class {
|
|
|
3439
4083
|
status: 429,
|
|
3440
4084
|
error: err,
|
|
3441
4085
|
action: "retry",
|
|
3442
|
-
retryAfterMs:
|
|
4086
|
+
retryAfterMs: retryAfterMs(r.headers.get("retry-after"))
|
|
3443
4087
|
};
|
|
3444
4088
|
return { ok: false, acknowledged: 0, status: r.status, error: err, action: "retry" };
|
|
3445
4089
|
} catch (e) {
|
|
@@ -3452,10 +4096,10 @@ var HttpSink = class {
|
|
|
3452
4096
|
|
|
3453
4097
|
// src/secrets.ts
|
|
3454
4098
|
import { spawnSync } from "node:child_process";
|
|
3455
|
-
import { createHash as
|
|
4099
|
+
import { createHash as createHash6, randomBytes as randomBytes2 } from "node:crypto";
|
|
3456
4100
|
var SERVICE = "sessionpipe";
|
|
3457
|
-
var defaultRunner = (
|
|
3458
|
-
const r = spawnSync(
|
|
4101
|
+
var defaultRunner = (cmd, args2, input) => {
|
|
4102
|
+
const r = spawnSync(cmd, args2, {
|
|
3459
4103
|
input: input ?? "",
|
|
3460
4104
|
encoding: "utf8",
|
|
3461
4105
|
timeout: 8e3,
|
|
@@ -3464,7 +4108,7 @@ var defaultRunner = (cmd2, args2, input) => {
|
|
|
3464
4108
|
return { status: r.error ? null : r.status, stdout: r.stdout ?? "" };
|
|
3465
4109
|
};
|
|
3466
4110
|
var sinkAccount = (name, what = "token") => {
|
|
3467
|
-
const scope = process.env.SESSIONPIPE_CONFIG ? `@${
|
|
4111
|
+
const scope = process.env.SESSIONPIPE_CONFIG ? `@${createHash6("sha256").update(configFile()).digest("hex").slice(0, 8)}` : "";
|
|
3468
4112
|
return `sink:${name}:${what}${scope}`;
|
|
3469
4113
|
};
|
|
3470
4114
|
var controlAccount = (machine) => `control:${machine}`;
|
|
@@ -3566,7 +4210,7 @@ function dropSinkSecrets(s, run = defaultRunner) {
|
|
|
3566
4210
|
// src/run.ts
|
|
3567
4211
|
async function tellDaemon(state2, job, session, cwd, transcript) {
|
|
3568
4212
|
const sock = socketPath(state2);
|
|
3569
|
-
if (process.platform !== "win32" && !
|
|
4213
|
+
if (process.platform !== "win32" && !existsSync10(sock)) return;
|
|
3570
4214
|
await ask(
|
|
3571
4215
|
sock,
|
|
3572
4216
|
{
|
|
@@ -3579,20 +4223,27 @@ async function tellDaemon(state2, job, session, cwd, transcript) {
|
|
|
3579
4223
|
300
|
|
3580
4224
|
);
|
|
3581
4225
|
}
|
|
3582
|
-
var VERSION = true ? "0.
|
|
4226
|
+
var VERSION = true ? "0.9.0" : "0.0.0";
|
|
3583
4227
|
function enqueueJob(state2, job) {
|
|
3584
4228
|
const jobs = jobsDir(state2);
|
|
3585
|
-
|
|
3586
|
-
const f =
|
|
3587
|
-
|
|
4229
|
+
mkdirSync9(jobs, { recursive: true, mode: 448 });
|
|
4230
|
+
const f = path20.join(jobs, `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}.json`);
|
|
4231
|
+
writeFileSync8(f, JSON.stringify(job), { mode: 384 });
|
|
3588
4232
|
return f;
|
|
3589
4233
|
}
|
|
4234
|
+
function releaseJob(claimed) {
|
|
4235
|
+
if (!claimed) return;
|
|
4236
|
+
try {
|
|
4237
|
+
unlinkSync4(claimed);
|
|
4238
|
+
} catch {
|
|
4239
|
+
}
|
|
4240
|
+
}
|
|
3590
4241
|
function log(state2, line) {
|
|
3591
4242
|
try {
|
|
3592
|
-
|
|
3593
|
-
const f =
|
|
4243
|
+
mkdirSync9(state2, { recursive: true, mode: 448 });
|
|
4244
|
+
const f = path20.join(state2, "log");
|
|
3594
4245
|
try {
|
|
3595
|
-
if (statSync7(f).size > 1024 * 1024)
|
|
4246
|
+
if (statSync7(f).size > 1024 * 1024) renameSync4(f, `${f}.1`);
|
|
3596
4247
|
} catch {
|
|
3597
4248
|
}
|
|
3598
4249
|
appendFileSync3(f, `${(/* @__PURE__ */ new Date()).toISOString()} ${line}
|
|
@@ -3601,11 +4252,11 @@ function log(state2, line) {
|
|
|
3601
4252
|
}
|
|
3602
4253
|
}
|
|
3603
4254
|
function factsState(state2) {
|
|
3604
|
-
const file = (h, s) =>
|
|
4255
|
+
const file = (h, s) => path20.join(state2, "facts", safeId(h), `${safeId(s)}.json`);
|
|
3605
4256
|
return {
|
|
3606
4257
|
get(h, s) {
|
|
3607
4258
|
try {
|
|
3608
|
-
return JSON.parse(
|
|
4259
|
+
return JSON.parse(readFileSync11(file(h, s), "utf8"));
|
|
3609
4260
|
} catch {
|
|
3610
4261
|
return {};
|
|
3611
4262
|
}
|
|
@@ -3614,8 +4265,8 @@ function factsState(state2) {
|
|
|
3614
4265
|
const cur = this.get(h, s);
|
|
3615
4266
|
const next = { ...cur, ...patch };
|
|
3616
4267
|
if (JSON.stringify(next) === JSON.stringify(cur)) return;
|
|
3617
|
-
|
|
3618
|
-
|
|
4268
|
+
mkdirSync9(path20.dirname(file(h, s)), { recursive: true, mode: 448 });
|
|
4269
|
+
writeFileSync8(file(h, s), JSON.stringify(next), { mode: 384 });
|
|
3619
4270
|
}
|
|
3620
4271
|
};
|
|
3621
4272
|
}
|
|
@@ -3631,44 +4282,80 @@ function buildSinks(sinks, o = {}) {
|
|
|
3631
4282
|
const tier = Math.min(s.tier, s.max_tier ?? 3);
|
|
3632
4283
|
if (s.url === "stdout") out2.push(new StdoutSink(s.name, tier, !!s.pii));
|
|
3633
4284
|
else if (s.url.startsWith("file:"))
|
|
3634
|
-
out2.push(new FileSink(s.name, s.url.slice(5).replace(/^~(?=\/|$)/,
|
|
4285
|
+
out2.push(new FileSink(s.name, s.url.slice(5).replace(/^~(?=\/|$)/, os7.homedir()), tier, !!s.pii));
|
|
3635
4286
|
else if (/^https?:\/\//.test(s.url)) out2.push(new HttpSink(s, tier));
|
|
3636
4287
|
}
|
|
3637
4288
|
return out2;
|
|
3638
4289
|
}
|
|
4290
|
+
var PROGRESS_MS = 2e4;
|
|
4291
|
+
function openTurn(read, o) {
|
|
4292
|
+
const last = read.turns[read.turns.length - 1];
|
|
4293
|
+
const fresh = (user, at) => ({
|
|
4294
|
+
turn: o.base + read.turns.length,
|
|
4295
|
+
user: user.slice(0, MAX_TURN_SIDE),
|
|
4296
|
+
assistant: "",
|
|
4297
|
+
at
|
|
4298
|
+
});
|
|
4299
|
+
if (read.open) return fresh(read.open.user, read.open.at);
|
|
4300
|
+
const prompt = o.prompt?.trim();
|
|
4301
|
+
if (prompt && !isInjected(prompt) && !(last && last.user.includes(prompt.slice(0, 200)))) return fresh(prompt, o.at);
|
|
4302
|
+
if (!last) return null;
|
|
4303
|
+
return {
|
|
4304
|
+
turn: o.base + read.turns.length - 1,
|
|
4305
|
+
user: last.user.slice(0, MAX_TURN_SIDE),
|
|
4306
|
+
assistant: capTail(last.assistant, MAX_TURN_SIDE),
|
|
4307
|
+
at: last.at
|
|
4308
|
+
};
|
|
4309
|
+
}
|
|
4310
|
+
var MAX_TURN_SIDE = 2e4;
|
|
3639
4311
|
async function runJob(job, opts = {}) {
|
|
3640
4312
|
const state2 = opts.state ?? stateDir();
|
|
3641
4313
|
const cfg = readConfig();
|
|
3642
4314
|
const adapter = adapterByName(job.harness);
|
|
3643
|
-
if (!adapter)
|
|
4315
|
+
if (!adapter) {
|
|
4316
|
+
log(state2, `${job.harness} ${job.event}: no adapter for harness "${job.harness}"; event dropped`);
|
|
4317
|
+
return { events: [], delivered: {} };
|
|
4318
|
+
}
|
|
3644
4319
|
const r = adapter.fromHook({ argv: job.argv, stdin: job.stdin, env: { ...process.env, ...job.env }, cwd: job.cwd });
|
|
3645
|
-
if (!r || !r.session.id)
|
|
3646
|
-
|
|
3647
|
-
|
|
4320
|
+
if (!r || !r.session.id) {
|
|
4321
|
+
log(state2, `${job.harness} ${job.event}: the ${job.harness} adapter did not claim this payload; event dropped`);
|
|
4322
|
+
return { events: [], delivered: {} };
|
|
4323
|
+
}
|
|
4324
|
+
const id = receiverIdOf(readAliases(aliasFile(path20.join(state2, "control"))), job.harness, r.session.id) ?? r.session.id;
|
|
4325
|
+
const claudeEnd = job.harness === "claude-code" && r.events.some((e) => e.type === "session.ended");
|
|
4326
|
+
if (!claudeEnd) await tellDaemon(state2, job, id, r.session.cwd, r.transcript);
|
|
3648
4327
|
const outbox = new Outbox(state2);
|
|
3649
4328
|
const ref = { harness: job.harness, session: id };
|
|
3650
4329
|
let release = takeLock(state2, ref);
|
|
3651
|
-
|
|
3652
|
-
|
|
4330
|
+
const lockWait = opts.lockWaitMs ?? LOCK_WAIT_MS;
|
|
4331
|
+
for (const until = Date.now() + lockWait; !release && Date.now() < until; ) {
|
|
4332
|
+
await new Promise((res) => setTimeout(res, 50));
|
|
3653
4333
|
release = takeLock(state2, ref);
|
|
3654
4334
|
}
|
|
3655
4335
|
if (!release) {
|
|
3656
|
-
log(state2, `${job.harness} ${job.event}: session ${id} locked for
|
|
3657
|
-
enqueueJob(state2, job);
|
|
3658
|
-
|
|
4336
|
+
log(state2, `${job.harness} ${job.event}: session ${id} locked for ${lockWait / 1e3} s, re-queued`);
|
|
4337
|
+
const requeued = enqueueJob(state2, { ...job, tries: (job.tries ?? 0) + 1 });
|
|
4338
|
+
releaseJob(opts.claimed);
|
|
4339
|
+
return { events: [], delivered: {}, requeued };
|
|
3659
4340
|
}
|
|
4341
|
+
const events = [];
|
|
3660
4342
|
try {
|
|
3661
4343
|
const facts = factsState(state2);
|
|
3662
4344
|
const mem = facts.get(job.harness, id);
|
|
4345
|
+
if (claudeEnd && !existsSync10(outbox.file(ref)) && Object.keys(mem).length === 0 && !(r.transcript && existsSync10(r.transcript))) {
|
|
4346
|
+
releaseJob(opts.claimed);
|
|
4347
|
+
return { events: [], delivered: {} };
|
|
4348
|
+
}
|
|
4349
|
+
if (claudeEnd) await tellDaemon(state2, job, id, r.session.cwd, r.transcript);
|
|
3663
4350
|
const heavy = !/^(PreToolUse|PostToolUse|PostToolUseFailure|BeforeTool|AfterTool|PostInvocation)$/.test(job.event);
|
|
3664
4351
|
let learned = {};
|
|
3665
4352
|
if (heavy) {
|
|
3666
|
-
const f = adapter.facts(r.session, r.transcript, r.hints ?? {}, facts);
|
|
4353
|
+
const f = adapter.facts(r.session, r.transcript, r.hints ?? {}, facts, { ...process.env, ...job.env });
|
|
3667
4354
|
const { started_at: _s, last_at: _l, first_ask: _fa, harness_version, ...rest } = f;
|
|
3668
4355
|
learned = rest;
|
|
3669
4356
|
if (harness_version) facts.save(job.harness, id, { harness_version });
|
|
3670
4357
|
facts.save(job.harness, id, { session: { ...mem.session, ...learned } });
|
|
3671
|
-
if (job.harness
|
|
4358
|
+
if (CWD_FROM_HOOKS.has(job.harness) && r.session.cwd && path20.isAbsolute(r.session.cwd))
|
|
3672
4359
|
facts.save(job.harness, id, { cwd: r.session.cwd });
|
|
3673
4360
|
}
|
|
3674
4361
|
const known = mem.session ?? {};
|
|
@@ -3681,12 +4368,11 @@ async function runJob(job, opts = {}) {
|
|
|
3681
4368
|
...learned.branch ? { branch: learned.branch } : {},
|
|
3682
4369
|
id,
|
|
3683
4370
|
seq: 0,
|
|
3684
|
-
machine: machineName(cfg,
|
|
4371
|
+
machine: machineName(cfg, os7.hostname()),
|
|
3685
4372
|
...cwdRaw ? { cwd: tilde(cwdRaw) } : {},
|
|
3686
4373
|
...r.session.model ? { model: r.session.model } : {}
|
|
3687
4374
|
};
|
|
3688
4375
|
const version = facts.get(job.harness, id).harness_version ?? void 0;
|
|
3689
|
-
const events = [];
|
|
3690
4376
|
for (const a of r.events) {
|
|
3691
4377
|
const e = makeEvent(a, {
|
|
3692
4378
|
harness: { name: job.harness, ...version ? { version } : {} },
|
|
@@ -3696,6 +4382,37 @@ async function runJob(job, opts = {}) {
|
|
|
3696
4382
|
events.push(e);
|
|
3697
4383
|
}
|
|
3698
4384
|
const wantsTranscript = cfg.sinks.some((s) => !s.paused && Math.min(s.tier, s.max_tier ?? 3) >= 2);
|
|
4385
|
+
const ending = r.events.some((e) => e.type === "turn.ended" || e.type === "session.ended");
|
|
4386
|
+
const starting = r.events.some((e) => e.type === "turn.started");
|
|
4387
|
+
const startedId = r.events.find((e) => e.type === "turn.started")?.data.turn_id;
|
|
4388
|
+
const absorbed = starting && typeof startedId === "string" && facts.get(job.harness, id).open_turn === startedId;
|
|
4389
|
+
if (wantsTranscript && r.transcript && !ending && (starting || r.events.some((e) => e.type === "tool.ended"))) {
|
|
4390
|
+
const cur = facts.get(job.harness, id);
|
|
4391
|
+
const at = opts.now ?? job.at;
|
|
4392
|
+
if (starting || at - (Number(cur.progress_at) || 0) >= PROGRESS_MS) {
|
|
4393
|
+
const p = openTurn(adapter.readTranscript(r.transcript, Number(cur.transcript_line) || 0), {
|
|
4394
|
+
base: Number(cur.transcript_turns) || 0,
|
|
4395
|
+
prompt: !absorbed && typeof r.hints?.prompt === "string" ? r.hints.prompt : void 0,
|
|
4396
|
+
at
|
|
4397
|
+
});
|
|
4398
|
+
const sig = p && `${p.turn}:${p.user.length}:${p.assistant.length}`;
|
|
4399
|
+
if (p && (starting || sig !== cur.progress_sig)) {
|
|
4400
|
+
events.push(
|
|
4401
|
+
makeEvent(
|
|
4402
|
+
{ type: "turn.progress", harnessEvent: job.event, data: { ...p, at: new Date(p.at).toISOString() } },
|
|
4403
|
+
{
|
|
4404
|
+
harness: { name: job.harness, ...version ? { version } : {} },
|
|
4405
|
+
session: { ...session, seq: outbox.nextSeq(ref) },
|
|
4406
|
+
now: at
|
|
4407
|
+
}
|
|
4408
|
+
)
|
|
4409
|
+
);
|
|
4410
|
+
facts.save(job.harness, id, { progress_at: at, progress_sig: sig });
|
|
4411
|
+
}
|
|
4412
|
+
}
|
|
4413
|
+
}
|
|
4414
|
+
if (starting && typeof startedId === "string" && !absorbed) facts.save(job.harness, id, { open_turn: startedId });
|
|
4415
|
+
if (ending) facts.save(job.harness, id, { open_turn: null });
|
|
3699
4416
|
if (wantsTranscript && r.transcript && r.events.some((e) => e.type === "turn.ended" || e.type === "session.ended")) {
|
|
3700
4417
|
const cur = facts.get(job.harness, id);
|
|
3701
4418
|
const fromLine = Number(cur.transcript_line) || 0;
|
|
@@ -3723,17 +4440,80 @@ async function runJob(job, opts = {}) {
|
|
|
3723
4440
|
ref,
|
|
3724
4441
|
events.map((e) => ({ ...e, session: redactDeep(e.session), data: redactDeep(e.data) }))
|
|
3725
4442
|
);
|
|
3726
|
-
|
|
3727
|
-
|
|
3728
|
-
|
|
3729
|
-
|
|
4443
|
+
releaseJob(opts.claimed);
|
|
4444
|
+
log(
|
|
4445
|
+
state2,
|
|
4446
|
+
`${job.harness} ${job.event}: ${events.map((e) => e.type).join(",") || "nothing"} (${shortId(job.harness, id)})`
|
|
4447
|
+
);
|
|
3730
4448
|
} finally {
|
|
3731
4449
|
release();
|
|
3732
4450
|
}
|
|
4451
|
+
const delivered = await drain(state2, cfg.sinks, outbox, ref) ?? {};
|
|
4452
|
+
if (Math.random() < 0.02) outbox.prune(cfg.keep_days ?? 30);
|
|
4453
|
+
return { events, delivered };
|
|
4454
|
+
}
|
|
4455
|
+
async function drain(state2, sinkConfigs, outbox, first) {
|
|
4456
|
+
const more = path20.join(state2, "locks", "flush.more");
|
|
4457
|
+
let release = takeNamedLock(state2, "flush");
|
|
4458
|
+
if (!release) {
|
|
4459
|
+
try {
|
|
4460
|
+
writeFileSync8(more, "", { mode: 384 });
|
|
4461
|
+
} catch {
|
|
4462
|
+
}
|
|
4463
|
+
release = takeNamedLock(state2, "flush");
|
|
4464
|
+
if (!release) return null;
|
|
4465
|
+
}
|
|
4466
|
+
const delivered = {};
|
|
4467
|
+
for (; ; ) {
|
|
4468
|
+
try {
|
|
4469
|
+
unlinkSync4(more);
|
|
4470
|
+
} catch {
|
|
4471
|
+
}
|
|
4472
|
+
try {
|
|
4473
|
+
for (const [k, v] of Object.entries(await flush(state2, sinkConfigs, outbox, first)))
|
|
4474
|
+
delivered[k] = (delivered[k] ?? 0) + v;
|
|
4475
|
+
} finally {
|
|
4476
|
+
release();
|
|
4477
|
+
}
|
|
4478
|
+
if (!existsSync10(more)) return delivered;
|
|
4479
|
+
release = takeNamedLock(state2, "flush");
|
|
4480
|
+
if (!release) return delivered;
|
|
4481
|
+
}
|
|
3733
4482
|
}
|
|
4483
|
+
var BACKOFF_MS = [2e3, 1e4, 6e4, 5 * 6e4, 30 * 6e4];
|
|
4484
|
+
var backoffFile = (state2, sink2) => path20.join(state2, "backoff", `${safeId(sink2)}.json`);
|
|
4485
|
+
function readBackoff(state2, sink2) {
|
|
4486
|
+
try {
|
|
4487
|
+
const b = JSON.parse(readFileSync11(backoffFile(state2, sink2), "utf8"));
|
|
4488
|
+
return typeof b.until === "number" ? b : null;
|
|
4489
|
+
} catch {
|
|
4490
|
+
return null;
|
|
4491
|
+
}
|
|
4492
|
+
}
|
|
4493
|
+
function noteFailure(state2, sink2, res, prev) {
|
|
4494
|
+
const failures = (prev?.failures ?? 0) + 1;
|
|
4495
|
+
const ladder = BACKOFF_MS[Math.min(failures, BACKOFF_MS.length) - 1];
|
|
4496
|
+
const wait = Number.isFinite(res.retryAfterMs) ? res.retryAfterMs : ladder;
|
|
4497
|
+
const b = { until: Date.now() + wait, failures, why: `${res.status ?? ""} ${res.error ?? ""}`.trim() };
|
|
4498
|
+
const f = backoffFile(state2, sink2);
|
|
4499
|
+
try {
|
|
4500
|
+
mkdirSync9(path20.dirname(f), { recursive: true, mode: 448 });
|
|
4501
|
+
writeFileSync8(f, JSON.stringify(b), { mode: 384 });
|
|
4502
|
+
} catch {
|
|
4503
|
+
}
|
|
4504
|
+
return b;
|
|
4505
|
+
}
|
|
4506
|
+
function clearBackoff(state2, sink2) {
|
|
4507
|
+
try {
|
|
4508
|
+
unlinkSync4(backoffFile(state2, sink2));
|
|
4509
|
+
} catch {
|
|
4510
|
+
}
|
|
4511
|
+
}
|
|
4512
|
+
var LOCK_WAIT_MS = 6e3;
|
|
4513
|
+
var CWD_FROM_HOOKS = /* @__PURE__ */ new Set(["antigravity", "devin"]);
|
|
3734
4514
|
var BATCH = 50;
|
|
3735
4515
|
var MAX_PER_RUN = 500;
|
|
3736
|
-
async function flush(state2, sinkConfigs, outbox, first) {
|
|
4516
|
+
async function flush(state2, sinkConfigs, outbox, first, o = {}) {
|
|
3737
4517
|
const delivered = {};
|
|
3738
4518
|
const sinks = buildSinks(sinkConfigs, {
|
|
3739
4519
|
locked: (name) => log(state2, `sink ${name}: its token is in a keychain this process can't read; left for later`)
|
|
@@ -3743,13 +4523,39 @@ async function flush(state2, sinkConfigs, outbox, first) {
|
|
|
3743
4523
|
const k = `${first.harness}/${safeId(first.session)}`;
|
|
3744
4524
|
refs.sort((a, b) => `${a.harness}/${a.session}` === k ? -1 : `${b.harness}/${b.session}` === k ? 1 : 0);
|
|
3745
4525
|
}
|
|
3746
|
-
|
|
4526
|
+
const drainSink = async (sink2) => {
|
|
4527
|
+
let backoff = readBackoff(state2, sink2.name);
|
|
4528
|
+
if (backoff && !o.force && backoff.until > Date.now()) {
|
|
4529
|
+
delivered[sink2.name] = 0;
|
|
4530
|
+
return;
|
|
4531
|
+
}
|
|
3747
4532
|
const cursors = new Cursors(state2, sink2.name);
|
|
3748
4533
|
const c = cursors.read();
|
|
3749
4534
|
let sent = 0;
|
|
3750
4535
|
let stalled = false;
|
|
3751
4536
|
let batch = [];
|
|
3752
4537
|
let pending = {};
|
|
4538
|
+
const deliver = async (events) => {
|
|
4539
|
+
const res = await sink2.send(events);
|
|
4540
|
+
if (res.ok || res.action !== "split") return res;
|
|
4541
|
+
if (events.length === 1) {
|
|
4542
|
+
const e = events[0];
|
|
4543
|
+
log(state2, `sink ${sink2.name}: 413 ${res.error ?? ""} \u2192 event ${e.id} (${e.type}) is too large alone, dropped`);
|
|
4544
|
+
return { ok: true, acknowledged: 0, accepted: 0, duplicates: 0, rejected: [{ id: e.id, reason: "too_large" }] };
|
|
4545
|
+
}
|
|
4546
|
+
const half = Math.ceil(events.length / 2);
|
|
4547
|
+
const a = await deliver(events.slice(0, half));
|
|
4548
|
+
if (!a.ok) return a;
|
|
4549
|
+
const b = await deliver(events.slice(half));
|
|
4550
|
+
if (!b.ok) return b;
|
|
4551
|
+
return {
|
|
4552
|
+
ok: true,
|
|
4553
|
+
acknowledged: a.acknowledged + b.acknowledged,
|
|
4554
|
+
accepted: (a.accepted ?? 0) + (b.accepted ?? 0),
|
|
4555
|
+
duplicates: (a.duplicates ?? 0) + (b.duplicates ?? 0),
|
|
4556
|
+
rejected: [...a.rejected ?? [], ...b.rejected ?? []]
|
|
4557
|
+
};
|
|
4558
|
+
};
|
|
3753
4559
|
const send = async () => {
|
|
3754
4560
|
if (!batch.length) {
|
|
3755
4561
|
Object.assign(c, pending);
|
|
@@ -3757,15 +4563,24 @@ async function flush(state2, sinkConfigs, outbox, first) {
|
|
|
3757
4563
|
cursors.write(c);
|
|
3758
4564
|
return true;
|
|
3759
4565
|
}
|
|
3760
|
-
const res = await
|
|
4566
|
+
const res = await deliver(batch);
|
|
3761
4567
|
if (!res.ok) {
|
|
3762
|
-
|
|
4568
|
+
const action = res.action ?? "retry";
|
|
4569
|
+
let wait = "";
|
|
4570
|
+
if (action === "retry") {
|
|
4571
|
+
backoff = noteFailure(state2, sink2.name, res, backoff);
|
|
4572
|
+
wait = `, waits ${Math.round((backoff.until - Date.now()) / 1e3)} s`;
|
|
4573
|
+
}
|
|
4574
|
+
log(state2, `sink ${sink2.name}: ${res.status ?? ""} ${res.error ?? ""} \u2192 ${action}${wait}`);
|
|
3763
4575
|
if (res.action === "pause" || res.action === "lower-tier") applySinkVerdict(sink2.name, res);
|
|
3764
4576
|
if (res.action !== "drop") {
|
|
3765
4577
|
batch = [];
|
|
3766
4578
|
pending = {};
|
|
3767
4579
|
return false;
|
|
3768
4580
|
}
|
|
4581
|
+
} else if (backoff) {
|
|
4582
|
+
clearBackoff(state2, sink2.name);
|
|
4583
|
+
backoff = null;
|
|
3769
4584
|
}
|
|
3770
4585
|
sent += batch.length;
|
|
3771
4586
|
Object.assign(c, pending);
|
|
@@ -3782,7 +4597,7 @@ async function flush(state2, sinkConfigs, outbox, first) {
|
|
|
3782
4597
|
const { events, offset: next } = outbox.read(ref, offset, room);
|
|
3783
4598
|
if (!events.length) break;
|
|
3784
4599
|
for (const e of events) {
|
|
3785
|
-
const f = filterEvent(e, sink2.tier, sink2.pii, { home:
|
|
4600
|
+
const f = filterEvent(e, sink2.tier, sink2.pii, { home: os7.homedir() });
|
|
3786
4601
|
if (f) batch.push(f);
|
|
3787
4602
|
}
|
|
3788
4603
|
offset = next;
|
|
@@ -3800,7 +4615,8 @@ async function flush(state2, sinkConfigs, outbox, first) {
|
|
|
3800
4615
|
if (!await send()) stalled = true;
|
|
3801
4616
|
}
|
|
3802
4617
|
delivered[sink2.name] = sent;
|
|
3803
|
-
}
|
|
4618
|
+
};
|
|
4619
|
+
await Promise.all(sinks.map(drainSink));
|
|
3804
4620
|
return delivered;
|
|
3805
4621
|
}
|
|
3806
4622
|
function applySinkVerdict(name, res) {
|
|
@@ -3814,7 +4630,7 @@ function applySinkVerdict(name, res) {
|
|
|
3814
4630
|
} catch {
|
|
3815
4631
|
}
|
|
3816
4632
|
}
|
|
3817
|
-
var jobsDir = (state2) =>
|
|
4633
|
+
var jobsDir = (state2) => path20.join(state2, "jobs");
|
|
3818
4634
|
|
|
3819
4635
|
// src/runtime.ts
|
|
3820
4636
|
function stableNode(exe) {
|
|
@@ -3837,26 +4653,59 @@ function stableNode(exe) {
|
|
|
3837
4653
|
function viaNpx(distDir2) {
|
|
3838
4654
|
return /[\\/]_npx[\\/]/.test(distDir2) || /[\\/]\.npm[\\/]/.test(distDir2);
|
|
3839
4655
|
}
|
|
3840
|
-
function
|
|
4656
|
+
function installLocation() {
|
|
4657
|
+
let prefix = "";
|
|
4658
|
+
try {
|
|
4659
|
+
prefix = execFileSync6("npm", ["prefix", "-g"], {
|
|
4660
|
+
encoding: "utf8",
|
|
4661
|
+
timeout: 15e3,
|
|
4662
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
4663
|
+
}).trim();
|
|
4664
|
+
} catch {
|
|
4665
|
+
}
|
|
4666
|
+
return {
|
|
4667
|
+
prefix,
|
|
4668
|
+
target: /\/Cellar\/node(?:@\d+)?\/[^/]+/.test(prefix) ? path21.join(os8.homedir(), ".local") : prefix
|
|
4669
|
+
};
|
|
4670
|
+
}
|
|
4671
|
+
function assertSetupVersion(allowDowngrade, target) {
|
|
4672
|
+
if (allowDowngrade) return;
|
|
4673
|
+
const check = (installed2, where) => {
|
|
4674
|
+
if (installed2 && isNewer(installed2, VERSION, true))
|
|
4675
|
+
throw new Error(
|
|
4676
|
+
`refusing to downgrade sessionpipe: ${installed2} is installed at ${where}; this command would install ${VERSION}. Use the installed newer CLI, or pass --allow-downgrade to force ${VERSION}.`
|
|
4677
|
+
);
|
|
4678
|
+
};
|
|
4679
|
+
const hooks = launcherState(VERSION);
|
|
4680
|
+
check(hooks.version, hooks.file);
|
|
4681
|
+
const prefix = target ?? installLocation().target;
|
|
4682
|
+
if (!prefix) return;
|
|
4683
|
+
const pkg = path21.join(
|
|
4684
|
+
prefix,
|
|
4685
|
+
...process.platform === "win32" ? [] : ["lib"],
|
|
4686
|
+
"node_modules",
|
|
4687
|
+
"sessionpipe",
|
|
4688
|
+
"package.json"
|
|
4689
|
+
);
|
|
4690
|
+
let installed;
|
|
4691
|
+
try {
|
|
4692
|
+
installed = JSON.parse(readFileSync12(pkg, "utf8")).version;
|
|
4693
|
+
} catch {
|
|
4694
|
+
}
|
|
4695
|
+
if (typeof installed === "string") check(installed, pkg);
|
|
4696
|
+
}
|
|
4697
|
+
function rerunGlobally(argv, out2, setup) {
|
|
4698
|
+
const { prefix, target } = installLocation();
|
|
4699
|
+
if (setup) assertSetupVersion(setup.allowDowngrade, target);
|
|
3841
4700
|
out2(` Installing sessionpipe@${VERSION} globally, so hooks and services use a persistent copy\u2026`);
|
|
3842
4701
|
let bin;
|
|
3843
4702
|
try {
|
|
3844
|
-
|
|
3845
|
-
try {
|
|
3846
|
-
prefix = execFileSync5("npm", ["prefix", "-g"], {
|
|
3847
|
-
encoding: "utf8",
|
|
3848
|
-
timeout: 15e3,
|
|
3849
|
-
stdio: ["ignore", "pipe", "ignore"]
|
|
3850
|
-
}).trim();
|
|
3851
|
-
} catch {
|
|
3852
|
-
}
|
|
3853
|
-
const target = /\/Cellar\/node(?:@\d+)?\/[^/]+/.test(prefix) ? path19.join(os7.homedir(), ".local") : prefix;
|
|
3854
|
-
execFileSync5("npm", ["install", "-g", `sessionpipe@${VERSION}`, ...target ? ["--prefix", target] : []], {
|
|
4703
|
+
execFileSync6("npm", ["install", "-g", `sessionpipe@${VERSION}`, ...target ? ["--prefix", target] : []], {
|
|
3855
4704
|
stdio: ["ignore", "ignore", "inherit"]
|
|
3856
4705
|
});
|
|
3857
|
-
bin = process.platform === "win32" ?
|
|
4706
|
+
bin = process.platform === "win32" ? path21.join(target, "sessionpipe.cmd") : target ? path21.join(target, "bin", "sessionpipe") : "sessionpipe";
|
|
3858
4707
|
if (target && target !== prefix)
|
|
3859
|
-
out2(` Installed into ${target}; make sure ${
|
|
4708
|
+
out2(` Installed into ${target}; make sure ${path21.join(target, "bin")} is on your PATH.`);
|
|
3860
4709
|
} catch {
|
|
3861
4710
|
out2(
|
|
3862
4711
|
" ! couldn't install globally (`npm install -g sessionpipe` failed). Run it yourself, then retry this command."
|
|
@@ -3864,7 +4713,7 @@ function rerunGlobally(argv, out2) {
|
|
|
3864
4713
|
process.exit(1);
|
|
3865
4714
|
}
|
|
3866
4715
|
try {
|
|
3867
|
-
|
|
4716
|
+
execFileSync6(bin, argv, { stdio: "inherit", shell: process.platform === "win32" });
|
|
3868
4717
|
} catch (e) {
|
|
3869
4718
|
process.exit(typeof e.status === "number" ? e.status : 1);
|
|
3870
4719
|
}
|
|
@@ -3888,9 +4737,9 @@ function compareVersions(a, b) {
|
|
|
3888
4737
|
if (y.pre === null) return -1;
|
|
3889
4738
|
return x.pre.localeCompare(y.pre, "en", { numeric: true });
|
|
3890
4739
|
}
|
|
3891
|
-
function isNewer(a, b) {
|
|
4740
|
+
function isNewer(a, b, includePrereleases = false) {
|
|
3892
4741
|
const x = parse2(a);
|
|
3893
|
-
if (!x || x.pre !== null) return false;
|
|
4742
|
+
if (!x || !includePrereleases && x.pre !== null) return false;
|
|
3894
4743
|
const c = compareVersions(a, b);
|
|
3895
4744
|
return c !== null && c > 0;
|
|
3896
4745
|
}
|
|
@@ -3912,21 +4761,21 @@ function globalPrefix(distDir2, platform = process.platform) {
|
|
|
3912
4761
|
if (viaNpx(distDir2)) return null;
|
|
3913
4762
|
const prefix = prefixOf(distDir2, platform);
|
|
3914
4763
|
if (!prefix) return null;
|
|
3915
|
-
const bin = platform === "win32" ?
|
|
3916
|
-
return
|
|
4764
|
+
const bin = platform === "win32" ? path22.join(prefix, "sessionpipe.cmd") : path22.join(prefix, "bin", "sessionpipe");
|
|
4765
|
+
return existsSync11(bin) ? prefix : null;
|
|
3917
4766
|
}
|
|
3918
4767
|
function unwritable(prefix, platform = process.platform) {
|
|
3919
|
-
const dir = platform === "win32" ?
|
|
4768
|
+
const dir = platform === "win32" ? path22.join(prefix, "node_modules") : path22.join(prefix, "lib", "node_modules");
|
|
3920
4769
|
try {
|
|
3921
|
-
|
|
4770
|
+
accessSync3(dir, constants3.W_OK);
|
|
3922
4771
|
return null;
|
|
3923
4772
|
} catch {
|
|
3924
4773
|
return `${dir} isn't writable by this user (installed with sudo?)`;
|
|
3925
4774
|
}
|
|
3926
4775
|
}
|
|
3927
4776
|
function npmArgv(node, platform = process.platform) {
|
|
3928
|
-
const dir =
|
|
3929
|
-
const candidates = platform === "win32" ? [
|
|
4777
|
+
const dir = path22.dirname(node);
|
|
4778
|
+
const candidates = platform === "win32" ? [path22.join(dir, "node_modules", "npm", "bin", "npm-cli.js")] : [path22.join(dir, "npm"), path22.join(dir, "..", "lib", "node_modules", "npm", "bin", "npm-cli.js")];
|
|
3930
4779
|
for (const c of candidates) {
|
|
3931
4780
|
try {
|
|
3932
4781
|
const real = realpathSync3(c);
|
|
@@ -3937,17 +4786,17 @@ function npmArgv(node, platform = process.platform) {
|
|
|
3937
4786
|
return [platform === "win32" ? "npm.cmd" : "npm"];
|
|
3938
4787
|
}
|
|
3939
4788
|
var execCapture = (argv, o) => new Promise((resolve) => {
|
|
3940
|
-
const [
|
|
4789
|
+
const [cmd, ...rest] = argv;
|
|
3941
4790
|
let stdout = "";
|
|
3942
4791
|
let stderr = "";
|
|
3943
4792
|
const keep = (s, b) => (s + b.toString("utf8")).slice(-16e3);
|
|
3944
4793
|
let child;
|
|
3945
4794
|
try {
|
|
3946
|
-
child = spawn2(
|
|
4795
|
+
child = spawn2(cmd, rest, {
|
|
3947
4796
|
env: o.env,
|
|
3948
4797
|
stdio: ["ignore", "pipe", "pipe"],
|
|
3949
4798
|
windowsHide: true,
|
|
3950
|
-
shell: process.platform === "win32" &&
|
|
4799
|
+
shell: process.platform === "win32" && cmd.endsWith(".cmd")
|
|
3951
4800
|
});
|
|
3952
4801
|
} catch (e) {
|
|
3953
4802
|
resolve({ code: null, stdout, stderr, error: e.message });
|
|
@@ -3971,7 +4820,7 @@ var execCapture = (argv, o) => new Promise((resolve) => {
|
|
|
3971
4820
|
});
|
|
3972
4821
|
var lastLine = (r) => (r.error || r.stderr.trim().split("\n").filter(Boolean).slice(-2).join(" ") || `exit ${r.code}`).slice(0, 300);
|
|
3973
4822
|
async function installedVersion(o) {
|
|
3974
|
-
const r = await (o.exec ?? execCapture)([o.node,
|
|
4823
|
+
const r = await (o.exec ?? execCapture)([o.node, path22.join(o.distDir, "cli.js"), "--version"], {
|
|
3975
4824
|
env: o.env ?? process.env,
|
|
3976
4825
|
timeoutMs: 3e4
|
|
3977
4826
|
});
|
|
@@ -3983,7 +4832,7 @@ async function installVersion(version, o) {
|
|
|
3983
4832
|
const platform = o.platform ?? process.platform;
|
|
3984
4833
|
const exec = o.exec ?? execCapture;
|
|
3985
4834
|
const base = o.env ?? process.env;
|
|
3986
|
-
const env = { ...base, PATH: [
|
|
4835
|
+
const env = { ...base, PATH: [path22.dirname(o.node), base.PATH].filter(Boolean).join(path22.delimiter) };
|
|
3987
4836
|
const prefix = globalPrefix(o.distDir, platform);
|
|
3988
4837
|
if (!prefix)
|
|
3989
4838
|
return { ok: false, ms: Date.now() - t0, stage: "prefix", error: "this copy isn't a global npm install" };
|
|
@@ -4014,7 +4863,7 @@ async function installVersion(version, o) {
|
|
|
4014
4863
|
let hooks = "no harness set up here; nothing to re-arm";
|
|
4015
4864
|
if (o.harnesses.length) {
|
|
4016
4865
|
const h = await exec(
|
|
4017
|
-
[o.node,
|
|
4866
|
+
[o.node, path22.join(o.distDir, "cli.js"), "install", "--backfill", "0", ...o.harnesses.map((n) => `--${n}`)],
|
|
4018
4867
|
{ env, timeoutMs: 2 * 6e4 }
|
|
4019
4868
|
);
|
|
4020
4869
|
hooks = h.code === 0 ? `hooks re-armed for ${o.harnesses.join(", ")}` : `re-arming the hooks failed (${lastLine(h)}); \`sessionpipe install\` does it`;
|
|
@@ -4025,17 +4874,17 @@ async function installVersion(version, o) {
|
|
|
4025
4874
|
// src/control/store.ts
|
|
4026
4875
|
import {
|
|
4027
4876
|
appendFileSync as appendFileSync4,
|
|
4028
|
-
chmodSync as
|
|
4877
|
+
chmodSync as chmodSync4,
|
|
4029
4878
|
closeSync as closeSync4,
|
|
4030
|
-
existsSync as
|
|
4879
|
+
existsSync as existsSync12,
|
|
4031
4880
|
fsyncSync,
|
|
4032
|
-
mkdirSync as
|
|
4881
|
+
mkdirSync as mkdirSync10,
|
|
4033
4882
|
openSync as openSync4,
|
|
4034
|
-
readFileSync as
|
|
4035
|
-
renameSync as
|
|
4036
|
-
writeFileSync as
|
|
4883
|
+
readFileSync as readFileSync13,
|
|
4884
|
+
renameSync as renameSync5,
|
|
4885
|
+
writeFileSync as writeFileSync9
|
|
4037
4886
|
} from "node:fs";
|
|
4038
|
-
import
|
|
4887
|
+
import path23 from "node:path";
|
|
4039
4888
|
|
|
4040
4889
|
// ../core/src/control/verify.ts
|
|
4041
4890
|
var GRANT_MAX_MS = 24 * 36e5;
|
|
@@ -4047,6 +4896,11 @@ var CMD_MAX = 48e3;
|
|
|
4047
4896
|
var TEXT_MAX = 2e4;
|
|
4048
4897
|
var NOTE_MAX = 2e3;
|
|
4049
4898
|
var GRANT_STR_MAX = 2e3;
|
|
4899
|
+
var FILES_MAX = 10;
|
|
4900
|
+
var FILE_SIZE_MAX = 26214400;
|
|
4901
|
+
var FILES_TOTAL_MAX = 52428800;
|
|
4902
|
+
var FILE_NAME_MAX = 200;
|
|
4903
|
+
var FILE_TYPE_MAX = 100;
|
|
4050
4904
|
var COMMAND_TYPE = "sessionpipe.control";
|
|
4051
4905
|
var GRANT_TYPE = "sessionpipe.grant";
|
|
4052
4906
|
var CONTROL_KINDS = ["prompt", "permission.answer", "cancel", "start", "key.add"];
|
|
@@ -4059,6 +4913,9 @@ var HARNESS_RE = /^[a-z][a-z0-9-]{0,39}$/;
|
|
|
4059
4913
|
var SESSION_ID_RE = /^[A-Za-z0-9._:-]{1,128}$/;
|
|
4060
4914
|
var UUID_RE2 = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
4061
4915
|
var B64URL_RE = /^[A-Za-z0-9_-]*$/;
|
|
4916
|
+
var SHA256_RE = /^[A-Za-z0-9_-]{43}$/;
|
|
4917
|
+
var MEDIA_TYPE_RE = /^[a-z0-9][a-z0-9!#$&^_.+-]*\/[a-z0-9][a-z0-9!#$&^_.+-]*$/i;
|
|
4918
|
+
var BAD_NAME_CHAR_RE = /[/\\\x00-\x1f\x7f]/;
|
|
4062
4919
|
var UP = 1;
|
|
4063
4920
|
var UV = 4;
|
|
4064
4921
|
var enc = new TextEncoder();
|
|
@@ -4129,7 +4986,7 @@ async function importKey(key) {
|
|
|
4129
4986
|
const k = await crypto.subtle.importKey("spki", u8(spki), { name: "RSASSA-PKCS1-v1_5", hash: "SHA-256" }, false, [
|
|
4130
4987
|
"verify"
|
|
4131
4988
|
]);
|
|
4132
|
-
const bits = k.algorithm.modulusLength
|
|
4989
|
+
const bits = k.algorithm.modulusLength;
|
|
4133
4990
|
return bits >= 2048 ? k : null;
|
|
4134
4991
|
}
|
|
4135
4992
|
} catch {
|
|
@@ -4248,7 +5105,11 @@ function parseCommand(str) {
|
|
|
4248
5105
|
if (!text()) return bad("bad_fields", `a new session needs text, at most ${TEXT_MAX} characters`);
|
|
4249
5106
|
if (typeof c.cwd !== "string" || !isAbsolute(c.cwd) || c.cwd.length > 1024)
|
|
4250
5107
|
return bad("bad_fields", "a new session needs an absolute folder");
|
|
4251
|
-
if (has2("for") || has2("decision")) return bad("bad_fields", "a start carries text and
|
|
5108
|
+
if (has2("for") || has2("decision")) return bad("bad_fields", "a start carries text, cwd and files");
|
|
5109
|
+
if (has2("files")) {
|
|
5110
|
+
const why = filesProblem(c.files);
|
|
5111
|
+
if (why) return bad("bad_fields", why);
|
|
5112
|
+
}
|
|
4252
5113
|
break;
|
|
4253
5114
|
case "permission.answer":
|
|
4254
5115
|
if (typeof c.for !== "string" || !c.for || c.for.length > 128) return bad("bad_fields", "which attention?");
|
|
@@ -4273,10 +5134,42 @@ function parseCommand(str) {
|
|
|
4273
5134
|
}
|
|
4274
5135
|
}
|
|
4275
5136
|
if (c.kind !== "key.add" && (has2("key") || has2("name"))) return bad("bad_fields", "only a key.add carries a key");
|
|
5137
|
+
if (c.kind !== "start" && has2("files")) return bad("bad_fields", "only a start carries files");
|
|
4276
5138
|
if (has2("note") && (typeof c.note !== "string" || c.note.length > NOTE_MAX))
|
|
4277
5139
|
return bad("bad_fields", `a note is text, at most ${NOTE_MAX} characters`);
|
|
4278
5140
|
return { ok: true, cmd: c };
|
|
4279
5141
|
}
|
|
5142
|
+
function filesProblem(files) {
|
|
5143
|
+
if (!Array.isArray(files) || files.length < 1 || files.length > FILES_MAX)
|
|
5144
|
+
return `files is a list of 1 to ${FILES_MAX} files`;
|
|
5145
|
+
const seen = /* @__PURE__ */ new Set();
|
|
5146
|
+
let total = 0;
|
|
5147
|
+
for (const f of files) {
|
|
5148
|
+
if (!f || typeof f !== "object" || Array.isArray(f)) return "each file is an object";
|
|
5149
|
+
const { name, type, size, sha256: hash } = f;
|
|
5150
|
+
const why = fileNameProblem(name);
|
|
5151
|
+
if (why) return why;
|
|
5152
|
+
const key = name.normalize("NFC").toLowerCase();
|
|
5153
|
+
if (seen.has(key)) return "two files have the same name";
|
|
5154
|
+
seen.add(key);
|
|
5155
|
+
if (typeof type !== "string" || type.length > FILE_TYPE_MAX || !MEDIA_TYPE_RE.test(type))
|
|
5156
|
+
return "a file's type isn't a media type";
|
|
5157
|
+
if (!isNum(size) || !Number.isInteger(size) || size < 1 || size > FILE_SIZE_MAX)
|
|
5158
|
+
return `a file is 1 byte to ${FILE_SIZE_MAX / 1048576} MiB`;
|
|
5159
|
+
total += size;
|
|
5160
|
+
if (typeof hash !== "string" || !SHA256_RE.test(hash)) return "a file's sha256 isn't 43 base64url characters";
|
|
5161
|
+
}
|
|
5162
|
+
if (total > FILES_TOTAL_MAX) return `the files add up to more than ${FILES_TOTAL_MAX / 1048576} MiB`;
|
|
5163
|
+
return null;
|
|
5164
|
+
}
|
|
5165
|
+
function fileNameProblem(name) {
|
|
5166
|
+
if (typeof name !== "string") return "a file has no name";
|
|
5167
|
+
const n = name.normalize("NFC");
|
|
5168
|
+
if (n.length < 1 || n.length > FILE_NAME_MAX) return `a file name is 1 to ${FILE_NAME_MAX} characters`;
|
|
5169
|
+
if (BAD_NAME_CHAR_RE.test(n)) return "a file name has a slash, a backslash or a control character";
|
|
5170
|
+
if (n.startsWith(".")) return "a file name can't start with a dot";
|
|
5171
|
+
return null;
|
|
5172
|
+
}
|
|
4280
5173
|
function isAbsolute(p) {
|
|
4281
5174
|
return p.startsWith("/") || /^[A-Za-z]:[\\/]/.test(p);
|
|
4282
5175
|
}
|
|
@@ -4298,7 +5191,7 @@ function parseGrant(str, rpId) {
|
|
|
4298
5191
|
if (g.exp - g.iat > GRANT_MAX_MS + SKEW_MS) return { ok: false, why: "the grant lasts longer than a day" };
|
|
4299
5192
|
return { ok: true, grant: g };
|
|
4300
5193
|
}
|
|
4301
|
-
async function verifyCsig(
|
|
5194
|
+
async function verifyCsig(cmd, csig, pub) {
|
|
4302
5195
|
const sig = fromB64url(csig);
|
|
4303
5196
|
const spki = fromB64url(pub);
|
|
4304
5197
|
if (sig?.length !== 64 || !spki) return false;
|
|
@@ -4306,7 +5199,7 @@ async function verifyCsig(cmd2, csig, pub) {
|
|
|
4306
5199
|
const key = await crypto.subtle.importKey("spki", u8(spki), { name: "ECDSA", namedCurve: "P-256" }, false, [
|
|
4307
5200
|
"verify"
|
|
4308
5201
|
]);
|
|
4309
|
-
return await crypto.subtle.verify({ name: "ECDSA", hash: "SHA-256" }, key, u8(sig), u8(enc.encode(
|
|
5202
|
+
return await crypto.subtle.verify({ name: "ECDSA", hash: "SHA-256" }, key, u8(sig), u8(enc.encode(cmd)));
|
|
4310
5203
|
} catch {
|
|
4311
5204
|
return false;
|
|
4312
5205
|
}
|
|
@@ -4371,22 +5264,22 @@ async function checkGrantPath(s, c, o) {
|
|
|
4371
5264
|
}
|
|
4372
5265
|
|
|
4373
5266
|
// src/control/store.ts
|
|
4374
|
-
var controlFile = (env = process.env) =>
|
|
4375
|
-
var controlState = (env = process.env) =>
|
|
5267
|
+
var controlFile = (env = process.env) => path23.join(path23.dirname(configFile(env)), "control.json");
|
|
5268
|
+
var controlState = (env = process.env) => path23.join(stateDir(env), "control");
|
|
4376
5269
|
function writePrivate(file, value) {
|
|
4377
|
-
|
|
5270
|
+
mkdirSync10(path23.dirname(file), { recursive: true, mode: 448 });
|
|
4378
5271
|
const tmp = `${file}.${process.pid}.tmp`;
|
|
4379
|
-
|
|
5272
|
+
writeFileSync9(tmp, `${JSON.stringify(value, null, 2)}
|
|
4380
5273
|
`, { mode: 384 });
|
|
4381
|
-
|
|
5274
|
+
renameSync5(tmp, file);
|
|
4382
5275
|
try {
|
|
4383
|
-
|
|
5276
|
+
chmodSync4(file, 384);
|
|
4384
5277
|
} catch {
|
|
4385
5278
|
}
|
|
4386
5279
|
}
|
|
4387
5280
|
function readControl(env = process.env, run = defaultRunner) {
|
|
4388
5281
|
try {
|
|
4389
|
-
const j = JSON.parse(
|
|
5282
|
+
const j = JSON.parse(readFileSync13(controlFile(env), "utf8"));
|
|
4390
5283
|
if (!j || !Array.isArray(j.receivers)) return null;
|
|
4391
5284
|
return {
|
|
4392
5285
|
name: j.name || "machine",
|
|
@@ -4403,7 +5296,7 @@ function readControl(env = process.env, run = defaultRunner) {
|
|
|
4403
5296
|
}
|
|
4404
5297
|
function pairedCount(env = process.env) {
|
|
4405
5298
|
try {
|
|
4406
|
-
const j = JSON.parse(
|
|
5299
|
+
const j = JSON.parse(readFileSync13(controlFile(env), "utf8"));
|
|
4407
5300
|
return Array.isArray(j.receivers) ? j.receivers.length : 0;
|
|
4408
5301
|
} catch {
|
|
4409
5302
|
return 0;
|
|
@@ -4453,15 +5346,15 @@ var NonceStore = class {
|
|
|
4453
5346
|
this.now = now;
|
|
4454
5347
|
let raw = "";
|
|
4455
5348
|
try {
|
|
4456
|
-
raw =
|
|
5349
|
+
raw = readFileSync13(file, "utf8");
|
|
4457
5350
|
} catch {
|
|
4458
5351
|
}
|
|
4459
|
-
const
|
|
5352
|
+
const cut2 = this.now() - NONCE_TTL_MS;
|
|
4460
5353
|
for (const line of raw.split("\n")) {
|
|
4461
5354
|
if (!line) continue;
|
|
4462
5355
|
try {
|
|
4463
5356
|
const { n, t } = JSON.parse(line);
|
|
4464
|
-
if (typeof n === "string" && t >
|
|
5357
|
+
if (typeof n === "string" && t > cut2) this.seenAt.set(n, t);
|
|
4465
5358
|
} catch {
|
|
4466
5359
|
}
|
|
4467
5360
|
}
|
|
@@ -4474,7 +5367,7 @@ var NonceStore = class {
|
|
|
4474
5367
|
const t = this.seenAt.get(nonce);
|
|
4475
5368
|
if (t !== void 0 && t > this.now() - NONCE_TTL_MS) return true;
|
|
4476
5369
|
const at = this.now();
|
|
4477
|
-
|
|
5370
|
+
mkdirSync10(path23.dirname(this.file), { recursive: true, mode: 448 });
|
|
4478
5371
|
const fd = openSync4(this.file, "a", 384);
|
|
4479
5372
|
try {
|
|
4480
5373
|
appendFileSync4(fd, `${JSON.stringify({ n: nonce, t: at })}
|
|
@@ -4488,14 +5381,14 @@ var NonceStore = class {
|
|
|
4488
5381
|
};
|
|
4489
5382
|
/** Rewrite the file with only the live window (on start, and daily). */
|
|
4490
5383
|
compact() {
|
|
4491
|
-
const
|
|
4492
|
-
for (const [n, t] of this.seenAt) if (t <=
|
|
4493
|
-
if (!
|
|
5384
|
+
const cut2 = this.now() - NONCE_TTL_MS;
|
|
5385
|
+
for (const [n, t] of this.seenAt) if (t <= cut2) this.seenAt.delete(n);
|
|
5386
|
+
if (!existsSync12(this.file) && !this.seenAt.size) return;
|
|
4494
5387
|
const body = [...this.seenAt].map(([n, t]) => JSON.stringify({ n, t })).join("\n");
|
|
4495
|
-
|
|
4496
|
-
|
|
5388
|
+
mkdirSync10(path23.dirname(this.file), { recursive: true, mode: 448 });
|
|
5389
|
+
writeFileSync9(`${this.file}.tmp`, body ? `${body}
|
|
4497
5390
|
` : "", { mode: 384 });
|
|
4498
|
-
|
|
5391
|
+
renameSync5(`${this.file}.tmp`, this.file);
|
|
4499
5392
|
}
|
|
4500
5393
|
get size() {
|
|
4501
5394
|
return this.seenAt.size;
|
|
@@ -4508,27 +5401,27 @@ var AckOutbox = class {
|
|
|
4508
5401
|
file;
|
|
4509
5402
|
all() {
|
|
4510
5403
|
try {
|
|
4511
|
-
return
|
|
5404
|
+
return readFileSync13(this.file, "utf8").split("\n").filter(Boolean).map((l) => JSON.parse(l));
|
|
4512
5405
|
} catch {
|
|
4513
5406
|
return [];
|
|
4514
5407
|
}
|
|
4515
5408
|
}
|
|
4516
5409
|
add(a) {
|
|
4517
|
-
|
|
5410
|
+
mkdirSync10(path23.dirname(this.file), { recursive: true, mode: 448 });
|
|
4518
5411
|
appendFileSync4(this.file, `${JSON.stringify(a)}
|
|
4519
5412
|
`, { mode: 384 });
|
|
4520
5413
|
}
|
|
4521
5414
|
keep(left) {
|
|
4522
|
-
|
|
4523
|
-
|
|
5415
|
+
mkdirSync10(path23.dirname(this.file), { recursive: true, mode: 448 });
|
|
5416
|
+
writeFileSync9(`${this.file}.tmp`, left.map((a) => JSON.stringify(a)).join("\n") + (left.length ? "\n" : ""), {
|
|
4524
5417
|
mode: 384
|
|
4525
5418
|
});
|
|
4526
|
-
|
|
5419
|
+
renameSync5(`${this.file}.tmp`, this.file);
|
|
4527
5420
|
}
|
|
4528
5421
|
};
|
|
4529
5422
|
function readState(file) {
|
|
4530
5423
|
try {
|
|
4531
|
-
const j = JSON.parse(
|
|
5424
|
+
const j = JSON.parse(readFileSync13(file, "utf8"));
|
|
4532
5425
|
return { copies: j.copies && typeof j.copies === "object" ? j.copies : {} };
|
|
4533
5426
|
} catch {
|
|
4534
5427
|
return { copies: {} };
|
|
@@ -4541,10 +5434,10 @@ var UPDATE_EXIT = 75;
|
|
|
4541
5434
|
var DAY_MS = 24 * 36e5;
|
|
4542
5435
|
var LABEL3 = "org.sessionpipe.control";
|
|
4543
5436
|
var UNIT = "sessionpipe-control.service";
|
|
4544
|
-
var updateFile = (env = process.env) =>
|
|
5437
|
+
var updateFile = (env = process.env) => path24.join(controlState(env), "update.json");
|
|
4545
5438
|
function readUpdate(file) {
|
|
4546
5439
|
try {
|
|
4547
|
-
const j = JSON.parse(
|
|
5440
|
+
const j = JSON.parse(readFileSync14(file, "utf8"));
|
|
4548
5441
|
return j && typeof j === "object" ? j : {};
|
|
4549
5442
|
} catch {
|
|
4550
5443
|
return {};
|
|
@@ -4607,7 +5500,7 @@ function restartVia(o) {
|
|
|
4607
5500
|
if (o.platform === "linux") {
|
|
4608
5501
|
let cg = "";
|
|
4609
5502
|
try {
|
|
4610
|
-
cg = (o.cgroup ?? (() =>
|
|
5503
|
+
cg = (o.cgroup ?? (() => readFileSync14("/proc/self/cgroup", "utf8")))();
|
|
4611
5504
|
} catch {
|
|
4612
5505
|
}
|
|
4613
5506
|
const inUnit = cg.split("\n").some((l) => l.endsWith(`/${UNIT}`) || l.includes(`/${UNIT}/`));
|
|
@@ -4837,14 +5730,112 @@ function supervise(o) {
|
|
|
4837
5730
|
}
|
|
4838
5731
|
|
|
4839
5732
|
// src/control/cli.ts
|
|
4840
|
-
import { existsSync as
|
|
4841
|
-
import
|
|
4842
|
-
import
|
|
5733
|
+
import { existsSync as existsSync16, realpathSync as realpathSync6, statSync as statSync10, unwatchFile, watchFile } from "node:fs";
|
|
5734
|
+
import os10 from "node:os";
|
|
5735
|
+
import path28 from "node:path";
|
|
4843
5736
|
|
|
4844
5737
|
// src/control/daemon.ts
|
|
4845
|
-
import { existsSync as
|
|
5738
|
+
import { chmodSync as chmodSync5, existsSync as existsSync14, mkdirSync as mkdirSync12, readFileSync as readFileSync15, realpathSync as realpathSync5, statSync as statSync9, unlinkSync as unlinkSync5 } from "node:fs";
|
|
4846
5739
|
import net2 from "node:net";
|
|
4847
|
-
import
|
|
5740
|
+
import path26 from "node:path";
|
|
5741
|
+
|
|
5742
|
+
// src/control/files.ts
|
|
5743
|
+
import { createHash as createHash7 } from "node:crypto";
|
|
5744
|
+
import { existsSync as existsSync13, lstatSync, mkdirSync as mkdirSync11, realpathSync as realpathSync4, rmSync as rmSync2, writeFileSync as writeFileSync10 } from "node:fs";
|
|
5745
|
+
import path25 from "node:path";
|
|
5746
|
+
var FETCH_TIMEOUT_MS = 5 * 6e4;
|
|
5747
|
+
var FileProblem = class extends Error {
|
|
5748
|
+
};
|
|
5749
|
+
var b64url2 = (b) => b.toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
5750
|
+
var reason = (e) => (e instanceof Error ? e.message : String(e)).slice(0, 160);
|
|
5751
|
+
async function fetchFiles(files, o) {
|
|
5752
|
+
const out2 = [];
|
|
5753
|
+
for (const file of files) {
|
|
5754
|
+
let bytes;
|
|
5755
|
+
try {
|
|
5756
|
+
bytes = await fetchOne(file, o);
|
|
5757
|
+
} catch (e) {
|
|
5758
|
+
if (e instanceof FileProblem) throw e;
|
|
5759
|
+
throw new FileProblem(`couldn't download ${JSON.stringify(file.name)}: ${reason(e)}`);
|
|
5760
|
+
}
|
|
5761
|
+
out2.push({ file, name: file.name.normalize("NFC"), bytes });
|
|
5762
|
+
}
|
|
5763
|
+
return out2;
|
|
5764
|
+
}
|
|
5765
|
+
async function fetchOne(f, o) {
|
|
5766
|
+
const label = JSON.stringify(f.name);
|
|
5767
|
+
const ctl = new AbortController();
|
|
5768
|
+
const t = setTimeout(() => ctl.abort(), o.timeoutMs ?? FETCH_TIMEOUT_MS);
|
|
5769
|
+
try {
|
|
5770
|
+
const res = await o.fetch(`${o.control.replace(/\/$/, "")}/files/${f.sha256}`, {
|
|
5771
|
+
headers: { authorization: `Bearer ${o.token}` },
|
|
5772
|
+
signal: ctl.signal
|
|
5773
|
+
});
|
|
5774
|
+
if (res.status === 404) throw new FileProblem(`${label} isn't on the receiver (never uploaded, or expired)`);
|
|
5775
|
+
if (res.status !== 200) throw new FileProblem(`couldn't download ${label}: HTTP ${res.status}`);
|
|
5776
|
+
if (Number(res.headers.get("content-length")) > f.size) throw tooLong(label, f);
|
|
5777
|
+
const buf = new Uint8Array(f.size);
|
|
5778
|
+
let got = 0;
|
|
5779
|
+
const reader = res.body.getReader();
|
|
5780
|
+
for (; ; ) {
|
|
5781
|
+
const { done, value } = await reader.read();
|
|
5782
|
+
if (done) break;
|
|
5783
|
+
if (got + value.length > f.size) throw tooLong(label, f);
|
|
5784
|
+
buf.set(value, got);
|
|
5785
|
+
got += value.length;
|
|
5786
|
+
}
|
|
5787
|
+
if (got !== f.size) throw new FileProblem(`${label} is ${got} bytes, not its signed ${f.size}`);
|
|
5788
|
+
const hash = b64url2(createHash7("sha256").update(buf).digest());
|
|
5789
|
+
if (hash !== f.sha256) throw new FileProblem(`${label} doesn't match its signed hash: it was changed`);
|
|
5790
|
+
return buf;
|
|
5791
|
+
} finally {
|
|
5792
|
+
clearTimeout(t);
|
|
5793
|
+
ctl.abort();
|
|
5794
|
+
}
|
|
5795
|
+
}
|
|
5796
|
+
var tooLong = (label, f) => new FileProblem(`${label} is larger than its signed size (${f.size} bytes)`);
|
|
5797
|
+
function realDir(p, why) {
|
|
5798
|
+
mkdirSync11(p, { recursive: true, mode: 448 });
|
|
5799
|
+
if (lstatSync(p).isSymbolicLink()) throw new FileProblem(`${why} is a symlink`);
|
|
5800
|
+
}
|
|
5801
|
+
function writeFiles(cwd, session, files) {
|
|
5802
|
+
const top = path25.join(cwd, ".sessionpipe");
|
|
5803
|
+
const parent = path25.join(top, "files");
|
|
5804
|
+
const dir = path25.join(parent, session);
|
|
5805
|
+
let made = false;
|
|
5806
|
+
try {
|
|
5807
|
+
realDir(top, ".sessionpipe");
|
|
5808
|
+
const ignore = path25.join(top, ".gitignore");
|
|
5809
|
+
if (!existsSync13(ignore)) writeFileSync10(ignore, "*\n", { flag: "wx", mode: 384 });
|
|
5810
|
+
realDir(parent, ".sessionpipe/files");
|
|
5811
|
+
if (existsSync13(dir)) throw new FileProblem(`.sessionpipe/files/${session} already exists`);
|
|
5812
|
+
mkdirSync11(dir, { mode: 448 });
|
|
5813
|
+
made = true;
|
|
5814
|
+
if (realpathSync4.native(dir) !== dir) throw new FileProblem(".sessionpipe leads outside the session's folder");
|
|
5815
|
+
const rel = [];
|
|
5816
|
+
for (const f of files) {
|
|
5817
|
+
writeFileSync10(path25.join(dir, f.name), f.bytes, { flag: "wx", mode: 384 });
|
|
5818
|
+
rel.push(`.sessionpipe/files/${session}/${f.name}`);
|
|
5819
|
+
}
|
|
5820
|
+
return rel;
|
|
5821
|
+
} catch (e) {
|
|
5822
|
+
if (made) rmSync2(dir, { recursive: true, force: true });
|
|
5823
|
+
if (e instanceof FileProblem) throw e;
|
|
5824
|
+
throw new FileProblem(`couldn't save the files: ${reason(e)}`);
|
|
5825
|
+
}
|
|
5826
|
+
}
|
|
5827
|
+
function humanSize(n) {
|
|
5828
|
+
if (n < 1024) return `${n} B`;
|
|
5829
|
+
if (n < 1048576) return `${Math.round(n / 1024)} KB`;
|
|
5830
|
+
return `${(n / 1048576).toFixed(1)} MB`;
|
|
5831
|
+
}
|
|
5832
|
+
function withFiles(text, files, rel) {
|
|
5833
|
+
const lines = files.map((f, i) => `- ${rel[i]} (${f.file.type}, ${humanSize(f.file.size)})`);
|
|
5834
|
+
return `${text}
|
|
5835
|
+
|
|
5836
|
+
Attached files (saved in this folder):
|
|
5837
|
+
${lines.join("\n")}`;
|
|
5838
|
+
}
|
|
4848
5839
|
|
|
4849
5840
|
// src/control/reply.ts
|
|
4850
5841
|
import { closeSync as closeSync5, openSync as openSync5, readSync as readSync4, statSync as statSync8 } from "node:fs";
|
|
@@ -4891,9 +5882,9 @@ async function awaitAnswer(file, from, o) {
|
|
|
4891
5882
|
const n = readSync4(fd, buf, 0, buf.length, offset);
|
|
4892
5883
|
offset += n;
|
|
4893
5884
|
const chunk = partial + buf.subarray(0, n).toString("utf8");
|
|
4894
|
-
const
|
|
4895
|
-
partial =
|
|
4896
|
-
if (
|
|
5885
|
+
const cut2 = chunk.lastIndexOf("\n");
|
|
5886
|
+
partial = cut2 < 0 ? chunk : chunk.slice(cut2 + 1);
|
|
5887
|
+
if (cut2 >= 0) lines.push(...chunk.slice(0, cut2).split("\n").filter(Boolean));
|
|
4897
5888
|
} finally {
|
|
4898
5889
|
closeSync5(fd);
|
|
4899
5890
|
}
|
|
@@ -4926,7 +5917,7 @@ function kindPath(kind, harness) {
|
|
|
4926
5917
|
case "prompt":
|
|
4927
5918
|
return "prompt";
|
|
4928
5919
|
case "permission.answer":
|
|
4929
|
-
return harness === "gemini-cli" ? { unsupported: "Gemini CLI has no permission answer channel" } : "permission";
|
|
5920
|
+
return harness === "gemini-cli" ? { unsupported: "Gemini CLI has no permission answer channel" } : harness === "devin" ? { unsupported: "Devin has no permission answer channel" } : "permission";
|
|
4930
5921
|
case "cancel":
|
|
4931
5922
|
return "cancel";
|
|
4932
5923
|
case "start":
|
|
@@ -5046,16 +6037,16 @@ var {
|
|
|
5046
6037
|
recentlyWritten: recentlyWritten2,
|
|
5047
6038
|
sessionFolder: sessionFolder2
|
|
5048
6039
|
} = claude_code_control_exports;
|
|
5049
|
-
function startNamed(
|
|
5050
|
-
if (typeof
|
|
6040
|
+
function startNamed(cmd) {
|
|
6041
|
+
if (typeof cmd !== "string" || cmd.length > 64 * 1024 || !cmd.includes('"start"')) return null;
|
|
5051
6042
|
try {
|
|
5052
|
-
const c = JSON.parse(
|
|
6043
|
+
const c = JSON.parse(cmd);
|
|
5053
6044
|
return c?.kind === "start" && typeof c.session === "string" ? c.session : null;
|
|
5054
6045
|
} catch {
|
|
5055
6046
|
return null;
|
|
5056
6047
|
}
|
|
5057
6048
|
}
|
|
5058
|
-
var VERSION2 = true ? "0.
|
|
6049
|
+
var VERSION2 = true ? "0.9.0" : "0.0.0";
|
|
5059
6050
|
var MAX_REPLY = 2e4;
|
|
5060
6051
|
var HELLO_EVERY_MS = 10 * 6e4;
|
|
5061
6052
|
var TURN_FALLBACK_MS = 30 * 6e4;
|
|
@@ -5091,9 +6082,9 @@ var ControlDaemon = class {
|
|
|
5091
6082
|
this.dir = dir;
|
|
5092
6083
|
this.sock = sock;
|
|
5093
6084
|
this.deps = deps;
|
|
5094
|
-
this.nonces = new NonceStore(
|
|
5095
|
-
this.acks = new AckOutbox(
|
|
5096
|
-
this.state = readState(
|
|
6085
|
+
this.nonces = new NonceStore(path26.join(dir, "nonces.jsonl"), deps.now);
|
|
6086
|
+
this.acks = new AckOutbox(path26.join(dir, "acks.jsonl"));
|
|
6087
|
+
this.state = readState(path26.join(dir, "state.json"));
|
|
5097
6088
|
this.aliases = readAliases(aliasFile(dir));
|
|
5098
6089
|
}
|
|
5099
6090
|
cfg;
|
|
@@ -5149,7 +6140,7 @@ var ControlDaemon = class {
|
|
|
5149
6140
|
// --- lifecycle ------------------------------------------------------------------
|
|
5150
6141
|
async start() {
|
|
5151
6142
|
this.running = true;
|
|
5152
|
-
|
|
6143
|
+
mkdirSync12(this.dir, { recursive: true, mode: 448 });
|
|
5153
6144
|
await this.listen();
|
|
5154
6145
|
for (const r of this.cfg.receivers) void this.pollLoop(r);
|
|
5155
6146
|
this.helloTimer = setInterval(() => void this.helloAll(), HELLO_EVERY_MS);
|
|
@@ -5173,6 +6164,7 @@ var ControlDaemon = class {
|
|
|
5173
6164
|
this.running = false;
|
|
5174
6165
|
this.paused?.go();
|
|
5175
6166
|
for (const a of this.aborts) a.abort();
|
|
6167
|
+
for (const st of this.polling.values()) st.wake?.();
|
|
5176
6168
|
if (this.helloTimer) clearInterval(this.helloTimer);
|
|
5177
6169
|
if (this.helloSoon) clearTimeout(this.helloSoon);
|
|
5178
6170
|
if (this.ownTimer) clearInterval(this.ownTimer);
|
|
@@ -5242,7 +6234,12 @@ var ControlDaemon = class {
|
|
|
5242
6234
|
try {
|
|
5243
6235
|
await this.listen();
|
|
5244
6236
|
} catch (e) {
|
|
5245
|
-
this.deps.log(
|
|
6237
|
+
this.deps.log(
|
|
6238
|
+
`couldn't listen again: ${String(
|
|
6239
|
+
/* v8 ignore next -- listen() throws Errors (net, DaemonRunning) */
|
|
6240
|
+
e?.message || e
|
|
6241
|
+
)}`
|
|
6242
|
+
);
|
|
5246
6243
|
}
|
|
5247
6244
|
return;
|
|
5248
6245
|
}
|
|
@@ -5252,8 +6249,10 @@ var ControlDaemon = class {
|
|
|
5252
6249
|
}
|
|
5253
6250
|
reload(cfg) {
|
|
5254
6251
|
const known = new Set(this.cfg.receivers.map((r) => r.control + r.machine));
|
|
6252
|
+
const told = JSON.stringify([this.cfg.folders, this.cfg.mode, this.cfg.name]);
|
|
5255
6253
|
this.cfg = cfg;
|
|
5256
6254
|
for (const r of cfg.receivers) if (!known.has(r.control + r.machine)) void this.pollLoop(r);
|
|
6255
|
+
if (this.running && JSON.stringify([cfg.folders, cfg.mode, cfg.name]) !== told) void this.helloAll();
|
|
5257
6256
|
}
|
|
5258
6257
|
status() {
|
|
5259
6258
|
return {
|
|
@@ -5268,6 +6267,7 @@ var ControlDaemon = class {
|
|
|
5268
6267
|
keys: r.keys.length,
|
|
5269
6268
|
polling: !!p?.on,
|
|
5270
6269
|
last_poll: p?.last ?? null,
|
|
6270
|
+
last_poll_age_ms: p ? Math.max(0, this.deps.now() - (p.last ?? p.started)) : 0,
|
|
5271
6271
|
last_error: p?.error ?? null
|
|
5272
6272
|
};
|
|
5273
6273
|
}),
|
|
@@ -5290,7 +6290,7 @@ var ControlDaemon = class {
|
|
|
5290
6290
|
if (other?.op === "status") throw new DaemonRunning(other.status ?? {});
|
|
5291
6291
|
if (process.platform !== "win32") {
|
|
5292
6292
|
try {
|
|
5293
|
-
if (
|
|
6293
|
+
if (existsSync14(this.sock)) unlinkSync5(this.sock);
|
|
5294
6294
|
} catch {
|
|
5295
6295
|
}
|
|
5296
6296
|
}
|
|
@@ -5299,7 +6299,10 @@ var ControlDaemon = class {
|
|
|
5299
6299
|
this.server.once("error", reject);
|
|
5300
6300
|
this.server.listen(this.sock, () => resolve());
|
|
5301
6301
|
});
|
|
5302
|
-
if (process.platform !== "win32")
|
|
6302
|
+
if (process.platform !== "win32") {
|
|
6303
|
+
chmodSync5(this.sock, 384);
|
|
6304
|
+
this.sockId = socketId(this.sock);
|
|
6305
|
+
}
|
|
5303
6306
|
}
|
|
5304
6307
|
onConn(s) {
|
|
5305
6308
|
let buf = "";
|
|
@@ -5441,8 +6444,30 @@ ${IN_PLACE_ANSWER}`, id: p.msgId });
|
|
|
5441
6444
|
// --- receivers ------------------------------------------------------------------
|
|
5442
6445
|
async pollLoop(first) {
|
|
5443
6446
|
const key = first.control + first.machine;
|
|
5444
|
-
const st = {
|
|
6447
|
+
const st = {
|
|
6448
|
+
started: this.deps.now(),
|
|
6449
|
+
last: null,
|
|
6450
|
+
error: null,
|
|
6451
|
+
on: true,
|
|
6452
|
+
unauthorized: false,
|
|
6453
|
+
authBackoff: 0,
|
|
6454
|
+
wake: null
|
|
6455
|
+
};
|
|
5445
6456
|
this.polling.set(key, st);
|
|
6457
|
+
const retry = (ms) => new Promise((resolve) => {
|
|
6458
|
+
st.on = false;
|
|
6459
|
+
if (!this.running) {
|
|
6460
|
+
resolve();
|
|
6461
|
+
return;
|
|
6462
|
+
}
|
|
6463
|
+
const timer = setTimeout(() => st.wake?.(), ms);
|
|
6464
|
+
timer.unref();
|
|
6465
|
+
st.wake = () => {
|
|
6466
|
+
clearTimeout(timer);
|
|
6467
|
+
st.wake = null;
|
|
6468
|
+
resolve();
|
|
6469
|
+
};
|
|
6470
|
+
});
|
|
5446
6471
|
let backoff = 0;
|
|
5447
6472
|
let downSince = 0;
|
|
5448
6473
|
const steps = [1e3, 2e3, 5e3, 1e4, 15e3];
|
|
@@ -5451,6 +6476,7 @@ ${IN_PLACE_ANSWER}`, id: p.msgId });
|
|
|
5451
6476
|
const r = this.cfg.receivers.find((x) => x.control + x.machine === key);
|
|
5452
6477
|
if (!this.running || !r) break;
|
|
5453
6478
|
if (this.paused) {
|
|
6479
|
+
st.on = false;
|
|
5454
6480
|
await this.paused.until;
|
|
5455
6481
|
continue;
|
|
5456
6482
|
}
|
|
@@ -5463,11 +6489,12 @@ ${IN_PLACE_ANSWER}`, id: p.msgId });
|
|
|
5463
6489
|
if (!st.error?.startsWith("can't read"))
|
|
5464
6490
|
this.deps.log(`${r.url}: can't read this machine's token from ${r.token_in ?? "its store"}; retrying`);
|
|
5465
6491
|
st.error = `can't read this machine's token from ${r.token_in ?? "its store"} (locked?); retrying every 30 s`;
|
|
5466
|
-
await
|
|
6492
|
+
await retry(3e4);
|
|
5467
6493
|
continue;
|
|
5468
6494
|
}
|
|
5469
6495
|
}
|
|
5470
6496
|
const ctl = new AbortController();
|
|
6497
|
+
st.on = true;
|
|
5471
6498
|
this.aborts.add(ctl);
|
|
5472
6499
|
const t = setTimeout(() => ctl.abort(), 35e3);
|
|
5473
6500
|
try {
|
|
@@ -5476,15 +6503,21 @@ ${IN_PLACE_ANSWER}`, id: p.msgId });
|
|
|
5476
6503
|
u.searchParams.set("wait", "25");
|
|
5477
6504
|
const asked = this.deps.now();
|
|
5478
6505
|
const res = await this.deps.fetch(u, { headers: { authorization: `Bearer ${r.token}` }, signal: ctl.signal });
|
|
5479
|
-
|
|
5480
|
-
|
|
6506
|
+
if (res.status !== 200) clearTimeout(t);
|
|
6507
|
+
st.unauthorized = res.status === 401;
|
|
6508
|
+
if (downSince && (res.status === 200 || res.status === 204)) {
|
|
5481
6509
|
this.deps.log(`${r.url}: back after ${Math.round((asked - downSince) / 1e3)} s (${backoff} failed tries)`);
|
|
5482
6510
|
downSince = 0;
|
|
5483
6511
|
}
|
|
5484
6512
|
if (res.status === 401) {
|
|
5485
6513
|
st.error = "the receiver no longer knows this machine (401); run `sessionpipe control pair` again";
|
|
5486
6514
|
this.deps.log(`${r.url}: ${st.error}`);
|
|
5487
|
-
|
|
6515
|
+
downSince ||= this.deps.now();
|
|
6516
|
+
backoff++;
|
|
6517
|
+
const delay = Math.min(6e4 * 2 ** st.authBackoff, 30 * 6e4);
|
|
6518
|
+
st.authBackoff = Math.min(st.authBackoff + 1, 5);
|
|
6519
|
+
await retry(delay);
|
|
6520
|
+
continue;
|
|
5488
6521
|
}
|
|
5489
6522
|
if (res.status === 200) {
|
|
5490
6523
|
let fresh = 0;
|
|
@@ -5495,29 +6528,47 @@ ${IN_PLACE_ANSWER}`, id: p.msgId });
|
|
|
5495
6528
|
for (const m of body.messages ?? []) {
|
|
5496
6529
|
if (m && !this.inflight.has(m.id)) fresh++;
|
|
5497
6530
|
void this.handle(cur, m).catch(
|
|
5498
|
-
(e) => this.deps.log(
|
|
6531
|
+
(e) => this.deps.log(
|
|
6532
|
+
`handle ${String(m?.id).slice(-8)}: ${String(
|
|
6533
|
+
/* v8 ignore next -- handle() rejects with an Error (the ack file) */
|
|
6534
|
+
e?.message || e
|
|
6535
|
+
)}`
|
|
6536
|
+
)
|
|
5499
6537
|
);
|
|
5500
6538
|
}
|
|
5501
6539
|
} finally {
|
|
5502
6540
|
this.receiving--;
|
|
5503
6541
|
}
|
|
5504
|
-
|
|
6542
|
+
clearTimeout(t);
|
|
6543
|
+
st.last = this.deps.now();
|
|
5505
6544
|
st.error = null;
|
|
6545
|
+
st.authBackoff = 0;
|
|
5506
6546
|
backoff = 0;
|
|
6547
|
+
if (!fresh) await sleep2(2e3);
|
|
5507
6548
|
} else if (res.status === 204) {
|
|
6549
|
+
st.last = this.deps.now();
|
|
5508
6550
|
st.error = null;
|
|
6551
|
+
st.authBackoff = 0;
|
|
5509
6552
|
backoff = 0;
|
|
5510
6553
|
} else {
|
|
5511
6554
|
st.error = `HTTP ${res.status}`;
|
|
5512
6555
|
downSince ||= this.deps.now();
|
|
5513
|
-
|
|
6556
|
+
let delay = pause(backoff++);
|
|
6557
|
+
if (res.status === 503) {
|
|
6558
|
+
const after = res.headers.get("retry-after");
|
|
6559
|
+
const ms = after && /^\d+$/.test(after) ? Number(after) * 1e3 : Date.parse(after ?? "") - this.deps.now();
|
|
6560
|
+
if (Number.isFinite(ms)) delay = Math.max(delay, ms);
|
|
6561
|
+
}
|
|
6562
|
+
await retry(Math.min(delay, 2147483647));
|
|
5514
6563
|
}
|
|
5515
6564
|
} catch (e) {
|
|
5516
6565
|
if (!this.running) break;
|
|
5517
6566
|
if (this.paused) continue;
|
|
5518
6567
|
st.error = String(e?.message || e).slice(0, 120);
|
|
6568
|
+
st.unauthorized = false;
|
|
5519
6569
|
downSince ||= this.deps.now();
|
|
5520
|
-
|
|
6570
|
+
clearTimeout(t);
|
|
6571
|
+
await retry(pause(backoff++));
|
|
5521
6572
|
} finally {
|
|
5522
6573
|
clearTimeout(t);
|
|
5523
6574
|
this.aborts.delete(ctl);
|
|
@@ -5561,6 +6612,7 @@ ${IN_PLACE_ANSWER}`, id: p.msgId });
|
|
|
5561
6612
|
const waiting = [...this.live].filter(([, l]) => l.waiter).map(([k]) => k);
|
|
5562
6613
|
const modes = ["waiter", "turn", "resume", "fork", ...this.deps.sdk ? ["sdk"] : []];
|
|
5563
6614
|
const harnesses = this.harnesses();
|
|
6615
|
+
const accounts = this.accounts();
|
|
5564
6616
|
for (const r of this.cfg.receivers) {
|
|
5565
6617
|
const body = {
|
|
5566
6618
|
name: this.cfg.name,
|
|
@@ -5570,10 +6622,20 @@ ${IN_PLACE_ANSWER}`, id: p.msgId });
|
|
|
5570
6622
|
version: VERSION2,
|
|
5571
6623
|
waiting,
|
|
5572
6624
|
folders: this.cfg.folders,
|
|
5573
|
-
mode: this.cfg.mode
|
|
6625
|
+
mode: this.cfg.mode,
|
|
6626
|
+
...accounts.length ? { accounts } : {},
|
|
6627
|
+
// This daemon takes `files` on a start (an older one would start without them).
|
|
6628
|
+
files: true
|
|
5574
6629
|
};
|
|
5575
|
-
await this.post(r, "/hello", body).catch(() =>
|
|
5576
|
-
|
|
6630
|
+
const res = await this.post(r, "/hello", body).catch(() => null);
|
|
6631
|
+
const st = this.polling.get(r.control + r.machine);
|
|
6632
|
+
if (res?.ok && st?.unauthorized) {
|
|
6633
|
+
st.unauthorized = false;
|
|
6634
|
+
st.authBackoff = 0;
|
|
6635
|
+
st.error = null;
|
|
6636
|
+
this.deps.log(`${r.url}: hello accepted; retrying control poll now`);
|
|
6637
|
+
st.wake?.();
|
|
6638
|
+
}
|
|
5577
6639
|
}
|
|
5578
6640
|
}
|
|
5579
6641
|
async post(r, sub, body) {
|
|
@@ -5674,30 +6736,30 @@ ${IN_PLACE_ANSWER}`, id: p.msgId });
|
|
|
5674
6736
|
this.deps.log(`refused ${m.id.slice(-8)}: ${v.code} \u2014 ${v.why}`);
|
|
5675
6737
|
return this.finish(r, m.id, { outcome: "refused", code: v.code, detail: v.why });
|
|
5676
6738
|
}
|
|
5677
|
-
const
|
|
5678
|
-
if (
|
|
5679
|
-
const ref = parseSessionRef(
|
|
5680
|
-
const kp = kindPath(
|
|
6739
|
+
const cmd = v.cmd;
|
|
6740
|
+
if (cmd.kind === "key.add") return this.addKey(r, m.id, cmd, sent);
|
|
6741
|
+
const ref = parseSessionRef(cmd.session);
|
|
6742
|
+
const kp = kindPath(cmd.kind, ref.harness);
|
|
5681
6743
|
if (typeof kp === "object") return this.finish(r, m.id, { outcome: "unsupported", detail: kp.unsupported });
|
|
5682
6744
|
try {
|
|
5683
|
-
if (kp === "start") return await this.startSession(r, m.id,
|
|
5684
|
-
let place = this.place(
|
|
5685
|
-
const starting = kp === "prompt" ? this.startsPending.get(
|
|
6745
|
+
if (kp === "start") return await this.startSession(r, m.id, cmd, sent);
|
|
6746
|
+
let place = this.place(cmd.session);
|
|
6747
|
+
const starting = kp === "prompt" ? this.startsPending.get(cmd.session) : void 0;
|
|
5686
6748
|
if ("refused" in place && (starting || this.ownRuns.has(ref.id))) {
|
|
5687
6749
|
this.finish(r, m.id, { outcome: "taken" });
|
|
5688
6750
|
await starting;
|
|
5689
6751
|
await this.serial(ref.id, async () => {
|
|
5690
6752
|
});
|
|
5691
|
-
place = this.place(
|
|
6753
|
+
place = this.place(cmd.session);
|
|
5692
6754
|
}
|
|
5693
6755
|
if ("refused" in place)
|
|
5694
6756
|
return this.finish(r, m.id, { outcome: "refused", code: place.refused, detail: place.why });
|
|
5695
|
-
if (kp === "permission") return this.permission(r, m.id,
|
|
5696
|
-
if (kp === "cancel") return await this.cancel(r, m.id,
|
|
5697
|
-
return await this.serial(`deliver:${
|
|
5698
|
-
const now = this.place(
|
|
6757
|
+
if (kp === "permission") return this.permission(r, m.id, cmd, sent);
|
|
6758
|
+
if (kp === "cancel") return await this.cancel(r, m.id, cmd, sent);
|
|
6759
|
+
return await this.serial(`deliver:${cmd.session}`, async () => {
|
|
6760
|
+
const now = this.place(cmd.session);
|
|
5699
6761
|
if ("refused" in now) return this.finish(r, m.id, { outcome: "refused", code: now.refused, detail: now.why });
|
|
5700
|
-
return this.prompt(r, m.id,
|
|
6762
|
+
return this.prompt(r, m.id, cmd, now, sent);
|
|
5701
6763
|
});
|
|
5702
6764
|
} catch (e) {
|
|
5703
6765
|
this.finish(r, m.id, { outcome: "failed", detail: String(e?.message || e) });
|
|
@@ -5712,7 +6774,8 @@ ${IN_PLACE_ANSWER}`, id: p.msgId });
|
|
|
5712
6774
|
if (ref.harness === "claude-code") {
|
|
5713
6775
|
const target = this.state.copies[ref.id] ?? ref.id;
|
|
5714
6776
|
const t = findTranscript2(target, this.deps.claudeDirs()) ?? findTranscript2(ref.id, this.deps.claudeDirs());
|
|
5715
|
-
if (!t)
|
|
6777
|
+
if (!t)
|
|
6778
|
+
return { refused: "no_session", why: `no Claude Code session ${shortId(ref.harness, ref.id)} on this machine` };
|
|
5716
6779
|
const folder = sessionFolder2(t);
|
|
5717
6780
|
const ok2 = folder && this.allowed(folder);
|
|
5718
6781
|
if (!ok2) return { refused: "folder", why: "that session's folder isn't one this machine allows" };
|
|
@@ -5733,13 +6796,14 @@ ${IN_PLACE_ANSWER}`, id: p.msgId });
|
|
|
5733
6796
|
let file = null;
|
|
5734
6797
|
if (ref.harness === "codex") {
|
|
5735
6798
|
file = isUuid2(target) ? codex_control_exports.findRollout(target, this.deps.env ?? process.env) : null;
|
|
5736
|
-
if (!a && !file)
|
|
6799
|
+
if (!a && !file)
|
|
6800
|
+
return { refused: "no_session", why: `no Codex session ${shortId(ref.harness, ref.id)} on this machine` };
|
|
5737
6801
|
cwd ||= file ? codex_control_exports.codexFacts(file).cwd : void 0;
|
|
5738
6802
|
} else cwd ||= this.live.get(session)?.cwd || this.factsCwd(ref.harness, ref.id);
|
|
5739
6803
|
if (!cwd)
|
|
5740
6804
|
return {
|
|
5741
6805
|
refused: "no_session",
|
|
5742
|
-
why: `no folder known for ${ref.harness === "codex" ? "Codex" : "Antigravity"} session ${ref.
|
|
6806
|
+
why: `no folder known for ${ref.harness === "codex" ? "Codex" : "Antigravity"} session ${shortId(ref.harness, ref.id)}`
|
|
5743
6807
|
};
|
|
5744
6808
|
const ok = this.allowed(cwd);
|
|
5745
6809
|
if (!ok) return { refused: "folder", why: "that session's folder isn't one this machine allows" };
|
|
@@ -5748,9 +6812,9 @@ ${IN_PLACE_ANSWER}`, id: p.msgId });
|
|
|
5748
6812
|
/** The folder the worker recorded for a session from its hooks (run.ts, facts). */
|
|
5749
6813
|
factsCwd(harness, id) {
|
|
5750
6814
|
try {
|
|
5751
|
-
const f =
|
|
5752
|
-
const cwd = JSON.parse(
|
|
5753
|
-
return typeof cwd === "string" &&
|
|
6815
|
+
const f = path26.join(path26.dirname(this.dir), "facts", safeId(harness), `${safeId(id)}.json`);
|
|
6816
|
+
const cwd = JSON.parse(readFileSync15(f, "utf8")).cwd;
|
|
6817
|
+
return typeof cwd === "string" && path26.isAbsolute(cwd) ? cwd : void 0;
|
|
5754
6818
|
} catch {
|
|
5755
6819
|
return void 0;
|
|
5756
6820
|
}
|
|
@@ -5769,7 +6833,10 @@ ${IN_PLACE_ANSWER}`, id: p.msgId });
|
|
|
5769
6833
|
writeAliases(aliasFile(this.dir), this.aliases);
|
|
5770
6834
|
} catch (e) {
|
|
5771
6835
|
this.deps.log(
|
|
5772
|
-
`couldn't record ${harness}:${id.slice(0, 8)} \u2192 ${real.slice(0, 8)}: ${String(
|
|
6836
|
+
`couldn't record ${harness}:${id.slice(0, 8)} \u2192 ${real.slice(0, 8)}: ${String(
|
|
6837
|
+
/* v8 ignore next -- writeAliases throws fs Errors */
|
|
6838
|
+
e?.message || e
|
|
6839
|
+
)}`
|
|
5773
6840
|
);
|
|
5774
6841
|
}
|
|
5775
6842
|
}
|
|
@@ -5802,6 +6869,14 @@ ${IN_PLACE_ANSWER}`, id: p.msgId });
|
|
|
5802
6869
|
};
|
|
5803
6870
|
return null;
|
|
5804
6871
|
}
|
|
6872
|
+
/** Who is signed in here, for hello. A bad .claude.json names nobody, never fails a hello. */
|
|
6873
|
+
accounts() {
|
|
6874
|
+
try {
|
|
6875
|
+
return this.deps.accounts ? this.deps.accounts() : claudeAccounts(this.deps.claudeDirs());
|
|
6876
|
+
} catch {
|
|
6877
|
+
return [];
|
|
6878
|
+
}
|
|
6879
|
+
}
|
|
5805
6880
|
/** The harnesses this machine can run a message in, for hello. */
|
|
5806
6881
|
harnesses() {
|
|
5807
6882
|
if (this.deps.harnesses) return this.deps.harnesses();
|
|
@@ -5812,10 +6887,10 @@ ${IN_PLACE_ANSWER}`, id: p.msgId });
|
|
|
5812
6887
|
];
|
|
5813
6888
|
}
|
|
5814
6889
|
allowed(dir) {
|
|
5815
|
-
if (!
|
|
6890
|
+
if (!path26.isAbsolute(dir)) return null;
|
|
5816
6891
|
let r;
|
|
5817
6892
|
try {
|
|
5818
|
-
r =
|
|
6893
|
+
r = realpathSync5.native(dir);
|
|
5819
6894
|
if (!statSync9(r).isDirectory()) return null;
|
|
5820
6895
|
} catch {
|
|
5821
6896
|
return null;
|
|
@@ -5823,18 +6898,18 @@ ${IN_PLACE_ANSWER}`, id: p.msgId });
|
|
|
5823
6898
|
for (const f of this.cfg.folders) {
|
|
5824
6899
|
let rf = f;
|
|
5825
6900
|
try {
|
|
5826
|
-
rf =
|
|
6901
|
+
rf = realpathSync5.native(f);
|
|
5827
6902
|
} catch {
|
|
5828
6903
|
}
|
|
5829
|
-
if (r === rf || r.startsWith(rf.endsWith(
|
|
6904
|
+
if (r === rf || r.startsWith(rf.endsWith(path26.sep) ? rf : rf + path26.sep)) return r;
|
|
5830
6905
|
}
|
|
5831
6906
|
return null;
|
|
5832
6907
|
}
|
|
5833
|
-
permission(r, id,
|
|
5834
|
-
const l = this.live.get(
|
|
5835
|
-
const hook = l?.attentions.get(
|
|
6908
|
+
permission(r, id, cmd, sent) {
|
|
6909
|
+
const l = this.live.get(cmd.session);
|
|
6910
|
+
const hook = l?.attentions.get(cmd.for);
|
|
5836
6911
|
if (!hook) {
|
|
5837
|
-
if (
|
|
6912
|
+
if (cmd.decision === "allow")
|
|
5838
6913
|
return this.finish(r, id, {
|
|
5839
6914
|
outcome: "refused",
|
|
5840
6915
|
code: "no_attention",
|
|
@@ -5843,17 +6918,17 @@ ${IN_PLACE_ANSWER}`, id: p.msgId });
|
|
|
5843
6918
|
this.finish(r, id, { outcome: "expired", detail: "the prompt was already answered or closed" });
|
|
5844
6919
|
return;
|
|
5845
6920
|
}
|
|
5846
|
-
l.attentions.delete(
|
|
6921
|
+
l.attentions.delete(cmd.for);
|
|
5847
6922
|
this.reply(hook, {
|
|
5848
6923
|
op: "decision",
|
|
5849
|
-
behavior:
|
|
5850
|
-
...
|
|
6924
|
+
behavior: cmd.decision,
|
|
6925
|
+
...cmd.note ? { message: cmd.note } : {},
|
|
5851
6926
|
id
|
|
5852
6927
|
});
|
|
5853
6928
|
this.finish(r, id, { outcome: "delivered", mode: "turn" }, sent);
|
|
5854
6929
|
}
|
|
5855
|
-
async cancel(r, id,
|
|
5856
|
-
const ref = parseSessionRef(
|
|
6930
|
+
async cancel(r, id, cmd, sent) {
|
|
6931
|
+
const ref = parseSessionRef(cmd.session);
|
|
5857
6932
|
const held = this.sdkSessions.get(ref.id);
|
|
5858
6933
|
if (!held)
|
|
5859
6934
|
return this.finish(r, id, {
|
|
@@ -5880,7 +6955,10 @@ ${IN_PLACE_ANSWER}`, id: p.msgId });
|
|
|
5880
6955
|
};
|
|
5881
6956
|
}
|
|
5882
6957
|
const transcript = place.transcript;
|
|
5883
|
-
const claude = ref.harness === "claude-code" ? this.deps.claude() :
|
|
6958
|
+
const claude = ref.harness === "claude-code" ? this.deps.claude() : (
|
|
6959
|
+
/* v8 ignore next -- every other harness returned above */
|
|
6960
|
+
null
|
|
6961
|
+
);
|
|
5884
6962
|
const target = this.state.copies[ref.id] ?? ref.id;
|
|
5885
6963
|
const t = transcript;
|
|
5886
6964
|
const live = !this.ownRuns.has(target) && !!t && (liveProcess2(target, t.configDir) || recentlyWritten2(t.file, this.lastEnd.get(target), this.deps.now()));
|
|
@@ -5895,25 +6973,25 @@ ${IN_PLACE_ANSWER}`, id: p.msgId });
|
|
|
5895
6973
|
api: false
|
|
5896
6974
|
};
|
|
5897
6975
|
}
|
|
5898
|
-
async prompt(r, id,
|
|
5899
|
-
const route = routePrompt(this.view(
|
|
6976
|
+
async prompt(r, id, cmd, place, sent) {
|
|
6977
|
+
const route = routePrompt(this.view(cmd.session, place));
|
|
5900
6978
|
if ("unsupported" in route) return this.finish(r, id, { outcome: "unsupported", detail: route.unsupported });
|
|
5901
6979
|
if ("refused" in route) return this.finish(r, id, { outcome: "refused", code: route.refused });
|
|
5902
|
-
const ref = parseSessionRef(
|
|
5903
|
-
const text =
|
|
6980
|
+
const ref = parseSessionRef(cmd.session);
|
|
6981
|
+
const text = cmd.text;
|
|
5904
6982
|
switch (route.mode) {
|
|
5905
6983
|
case "waiter": {
|
|
5906
|
-
const l = this.live.get(
|
|
6984
|
+
const l = this.live.get(cmd.session);
|
|
5907
6985
|
const w = l.waiter;
|
|
5908
6986
|
l.waiter = null;
|
|
5909
|
-
const from = this.answerFrom(
|
|
6987
|
+
const from = this.answerFrom(cmd.session);
|
|
5910
6988
|
this.reply(w, { op: "message", text: frame(text, r.url), id });
|
|
5911
6989
|
this.scheduleHello();
|
|
5912
|
-
return this.followAnswer(r, id, "waiter",
|
|
6990
|
+
return this.followAnswer(r, id, "waiter", cmd.session, from, sent);
|
|
5913
6991
|
}
|
|
5914
6992
|
case "turn": {
|
|
5915
|
-
const l = this.liveOf(
|
|
5916
|
-
const p = { receiver: r, msgId: id, text, session:
|
|
6993
|
+
const l = this.liveOf(cmd.session);
|
|
6994
|
+
const p = { receiver: r, msgId: id, text, session: cmd.session, queuedAt: sent };
|
|
5917
6995
|
l.turnQueue.push(p);
|
|
5918
6996
|
this.finish(r, id, { outcome: "taken" });
|
|
5919
6997
|
const t = setTimeout(() => {
|
|
@@ -5938,7 +7016,8 @@ ${IN_PLACE_ANSWER}`, id: p.msgId });
|
|
|
5938
7016
|
}
|
|
5939
7017
|
case "resume":
|
|
5940
7018
|
case "fork":
|
|
5941
|
-
return this.headless(r, id,
|
|
7019
|
+
return this.headless(r, id, cmd, place, route.mode, sent);
|
|
7020
|
+
/* v8 ignore next -- no harness has an input API wired yet (view() says api: false) */
|
|
5942
7021
|
case "api":
|
|
5943
7022
|
return this.finish(r, id, { outcome: "unsupported", detail: "no input API wired for this harness" });
|
|
5944
7023
|
}
|
|
@@ -5949,10 +7028,10 @@ ${IN_PLACE_ANSWER}`, id: p.msgId });
|
|
|
5949
7028
|
l.midTurn = false;
|
|
5950
7029
|
const place = this.place(p.session);
|
|
5951
7030
|
if ("refused" in place) return this.finish(p.receiver, p.msgId, { outcome: "refused", code: place.refused });
|
|
5952
|
-
const
|
|
7031
|
+
const cmd = { kind: "prompt", session: p.session, text: p.text };
|
|
5953
7032
|
this.inflight.delete(p.msgId);
|
|
5954
7033
|
this.inflight.add(p.msgId);
|
|
5955
|
-
await this.prompt(p.receiver, p.msgId,
|
|
7034
|
+
await this.prompt(p.receiver, p.msgId, cmd, place, p.queuedAt).catch(
|
|
5956
7035
|
(e) => this.finish(p.receiver, p.msgId, { outcome: "failed", detail: String(e?.message || e) })
|
|
5957
7036
|
);
|
|
5958
7037
|
}
|
|
@@ -5994,14 +7073,19 @@ ${IN_PLACE_ANSWER}`, id: p.msgId });
|
|
|
5994
7073
|
const login = drv.login;
|
|
5995
7074
|
if (login && !await this.signedIn(`${drv.cmd}:`, () => login(bin))) {
|
|
5996
7075
|
this.deps.log(`${drv.cmd} isn't signed in as the daemon sees it`);
|
|
5997
|
-
return {
|
|
7076
|
+
return {
|
|
7077
|
+
failed: (
|
|
7078
|
+
/* v8 ignore next -- only Codex has a login check, and it has a hint */
|
|
7079
|
+
drv.loginHint ?? `${drv.label} isn't signed in on this machine`
|
|
7080
|
+
)
|
|
7081
|
+
};
|
|
5998
7082
|
}
|
|
5999
7083
|
return { bin };
|
|
6000
7084
|
}
|
|
6001
7085
|
/** A Codex or Antigravity session's next message: a headless turn resumed under the
|
|
6002
7086
|
* harness's own id, after any run of the daemon's own that holds it. */
|
|
6003
|
-
async resumeDriven(r, id,
|
|
6004
|
-
const ref = parseSessionRef(
|
|
7087
|
+
async resumeDriven(r, id, cmd, place, sent) {
|
|
7088
|
+
const ref = parseSessionRef(cmd.session);
|
|
6005
7089
|
const drv = this.driver(ref.harness);
|
|
6006
7090
|
const ready = await this.drivenProblem(drv);
|
|
6007
7091
|
if ("failed" in ready) return this.finish(r, id, { outcome: "failed", detail: ready.failed });
|
|
@@ -6010,7 +7094,7 @@ ${IN_PLACE_ANSWER}`, id: p.msgId });
|
|
|
6010
7094
|
const target = this.aliasOf(ref.harness, ref.id)?.id ?? place.target ?? ref.id;
|
|
6011
7095
|
const args2 = drv.args({
|
|
6012
7096
|
mode: this.cfg.mode,
|
|
6013
|
-
prompt: frame(
|
|
7097
|
+
prompt: frame(cmd.text, r.url),
|
|
6014
7098
|
cwd: place.folder,
|
|
6015
7099
|
resume: target
|
|
6016
7100
|
});
|
|
@@ -6062,8 +7146,8 @@ ${IN_PLACE_ANSWER}`, id: p.msgId });
|
|
|
6062
7146
|
(${drv.label} didn't find that session on this machine, so this ran in a new one; later messages continue there.)` : "";
|
|
6063
7147
|
this.finish(r, id, { outcome: "delivered", mode: "resume", reply: out2.reply + moved }, sent);
|
|
6064
7148
|
}
|
|
6065
|
-
async headless(r, id,
|
|
6066
|
-
if (parseSessionRef(
|
|
7149
|
+
async headless(r, id, cmd, place, mode, sent) {
|
|
7150
|
+
if (parseSessionRef(cmd.session)?.harness !== "claude-code") return this.resumeDriven(r, id, cmd, place, sent);
|
|
6067
7151
|
const claude = this.deps.claude();
|
|
6068
7152
|
if (!claude)
|
|
6069
7153
|
return this.finish(r, id, { outcome: "failed", detail: "Claude Code isn't installed on this machine" });
|
|
@@ -6071,7 +7155,7 @@ ${IN_PLACE_ANSWER}`, id: p.msgId });
|
|
|
6071
7155
|
if ("error" in mf) return this.finish(r, id, { outcome: "refused", code: "mode", detail: mf.error });
|
|
6072
7156
|
const signedOut = await this.loginProblem(place.transcript?.configDir);
|
|
6073
7157
|
if (signedOut) return this.finish(r, id, { outcome: "failed", detail: signedOut });
|
|
6074
|
-
const ref = parseSessionRef(
|
|
7158
|
+
const ref = parseSessionRef(cmd.session);
|
|
6075
7159
|
this.finish(r, id, { outcome: "taken" });
|
|
6076
7160
|
await this.serial(ref.id, async () => {
|
|
6077
7161
|
const target = this.state.copies[ref.id] ?? ref.id;
|
|
@@ -6083,7 +7167,7 @@ ${IN_PLACE_ANSWER}`, id: p.msgId });
|
|
|
6083
7167
|
...mf.args,
|
|
6084
7168
|
...allow.length ? [`--allowedTools=${allow.join(",")}`] : [],
|
|
6085
7169
|
...resume,
|
|
6086
|
-
frame(
|
|
7170
|
+
frame(cmd.text, r.url)
|
|
6087
7171
|
];
|
|
6088
7172
|
this.deps.log(`${id.slice(-8)}: claude -p ${[...mf.args, ...resume].join(" ")} in ${place.folder}`);
|
|
6089
7173
|
const stream = this.answerStream(r, id);
|
|
@@ -6105,7 +7189,7 @@ ${IN_PLACE_ANSWER}`, id: p.msgId });
|
|
|
6105
7189
|
const ran = isUuid2(out2.session) ? out2.session : target;
|
|
6106
7190
|
if (mode === "fork" && ran !== target) {
|
|
6107
7191
|
this.state.copies[ref.id] = ran;
|
|
6108
|
-
writeState(
|
|
7192
|
+
writeState(path26.join(this.dir, "state.json"), this.state);
|
|
6109
7193
|
this.lastEnd.set(ran, this.deps.now());
|
|
6110
7194
|
}
|
|
6111
7195
|
this.finish(
|
|
@@ -6134,9 +7218,9 @@ ${IN_PLACE_ANSWER}`, id: p.msgId });
|
|
|
6134
7218
|
const text = typeof j.result === "string" && j.result.trim() ? j.result : "(Claude Code finished without a written reply.)";
|
|
6135
7219
|
return { reply: text + tail2, ...typeof j.session_id === "string" ? { session: j.session_id } : {} };
|
|
6136
7220
|
}
|
|
6137
|
-
addKey(r, id,
|
|
6138
|
-
const k =
|
|
6139
|
-
const name = typeof
|
|
7221
|
+
addKey(r, id, cmd, sent) {
|
|
7222
|
+
const k = cmd.key;
|
|
7223
|
+
const name = typeof cmd.name === "string" ? cmd.name.trim().slice(0, 80) : void 0;
|
|
6140
7224
|
const rec = this.cfg.receivers.find((x) => x.control === r.control && x.machine === r.machine) ?? r;
|
|
6141
7225
|
if (rec.keys.some((x) => x.id === k.id))
|
|
6142
7226
|
return this.finish(r, id, { outcome: "delivered", detail: "that device was already trusted" }, sent);
|
|
@@ -6153,7 +7237,10 @@ ${IN_PLACE_ANSWER}`, id: p.msgId });
|
|
|
6153
7237
|
rec.keys = rec.keys.filter((x) => x.id !== k.id);
|
|
6154
7238
|
return this.finish(r, id, {
|
|
6155
7239
|
outcome: "failed",
|
|
6156
|
-
detail: `couldn't save the key: ${String(
|
|
7240
|
+
detail: `couldn't save the key: ${String(
|
|
7241
|
+
/* v8 ignore next -- writeControl throws fs Errors */
|
|
7242
|
+
e?.message || e
|
|
7243
|
+
)}`
|
|
6157
7244
|
});
|
|
6158
7245
|
}
|
|
6159
7246
|
this.deps.log(
|
|
@@ -6184,7 +7271,16 @@ ${IN_PLACE_ANSWER}`, id: p.msgId });
|
|
|
6184
7271
|
}) : null;
|
|
6185
7272
|
stream.close();
|
|
6186
7273
|
this.finish(r, id, { outcome: "delivered", mode, ...answer ? { reply: answer } : {} }, sent);
|
|
6187
|
-
})().catch(
|
|
7274
|
+
})().catch(
|
|
7275
|
+
(e) => this.finish(r, id, {
|
|
7276
|
+
outcome: "delivered",
|
|
7277
|
+
mode,
|
|
7278
|
+
detail: String(
|
|
7279
|
+
/* v8 ignore next -- awaitAnswer throws fs Errors */
|
|
7280
|
+
e?.message || e
|
|
7281
|
+
)
|
|
7282
|
+
})
|
|
7283
|
+
);
|
|
6188
7284
|
}
|
|
6189
7285
|
/** The answer so far, sent as a `progress` ack: whole, with a rising seq, at most
|
|
6190
7286
|
* one in flight. Fire-and-forget — the final ack carries everything. */
|
|
@@ -6238,7 +7334,7 @@ ${IN_PLACE_ANSWER}`, id: p.msgId });
|
|
|
6238
7334
|
this.procWaiters.shift()?.();
|
|
6239
7335
|
}
|
|
6240
7336
|
}
|
|
6241
|
-
async startSession(r, id,
|
|
7337
|
+
async startSession(r, id, cmd, sent) {
|
|
6242
7338
|
const lim = this.limits();
|
|
6243
7339
|
const now = this.deps.now();
|
|
6244
7340
|
this.startTimes = this.startTimes.filter((t) => now - t < 36e5);
|
|
@@ -6249,23 +7345,23 @@ ${IN_PLACE_ANSWER}`, id: p.msgId });
|
|
|
6249
7345
|
}
|
|
6250
7346
|
this.startsRunning++;
|
|
6251
7347
|
this.startTimes.push(now);
|
|
6252
|
-
const sid = parseSessionRef(
|
|
7348
|
+
const sid = parseSessionRef(cmd.session).id;
|
|
6253
7349
|
try {
|
|
6254
|
-
await this.serial(sid, () => this.owning(sid, () => this.startOne(r, id,
|
|
7350
|
+
await this.serial(sid, () => this.owning(sid, () => this.startOne(r, id, cmd, sent)));
|
|
6255
7351
|
} finally {
|
|
6256
7352
|
this.startsRunning--;
|
|
6257
7353
|
}
|
|
6258
7354
|
}
|
|
6259
|
-
async startOne(r, id,
|
|
6260
|
-
const ref = parseSessionRef(
|
|
6261
|
-
const cwd = this.allowed(
|
|
7355
|
+
async startOne(r, id, cmd, sent) {
|
|
7356
|
+
const ref = parseSessionRef(cmd.session);
|
|
7357
|
+
const cwd = this.allowed(cmd.cwd);
|
|
6262
7358
|
if (!cwd)
|
|
6263
7359
|
return this.finish(r, id, {
|
|
6264
7360
|
outcome: "refused",
|
|
6265
7361
|
code: "folder",
|
|
6266
7362
|
detail: "that folder isn't one this machine allows"
|
|
6267
7363
|
});
|
|
6268
|
-
if (ref.harness !== "claude-code") return this.startDriven(r, id,
|
|
7364
|
+
if (ref.harness !== "claude-code") return this.startDriven(r, id, cmd, ref, cwd, sent);
|
|
6269
7365
|
if (findTranscript2(ref.id, this.deps.claudeDirs()) || this.sdkSessions.has(ref.id))
|
|
6270
7366
|
return this.finish(r, id, {
|
|
6271
7367
|
outcome: "refused",
|
|
@@ -6275,12 +7371,14 @@ ${IN_PLACE_ANSWER}`, id: p.msgId });
|
|
|
6275
7371
|
const signedOut = await this.loginProblem(this.deps.env?.CLAUDE_CONFIG_DIR);
|
|
6276
7372
|
if (signedOut) return this.finish(r, id, { outcome: "failed", detail: signedOut });
|
|
6277
7373
|
this.finish(r, id, { outcome: "taken" });
|
|
7374
|
+
const text = await this.attached(r, id, cmd, ref, cwd);
|
|
7375
|
+
if (text === null) return;
|
|
6278
7376
|
if (this.deps.sdk) {
|
|
6279
7377
|
const sdk = this.deps.sdk;
|
|
6280
7378
|
const out3 = await this.slot(async () => {
|
|
6281
7379
|
const s = sdk.start({ session: ref.id, cwd, mode: this.cfg.mode, allowedTools: this.allowedFor(r) });
|
|
6282
7380
|
this.sdkSessions.set(ref.id, s);
|
|
6283
|
-
return s.send(frameStart(
|
|
7381
|
+
return s.send(frameStart(text, r.url));
|
|
6284
7382
|
});
|
|
6285
7383
|
return this.finish(
|
|
6286
7384
|
r,
|
|
@@ -6305,7 +7403,7 @@ ${IN_PLACE_ANSWER}`, id: p.msgId });
|
|
|
6305
7403
|
...this.allowedFor(r).length ? [`--allowedTools=${this.allowedFor(r).join(",")}`] : [],
|
|
6306
7404
|
"--session-id",
|
|
6307
7405
|
ref.id,
|
|
6308
|
-
frameStart(
|
|
7406
|
+
frameStart(text, r.url)
|
|
6309
7407
|
],
|
|
6310
7408
|
{
|
|
6311
7409
|
cwd,
|
|
@@ -6323,10 +7421,10 @@ ${IN_PLACE_ANSWER}`, id: p.msgId });
|
|
|
6323
7421
|
}
|
|
6324
7422
|
/** A new Codex or Antigravity session: the harness picks its id, and the start's id
|
|
6325
7423
|
* becomes its name (aliases.ts) as soon as the run says what it picked. */
|
|
6326
|
-
async startDriven(r, id,
|
|
7424
|
+
async startDriven(r, id, cmd, ref, cwd, sent) {
|
|
6327
7425
|
const drv = this.driver(ref.harness);
|
|
6328
7426
|
const env = this.deps.env ?? process.env;
|
|
6329
|
-
if (this.aliasOf(ref.harness, ref.id) || this.live.get(
|
|
7427
|
+
if (this.aliasOf(ref.harness, ref.id) || this.live.get(cmd.session)?.cwd || ref.harness === "codex" && isUuid2(ref.id) && codex_control_exports.findRollout(ref.id, env))
|
|
6330
7428
|
return this.finish(r, id, {
|
|
6331
7429
|
outcome: "refused",
|
|
6332
7430
|
code: "bad_session",
|
|
@@ -6335,10 +7433,38 @@ ${IN_PLACE_ANSWER}`, id: p.msgId });
|
|
|
6335
7433
|
const ready = await this.drivenProblem(drv);
|
|
6336
7434
|
if ("failed" in ready) return this.finish(r, id, { outcome: "failed", detail: ready.failed });
|
|
6337
7435
|
this.finish(r, id, { outcome: "taken" });
|
|
6338
|
-
const
|
|
6339
|
-
|
|
7436
|
+
const text = await this.attached(r, id, cmd, ref, cwd);
|
|
7437
|
+
if (text === null) return;
|
|
7438
|
+
const args2 = drv.args({ mode: this.cfg.mode, prompt: frameStart(text, r.url), cwd });
|
|
7439
|
+
this.deps.log(
|
|
7440
|
+
`${id.slice(-8)}: ${drv.cmd} new session for ${shortId(ref.harness, ref.id)} (${this.cfg.mode}) in ${cwd}`
|
|
7441
|
+
);
|
|
6340
7442
|
await this.drive(r, id, ref, drv, ready.bin, args2, cwd, sent, null);
|
|
6341
7443
|
}
|
|
7444
|
+
/** A start's text, with its files (CONTROL.md §6) fetched, checked and written under
|
|
7445
|
+
* `cwd` first — every check in §5 passed and the nonce is recorded by now, so the
|
|
7446
|
+
* `taken` ack is out and the download may take its time. Null when a file failed:
|
|
7447
|
+
* the command is acked `failed` with code `file`, nothing is kept, no session starts. */
|
|
7448
|
+
async attached(r, id, cmd, ref, cwd) {
|
|
7449
|
+
const text = cmd.text;
|
|
7450
|
+
if (!cmd.files) return text;
|
|
7451
|
+
try {
|
|
7452
|
+
const got = await fetchFiles(cmd.files, {
|
|
7453
|
+
control: r.control,
|
|
7454
|
+
token: r.token,
|
|
7455
|
+
fetch: this.deps.fetch,
|
|
7456
|
+
timeoutMs: this.deps.fileTimeoutMs
|
|
7457
|
+
});
|
|
7458
|
+
const rel = writeFiles(cwd, ref.id, got);
|
|
7459
|
+
this.deps.log(`${id.slice(-8)}: ${got.length} file(s) saved in ${cwd}/.sessionpipe/files/${ref.id}`);
|
|
7460
|
+
return withFiles(text, got, rel);
|
|
7461
|
+
} catch (e) {
|
|
7462
|
+
const detail = e.message;
|
|
7463
|
+
this.deps.log(`${id.slice(-8)}: file \u2014 ${detail}`);
|
|
7464
|
+
this.finish(r, id, { outcome: "failed", code: "file", detail });
|
|
7465
|
+
return null;
|
|
7466
|
+
}
|
|
7467
|
+
}
|
|
6342
7468
|
closeIdleSdk() {
|
|
6343
7469
|
const idle = this.deps.sdkIdleMs ?? 10 * 6e4;
|
|
6344
7470
|
for (const [id, s] of this.sdkSessions)
|
|
@@ -6362,12 +7488,13 @@ function socketId(p) {
|
|
|
6362
7488
|
}
|
|
6363
7489
|
|
|
6364
7490
|
// src/control/pair.ts
|
|
6365
|
-
import { execFileSync as
|
|
7491
|
+
import { execFileSync as execFileSync7 } from "node:child_process";
|
|
6366
7492
|
import { randomBytes as randomBytes3 } from "node:crypto";
|
|
6367
|
-
import { existsSync as
|
|
6368
|
-
import
|
|
6369
|
-
import
|
|
6370
|
-
var VERSION3 = true ? "0.
|
|
7493
|
+
import { existsSync as existsSync15, mkdirSync as mkdirSync13, unlinkSync as unlinkSync6, writeFileSync as writeFileSync11 } from "node:fs";
|
|
7494
|
+
import os9 from "node:os";
|
|
7495
|
+
import path27 from "node:path";
|
|
7496
|
+
var VERSION3 = true ? "0.9.0" : "0.0.0";
|
|
7497
|
+
var RENEW_FOR_MS = 60 * 6e4;
|
|
6371
7498
|
async function json(r) {
|
|
6372
7499
|
try {
|
|
6373
7500
|
return await r.json();
|
|
@@ -6402,55 +7529,89 @@ async function pair(o) {
|
|
|
6402
7529
|
const cfg = readControl(o.env) ?? { name: o.name, folders: [], mode: o.mode, receivers: [] };
|
|
6403
7530
|
cfg.name = o.name || cfg.name;
|
|
6404
7531
|
cfg.mode = o.mode;
|
|
6405
|
-
cfg.folders = [.../* @__PURE__ */ new Set([...cfg.folders, ...o.folders.map((d) =>
|
|
7532
|
+
cfg.folders = [.../* @__PURE__ */ new Set([...cfg.folders, ...o.folders.map((d) => path27.resolve(d))])];
|
|
6406
7533
|
const existing = cfg.receivers.find((r) => r.control === control);
|
|
7534
|
+
let accounts = o.accounts;
|
|
7535
|
+
if (!accounts)
|
|
7536
|
+
try {
|
|
7537
|
+
const e = o.env ?? process.env;
|
|
7538
|
+
accounts = claudeAccounts(claudeDirs(e), e.HOME);
|
|
7539
|
+
} catch {
|
|
7540
|
+
accounts = [];
|
|
7541
|
+
}
|
|
6407
7542
|
const auth = { authorization: `Bearer ${o.token ?? pollKey}`, "content-type": "application/json" };
|
|
6408
|
-
const
|
|
6409
|
-
|
|
6410
|
-
|
|
6411
|
-
|
|
6412
|
-
|
|
6413
|
-
|
|
6414
|
-
|
|
6415
|
-
|
|
6416
|
-
|
|
6417
|
-
|
|
6418
|
-
|
|
6419
|
-
|
|
6420
|
-
|
|
6421
|
-
|
|
6422
|
-
|
|
6423
|
-
|
|
6424
|
-
|
|
6425
|
-
|
|
6426
|
-
);
|
|
6427
|
-
|
|
6428
|
-
|
|
6429
|
-
|
|
6430
|
-
|
|
6431
|
-
|
|
7543
|
+
const hello = {
|
|
7544
|
+
...pollKey ? { poll_key: pollKey } : {},
|
|
7545
|
+
name: cfg.name,
|
|
7546
|
+
harnesses: o.harnesses ?? drivableHarnesses(o.env ?? process.env),
|
|
7547
|
+
modes: ["waiter", "turn", "resume", "fork"],
|
|
7548
|
+
version: VERSION3,
|
|
7549
|
+
folders: cfg.folders,
|
|
7550
|
+
mode: cfg.mode,
|
|
7551
|
+
...accounts.length ? { accounts } : {},
|
|
7552
|
+
// This daemon takes `files` on a start (an older one would start without them).
|
|
7553
|
+
files: true
|
|
7554
|
+
};
|
|
7555
|
+
const start = async (machine) => {
|
|
7556
|
+
const started = await f(`${control}/pair`, {
|
|
7557
|
+
method: "POST",
|
|
7558
|
+
headers: o.token ? auth : { "content-type": "application/json" },
|
|
7559
|
+
body: JSON.stringify({ ...hello, ...machine ? { machine } : {} })
|
|
7560
|
+
});
|
|
7561
|
+
const s = await json(started);
|
|
7562
|
+
if (started.status !== 201 && started.status !== 200)
|
|
7563
|
+
throw new Error(
|
|
7564
|
+
`the receiver refused to start pairing (HTTP ${started.status}${s.reason ? `: ${String(s.reason)}` : ""})`
|
|
7565
|
+
);
|
|
7566
|
+
return {
|
|
7567
|
+
machine: String(s.machine),
|
|
7568
|
+
code: String(s.code),
|
|
7569
|
+
url: String(s.url),
|
|
7570
|
+
check: String(s.check),
|
|
7571
|
+
until: Date.parse(String(s.expires_at)) || Date.now() + 10 * 6e4
|
|
7572
|
+
};
|
|
7573
|
+
};
|
|
6432
7574
|
const host = new URL(o.url).host;
|
|
6433
|
-
|
|
6434
|
-
|
|
6435
|
-
o.out(
|
|
6436
|
-
o.out(
|
|
6437
|
-
|
|
6438
|
-
}
|
|
6439
|
-
|
|
6440
|
-
|
|
6441
|
-
|
|
6442
|
-
|
|
6443
|
-
|
|
6444
|
-
|
|
7575
|
+
const announce = (p) => {
|
|
7576
|
+
const spaced = /^\d{6}$/.test(p.check) ? `${p.check.slice(0, 3)} ${p.check.slice(3)}` : p.check;
|
|
7577
|
+
o.out("");
|
|
7578
|
+
o.out(` On any phone or computer where you're signed in to ${host} (it doesn't have to be this one),`);
|
|
7579
|
+
if (pollKey) {
|
|
7580
|
+
o.out(` open ${p.url} and enter: ${spaced}`);
|
|
7581
|
+
o.out(" then approve with that device's passkey (Touch ID, Face ID, a security key).");
|
|
7582
|
+
o.out(" Pairing several machines? Run this on each one: every machine shows its own digits.");
|
|
7583
|
+
} else {
|
|
7584
|
+
o.out(" open this link and approve with that device's passkey (Touch ID, Face ID, a security key):");
|
|
7585
|
+
o.out(` ${p.url}`);
|
|
7586
|
+
o.out(` The page shows these digits too: ${p.check}`);
|
|
7587
|
+
}
|
|
7588
|
+
const mins = Math.max(1, Math.round((p.until - Date.now()) / 6e4));
|
|
7589
|
+
o.out(` This code is good for ${mins} minute${mins === 1 ? "" : "s"}; if it runs out, a fresh one appears here.`);
|
|
7590
|
+
o.out("");
|
|
7591
|
+
};
|
|
7592
|
+
const renewUntil = Date.now() + (o.renewFor ?? RENEW_FOR_MS);
|
|
7593
|
+
let cur = await start(existing?.machine);
|
|
7594
|
+
announce(cur);
|
|
6445
7595
|
for (; ; ) {
|
|
6446
|
-
if (Date.now() > until) throw new Error("the pairing link expired; run `sessionpipe control pair` again");
|
|
6447
7596
|
await sleep3(2e3);
|
|
6448
|
-
|
|
6449
|
-
|
|
6450
|
-
if (
|
|
6451
|
-
|
|
6452
|
-
|
|
6453
|
-
|
|
7597
|
+
let p = {};
|
|
7598
|
+
let expired = Date.now() > cur.until;
|
|
7599
|
+
if (!expired) {
|
|
7600
|
+
const r = await f(`${control}/pair?code=${encodeURIComponent(cur.code)}`, { headers: auth }).catch(() => null);
|
|
7601
|
+
if (!r) continue;
|
|
7602
|
+
if (r.status === 404) throw new Error("the receiver lost the pairing; run `sessionpipe control pair` again");
|
|
7603
|
+
p = await json(r);
|
|
7604
|
+
if (p.status === "expired") expired = true;
|
|
7605
|
+
else if (p.status !== "paired") continue;
|
|
7606
|
+
}
|
|
7607
|
+
if (expired) {
|
|
7608
|
+
if (Date.now() >= renewUntil) throw new Error("the pairing link expired; run `sessionpipe control pair` again");
|
|
7609
|
+
cur = await start(o.token ? cur.machine : existing?.machine);
|
|
7610
|
+
o.out(" Nobody approved that code before it expired. Here is a fresh one; the old digits no longer work.");
|
|
7611
|
+
announce(cur);
|
|
7612
|
+
continue;
|
|
7613
|
+
}
|
|
7614
|
+
const { machine, code } = cur;
|
|
6454
7615
|
const key = p.key;
|
|
6455
7616
|
const proof = p.proof;
|
|
6456
7617
|
const v = await verifyEnrollment({ machine, code, key, proof, rpId });
|
|
@@ -6470,6 +7631,9 @@ async function pair(o) {
|
|
|
6470
7631
|
if (!existing) cfg.receivers.push(rec);
|
|
6471
7632
|
writeControl(cfg, o.env);
|
|
6472
7633
|
o.out(` \u2713 Paired with ${rec.url} as ${machine} \xB7 ${rec.keys.length} key(s) trusted on this machine`);
|
|
7634
|
+
const named = accounts.flatMap((a) => a.email ? [a.email] : []);
|
|
7635
|
+
if (named.length)
|
|
7636
|
+
o.out(` Its session board names your Claude Code account${named.length > 1 ? "s" : ""}: ${named.join(", ")}`);
|
|
6473
7637
|
if (typeof p.account === "string")
|
|
6474
7638
|
o.out(` Approved by ${p.account}. If that isn't you, run \`sessionpipe control off\` now.`);
|
|
6475
7639
|
if (typeof p.sink_token === "string" && o.addSink) o.addSink(p.sink_token);
|
|
@@ -6505,7 +7669,7 @@ function removeKey(id, env) {
|
|
|
6505
7669
|
var LABEL4 = "org.sessionpipe.control";
|
|
6506
7670
|
var UNIT2 = "sessionpipe-control.service";
|
|
6507
7671
|
function serviceFiles(o) {
|
|
6508
|
-
const home = o.home ??
|
|
7672
|
+
const home = o.home ?? os9.homedir();
|
|
6509
7673
|
const xml = (s) => s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
6510
7674
|
const plist = `<?xml version="1.0" encoding="UTF-8"?>
|
|
6511
7675
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
@@ -6518,8 +7682,8 @@ function serviceFiles(o) {
|
|
|
6518
7682
|
<key>RunAtLoad</key><true/>
|
|
6519
7683
|
<key>KeepAlive</key><dict><key>SuccessfulExit</key><false/></dict>
|
|
6520
7684
|
<key>ProcessType</key><string>Background</string>
|
|
6521
|
-
<key>StandardOutPath</key><string>${xml(
|
|
6522
|
-
<key>StandardErrorPath</key><string>${xml(
|
|
7685
|
+
<key>StandardOutPath</key><string>${xml(path27.join(o.logDir, "daemon.log"))}</string>
|
|
7686
|
+
<key>StandardErrorPath</key><string>${xml(path27.join(o.logDir, "daemon.log"))}</string>
|
|
6523
7687
|
</dict>
|
|
6524
7688
|
</plist>
|
|
6525
7689
|
`;
|
|
@@ -6539,11 +7703,11 @@ RestartSec=5
|
|
|
6539
7703
|
WantedBy=default.target
|
|
6540
7704
|
`;
|
|
6541
7705
|
return {
|
|
6542
|
-
plist: { file:
|
|
6543
|
-
unit: { file:
|
|
7706
|
+
plist: { file: path27.join(home, "Library", "LaunchAgents", `${LABEL4}.plist`), body: plist },
|
|
7707
|
+
unit: { file: path27.join(home, ".config", "systemd", "user", UNIT2), body: unit }
|
|
6544
7708
|
};
|
|
6545
7709
|
}
|
|
6546
|
-
var execQuiet = (
|
|
7710
|
+
var execQuiet = (cmd, args2) => execFileSync7(cmd, args2, { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], timeout: 15e3 });
|
|
6547
7711
|
function ensureLinger(user, exec = execQuiet) {
|
|
6548
7712
|
const lingering = () => {
|
|
6549
7713
|
try {
|
|
@@ -6565,13 +7729,13 @@ function ensureLinger(user, exec = execQuiet) {
|
|
|
6565
7729
|
}
|
|
6566
7730
|
function installService(o) {
|
|
6567
7731
|
const logDir = controlState(o.env);
|
|
6568
|
-
|
|
7732
|
+
mkdirSync13(logDir, { recursive: true, mode: 448 });
|
|
6569
7733
|
const f = serviceFiles({ node: o.node, cli: o.cli, logDir });
|
|
6570
7734
|
const run = o.exec ?? execQuiet;
|
|
6571
7735
|
const platform = o.platform ?? process.platform;
|
|
6572
7736
|
if (platform === "darwin") {
|
|
6573
|
-
|
|
6574
|
-
|
|
7737
|
+
mkdirSync13(path27.dirname(f.plist.file), { recursive: true });
|
|
7738
|
+
writeFileSync11(f.plist.file, f.plist.body);
|
|
6575
7739
|
const uid = String(process.getuid?.() ?? "");
|
|
6576
7740
|
for (const d of ["gui", "user"])
|
|
6577
7741
|
try {
|
|
@@ -6580,40 +7744,40 @@ function installService(o) {
|
|
|
6580
7744
|
}
|
|
6581
7745
|
try {
|
|
6582
7746
|
run("launchctl", ["bootstrap", `gui/${uid}`, f.plist.file]);
|
|
6583
|
-
return `launchd agent ${LABEL4} (log: ${
|
|
7747
|
+
return `launchd agent ${LABEL4} (log: ${path27.join(logDir, "daemon.log")})`;
|
|
6584
7748
|
} catch {
|
|
6585
7749
|
}
|
|
6586
7750
|
run("launchctl", ["bootstrap", `user/${uid}`, f.plist.file]);
|
|
6587
|
-
return `launchd agent ${LABEL4} in the background domain, since nobody is logged in at this Mac's screen; after a restart it starts again when someone logs in there (log: ${
|
|
7751
|
+
return `launchd agent ${LABEL4} in the background domain, since nobody is logged in at this Mac's screen; after a restart it starts again when someone logs in there (log: ${path27.join(logDir, "daemon.log")})`;
|
|
6588
7752
|
}
|
|
6589
7753
|
if (platform === "linux") {
|
|
6590
|
-
|
|
6591
|
-
|
|
7754
|
+
mkdirSync13(path27.dirname(f.unit.file), { recursive: true });
|
|
7755
|
+
writeFileSync11(f.unit.file, f.unit.body);
|
|
6592
7756
|
try {
|
|
6593
7757
|
run("systemctl", ["--user", "daemon-reload"]);
|
|
6594
7758
|
run("systemctl", ["--user", "enable", "--now", UNIT2]);
|
|
6595
7759
|
} catch {
|
|
6596
7760
|
return `wrote ${f.unit.file}, but systemd --user isn't reachable here; run \`sessionpipe control run\` yourself (tmux, screen)`;
|
|
6597
7761
|
}
|
|
6598
|
-
const linger = ensureLinger(
|
|
7762
|
+
const linger = ensureLinger(os9.userInfo().username, run);
|
|
6599
7763
|
return `systemd user unit ${UNIT2}; ${linger.note} (journalctl --user -u ${UNIT2})`;
|
|
6600
7764
|
}
|
|
6601
7765
|
return "no service manager for this platform yet; run `sessionpipe control run` yourself";
|
|
6602
7766
|
}
|
|
6603
7767
|
function uninstallService(env) {
|
|
6604
7768
|
const f = serviceFiles({ node: "", cli: "", logDir: controlState(env) });
|
|
6605
|
-
const run = (
|
|
7769
|
+
const run = (cmd, args2) => {
|
|
6606
7770
|
try {
|
|
6607
|
-
|
|
7771
|
+
execFileSync7(cmd, args2, { stdio: "ignore", timeout: 15e3 });
|
|
6608
7772
|
} catch {
|
|
6609
7773
|
}
|
|
6610
7774
|
};
|
|
6611
|
-
if (process.platform === "darwin" &&
|
|
7775
|
+
if (process.platform === "darwin" && existsSync15(f.plist.file)) {
|
|
6612
7776
|
run("launchctl", ["bootout", `gui/${process.getuid?.() ?? ""}/${LABEL4}`]);
|
|
6613
7777
|
run("launchctl", ["bootout", `user/${process.getuid?.() ?? ""}/${LABEL4}`]);
|
|
6614
7778
|
unlinkSync6(f.plist.file);
|
|
6615
7779
|
}
|
|
6616
|
-
if (process.platform === "linux" &&
|
|
7780
|
+
if (process.platform === "linux" && existsSync15(f.unit.file)) {
|
|
6617
7781
|
run("systemctl", ["--user", "disable", "--now", UNIT2]);
|
|
6618
7782
|
unlinkSync6(f.unit.file);
|
|
6619
7783
|
run("systemctl", ["--user", "daemon-reload"]);
|
|
@@ -6733,9 +7897,10 @@ ${RE_ARM}`;
|
|
|
6733
7897
|
}
|
|
6734
7898
|
|
|
6735
7899
|
// src/control/cli.ts
|
|
6736
|
-
var { claudeLogin:
|
|
7900
|
+
var { claudeLogin: claudeLogin3, detectCaps: detectCaps2, findClaude: findClaude2, runClaude: runClaude2 } = claude_code_control_exports;
|
|
6737
7901
|
var CONTROL_HELP = ` sessionpipe control pair <receiver> [--folder DIR]\u2026 [--mode safe|auto] [--tier 0-3] [--token T] [--name N] [--no-service]
|
|
6738
7902
|
sessionpipe control mode auto|safe (how sessions run when you message them)
|
|
7903
|
+
sessionpipe control folders [add|remove DIR\u2026] (where sessions you message may run)
|
|
6739
7904
|
sessionpipe control status | keys [remove <id>] | off [<receiver>] | run
|
|
6740
7905
|
sessionpipe wait [--session <harness>:<id>] (a session runs this in the background)`;
|
|
6741
7906
|
var MODE_TEXT = ` auto Claude Code's auto mode: the session reads, edits and runs commands, and Claude Code's
|
|
@@ -6765,6 +7930,42 @@ async function askMode(out2) {
|
|
|
6765
7930
|
rl.close();
|
|
6766
7931
|
}
|
|
6767
7932
|
}
|
|
7933
|
+
function changeFolders(folders, op, dirs, home = os10.homedir()) {
|
|
7934
|
+
const real = (d) => {
|
|
7935
|
+
try {
|
|
7936
|
+
return realpathSync6.native(d);
|
|
7937
|
+
} catch {
|
|
7938
|
+
return path28.resolve(d);
|
|
7939
|
+
}
|
|
7940
|
+
};
|
|
7941
|
+
const next = [...folders];
|
|
7942
|
+
const said = [];
|
|
7943
|
+
for (const d of dirs) {
|
|
7944
|
+
const abs = path28.resolve(d);
|
|
7945
|
+
const at = next.findIndex((f) => real(f) === real(abs));
|
|
7946
|
+
if (op === "remove") {
|
|
7947
|
+
if (at < 0) said.push(` \xB7 ${abs} wasn't allowed`);
|
|
7948
|
+
else said.push(` \u2713 No longer allowed: ${next.splice(at, 1)[0]}`);
|
|
7949
|
+
continue;
|
|
7950
|
+
}
|
|
7951
|
+
let dir = false;
|
|
7952
|
+
try {
|
|
7953
|
+
dir = statSync10(abs).isDirectory();
|
|
7954
|
+
} catch {
|
|
7955
|
+
}
|
|
7956
|
+
if (!dir) said.push(` \u2717 ${abs} isn't a folder on this machine`);
|
|
7957
|
+
else if (isBroad(abs, home))
|
|
7958
|
+
said.push(
|
|
7959
|
+
` \u2717 Not allowing all of ${abs}: name a project folder inside it, so a message can't run anywhere you own`
|
|
7960
|
+
);
|
|
7961
|
+
else if (at >= 0) said.push(` \xB7 ${abs} is already allowed`);
|
|
7962
|
+
else {
|
|
7963
|
+
next.push(abs);
|
|
7964
|
+
said.push(` \u2713 Allowed ${abs}`);
|
|
7965
|
+
}
|
|
7966
|
+
}
|
|
7967
|
+
return { folders: next, said };
|
|
7968
|
+
}
|
|
6768
7969
|
async function controlMain(argv, out2, distDir2) {
|
|
6769
7970
|
const flag2 = (n) => {
|
|
6770
7971
|
const i = argv.indexOf(n);
|
|
@@ -6789,7 +7990,7 @@ ${CONTROL_HELP}`);
|
|
|
6789
7990
|
const folders = flags("--folder");
|
|
6790
7991
|
const existing = readControl();
|
|
6791
7992
|
if (!folders.length && !existing?.folders.length) {
|
|
6792
|
-
if (!isBroad(process.cwd(),
|
|
7993
|
+
if (!isBroad(process.cwd(), os10.homedir())) folders.push(process.cwd());
|
|
6793
7994
|
else
|
|
6794
7995
|
out2(
|
|
6795
7996
|
` ! Not allowing all of ${process.cwd()}: sessions you message may run only in folders you name. Pass --folder <project>, or run this from inside one.`
|
|
@@ -6811,7 +8012,7 @@ ${CONTROL_HELP}`);
|
|
|
6811
8012
|
},
|
|
6812
8013
|
folders,
|
|
6813
8014
|
mode,
|
|
6814
|
-
name: flag2("--name") ?? existing?.name ?? cfg
|
|
8015
|
+
name: flag2("--name") ?? existing?.name ?? machineName(cfg, os10.hostname()),
|
|
6815
8016
|
out: out2
|
|
6816
8017
|
});
|
|
6817
8018
|
const c = readControl();
|
|
@@ -6819,9 +8020,9 @@ ${CONTROL_HELP}`);
|
|
|
6819
8020
|
out2(
|
|
6820
8021
|
` Mode: ${c.mode} (${c.mode === "safe" ? "nothing that needs approval runs unattended" : "Claude Code's auto mode"})`
|
|
6821
8022
|
);
|
|
6822
|
-
let cli =
|
|
8023
|
+
let cli = path28.join(distDir2, "cli.js");
|
|
6823
8024
|
try {
|
|
6824
|
-
cli =
|
|
8025
|
+
cli = realpathSync6(cli);
|
|
6825
8026
|
} catch {
|
|
6826
8027
|
}
|
|
6827
8028
|
if (argv.includes("--no-service"))
|
|
@@ -6845,6 +8046,28 @@ ${MODE_TEXT}`);
|
|
|
6845
8046
|
` \u2713 Sessions you message now run in ${want} mode${want === "auto" ? " (Claude Code's auto mode decides what's safe)" : " (only what each folder's settings already allow)"}.`
|
|
6846
8047
|
);
|
|
6847
8048
|
}
|
|
8049
|
+
case "folders": {
|
|
8050
|
+
const op = argv[2];
|
|
8051
|
+
const usage = " usage: sessionpipe control folders [add|remove <folder>\u2026]";
|
|
8052
|
+
if (op !== void 0 && op !== "add" && op !== "remove") return out2(usage);
|
|
8053
|
+
const c = readControl();
|
|
8054
|
+
if (!c) return out2(" control isn't set up on this machine: run `sessionpipe control pair <receiver>` first");
|
|
8055
|
+
if (op) {
|
|
8056
|
+
const dirs = argv.slice(3);
|
|
8057
|
+
if (!dirs.length) return out2(usage);
|
|
8058
|
+
const r = changeFolders(c.folders, op, dirs);
|
|
8059
|
+
for (const l of r.said) out2(l);
|
|
8060
|
+
if (r.folders.join("\n") !== c.folders.join("\n")) {
|
|
8061
|
+
c.folders = r.folders;
|
|
8062
|
+
writeControl(c);
|
|
8063
|
+
out2(" The control daemon uses the new list within seconds.");
|
|
8064
|
+
}
|
|
8065
|
+
}
|
|
8066
|
+
return out2(
|
|
8067
|
+
c.folders.length ? ` Sessions you message may run in:
|
|
8068
|
+
${c.folders.map((f) => ` ${f}`).join("\n")}` : " No folder is allowed: a message to a session here is refused until you add one."
|
|
8069
|
+
);
|
|
8070
|
+
}
|
|
6848
8071
|
case "keys": {
|
|
6849
8072
|
const c = readControl();
|
|
6850
8073
|
if (argv[2] === "remove" && argv[3]) {
|
|
@@ -6890,7 +8113,7 @@ async function runDaemon(out2, distDir2) {
|
|
|
6890
8113
|
const claudeNow = () => {
|
|
6891
8114
|
const bin = findClaude2();
|
|
6892
8115
|
if (!bin) return null;
|
|
6893
|
-
const sig = `${bin}:${
|
|
8116
|
+
const sig = `${bin}:${existsSync16(bin) ? realpathSync6(bin) : ""}`;
|
|
6894
8117
|
if (claude?.sig !== sig) claude = { bin, caps: detectCaps2(bin), sig };
|
|
6895
8118
|
return claude;
|
|
6896
8119
|
};
|
|
@@ -6910,7 +8133,7 @@ async function runDaemon(out2, distDir2) {
|
|
|
6910
8133
|
sdk,
|
|
6911
8134
|
login: async (configDir) => {
|
|
6912
8135
|
const c = claudeNow();
|
|
6913
|
-
return c ?
|
|
8136
|
+
return c ? claudeLogin3(c.bin, configDir) : { loggedIn: null };
|
|
6914
8137
|
},
|
|
6915
8138
|
token: (r) => r.token_in ? secretGet(r.token_in, controlAccount(r.machine)) : null,
|
|
6916
8139
|
onRestart: () => updater?.restartWhenIdle() ?? false,
|
|
@@ -6925,12 +8148,12 @@ async function runDaemon(out2, distDir2) {
|
|
|
6925
8148
|
log2(`${e.message}; this one exits`);
|
|
6926
8149
|
process.exit(0);
|
|
6927
8150
|
}
|
|
6928
|
-
const
|
|
6929
|
-
const sinks = readConfig().sinks.filter((s) =>
|
|
6930
|
-
if (sinks.length) void
|
|
8151
|
+
const drainTimer = setInterval(() => {
|
|
8152
|
+
const sinks = readConfig().sinks.filter((s) => !s.paused);
|
|
8153
|
+
if (sinks.length) void drain(stateDir(), sinks, new Outbox(stateDir())).catch(() => {
|
|
6931
8154
|
});
|
|
6932
8155
|
}, 6e4);
|
|
6933
|
-
|
|
8156
|
+
drainTimer.unref();
|
|
6934
8157
|
watchFile(controlFile(), { interval: 5e3 }, () => {
|
|
6935
8158
|
const next = readControl();
|
|
6936
8159
|
if (next) d.reload(next);
|
|
@@ -6948,7 +8171,7 @@ async function runDaemon(out2, distDir2) {
|
|
|
6948
8171
|
const restart = async (_from, to) => {
|
|
6949
8172
|
stopping = true;
|
|
6950
8173
|
updater?.stop();
|
|
6951
|
-
clearInterval(
|
|
8174
|
+
clearInterval(drainTimer);
|
|
6952
8175
|
unwatchFile(controlFile());
|
|
6953
8176
|
for (const sig of signals) process.off(sig, bye);
|
|
6954
8177
|
await d.stop();
|
|
@@ -6957,9 +8180,9 @@ async function runDaemon(out2, distDir2) {
|
|
|
6957
8180
|
log2(`exiting ${UPDATE_EXIT} so ${via === "supervisor" ? "the supervising sessionpipe" : via} starts ${to}`);
|
|
6958
8181
|
process.exit(UPDATE_EXIT);
|
|
6959
8182
|
}
|
|
6960
|
-
let cli =
|
|
8183
|
+
let cli = path28.join(distDir2, "cli.js");
|
|
6961
8184
|
try {
|
|
6962
|
-
cli =
|
|
8185
|
+
cli = realpathSync6(cli);
|
|
6963
8186
|
} catch {
|
|
6964
8187
|
}
|
|
6965
8188
|
log2(`no service manager restarts this daemon: pid ${process.pid} now runs ${to} as its child`);
|
|
@@ -7001,126 +8224,96 @@ function adoptSinkToken(url, token, tier, file) {
|
|
|
7001
8224
|
return same ? "filled" : "added";
|
|
7002
8225
|
}
|
|
7003
8226
|
|
|
7004
|
-
// src/
|
|
7005
|
-
|
|
7006
|
-
|
|
7007
|
-
|
|
7008
|
-
|
|
7009
|
-
|
|
7010
|
-
|
|
7011
|
-
|
|
7012
|
-
|
|
7013
|
-
|
|
7014
|
-
|
|
7015
|
-
|
|
7016
|
-
|
|
7017
|
-
|
|
7018
|
-
|
|
7019
|
-
|
|
7020
|
-
|
|
7021
|
-
|
|
7022
|
-
|
|
7023
|
-
|
|
7024
|
-
|
|
7025
|
-
|
|
7026
|
-
|
|
7027
|
-
|
|
7028
|
-
|
|
7029
|
-
|
|
7030
|
-
|
|
7031
|
-
|
|
7032
|
-
|
|
7033
|
-
|
|
7034
|
-
|
|
7035
|
-
|
|
7036
|
-
|
|
7037
|
-
|
|
7038
|
-
|
|
7039
|
-
|
|
7040
|
-
return true;
|
|
7041
|
-
}
|
|
7042
|
-
function installLauncher(o) {
|
|
7043
|
-
const dir = dataDir(o.env);
|
|
7044
|
-
mkdirSync12(dir, { recursive: true, mode: 493 });
|
|
7045
|
-
let changed = false;
|
|
7046
|
-
for (const f of COPIED)
|
|
7047
|
-
changed = writeIfChanged(path26.join(dir, f), readFileSync14(path26.join(o.distDir, f)), 420) || changed;
|
|
7048
|
-
const launcher = path26.join(dir, LAUNCHER);
|
|
7049
|
-
changed = writeIfChanged(launcher, launcherScript(o.node, path26.join(dir, "hook.js"), o.version), 493) || changed;
|
|
7050
|
-
return { launcher, changed };
|
|
7051
|
-
}
|
|
7052
|
-
function removeLauncher(env = process.env) {
|
|
7053
|
-
for (const f of [LAUNCHER, ...COPIED]) rmSync(path26.join(dataDir(env), f), { force: true });
|
|
8227
|
+
// src/help.ts
|
|
8228
|
+
function usageTable(harnesses) {
|
|
8229
|
+
return {
|
|
8230
|
+
connect: "connect <receiver> [--machine NAME] [--rename-machine] [--allow-downgrade] [--tier 0-3] [--mode safe|auto] [--folder DIR]\u2026 [--no-service]",
|
|
8231
|
+
install: `install [${harnesses.map((h) => `--${h}`).join(" ")}] [--machine NAME] [--rename-machine] [--allow-downgrade] [--backfill DAYS] [--sink URL --tier N]`,
|
|
8232
|
+
sink: "sink add|list|remove|test (use --help after a subcommand for its options)",
|
|
8233
|
+
"sink add": "sink add <url|file:PATH|stdout> [--tier 0-3] [--token T] [--secret S] [--pii] [--name N]",
|
|
8234
|
+
"sink list": "sink list",
|
|
8235
|
+
"sink remove": "sink remove <name>",
|
|
8236
|
+
"sink test": "sink test <name>",
|
|
8237
|
+
secrets: "secrets [move keychain|secret-service|file]",
|
|
8238
|
+
"secrets move": "secrets move keychain|secret-service|file",
|
|
8239
|
+
status: "status [--json]",
|
|
8240
|
+
doctor: "doctor [--json]",
|
|
8241
|
+
tail: "tail [--session ID] [--tier 0-3] [--harness NAME] [--all] [--json]",
|
|
8242
|
+
backfill: `backfill [--days 30] [${harnesses.map((h) => `--${h}`).join(" ")}]`,
|
|
8243
|
+
forget: "forget <harness> <session>",
|
|
8244
|
+
replay: "replay [--sink NAME] [--from-start]",
|
|
8245
|
+
update: "update [off|on] (install the latest now; off/on: the daemon's daily update)",
|
|
8246
|
+
"update off": "update off",
|
|
8247
|
+
"update on": "update on",
|
|
8248
|
+
control: "control pair|mode|status|keys|off|run (use --help after a subcommand for its options)",
|
|
8249
|
+
"control pair": "control pair <receiver> [--folder DIR]\u2026 [--mode safe|auto] [--tier 0-3] [--token T] [--name N] [--no-service]",
|
|
8250
|
+
"control mode": "control mode auto|safe",
|
|
8251
|
+
"control status": "control status",
|
|
8252
|
+
"control keys": "control keys [remove <id>]",
|
|
8253
|
+
"control keys remove": "control keys remove <id>",
|
|
8254
|
+
"control off": "control off [<receiver>]",
|
|
8255
|
+
"control run": "control run",
|
|
8256
|
+
wait: "wait [--session <harness>:<id>]",
|
|
8257
|
+
worker: "worker <job-file> (internal: process one queued job)",
|
|
8258
|
+
hook: "hook (internal: harnesses run dist/hook.js directly)",
|
|
8259
|
+
version: "version (print the installed version)",
|
|
8260
|
+
"--version": "--version (print the installed version)",
|
|
8261
|
+
"-v": "-v (print the installed version)"
|
|
8262
|
+
};
|
|
7054
8263
|
}
|
|
7055
|
-
function
|
|
7056
|
-
const
|
|
7057
|
-
let
|
|
7058
|
-
|
|
7059
|
-
|
|
7060
|
-
|
|
7061
|
-
|
|
7062
|
-
file,
|
|
7063
|
-
present: false,
|
|
7064
|
-
node: null,
|
|
7065
|
-
nodeOk: false,
|
|
7066
|
-
version: null,
|
|
7067
|
-
problems: ["the hook launcher is missing; run `sessionpipe install` again"]
|
|
7068
|
-
};
|
|
7069
|
-
}
|
|
7070
|
-
const node = /^n='((?:[^']|'\\'')*)'$/m.exec(text)?.[1]?.replace(/'\\''/g, "'") ?? null;
|
|
7071
|
-
const v = /^# sessionpipe (\S+) hook launcher/m.exec(text)?.[1] ?? null;
|
|
7072
|
-
let nodeOk = false;
|
|
7073
|
-
try {
|
|
7074
|
-
if (node) {
|
|
7075
|
-
accessSync3(node, constants3.X_OK);
|
|
7076
|
-
nodeOk = true;
|
|
7077
|
-
}
|
|
7078
|
-
} catch {
|
|
8264
|
+
function commandHelp(args2, harnesses) {
|
|
8265
|
+
const usage = usageTable(harnesses);
|
|
8266
|
+
for (let n = Math.min(args2.length, 3); n > 0; n--) {
|
|
8267
|
+
const line = usage[args2.slice(0, n).join(" ")];
|
|
8268
|
+
if (line) return `Usage: sessionpipe ${line}
|
|
8269
|
+
|
|
8270
|
+
-h, --help Show help without running the command.`;
|
|
7079
8271
|
}
|
|
7080
|
-
|
|
7081
|
-
if (!nodeOk)
|
|
7082
|
-
problems.push(
|
|
7083
|
-
`the node the hook launcher names (${node ?? "?"}) is gone; it falls back to the node on PATH. \`sessionpipe install\` points it at this one.`
|
|
7084
|
-
);
|
|
7085
|
-
for (const f of COPIED)
|
|
7086
|
-
try {
|
|
7087
|
-
accessSync3(path26.join(path26.dirname(file), f), constants3.R_OK);
|
|
7088
|
-
} catch {
|
|
7089
|
-
problems.push(`${f} is missing beside the hook launcher; run \`sessionpipe install\` again`);
|
|
7090
|
-
}
|
|
7091
|
-
if (v && v !== version)
|
|
7092
|
-
problems.push(
|
|
7093
|
-
`the hooks run sessionpipe ${v}, this is ${version}; run \`sessionpipe install\` to bring them up to date`
|
|
7094
|
-
);
|
|
7095
|
-
return { file, present: true, node, nodeOk, version: v, problems };
|
|
8272
|
+
return void 0;
|
|
7096
8273
|
}
|
|
7097
8274
|
|
|
7098
8275
|
// src/cli.ts
|
|
7099
|
-
|
|
7100
|
-
|
|
7101
|
-
|
|
7102
|
-
|
|
7103
|
-
var
|
|
8276
|
+
var print = (s = "") => {
|
|
8277
|
+
process.stdout.write(`${s}
|
|
8278
|
+
`);
|
|
8279
|
+
};
|
|
8280
|
+
var args = [];
|
|
8281
|
+
var out = print;
|
|
8282
|
+
var state = "";
|
|
7104
8283
|
var flag = (name) => {
|
|
7105
8284
|
const i = args.indexOf(name);
|
|
7106
8285
|
return i >= 0 ? args[i + 1] : void 0;
|
|
7107
8286
|
};
|
|
7108
8287
|
var has = (name) => args.includes(name);
|
|
7109
|
-
var
|
|
7110
|
-
|
|
8288
|
+
var distDir = path29.dirname(fileURLToPath(import.meta.url));
|
|
8289
|
+
async function main(argv, io = { out: print, err: (s) => void process.stderr.write(s), exit: (c) => process.exit(c) }) {
|
|
8290
|
+
args = argv;
|
|
8291
|
+
out = io.out;
|
|
8292
|
+
state = stateDir();
|
|
8293
|
+
try {
|
|
8294
|
+
await command(args[0] ?? "help");
|
|
8295
|
+
} catch (e) {
|
|
8296
|
+
io.err(`sessionpipe: ${e.message}
|
|
7111
8297
|
`);
|
|
7112
|
-
|
|
7113
|
-
|
|
7114
|
-
|
|
8298
|
+
io.exit(1);
|
|
8299
|
+
}
|
|
8300
|
+
}
|
|
8301
|
+
function isEntrypoint(argv1, url) {
|
|
8302
|
+
try {
|
|
8303
|
+
return !!argv1 && realpathSync7(argv1) === fileURLToPath(url);
|
|
8304
|
+
} catch {
|
|
8305
|
+
return false;
|
|
8306
|
+
}
|
|
8307
|
+
}
|
|
7115
8308
|
function hookCommand() {
|
|
7116
8309
|
if (usesLauncher()) {
|
|
7117
8310
|
const l = launcherPath();
|
|
7118
8311
|
return (a) => ({ argv: [l, ...a], command: `${JSON.stringify(l)} ${a.join(" ")}` });
|
|
7119
8312
|
}
|
|
7120
8313
|
const node = stableNode(process.execPath);
|
|
7121
|
-
let script =
|
|
8314
|
+
let script = path29.join(distDir, "hook.js");
|
|
7122
8315
|
try {
|
|
7123
|
-
script =
|
|
8316
|
+
script = realpathSync7(script);
|
|
7124
8317
|
} catch {
|
|
7125
8318
|
}
|
|
7126
8319
|
return (a) => ({
|
|
@@ -7133,7 +8326,14 @@ function selectedAdapters() {
|
|
|
7133
8326
|
if (named.length) return named;
|
|
7134
8327
|
return ADAPTERS.filter((a) => a.detect());
|
|
7135
8328
|
}
|
|
7136
|
-
async function
|
|
8329
|
+
async function command(cmd) {
|
|
8330
|
+
if (has("--help") || has("-h")) {
|
|
8331
|
+
const usage = commandHelp(
|
|
8332
|
+
args,
|
|
8333
|
+
ADAPTERS.map((a) => a.name)
|
|
8334
|
+
);
|
|
8335
|
+
return usage ? out(usage) : help();
|
|
8336
|
+
}
|
|
7137
8337
|
switch (cmd) {
|
|
7138
8338
|
case "connect":
|
|
7139
8339
|
return connect();
|
|
@@ -7180,14 +8380,15 @@ async function main() {
|
|
|
7180
8380
|
function help() {
|
|
7181
8381
|
out(`sessionpipe ${VERSION} \u2014 an open protocol for what your coding agents are doing
|
|
7182
8382
|
|
|
7183
|
-
|
|
8383
|
+
Usage:
|
|
8384
|
+
sessionpipe connect <receiver> [--machine NAME] [--rename-machine] [--allow-downgrade] [--tier 0-3] [--mode safe|auto] [--folder DIR]\u2026
|
|
7184
8385
|
(everything, in one go: hooks, a sink, signed messages, keys)
|
|
7185
|
-
sessionpipe install [--claude-code --codex --gemini-cli --antigravity] [--machine NAME] [--backfill DAYS] [--sink URL --tier N]
|
|
7186
|
-
sessionpipe uninstall [--keep-state]
|
|
8386
|
+
sessionpipe install [--claude-code --codex --gemini-cli --antigravity --devin] [--machine NAME] [--rename-machine] [--allow-downgrade] [--backfill DAYS] [--sink URL --tier N]
|
|
8387
|
+
sessionpipe uninstall [--claude-code --codex --gemini-cli --antigravity --devin | --all] [--keep-state]
|
|
7187
8388
|
sessionpipe sink add <url|file:PATH|stdout> [--tier 0-3] [--token T] [--pii] [--name N]
|
|
7188
8389
|
sessionpipe sink list | remove <name> | test <name>
|
|
7189
8390
|
sessionpipe secrets [move keychain|secret-service|file] (where the machine's keys live)
|
|
7190
|
-
sessionpipe status | doctor [--json]
|
|
8391
|
+
sessionpipe status [--json] | doctor [--json]
|
|
7191
8392
|
sessionpipe tail [--session ID] [--tier N] [--harness NAME]
|
|
7192
8393
|
sessionpipe backfill [--days 30] | forget <harness> <session> | replay --sink NAME
|
|
7193
8394
|
sessionpipe update [off|on] (install the latest now; off/on: the daemon's daily update)
|
|
@@ -7195,17 +8396,41 @@ ${CONTROL_HELP}
|
|
|
7195
8396
|
|
|
7196
8397
|
Docs: https://sessionpipe.org \xB7 nothing leaves this machine until you add a sink.`);
|
|
7197
8398
|
}
|
|
7198
|
-
|
|
7199
|
-
if (viaNpx(distDir)) return rerunGlobally(args, out);
|
|
8399
|
+
function setupConfig() {
|
|
7200
8400
|
const cfg = readConfig();
|
|
7201
|
-
const
|
|
7202
|
-
if (machine)
|
|
8401
|
+
const requested = flag("--machine");
|
|
8402
|
+
if ((has("--machine") || has("--rename-machine")) && (!requested || requested.startsWith("-")))
|
|
8403
|
+
throw new Error("--machine NAME is required when naming or renaming a machine");
|
|
8404
|
+
const machine = requested?.slice(0, 80);
|
|
8405
|
+
const control = readControl();
|
|
8406
|
+
const hooksName = existsSync17(configFile()) ? machineName(cfg, os11.hostname()) : void 0;
|
|
8407
|
+
const pairedName = control && hooksName && machineName({}, control.name) === hooksName ? hooksName : control?.name;
|
|
8408
|
+
const current = cfg.machine || pairedName || hooksName;
|
|
8409
|
+
if (current && machine && machine !== current && !has("--rename-machine")) {
|
|
8410
|
+
out(
|
|
8411
|
+
` Keeping configured machine name ${current}; ignoring --machine ${machine}. Use --rename-machine with --machine NAME for an explicit rename.`
|
|
8412
|
+
);
|
|
8413
|
+
cfg.machine = current;
|
|
8414
|
+
} else {
|
|
8415
|
+
const name = machine || current || machineName(cfg, os11.hostname());
|
|
8416
|
+
if (control && control.name !== name && (machine || name === pairedName)) {
|
|
8417
|
+
writeControl({ ...control, name });
|
|
8418
|
+
out(` Renamed the control daemon ${control.name} \u2192 ${name}; its next hello tells each receiver.`);
|
|
8419
|
+
}
|
|
8420
|
+
cfg.machine = name;
|
|
8421
|
+
}
|
|
8422
|
+
return cfg;
|
|
8423
|
+
}
|
|
8424
|
+
async function install() {
|
|
8425
|
+
const cfg = setupConfig();
|
|
8426
|
+
if (viaNpx(distDir)) return rerunGlobally(args, out, { allowDowngrade: has("--allow-downgrade") });
|
|
8427
|
+
assertSetupVersion(has("--allow-downgrade"));
|
|
7203
8428
|
const sinkUrl = flag("--sink");
|
|
7204
8429
|
if (sinkUrl) await sinkAdd(sinkUrl, cfg);
|
|
7205
8430
|
const adapters = selectedAdapters();
|
|
7206
8431
|
if (!adapters.length) {
|
|
7207
8432
|
out(
|
|
7208
|
-
" No supported harness found on this machine (Claude Code, Codex, Gemini CLI, Antigravity). Pass --<harness> to force one."
|
|
8433
|
+
" No supported harness found on this machine (Claude Code, Codex, Gemini CLI, Antigravity, Devin). Pass --<harness> to force one."
|
|
7209
8434
|
);
|
|
7210
8435
|
return;
|
|
7211
8436
|
}
|
|
@@ -7253,6 +8478,10 @@ function installHarnesses(adapters, cfg, lean) {
|
|
|
7253
8478
|
cfg.harnesses[a.name] = { enabled: true, ...created.size ? { created: [...created] } : {} };
|
|
7254
8479
|
if (a.name === "gemini-cli")
|
|
7255
8480
|
out(" note: Gemini CLI's own telemetry defaults logPrompts on; that is Google's setting, not sessionpipe's.");
|
|
8481
|
+
if (a.name === "devin" && !reports.some((r) => r.skipped))
|
|
8482
|
+
out(
|
|
8483
|
+
" note: Devin ignores a hooks file whole if it holds one event name it doesn't know; run `/hooks` in `devin` to see what loaded."
|
|
8484
|
+
);
|
|
7256
8485
|
if (a.name === "codex" && !reports.some((r) => r.skipped))
|
|
7257
8486
|
out(
|
|
7258
8487
|
" note: Codex runs non-managed hooks only after you approve them: start `codex` and accept the hook review. The notify fallback reports turn ends meanwhile."
|
|
@@ -7263,14 +8492,13 @@ async function connect() {
|
|
|
7263
8492
|
const raw = args[1];
|
|
7264
8493
|
if (!raw || raw.startsWith("-"))
|
|
7265
8494
|
return out(
|
|
7266
|
-
"usage: sessionpipe connect <receiver> [--machine NAME] [--tier 0-3] [--mode safe|auto] [--folder DIR]\u2026 [--no-service]"
|
|
8495
|
+
"usage: sessionpipe connect <receiver> [--machine NAME] [--rename-machine] [--allow-downgrade] [--tier 0-3] [--mode safe|auto] [--folder DIR]\u2026 [--no-service]"
|
|
7267
8496
|
);
|
|
7268
|
-
|
|
8497
|
+
const cfg = setupConfig();
|
|
8498
|
+
if (viaNpx(distDir)) return rerunGlobally(args, out, { allowDowngrade: has("--allow-downgrade") });
|
|
8499
|
+
assertSetupVersion(has("--allow-downgrade"));
|
|
7269
8500
|
const url = (/^https?:\/\//.test(raw) ? raw : `https://${raw}`).replace(/\/$/, "");
|
|
7270
8501
|
const host = new URL(url).host;
|
|
7271
|
-
const cfg = readConfig();
|
|
7272
|
-
const machine = flag("--machine");
|
|
7273
|
-
if (machine) cfg.machine = machine.slice(0, 80);
|
|
7274
8502
|
const name = machineName(cfg, os11.hostname());
|
|
7275
8503
|
const before = cfg.sinks.find((x) => x.url.replace(/\/$/, "") === url);
|
|
7276
8504
|
const tier = connectTier(flag("--tier"), before?.tier);
|
|
@@ -7278,19 +8506,19 @@ async function connect() {
|
|
|
7278
8506
|
const adapters = ADAPTERS.filter((a) => a.detect());
|
|
7279
8507
|
if (!adapters.length)
|
|
7280
8508
|
out(
|
|
7281
|
-
" ! No coding agent here yet (Claude Code, Codex, Gemini CLI, Antigravity): run `sessionpipe install` once you have one."
|
|
8509
|
+
" ! No coding agent here yet (Claude Code, Codex, Gemini CLI, Antigravity, Devin): run `sessionpipe install` once you have one."
|
|
7282
8510
|
);
|
|
7283
8511
|
else installHarnesses(adapters, cfg, tier < 1);
|
|
7284
8512
|
writeConfig(cfg);
|
|
7285
8513
|
await reportClaudeAccounts();
|
|
7286
|
-
const
|
|
8514
|
+
const rows2 = collectRows(adapters, 30);
|
|
7287
8515
|
const control = readControl();
|
|
7288
8516
|
const paired = control?.receivers.find((r) => r.url === url);
|
|
7289
8517
|
const sinkNow = readConfig().sinks.find((x) => x.url.replace(/\/$/, "") === url);
|
|
7290
8518
|
if (paired && sinkNow && (sinkNow.token || sinkNow.token_in)) {
|
|
7291
8519
|
out(` \u2713 Already paired with ${host} as ${paired.machine}: nothing to approve.`);
|
|
7292
8520
|
} else {
|
|
7293
|
-
const recent =
|
|
8521
|
+
const recent = rows2.slice().sort((a, b) => b.row.last_at - a.row.last_at).map((r) => r.row.session.cwd ?? "");
|
|
7294
8522
|
const choice = connectFolders({
|
|
7295
8523
|
cwd: process.cwd(),
|
|
7296
8524
|
home: os11.homedir(),
|
|
@@ -7299,7 +8527,7 @@ async function connect() {
|
|
|
7299
8527
|
recent,
|
|
7300
8528
|
exists: (d) => {
|
|
7301
8529
|
try {
|
|
7302
|
-
return
|
|
8530
|
+
return statSync11(d).isDirectory();
|
|
7303
8531
|
} catch {
|
|
7304
8532
|
return false;
|
|
7305
8533
|
}
|
|
@@ -7352,9 +8580,9 @@ async function connect() {
|
|
|
7352
8580
|
out(` \u2713 Keys: in ${storeLabel(store)}${kept.length ? ` (left in the file: ${kept.join(", ")})` : ""}`);
|
|
7353
8581
|
} else
|
|
7354
8582
|
out(
|
|
7355
|
-
` Keys: in ${tilde(
|
|
8583
|
+
` Keys: in ${tilde(path29.dirname(configFile()))} at 0600 \u2014 no unlocked keychain in this session${process.env.SSH_CONNECTION ? " (an ssh shell)" : ""}`
|
|
7356
8584
|
);
|
|
7357
|
-
if (
|
|
8585
|
+
if (rows2.length) queueBackfill(rows2, 30, readConfig());
|
|
7358
8586
|
out("");
|
|
7359
8587
|
out(` Done: ${name} reports to ${host} and takes messages you sign on a device it trusts.`);
|
|
7360
8588
|
out(" Run this again any time; it only fixes what's missing. `sessionpipe doctor` checks everything.");
|
|
@@ -7408,9 +8636,37 @@ function secrets() {
|
|
|
7408
8636
|
if (!cfg.sinks.length && !readControl()?.receivers.length) out(" no keys on this machine");
|
|
7409
8637
|
out(` best store from this session: ${storeLabel(bestStore())}`);
|
|
7410
8638
|
}
|
|
8639
|
+
var AWAY_FROM_HOME = {
|
|
8640
|
+
"claude-code": "CLAUDE_CONFIG_DIR",
|
|
8641
|
+
codex: "CODEX_HOME",
|
|
8642
|
+
"gemini-cli": "GEMINI_CLI_HOME",
|
|
8643
|
+
antigravity: "GEMINI_CLI_HOME",
|
|
8644
|
+
devin: "XDG_CONFIG_HOME"
|
|
8645
|
+
};
|
|
8646
|
+
function unisolated() {
|
|
8647
|
+
if (!process.env.SESSIONPIPE_CONFIG && !process.env.SESSIONPIPE_STATE) return [];
|
|
8648
|
+
return ADAPTERS.filter((a) => {
|
|
8649
|
+
const v = AWAY_FROM_HOME[a.name];
|
|
8650
|
+
return v === null || typeof v === "string" && !process.env[v];
|
|
8651
|
+
}).map((a) => a.name);
|
|
8652
|
+
}
|
|
7411
8653
|
function uninstall() {
|
|
8654
|
+
const named = ADAPTERS.filter((a) => has(`--${a.name}`));
|
|
8655
|
+
const all = !named.length;
|
|
8656
|
+
if (all && !has("--all")) {
|
|
8657
|
+
const exposed = unisolated();
|
|
8658
|
+
if (exposed.length) {
|
|
8659
|
+
out(
|
|
8660
|
+
` ! Refusing an uninstall of every harness: SESSIONPIPE_CONFIG/SESSIONPIPE_STATE point away from this machine, but ${exposed.join(", ")} still resolve from ${tilde(os11.homedir())}.`
|
|
8661
|
+
);
|
|
8662
|
+
out(` Nothing was changed. Name what to remove (${exposed.map((n) => `--${n}`).join(" ")}),`);
|
|
8663
|
+
out(" pass --all if you do mean every harness on this machine, or unset SESSIONPIPE_CONFIG/SESSIONPIPE_STATE.");
|
|
8664
|
+
process.exitCode = 2;
|
|
8665
|
+
return;
|
|
8666
|
+
}
|
|
8667
|
+
}
|
|
7412
8668
|
const cfg = readConfig();
|
|
7413
|
-
for (const a of ADAPTERS) {
|
|
8669
|
+
for (const a of all ? ADAPTERS : named) {
|
|
7414
8670
|
const created = cfg.harnesses[a.name]?.created ?? [];
|
|
7415
8671
|
for (const r of a.uninstall(process.env, { created }))
|
|
7416
8672
|
if (r.changed) out(` \u2713 ${a.name}: ${r.note ?? `hooks removed from ${tilde(r.file)}`}`);
|
|
@@ -7418,7 +8674,11 @@ function uninstall() {
|
|
|
7418
8674
|
delete cfg.harnesses[a.name];
|
|
7419
8675
|
}
|
|
7420
8676
|
writeConfig(cfg);
|
|
7421
|
-
if (usesLauncher()) removeLauncher();
|
|
8677
|
+
if (all && usesLauncher()) removeLauncher();
|
|
8678
|
+
if (!all) {
|
|
8679
|
+
out(` Still installed: ${Object.keys(cfg.harnesses).join(", ") || "nothing"}.`);
|
|
8680
|
+
return;
|
|
8681
|
+
}
|
|
7422
8682
|
if (!has("--keep-state"))
|
|
7423
8683
|
out(
|
|
7424
8684
|
` Outbox and state kept under ${tilde(state)} (delete it yourself, or pass nothing: it prunes after ${readConfig().keep_days ?? 30} days).`
|
|
@@ -7518,23 +8778,23 @@ var TIER_TEXT = {
|
|
|
7518
8778
|
};
|
|
7519
8779
|
function collectRows(adapters, days) {
|
|
7520
8780
|
const since = Date.now() - days * 864e5;
|
|
7521
|
-
const
|
|
8781
|
+
const rows2 = [];
|
|
7522
8782
|
for (const a of adapters) {
|
|
7523
8783
|
try {
|
|
7524
|
-
for (const row of a.backfill(since))
|
|
8784
|
+
for (const row of a.backfill(since)) rows2.push({ harness: a.name, row });
|
|
7525
8785
|
} catch {
|
|
7526
8786
|
}
|
|
7527
8787
|
}
|
|
7528
|
-
return
|
|
8788
|
+
return rows2;
|
|
7529
8789
|
}
|
|
7530
8790
|
function backfillRows(adapters, days, cfg) {
|
|
7531
8791
|
queueBackfill(collectRows(adapters, days), days, cfg);
|
|
7532
8792
|
}
|
|
7533
|
-
function queueBackfill(
|
|
8793
|
+
function queueBackfill(rows2, days, cfg) {
|
|
7534
8794
|
const outbox = new Outbox(state);
|
|
7535
8795
|
let n = 0;
|
|
7536
8796
|
const machine = machineName(cfg, os11.hostname());
|
|
7537
|
-
for (const { harness, row } of
|
|
8797
|
+
for (const { harness, row } of rows2) {
|
|
7538
8798
|
const ref = { harness, session: row.session.id };
|
|
7539
8799
|
const facts = factsState(state).get(harness, row.session.id);
|
|
7540
8800
|
if (facts.backfilled) continue;
|
|
@@ -7560,7 +8820,7 @@ function queueBackfill(rows, days, cfg) {
|
|
|
7560
8820
|
n++;
|
|
7561
8821
|
}
|
|
7562
8822
|
out(` \u2713 Backfill: ${n} session${n === 1 ? "" : "s"} from the last ${days} days queued as session.backfill`);
|
|
7563
|
-
void
|
|
8823
|
+
void drain(state, cfg.sinks, outbox);
|
|
7564
8824
|
}
|
|
7565
8825
|
function backfill() {
|
|
7566
8826
|
backfillRows(selectedAdapters(), Number(flag("--days") ?? 30), readConfig());
|
|
@@ -7594,21 +8854,65 @@ async function replay() {
|
|
|
7594
8854
|
for (const s of sinks) {
|
|
7595
8855
|
if (has("--from-start")) new Cursors(state, s.name).write({});
|
|
7596
8856
|
}
|
|
7597
|
-
const d = await flush(state, sinks, new Outbox(state));
|
|
8857
|
+
const d = await flush(state, sinks, new Outbox(state), void 0, { force: true });
|
|
7598
8858
|
for (const [k, v] of Object.entries(d)) out(` ${k}: ${v} event${v === 1 ? "" : "s"} delivered`);
|
|
7599
8859
|
}
|
|
7600
|
-
function
|
|
8860
|
+
async function controlHealth() {
|
|
8861
|
+
if (!pairedCount()) return null;
|
|
8862
|
+
const reply = await ask(socketPath(state), { op: "status" }, 1500);
|
|
8863
|
+
if (reply?.op !== "status") return { running: false, receivers: [] };
|
|
8864
|
+
const status2 = reply.status;
|
|
8865
|
+
return {
|
|
8866
|
+
running: true,
|
|
8867
|
+
receivers: status2.receivers.map((r) => ({
|
|
8868
|
+
url: r.url,
|
|
8869
|
+
polling: r.polling,
|
|
8870
|
+
last_poll: r.last_poll,
|
|
8871
|
+
last_error: r.last_error,
|
|
8872
|
+
// An older daemon has no age field. Its last_poll still exposes a loop
|
|
8873
|
+
// that stopped long ago, and polling=false catches the terminal 401.
|
|
8874
|
+
last_poll_age_ms: r.last_poll_age_ms ?? (r.last_poll === null ? null : Math.max(0, Date.now() - r.last_poll))
|
|
8875
|
+
}))
|
|
8876
|
+
};
|
|
8877
|
+
}
|
|
8878
|
+
function controlLines(control) {
|
|
8879
|
+
if (!control) {
|
|
8880
|
+
out(" control: not paired");
|
|
8881
|
+
return;
|
|
8882
|
+
}
|
|
8883
|
+
if (!control.running) {
|
|
8884
|
+
out(" control: daemon not running or not responding; polling: no");
|
|
8885
|
+
return;
|
|
8886
|
+
}
|
|
8887
|
+
for (const r of control.receivers) {
|
|
8888
|
+
const age = r.last_poll_age_ms === null ? "unknown" : `${Math.floor(r.last_poll_age_ms / 6e4)} minutes`;
|
|
8889
|
+
out(
|
|
8890
|
+
` control: ${r.url} \xB7 polling: ${r.polling ? "yes" : "no"} \xB7 last successful poll: ${r.last_poll === null ? `never (${age} since start)` : `${age} ago`} \xB7 last error: ${r.last_error ?? "none"}`
|
|
8891
|
+
);
|
|
8892
|
+
}
|
|
8893
|
+
}
|
|
8894
|
+
async function status() {
|
|
7601
8895
|
const cfg = readConfig();
|
|
7602
8896
|
const hc = hookCommand();
|
|
7603
|
-
|
|
8897
|
+
const json2 = has("--json");
|
|
8898
|
+
const harnesses = {};
|
|
8899
|
+
const sinks = [];
|
|
8900
|
+
if (!json2) out(` machine: ${machineName(cfg, os11.hostname())} sessionpipe ${VERSION} state ${tilde(state)}`);
|
|
7604
8901
|
for (const a of ADAPTERS) {
|
|
7605
8902
|
if (!a.detect() && !cfg.harnesses[a.name]) continue;
|
|
7606
|
-
|
|
7607
|
-
|
|
7608
|
-
|
|
7609
|
-
|
|
8903
|
+
const installed = a.installed(hc, process.env, { lean: isLean(cfg) });
|
|
8904
|
+
harnesses[a.name] = installed.map((r) => ({
|
|
8905
|
+
file: tilde(r.file),
|
|
8906
|
+
state: r.state,
|
|
8907
|
+
...r.note ? { note: r.note } : {}
|
|
8908
|
+
}));
|
|
8909
|
+
if (!json2)
|
|
8910
|
+
for (const r of installed)
|
|
8911
|
+
out(
|
|
8912
|
+
` ${a.name.padEnd(12)} ${tilde(r.file).padEnd(44)} ${r.state}${r.state !== "current" && r.note ? ` \u2014 ${r.note}` : ""}`
|
|
8913
|
+
);
|
|
7610
8914
|
}
|
|
7611
|
-
if (!cfg.sinks.length) out(" sinks: none (events stay in the local outbox)");
|
|
8915
|
+
if (!json2 && !cfg.sinks.length) out(" sinks: none (events stay in the local outbox)");
|
|
7612
8916
|
for (const s of cfg.sinks) {
|
|
7613
8917
|
const c = new Cursors(state, s.name).read();
|
|
7614
8918
|
const outbox = new Outbox(state);
|
|
@@ -7616,34 +8920,67 @@ function status() {
|
|
|
7616
8920
|
for (const ref of outbox.sessions()) {
|
|
7617
8921
|
const f = outbox.file(ref);
|
|
7618
8922
|
try {
|
|
7619
|
-
pending += Math.max(0,
|
|
8923
|
+
pending += Math.max(0, statSync11(f).size - (c[`${ref.harness}/${ref.session}`] ?? 0)) > 0 ? 1 : 0;
|
|
7620
8924
|
} catch {
|
|
7621
8925
|
}
|
|
7622
8926
|
}
|
|
7623
|
-
|
|
7624
|
-
|
|
7625
|
-
|
|
8927
|
+
sinks.push({
|
|
8928
|
+
name: s.name,
|
|
8929
|
+
url: s.url,
|
|
8930
|
+
tier: s.tier,
|
|
8931
|
+
max_tier: s.max_tier ?? null,
|
|
8932
|
+
pii: !!s.pii,
|
|
8933
|
+
control: !!s.control,
|
|
8934
|
+
paused: s.paused ?? null,
|
|
8935
|
+
pending_sessions: pending
|
|
8936
|
+
});
|
|
8937
|
+
if (!json2)
|
|
8938
|
+
out(
|
|
8939
|
+
` sink ${s.name.padEnd(16)} tier ${Math.min(s.tier, s.max_tier ?? 3)} ${s.paused ? `PAUSED ${s.paused}` : pending ? `${pending} session file(s) with undelivered events` : "up to date"}`
|
|
8940
|
+
);
|
|
7626
8941
|
}
|
|
7627
|
-
|
|
8942
|
+
const control = await controlHealth();
|
|
8943
|
+
if (!json2) controlLines(control);
|
|
8944
|
+
const update2 = updateState(cfg);
|
|
8945
|
+
if (!json2) out(` update: ${updateLine(update2, Date.now())}`);
|
|
7628
8946
|
const t = timing();
|
|
7629
|
-
if (t)
|
|
8947
|
+
if (!json2 && t)
|
|
7630
8948
|
out(
|
|
7631
8949
|
` hook: in-process p50 ${t.p50} ms over ${t.n} runs (\`sessionpipe doctor\` measures the wall clock the harness waits)`
|
|
7632
8950
|
);
|
|
7633
|
-
|
|
8951
|
+
let jobs = 0;
|
|
7634
8952
|
try {
|
|
7635
|
-
|
|
7636
|
-
if (n) out(` jobs: ${n} waiting`);
|
|
8953
|
+
jobs = readdirSync8(jobsDir(state)).filter((n) => n.endsWith(".json")).length;
|
|
7637
8954
|
} catch {
|
|
7638
8955
|
}
|
|
8956
|
+
if (json2)
|
|
8957
|
+
out(
|
|
8958
|
+
JSON.stringify(
|
|
8959
|
+
{
|
|
8960
|
+
version: VERSION,
|
|
8961
|
+
machine: machineName(cfg, os11.hostname()),
|
|
8962
|
+
state,
|
|
8963
|
+
config: configFile(),
|
|
8964
|
+
harnesses,
|
|
8965
|
+
sinks,
|
|
8966
|
+
timing: t,
|
|
8967
|
+
control,
|
|
8968
|
+
update: update2,
|
|
8969
|
+
jobs
|
|
8970
|
+
},
|
|
8971
|
+
null,
|
|
8972
|
+
2
|
|
8973
|
+
)
|
|
8974
|
+
);
|
|
8975
|
+
else if (jobs) out(` jobs: ${jobs} waiting`);
|
|
7639
8976
|
}
|
|
7640
8977
|
function hookWall(runs = 8) {
|
|
7641
|
-
const hook =
|
|
7642
|
-
const tmp = mkdtempSync(
|
|
8978
|
+
const hook = path29.join(distDir, "hook.js");
|
|
8979
|
+
const tmp = mkdtempSync(path29.join(os11.tmpdir(), "sessionpipe-doctor-"));
|
|
7643
8980
|
const env = {
|
|
7644
8981
|
...process.env,
|
|
7645
8982
|
SESSIONPIPE_STATE: tmp,
|
|
7646
|
-
SESSIONPIPE_CONFIG:
|
|
8983
|
+
SESSIONPIPE_CONFIG: path29.join(tmp, "config.json"),
|
|
7647
8984
|
SESSIONPIPE_NO_WORKER: "1"
|
|
7648
8985
|
};
|
|
7649
8986
|
const input = JSON.stringify({
|
|
@@ -7663,18 +9000,24 @@ function hookWall(runs = 8) {
|
|
|
7663
9000
|
} catch {
|
|
7664
9001
|
return null;
|
|
7665
9002
|
} finally {
|
|
7666
|
-
|
|
9003
|
+
rmSync3(tmp, { recursive: true, force: true });
|
|
7667
9004
|
}
|
|
7668
9005
|
times.sort((a, b) => a - b);
|
|
7669
|
-
const q = (p) => Math.round(
|
|
9006
|
+
const q = (p) => Math.round(
|
|
9007
|
+
/* v8 ignore next */
|
|
9008
|
+
times[Math.min(times.length - 1, Math.floor(p * times.length))] ?? 0
|
|
9009
|
+
);
|
|
7670
9010
|
return { n: times.length, p50: q(0.5), p95: q(0.95) };
|
|
7671
9011
|
}
|
|
7672
9012
|
function timing() {
|
|
7673
9013
|
try {
|
|
7674
|
-
const lines =
|
|
9014
|
+
const lines = readFileSync16(path29.join(state, "timing.jsonl"), "utf8").trim().split("\n").slice(-100);
|
|
7675
9015
|
const ms = lines.map((l) => JSON.parse(l).ms).filter((x) => x >= 0).sort((a, b) => a - b);
|
|
7676
9016
|
if (!ms.length) return null;
|
|
7677
|
-
const q = (p) => Math.round(
|
|
9017
|
+
const q = (p) => Math.round(
|
|
9018
|
+
/* v8 ignore next */
|
|
9019
|
+
ms[Math.min(ms.length - 1, Math.floor(p * ms.length))] ?? 0
|
|
9020
|
+
);
|
|
7678
9021
|
return { n: ms.length, p50: q(0.5), p95: q(0.95) };
|
|
7679
9022
|
} catch {
|
|
7680
9023
|
return null;
|
|
@@ -7698,6 +9041,18 @@ async function doctor() {
|
|
|
7698
9041
|
warnings: []
|
|
7699
9042
|
};
|
|
7700
9043
|
const warnings = report.warnings;
|
|
9044
|
+
const health = await controlHealth();
|
|
9045
|
+
report.control = health;
|
|
9046
|
+
if (health && !health.running)
|
|
9047
|
+
warnings.push("the control daemon is not running or not responding; polling is stopped");
|
|
9048
|
+
for (const r of health?.receivers ?? []) {
|
|
9049
|
+
if (!r.polling || r.last_poll_age_ms !== null && r.last_poll_age_ms >= 6e4) {
|
|
9050
|
+
const age = r.last_poll_age_ms === null ? "never polled" : `not polled for ${Math.floor(r.last_poll_age_ms / 6e4)} minutes`;
|
|
9051
|
+
warnings.push(
|
|
9052
|
+
`${r.url}: the daemon has ${age}: ${r.last_error ?? (r.polling ? "no successful poll response" : "polling stopped")}`
|
|
9053
|
+
);
|
|
9054
|
+
}
|
|
9055
|
+
}
|
|
7701
9056
|
const upd = report.update;
|
|
7702
9057
|
if (upd.failed)
|
|
7703
9058
|
warnings.push(
|
|
@@ -7735,7 +9090,7 @@ async function doctor() {
|
|
|
7735
9090
|
let linger = null;
|
|
7736
9091
|
try {
|
|
7737
9092
|
linger = /^Linger=yes/m.test(
|
|
7738
|
-
|
|
9093
|
+
execFileSync8("loginctl", ["show-user", os11.userInfo().username, "--property=Linger"], {
|
|
7739
9094
|
encoding: "utf8",
|
|
7740
9095
|
timeout: 5e3,
|
|
7741
9096
|
stdio: ["ignore", "pipe", "ignore"]
|
|
@@ -7803,6 +9158,7 @@ async function doctor() {
|
|
|
7803
9158
|
out(
|
|
7804
9159
|
` sink ${s.name.padEnd(16)} tier ${s.tier}${s.max_tier !== null ? ` (receiver max ${s.max_tier})` : ""}${s.paused ? ` PAUSED ${s.paused}` : ""}`
|
|
7805
9160
|
);
|
|
9161
|
+
controlLines(health);
|
|
7806
9162
|
const lr = report.launcher;
|
|
7807
9163
|
if (lr)
|
|
7808
9164
|
out(` hooks run ${lr.file} \u2192 node ${lr.node ? tilde(lr.node) : "?"}${lr.node_ok ? "" : " (gone: PATH's node)"}`);
|
|
@@ -7858,6 +9214,9 @@ async function tail() {
|
|
|
7858
9214
|
case "turn.transcript":
|
|
7859
9215
|
detail = `#${d.turn} "${String(d.user).slice(0, 50).replace(/\s+/g, " ")}"`;
|
|
7860
9216
|
break;
|
|
9217
|
+
case "turn.progress":
|
|
9218
|
+
detail = `#${d.turn} ${String(d.assistant ?? "").length} chars so far`;
|
|
9219
|
+
break;
|
|
7861
9220
|
case "session.backfill":
|
|
7862
9221
|
detail = `${e.session.title ?? ""}`;
|
|
7863
9222
|
break;
|
|
@@ -7869,7 +9228,7 @@ async function tail() {
|
|
|
7869
9228
|
out(`sessionpipe tail \xB7 tier ${tier} \xB7 ${tilde(state)}/outbox (Ctrl-C to stop)`);
|
|
7870
9229
|
for (const ref of outbox.sessions()) {
|
|
7871
9230
|
try {
|
|
7872
|
-
offsets.set(`${ref.harness}/${ref.session}`, has("--all") ? 0 :
|
|
9231
|
+
offsets.set(`${ref.harness}/${ref.session}`, has("--all") ? 0 : statSync11(outbox.file(ref)).size);
|
|
7873
9232
|
} catch {
|
|
7874
9233
|
}
|
|
7875
9234
|
}
|
|
@@ -7892,7 +9251,7 @@ async function tail() {
|
|
|
7892
9251
|
async function workerFromArgv() {
|
|
7893
9252
|
const f = args[1];
|
|
7894
9253
|
if (!f) return;
|
|
7895
|
-
const job = JSON.parse(
|
|
9254
|
+
const job = JSON.parse(readFileSync16(f, "utf8"));
|
|
7896
9255
|
try {
|
|
7897
9256
|
unlinkSync7(f);
|
|
7898
9257
|
} catch {
|
|
@@ -7954,9 +9313,14 @@ async function update() {
|
|
|
7954
9313
|
` The control daemon still runs ${running} and can't restart itself: ${process.platform === "darwin" ? `\`launchctl kickstart -k gui/${process.getuid?.() ?? "$(id -u)"}/org.sessionpipe.control\`` : process.platform === "linux" ? "`systemctl --user restart sessionpipe-control.service`" : "stop `sessionpipe control run` and start it again"} moves it (a run in progress stops).`
|
|
7955
9314
|
);
|
|
7956
9315
|
}
|
|
7957
|
-
|
|
7958
|
-
process.
|
|
7959
|
-
|
|
7960
|
-
|
|
7961
|
-
|
|
9316
|
+
if (isEntrypoint(process.argv[1], import.meta.url)) {
|
|
9317
|
+
process.stdout.on("error", (e) => {
|
|
9318
|
+
if (e.code === "EPIPE") process.exit(0);
|
|
9319
|
+
});
|
|
9320
|
+
void main(process.argv.slice(2));
|
|
9321
|
+
}
|
|
9322
|
+
export {
|
|
9323
|
+
isEntrypoint,
|
|
9324
|
+
main
|
|
9325
|
+
};
|
|
7962
9326
|
//# sourceMappingURL=cli.js.map
|