sessionpipe 0.5.0 → 0.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +44 -0
- package/dist/cli.js +962 -258
- package/dist/cli.js.map +4 -4
- package/dist/control/autoupdate.d.ts +156 -0
- package/dist/control/autoupdate.d.ts.map +1 -0
- package/dist/control/cli.d.ts +4 -0
- package/dist/control/cli.d.ts.map +1 -1
- package/dist/control/daemon.d.ts +54 -1
- package/dist/control/daemon.d.ts.map +1 -1
- package/dist/control/local.d.ts +4 -0
- package/dist/control/local.d.ts.map +1 -1
- package/dist/control/pair.d.ts +3 -0
- package/dist/control/pair.d.ts.map +1 -1
- package/dist/control/route.d.ts +6 -0
- package/dist/control/route.d.ts.map +1 -1
- package/dist/control/store.d.ts +4 -0
- package/dist/control/store.d.ts.map +1 -1
- package/dist/hook.js +5 -2
- package/dist/hook.js.map +2 -2
- package/dist/index.js +16 -15
- package/dist/index.js.map +2 -2
- package/dist/launcher.d.ts +2 -1
- package/dist/launcher.d.ts.map +1 -1
- package/dist/update.d.ts +70 -0
- package/dist/update.d.ts.map +1 -0
- package/dist/worker.js +16 -15
- package/dist/worker.js.map +2 -2
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
// sessionpipe 0.
|
|
2
|
+
// sessionpipe 0.6.1 · 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,10 @@ var __export = (target, all) => {
|
|
|
7
7
|
};
|
|
8
8
|
|
|
9
9
|
// src/cli.ts
|
|
10
|
-
import { execFileSync as execFileSync7,
|
|
11
|
-
import { mkdtempSync, readdirSync as readdirSync8, readFileSync as
|
|
10
|
+
import { execFileSync as execFileSync7, spawnSync as spawnSync2 } from "node:child_process";
|
|
11
|
+
import { mkdtempSync, readdirSync as readdirSync8, readFileSync as readFileSync13, realpathSync as realpathSync6, rmSync as rmSync2, statSync as statSync9, unlinkSync as unlinkSync7 } from "node:fs";
|
|
12
12
|
import os9 from "node:os";
|
|
13
|
-
import
|
|
13
|
+
import path23 from "node:path";
|
|
14
14
|
import { fileURLToPath } from "node:url";
|
|
15
15
|
|
|
16
16
|
// ../core/src/adapters/antigravity.ts
|
|
@@ -381,20 +381,19 @@ function installedClaudeShaped(file, harness, events, cmd2, o) {
|
|
|
381
381
|
// ../core/src/adapters/antigravity.ts
|
|
382
382
|
var NAME = "antigravity";
|
|
383
383
|
var HOOK_NAME = "sessionpipe";
|
|
384
|
-
var AG_EVENTS = ["PreInvocation", "PostInvocation", "
|
|
384
|
+
var AG_EVENTS = ["PreInvocation", "PostInvocation", "PostToolUse", "Stop"];
|
|
385
|
+
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';
|
|
386
|
+
var antigravityAnswer = (event) => event === "PreToolUse" ? AG_STALE_PRETOOL : "{}\n";
|
|
385
387
|
var gemini = (env) => env.GEMINI_CLI_HOME || path4.join(HOME, ".gemini");
|
|
386
388
|
var hooksFile = (env) => path4.join(gemini(env), "config", "hooks.json");
|
|
387
389
|
var projectsDir = (env) => path4.join(gemini(env), "config", "projects");
|
|
388
390
|
var dataDirs = (env) => ["antigravity", "antigravity-cli", "antigravity-ide"].map((d) => path4.join(gemini(env), d));
|
|
389
391
|
var ID_RE = /^[A-Za-z0-9_-]{1,128}$/;
|
|
390
392
|
var validTool = (n) => typeof n === "string" && /^[A-Za-z0-9_.:-]{1,120}$/.test(n);
|
|
391
|
-
function entry(cmd2
|
|
393
|
+
function entry(cmd2) {
|
|
392
394
|
const handler = (e) => ({ type: "command", command: cmd2([NAME, e]).command, timeout: 5 });
|
|
393
395
|
const out2 = { enabled: true };
|
|
394
|
-
for (const e of AG_EVENTS) {
|
|
395
|
-
if (lean && e === "PreToolUse") continue;
|
|
396
|
-
out2[e] = /Tool/.test(e) ? [{ matcher: "*", hooks: [handler(e)] }] : [handler(e)];
|
|
397
|
-
}
|
|
396
|
+
for (const e of AG_EVENTS) out2[e] = /Tool/.test(e) ? [{ matcher: "*", hooks: [handler(e)] }] : [handler(e)];
|
|
398
397
|
return out2;
|
|
399
398
|
}
|
|
400
399
|
var antigravity = {
|
|
@@ -402,12 +401,12 @@ var antigravity = {
|
|
|
402
401
|
events: AG_EVENTS,
|
|
403
402
|
detect: (env = process.env) => existsSync2(path4.join(gemini(env), "config")) || dataDirs(env).some((d) => existsSync2(d)),
|
|
404
403
|
configFiles: (env = process.env) => [hooksFile(env)],
|
|
405
|
-
install(cmd2, env = process.env
|
|
404
|
+
install(cmd2, env = process.env) {
|
|
406
405
|
const file = hooksFile(env);
|
|
407
406
|
const f = readJsonFile(file);
|
|
408
407
|
if (!f.ok) return [{ file, changed: false, skipped: true, note: `${f.reason}; not touched` }];
|
|
409
408
|
const hooks = f.value;
|
|
410
|
-
const e = entry(cmd2
|
|
409
|
+
const e = entry(cmd2);
|
|
411
410
|
if (JSON.stringify(hooks[HOOK_NAME]) === JSON.stringify(e)) return [{ file, changed: false }];
|
|
412
411
|
hooks[HOOK_NAME] = e;
|
|
413
412
|
mkdirSync2(path4.dirname(file), { recursive: true });
|
|
@@ -436,14 +435,14 @@ var antigravity = {
|
|
|
436
435
|
writeFileSync2(file, formatJson(hooks, f));
|
|
437
436
|
return [{ file, changed: true }];
|
|
438
437
|
},
|
|
439
|
-
installed(cmd2, env = process.env
|
|
438
|
+
installed(cmd2, env = process.env) {
|
|
440
439
|
const file = hooksFile(env);
|
|
441
440
|
const f = readJsonFile(file);
|
|
442
441
|
const have = f.ok ? f.value[HOOK_NAME] : void 0;
|
|
443
442
|
return [
|
|
444
443
|
{
|
|
445
444
|
file,
|
|
446
|
-
state: !have ? "missing" : JSON.stringify(have) === JSON.stringify(entry(cmd2
|
|
445
|
+
state: !have ? "missing" : JSON.stringify(have) === JSON.stringify(entry(cmd2)) ? "current" : "stale"
|
|
447
446
|
}
|
|
448
447
|
];
|
|
449
448
|
},
|
|
@@ -455,7 +454,8 @@ var antigravity = {
|
|
|
455
454
|
} catch {
|
|
456
455
|
}
|
|
457
456
|
const id = s.conversationId ?? s.session_id ?? s.sessionId;
|
|
458
|
-
|
|
457
|
+
const stdout = antigravityAnswer(event);
|
|
458
|
+
if (!id) return { session: { id: "" }, events: [], stdout };
|
|
459
459
|
const workspaces = agWorkspaces(s.workspacePaths);
|
|
460
460
|
const session = { id: String(id), cwd: workspaces[0] ?? input.cwd };
|
|
461
461
|
if (typeof s.modelName === "string" && s.modelName !== "auto") session.model = s.modelName;
|
|
@@ -476,7 +476,8 @@ var antigravity = {
|
|
|
476
476
|
if (tool) events.push(ev("tool.started", { tool, input: toolCall?.args }));
|
|
477
477
|
break;
|
|
478
478
|
case "PostToolUse":
|
|
479
|
-
if (tool)
|
|
479
|
+
if (!tool) events.push(ev("session.heartbeat", {}));
|
|
480
|
+
else
|
|
480
481
|
events.push(
|
|
481
482
|
ev("tool.ended", {
|
|
482
483
|
tool,
|
|
@@ -490,9 +491,9 @@ var antigravity = {
|
|
|
490
491
|
events.push(ev("turn.ended", { reason: s.error ? "error" : "stop" }));
|
|
491
492
|
break;
|
|
492
493
|
default:
|
|
493
|
-
return { session, events: [], stdout
|
|
494
|
+
return { session, events: [], stdout };
|
|
494
495
|
}
|
|
495
|
-
const r = { session, events, stdout
|
|
496
|
+
const r = { session, events, stdout, hints: { workspaces } };
|
|
496
497
|
if (typeof s.transcriptPath === "string") r.transcript = s.transcriptPath;
|
|
497
498
|
return r;
|
|
498
499
|
},
|
|
@@ -2942,10 +2943,21 @@ function connectTier(flag2, current) {
|
|
|
2942
2943
|
return Math.max(2, Math.min(3, current ?? 0));
|
|
2943
2944
|
}
|
|
2944
2945
|
|
|
2945
|
-
// src/control/
|
|
2946
|
-
import {
|
|
2947
|
-
import
|
|
2948
|
-
import
|
|
2946
|
+
// src/control/autoupdate.ts
|
|
2947
|
+
import { spawn as spawn3 } from "node:child_process";
|
|
2948
|
+
import { readFileSync as readFileSync11 } from "node:fs";
|
|
2949
|
+
import path18 from "node:path";
|
|
2950
|
+
|
|
2951
|
+
// src/update.ts
|
|
2952
|
+
import { spawn as spawn2 } from "node:child_process";
|
|
2953
|
+
import { accessSync as accessSync2, constants as constants2, existsSync as existsSync9, realpathSync as realpathSync3 } from "node:fs";
|
|
2954
|
+
import path16 from "node:path";
|
|
2955
|
+
|
|
2956
|
+
// src/runtime.ts
|
|
2957
|
+
import { execFileSync as execFileSync5 } from "node:child_process";
|
|
2958
|
+
import { realpathSync as realpathSync2 } from "node:fs";
|
|
2959
|
+
import os5 from "node:os";
|
|
2960
|
+
import path15 from "node:path";
|
|
2949
2961
|
|
|
2950
2962
|
// src/run.ts
|
|
2951
2963
|
import {
|
|
@@ -3218,7 +3230,7 @@ async function tellDaemon(state2, job, session, transcript) {
|
|
|
3218
3230
|
300
|
|
3219
3231
|
);
|
|
3220
3232
|
}
|
|
3221
|
-
var VERSION = true ? "0.
|
|
3233
|
+
var VERSION = true ? "0.6.1" : "0.0.0";
|
|
3222
3234
|
function enqueueJob(state2, job) {
|
|
3223
3235
|
const jobs = jobsDir(state2);
|
|
3224
3236
|
mkdirSync7(jobs, { recursive: true, mode: 448 });
|
|
@@ -3457,10 +3469,6 @@ function applySinkVerdict(name, res) {
|
|
|
3457
3469
|
var jobsDir = (state2) => path14.join(state2, "jobs");
|
|
3458
3470
|
|
|
3459
3471
|
// src/runtime.ts
|
|
3460
|
-
import { execFileSync as execFileSync5 } from "node:child_process";
|
|
3461
|
-
import { realpathSync as realpathSync2 } from "node:fs";
|
|
3462
|
-
import os5 from "node:os";
|
|
3463
|
-
import path15 from "node:path";
|
|
3464
3472
|
function stableNode(exe) {
|
|
3465
3473
|
const m = /^(.*)\/Cellar\/(node(?:@\d+)?)\/[^/]+\/bin\/node$/.exec(exe);
|
|
3466
3474
|
if (!m) return exe;
|
|
@@ -3514,9 +3522,171 @@ function rerunGlobally(argv, out2) {
|
|
|
3514
3522
|
}
|
|
3515
3523
|
}
|
|
3516
3524
|
|
|
3517
|
-
// src/
|
|
3518
|
-
|
|
3519
|
-
|
|
3525
|
+
// src/update.ts
|
|
3526
|
+
var REGISTRY_LATEST = "https://registry.npmjs.org/sessionpipe/latest";
|
|
3527
|
+
var VERSION_RE = /^v?(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?$/;
|
|
3528
|
+
function parse2(v) {
|
|
3529
|
+
const m = VERSION_RE.exec(String(v).trim());
|
|
3530
|
+
if (!m) return null;
|
|
3531
|
+
return { n: [Number(m[1]), Number(m[2]), Number(m[3])], pre: m[4] ?? null };
|
|
3532
|
+
}
|
|
3533
|
+
function compareVersions(a, b) {
|
|
3534
|
+
const x = parse2(a);
|
|
3535
|
+
const y = parse2(b);
|
|
3536
|
+
if (!x || !y) return null;
|
|
3537
|
+
for (let i = 0; i < 3; i++) if (x.n[i] !== y.n[i]) return x.n[i] - y.n[i];
|
|
3538
|
+
if (x.pre === y.pre) return 0;
|
|
3539
|
+
if (x.pre === null) return 1;
|
|
3540
|
+
if (y.pre === null) return -1;
|
|
3541
|
+
return x.pre.localeCompare(y.pre, "en", { numeric: true });
|
|
3542
|
+
}
|
|
3543
|
+
function isNewer(a, b) {
|
|
3544
|
+
const x = parse2(a);
|
|
3545
|
+
if (!x || x.pre !== null) return false;
|
|
3546
|
+
const c = compareVersions(a, b);
|
|
3547
|
+
return c !== null && c > 0;
|
|
3548
|
+
}
|
|
3549
|
+
async function latestVersion(f = fetch, timeoutMs = 8e3) {
|
|
3550
|
+
const r = await f(REGISTRY_LATEST, {
|
|
3551
|
+
signal: AbortSignal.timeout(timeoutMs),
|
|
3552
|
+
headers: { accept: "application/json" }
|
|
3553
|
+
});
|
|
3554
|
+
if (!r.ok) throw new Error(`the npm registry answered HTTP ${r.status}`);
|
|
3555
|
+
const j = await r.json();
|
|
3556
|
+
if (typeof j.version !== "string" || !parse2(j.version)) throw new Error("the npm registry named no version");
|
|
3557
|
+
return j.version;
|
|
3558
|
+
}
|
|
3559
|
+
function prefixOf(distDir2, platform = process.platform) {
|
|
3560
|
+
const re = platform === "win32" ? /^(.*?)[\\/]node_modules[\\/]sessionpipe[\\/]/ : /^(.*)\/lib\/node_modules\/sessionpipe\//;
|
|
3561
|
+
return re.exec(`${distDir2}/`)?.[1] ?? null;
|
|
3562
|
+
}
|
|
3563
|
+
function globalPrefix(distDir2, platform = process.platform) {
|
|
3564
|
+
if (viaNpx(distDir2)) return null;
|
|
3565
|
+
const prefix = prefixOf(distDir2, platform);
|
|
3566
|
+
if (!prefix) return null;
|
|
3567
|
+
const bin = platform === "win32" ? path16.join(prefix, "sessionpipe.cmd") : path16.join(prefix, "bin", "sessionpipe");
|
|
3568
|
+
return existsSync9(bin) ? prefix : null;
|
|
3569
|
+
}
|
|
3570
|
+
function unwritable(prefix, platform = process.platform) {
|
|
3571
|
+
const dir = platform === "win32" ? path16.join(prefix, "node_modules") : path16.join(prefix, "lib", "node_modules");
|
|
3572
|
+
try {
|
|
3573
|
+
accessSync2(dir, constants2.W_OK);
|
|
3574
|
+
return null;
|
|
3575
|
+
} catch {
|
|
3576
|
+
return `${dir} isn't writable by this user (installed with sudo?)`;
|
|
3577
|
+
}
|
|
3578
|
+
}
|
|
3579
|
+
function npmArgv(node, platform = process.platform) {
|
|
3580
|
+
const dir = path16.dirname(node);
|
|
3581
|
+
const candidates = platform === "win32" ? [path16.join(dir, "node_modules", "npm", "bin", "npm-cli.js")] : [path16.join(dir, "npm"), path16.join(dir, "..", "lib", "node_modules", "npm", "bin", "npm-cli.js")];
|
|
3582
|
+
for (const c of candidates) {
|
|
3583
|
+
try {
|
|
3584
|
+
const real = realpathSync3(c);
|
|
3585
|
+
if (real.endsWith(".js")) return [node, real];
|
|
3586
|
+
} catch {
|
|
3587
|
+
}
|
|
3588
|
+
}
|
|
3589
|
+
return [platform === "win32" ? "npm.cmd" : "npm"];
|
|
3590
|
+
}
|
|
3591
|
+
var execCapture = (argv, o) => new Promise((resolve) => {
|
|
3592
|
+
const [cmd2, ...rest] = argv;
|
|
3593
|
+
let stdout = "";
|
|
3594
|
+
let stderr = "";
|
|
3595
|
+
const keep = (s, b) => (s + b.toString("utf8")).slice(-16e3);
|
|
3596
|
+
let child;
|
|
3597
|
+
try {
|
|
3598
|
+
child = spawn2(cmd2, rest, {
|
|
3599
|
+
env: o.env,
|
|
3600
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
3601
|
+
windowsHide: true,
|
|
3602
|
+
shell: process.platform === "win32" && cmd2.endsWith(".cmd")
|
|
3603
|
+
});
|
|
3604
|
+
} catch (e) {
|
|
3605
|
+
resolve({ code: null, stdout, stderr, error: e.message });
|
|
3606
|
+
return;
|
|
3607
|
+
}
|
|
3608
|
+
const t = setTimeout(() => child.kill("SIGTERM"), o.timeoutMs);
|
|
3609
|
+
child.stdout?.on("data", (b) => {
|
|
3610
|
+
stdout = keep(stdout, b);
|
|
3611
|
+
});
|
|
3612
|
+
child.stderr?.on("data", (b) => {
|
|
3613
|
+
stderr = keep(stderr, b);
|
|
3614
|
+
});
|
|
3615
|
+
child.on("error", (e) => {
|
|
3616
|
+
clearTimeout(t);
|
|
3617
|
+
resolve({ code: null, stdout, stderr, error: e.message });
|
|
3618
|
+
});
|
|
3619
|
+
child.on("close", (code) => {
|
|
3620
|
+
clearTimeout(t);
|
|
3621
|
+
resolve({ code, stdout, stderr });
|
|
3622
|
+
});
|
|
3623
|
+
});
|
|
3624
|
+
var lastLine = (r) => (r.error || r.stderr.trim().split("\n").filter(Boolean).slice(-2).join(" ") || `exit ${r.code}`).slice(0, 300);
|
|
3625
|
+
async function installedVersion(o) {
|
|
3626
|
+
const r = await (o.exec ?? execCapture)([o.node, path16.join(o.distDir, "cli.js"), "--version"], {
|
|
3627
|
+
env: o.env ?? process.env,
|
|
3628
|
+
timeoutMs: 3e4
|
|
3629
|
+
});
|
|
3630
|
+
const v = r.stdout.trim();
|
|
3631
|
+
return r.code === 0 && parse2(v) ? v : null;
|
|
3632
|
+
}
|
|
3633
|
+
async function installVersion(version, o) {
|
|
3634
|
+
const t0 = Date.now();
|
|
3635
|
+
const platform = o.platform ?? process.platform;
|
|
3636
|
+
const exec = o.exec ?? execCapture;
|
|
3637
|
+
const base = o.env ?? process.env;
|
|
3638
|
+
const env = { ...base, PATH: [path16.dirname(o.node), base.PATH].filter(Boolean).join(path16.delimiter) };
|
|
3639
|
+
const prefix = globalPrefix(o.distDir, platform);
|
|
3640
|
+
if (!prefix)
|
|
3641
|
+
return { ok: false, ms: Date.now() - t0, stage: "prefix", error: "this copy isn't a global npm install" };
|
|
3642
|
+
const npm = npmArgv(o.node, platform);
|
|
3643
|
+
const r = await exec(
|
|
3644
|
+
[
|
|
3645
|
+
...npm,
|
|
3646
|
+
"install",
|
|
3647
|
+
"--global",
|
|
3648
|
+
"--prefix",
|
|
3649
|
+
prefix,
|
|
3650
|
+
"--ignore-scripts",
|
|
3651
|
+
"--no-audit",
|
|
3652
|
+
"--no-fund",
|
|
3653
|
+
`sessionpipe@${version}`
|
|
3654
|
+
],
|
|
3655
|
+
{ env, timeoutMs: 5 * 6e4 }
|
|
3656
|
+
);
|
|
3657
|
+
if (r.code !== 0) return { ok: false, ms: Date.now() - t0, stage: "npm", error: `npm install: ${lastLine(r)}` };
|
|
3658
|
+
const now = await installedVersion({ node: o.node, distDir: o.distDir, env, exec });
|
|
3659
|
+
if (now !== version)
|
|
3660
|
+
return {
|
|
3661
|
+
ok: false,
|
|
3662
|
+
ms: Date.now() - t0,
|
|
3663
|
+
stage: "verify",
|
|
3664
|
+
error: `npm finished, but the copy in ${prefix} says ${now ?? "nothing"}, not ${version}`
|
|
3665
|
+
};
|
|
3666
|
+
let hooks = "no harness set up here; nothing to re-arm";
|
|
3667
|
+
if (o.harnesses.length) {
|
|
3668
|
+
const h = await exec(
|
|
3669
|
+
[o.node, path16.join(o.distDir, "cli.js"), "install", "--backfill", "0", ...o.harnesses.map((n) => `--${n}`)],
|
|
3670
|
+
{ env, timeoutMs: 2 * 6e4 }
|
|
3671
|
+
);
|
|
3672
|
+
hooks = h.code === 0 ? `hooks re-armed for ${o.harnesses.join(", ")}` : `re-arming the hooks failed (${lastLine(h)}); \`sessionpipe install\` does it`;
|
|
3673
|
+
}
|
|
3674
|
+
return { ok: true, ms: Date.now() - t0, hooks };
|
|
3675
|
+
}
|
|
3676
|
+
|
|
3677
|
+
// src/control/store.ts
|
|
3678
|
+
import {
|
|
3679
|
+
appendFileSync as appendFileSync4,
|
|
3680
|
+
chmodSync as chmodSync2,
|
|
3681
|
+
closeSync as closeSync4,
|
|
3682
|
+
existsSync as existsSync10,
|
|
3683
|
+
fsyncSync,
|
|
3684
|
+
mkdirSync as mkdirSync8,
|
|
3685
|
+
openSync as openSync4,
|
|
3686
|
+
readFileSync as readFileSync10,
|
|
3687
|
+
renameSync as renameSync3,
|
|
3688
|
+
writeFileSync as writeFileSync7
|
|
3689
|
+
} from "node:fs";
|
|
3520
3690
|
import path17 from "node:path";
|
|
3521
3691
|
|
|
3522
3692
|
// ../core/src/control/verify.ts
|
|
@@ -3853,122 +4023,11 @@ async function checkGrantPath(s, c, o) {
|
|
|
3853
4023
|
return { ok: true, key: key.id };
|
|
3854
4024
|
}
|
|
3855
4025
|
|
|
3856
|
-
// src/control/reply.ts
|
|
3857
|
-
import { closeSync as closeSync4, openSync as openSync4, readSync as readSync4, statSync as statSync7 } from "node:fs";
|
|
3858
|
-
function transcriptEnd(file) {
|
|
3859
|
-
try {
|
|
3860
|
-
return statSync7(file).size;
|
|
3861
|
-
} catch {
|
|
3862
|
-
return 0;
|
|
3863
|
-
}
|
|
3864
|
-
}
|
|
3865
|
-
function answerIn(lines) {
|
|
3866
|
-
const parts = [];
|
|
3867
|
-
for (const line of lines) {
|
|
3868
|
-
let j;
|
|
3869
|
-
try {
|
|
3870
|
-
j = JSON.parse(line);
|
|
3871
|
-
} catch {
|
|
3872
|
-
continue;
|
|
3873
|
-
}
|
|
3874
|
-
if (j.type !== "assistant" || j.isSidechain) continue;
|
|
3875
|
-
const content = j.message?.content;
|
|
3876
|
-
if (Array.isArray(content)) {
|
|
3877
|
-
for (const c of content)
|
|
3878
|
-
if (c?.type === "text" && typeof c.text === "string" && c.text.trim()) parts.push(c.text.trim());
|
|
3879
|
-
}
|
|
3880
|
-
if (j.message?.stop_reason === "end_turn") return { done: true, text: parts.join("\n\n") };
|
|
3881
|
-
}
|
|
3882
|
-
return { done: false, text: parts.join("\n\n") };
|
|
3883
|
-
}
|
|
3884
|
-
async function awaitAnswer(file, from, o) {
|
|
3885
|
-
const now = o.now ?? Date.now;
|
|
3886
|
-
const sleep2 = o.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms).unref?.()));
|
|
3887
|
-
const until = now() + o.timeoutMs;
|
|
3888
|
-
let offset = from;
|
|
3889
|
-
let partial = "";
|
|
3890
|
-
const lines = [];
|
|
3891
|
-
while (now() < until) {
|
|
3892
|
-
const size = transcriptEnd(file);
|
|
3893
|
-
if (size < offset) return null;
|
|
3894
|
-
if (size > offset) {
|
|
3895
|
-
const fd = openSync4(file, "r");
|
|
3896
|
-
try {
|
|
3897
|
-
const buf = Buffer.alloc(Math.min(size - offset, 4 * 1024 * 1024));
|
|
3898
|
-
const n = readSync4(fd, buf, 0, buf.length, offset);
|
|
3899
|
-
offset += n;
|
|
3900
|
-
const chunk = partial + buf.subarray(0, n).toString("utf8");
|
|
3901
|
-
const cut = chunk.lastIndexOf("\n");
|
|
3902
|
-
partial = cut < 0 ? chunk : chunk.slice(cut + 1);
|
|
3903
|
-
if (cut >= 0) lines.push(...chunk.slice(0, cut).split("\n").filter(Boolean));
|
|
3904
|
-
} finally {
|
|
3905
|
-
closeSync4(fd);
|
|
3906
|
-
}
|
|
3907
|
-
const a = answerIn(lines);
|
|
3908
|
-
if (a.done) return a.text || null;
|
|
3909
|
-
if (a.text) o.onText?.(a.text);
|
|
3910
|
-
}
|
|
3911
|
-
await sleep2(o.pollMs ?? 500);
|
|
3912
|
-
}
|
|
3913
|
-
return null;
|
|
3914
|
-
}
|
|
3915
|
-
|
|
3916
|
-
// src/control/route.ts
|
|
3917
|
-
function routePrompt(s) {
|
|
3918
|
-
if (s.sdk) return { mode: "sdk" };
|
|
3919
|
-
if (s.waiter) return { mode: "waiter" };
|
|
3920
|
-
if (s.midTurn) return { mode: "turn" };
|
|
3921
|
-
if (s.api) return { mode: "api" };
|
|
3922
|
-
if (s.harness !== "claude-code") return { unsupported: `no idle delivery for ${s.harness} yet` };
|
|
3923
|
-
if (!s.known) return { refused: "no_session" };
|
|
3924
|
-
if (!s.live) return { mode: "resume" };
|
|
3925
|
-
if (s.canFork) return { mode: "fork" };
|
|
3926
|
-
return { unsupported: "the session is open and this Claude Code can't copy it (no --fork-session)" };
|
|
3927
|
-
}
|
|
3928
|
-
function kindPath(kind, harness) {
|
|
3929
|
-
switch (kind) {
|
|
3930
|
-
case "prompt":
|
|
3931
|
-
return "prompt";
|
|
3932
|
-
case "permission.answer":
|
|
3933
|
-
return harness === "gemini-cli" ? { unsupported: "Gemini CLI has no permission answer channel" } : "permission";
|
|
3934
|
-
case "cancel":
|
|
3935
|
-
return "cancel";
|
|
3936
|
-
case "start":
|
|
3937
|
-
return harness === "claude-code" ? "start" : { unsupported: `can't start a ${harness} session` };
|
|
3938
|
-
default:
|
|
3939
|
-
return { unsupported: `unknown kind ${String(kind)}` };
|
|
3940
|
-
}
|
|
3941
|
-
}
|
|
3942
|
-
var IN_PLACE_ANSWER = "Answer it right here, as you would answer the person at the keyboard: your reply is sent back to them automatically. Don't run a command or call a tool just to send it.";
|
|
3943
|
-
function frame(text, receiver) {
|
|
3944
|
-
let host = receiver;
|
|
3945
|
-
try {
|
|
3946
|
-
host = new URL(receiver).host;
|
|
3947
|
-
} catch {
|
|
3948
|
-
}
|
|
3949
|
-
return `Message via ${host} (sessionpipe control, signed on the sender's device):
|
|
3950
|
-
|
|
3951
|
-
${text}`;
|
|
3952
|
-
}
|
|
3953
|
-
|
|
3954
4026
|
// src/control/store.ts
|
|
3955
|
-
|
|
3956
|
-
|
|
3957
|
-
chmodSync as chmodSync2,
|
|
3958
|
-
closeSync as closeSync5,
|
|
3959
|
-
existsSync as existsSync9,
|
|
3960
|
-
fsyncSync,
|
|
3961
|
-
mkdirSync as mkdirSync8,
|
|
3962
|
-
openSync as openSync5,
|
|
3963
|
-
readFileSync as readFileSync10,
|
|
3964
|
-
renameSync as renameSync3,
|
|
3965
|
-
writeFileSync as writeFileSync7
|
|
3966
|
-
} from "node:fs";
|
|
3967
|
-
import path16 from "node:path";
|
|
3968
|
-
var controlFile = (env = process.env) => path16.join(path16.dirname(configFile(env)), "control.json");
|
|
3969
|
-
var controlState = (env = process.env) => path16.join(stateDir(env), "control");
|
|
4027
|
+
var controlFile = (env = process.env) => path17.join(path17.dirname(configFile(env)), "control.json");
|
|
4028
|
+
var controlState = (env = process.env) => path17.join(stateDir(env), "control");
|
|
3970
4029
|
function writePrivate(file, value) {
|
|
3971
|
-
mkdirSync8(
|
|
4030
|
+
mkdirSync8(path17.dirname(file), { recursive: true, mode: 448 });
|
|
3972
4031
|
const tmp = `${file}.${process.pid}.tmp`;
|
|
3973
4032
|
writeFileSync7(tmp, `${JSON.stringify(value, null, 2)}
|
|
3974
4033
|
`, { mode: 384 });
|
|
@@ -3995,6 +4054,14 @@ function readControl(env = process.env, run = defaultRunner) {
|
|
|
3995
4054
|
return null;
|
|
3996
4055
|
}
|
|
3997
4056
|
}
|
|
4057
|
+
function pairedCount(env = process.env) {
|
|
4058
|
+
try {
|
|
4059
|
+
const j = JSON.parse(readFileSync10(controlFile(env), "utf8"));
|
|
4060
|
+
return Array.isArray(j.receivers) ? j.receivers.length : 0;
|
|
4061
|
+
} catch {
|
|
4062
|
+
return 0;
|
|
4063
|
+
}
|
|
4064
|
+
}
|
|
3998
4065
|
var writeControl = (c, env = process.env) => writePrivate(controlFile(env), {
|
|
3999
4066
|
...c,
|
|
4000
4067
|
receivers: c.receivers.map((r) => {
|
|
@@ -4060,14 +4127,14 @@ var NonceStore = class {
|
|
|
4060
4127
|
const t = this.seenAt.get(nonce);
|
|
4061
4128
|
if (t !== void 0 && t > this.now() - NONCE_TTL_MS) return true;
|
|
4062
4129
|
const at = this.now();
|
|
4063
|
-
mkdirSync8(
|
|
4064
|
-
const fd =
|
|
4130
|
+
mkdirSync8(path17.dirname(this.file), { recursive: true, mode: 448 });
|
|
4131
|
+
const fd = openSync4(this.file, "a", 384);
|
|
4065
4132
|
try {
|
|
4066
4133
|
appendFileSync4(fd, `${JSON.stringify({ n: nonce, t: at })}
|
|
4067
4134
|
`);
|
|
4068
4135
|
fsyncSync(fd);
|
|
4069
4136
|
} finally {
|
|
4070
|
-
|
|
4137
|
+
closeSync4(fd);
|
|
4071
4138
|
}
|
|
4072
4139
|
this.seenAt.set(nonce, at);
|
|
4073
4140
|
return false;
|
|
@@ -4076,9 +4143,9 @@ var NonceStore = class {
|
|
|
4076
4143
|
compact() {
|
|
4077
4144
|
const cut = this.now() - NONCE_TTL_MS;
|
|
4078
4145
|
for (const [n, t] of this.seenAt) if (t <= cut) this.seenAt.delete(n);
|
|
4079
|
-
if (!
|
|
4146
|
+
if (!existsSync10(this.file) && !this.seenAt.size) return;
|
|
4080
4147
|
const body = [...this.seenAt].map(([n, t]) => JSON.stringify({ n, t })).join("\n");
|
|
4081
|
-
mkdirSync8(
|
|
4148
|
+
mkdirSync8(path17.dirname(this.file), { recursive: true, mode: 448 });
|
|
4082
4149
|
writeFileSync7(`${this.file}.tmp`, body ? `${body}
|
|
4083
4150
|
` : "", { mode: 384 });
|
|
4084
4151
|
renameSync3(`${this.file}.tmp`, this.file);
|
|
@@ -4100,12 +4167,12 @@ var AckOutbox = class {
|
|
|
4100
4167
|
}
|
|
4101
4168
|
}
|
|
4102
4169
|
add(a) {
|
|
4103
|
-
mkdirSync8(
|
|
4170
|
+
mkdirSync8(path17.dirname(this.file), { recursive: true, mode: 448 });
|
|
4104
4171
|
appendFileSync4(this.file, `${JSON.stringify(a)}
|
|
4105
4172
|
`, { mode: 384 });
|
|
4106
4173
|
}
|
|
4107
4174
|
keep(left) {
|
|
4108
|
-
mkdirSync8(
|
|
4175
|
+
mkdirSync8(path17.dirname(this.file), { recursive: true, mode: 448 });
|
|
4109
4176
|
writeFileSync7(`${this.file}.tmp`, left.map((a) => JSON.stringify(a)).join("\n") + (left.length ? "\n" : ""), {
|
|
4110
4177
|
mode: 384
|
|
4111
4178
|
});
|
|
@@ -4122,6 +4189,424 @@ function readState(file) {
|
|
|
4122
4189
|
}
|
|
4123
4190
|
var writeState = (file, s) => writePrivate(file, s);
|
|
4124
4191
|
|
|
4192
|
+
// src/control/autoupdate.ts
|
|
4193
|
+
var UPDATE_EXIT = 75;
|
|
4194
|
+
var DAY_MS = 24 * 36e5;
|
|
4195
|
+
var LABEL = "org.sessionpipe.control";
|
|
4196
|
+
var UNIT = "sessionpipe-control.service";
|
|
4197
|
+
var updateFile = (env = process.env) => path18.join(controlState(env), "update.json");
|
|
4198
|
+
function readUpdate(file) {
|
|
4199
|
+
try {
|
|
4200
|
+
const j = JSON.parse(readFileSync11(file, "utf8"));
|
|
4201
|
+
return j && typeof j === "object" ? j : {};
|
|
4202
|
+
} catch {
|
|
4203
|
+
return {};
|
|
4204
|
+
}
|
|
4205
|
+
}
|
|
4206
|
+
function isDue(rec, now) {
|
|
4207
|
+
const t = Date.parse(rec.checked_at ?? "");
|
|
4208
|
+
if (!Number.isFinite(t)) return true;
|
|
4209
|
+
if (t > now + 5 * 6e4) return true;
|
|
4210
|
+
return now - t >= DAY_MS;
|
|
4211
|
+
}
|
|
4212
|
+
function autoUpdateOff(o) {
|
|
4213
|
+
if (o.env.SESSIONPIPE_NO_UPDATE_CHECK) return "SESSIONPIPE_NO_UPDATE_CHECK is set";
|
|
4214
|
+
if (o.config.update_check === false) return "update_check is false in config.json (`sessionpipe update on`)";
|
|
4215
|
+
const prefix = globalPrefix(o.distDir, o.platform);
|
|
4216
|
+
if (!prefix) return "this copy isn't a global npm install (npx, a checkout or a dev build)";
|
|
4217
|
+
return unwritable(prefix, o.platform);
|
|
4218
|
+
}
|
|
4219
|
+
function updateSummary(o) {
|
|
4220
|
+
const daemonEnv = o.rec.off?.startsWith("SESSIONPIPE_NO_UPDATE_CHECK") ? "SESSIONPIPE_NO_UPDATE_CHECK is set in the daemon's environment" : null;
|
|
4221
|
+
const off2 = o.off ?? (o.paired ? daemonEnv : "it runs in the control daemon, which isn't set up here (`sessionpipe connect`)");
|
|
4222
|
+
return {
|
|
4223
|
+
auto: !off2,
|
|
4224
|
+
off: off2,
|
|
4225
|
+
last_check: o.rec.checked_at ?? null,
|
|
4226
|
+
latest: o.rec.latest ?? null,
|
|
4227
|
+
last_error: o.rec.error ?? null,
|
|
4228
|
+
pending: o.rec.pending ?? null,
|
|
4229
|
+
updated: o.rec.updated ?? null,
|
|
4230
|
+
failed: o.rec.failed ?? null
|
|
4231
|
+
};
|
|
4232
|
+
}
|
|
4233
|
+
function ago(iso, now) {
|
|
4234
|
+
const ms = now - Date.parse(iso);
|
|
4235
|
+
if (!Number.isFinite(ms)) return "at an unreadable time";
|
|
4236
|
+
if (ms < 9e4) return "just now";
|
|
4237
|
+
if (ms < 90 * 6e4) return `${Math.round(ms / 6e4)} min ago`;
|
|
4238
|
+
if (ms < 36 * 36e5) return `${Math.round(ms / 36e5)} h ago`;
|
|
4239
|
+
return `${Math.round(ms / DAY_MS)} days ago`;
|
|
4240
|
+
}
|
|
4241
|
+
function updateLine(s, now) {
|
|
4242
|
+
const parts = [
|
|
4243
|
+
s.auto ? "automatic (the control daemon checks npm once a day and installs when idle; `sessionpipe update off` stops it)" : `not automatic: ${s.off}`
|
|
4244
|
+
];
|
|
4245
|
+
if (s.last_check)
|
|
4246
|
+
parts.push(
|
|
4247
|
+
s.last_error ? `last check ${ago(s.last_check, now)} failed: ${s.last_error}` : `last check ${ago(s.last_check, now)}: latest ${s.latest ?? "?"}`
|
|
4248
|
+
);
|
|
4249
|
+
else if (s.auto) parts.push("not checked yet (a minute after the daemon starts)");
|
|
4250
|
+
if (s.pending) parts.push(`${s.pending} waits for the daemon to be idle`);
|
|
4251
|
+
if (s.failed) parts.push(`installing ${s.failed.version} failed ${ago(s.failed.at, now)}: ${s.failed.error}`);
|
|
4252
|
+
else if (s.updated) parts.push(`updated ${s.updated.from} \u2192 ${s.updated.to} ${ago(s.updated.at, now)}`);
|
|
4253
|
+
return parts.join(" \xB7 ");
|
|
4254
|
+
}
|
|
4255
|
+
function restartVia(o) {
|
|
4256
|
+
const s = o.env.SESSIONPIPE_SERVICE ?? "";
|
|
4257
|
+
if (s.startsWith("supervisor:") && s === `supervisor:${o.ppid}`) return "supervisor";
|
|
4258
|
+
if (o.platform === "darwin" && o.ppid === 1 && (s === "launchd" || o.env.XPC_SERVICE_NAME === LABEL))
|
|
4259
|
+
return "launchd";
|
|
4260
|
+
if (o.platform === "linux") {
|
|
4261
|
+
let cg = "";
|
|
4262
|
+
try {
|
|
4263
|
+
cg = (o.cgroup ?? (() => readFileSync11("/proc/self/cgroup", "utf8")))();
|
|
4264
|
+
} catch {
|
|
4265
|
+
}
|
|
4266
|
+
const inUnit = cg.split("\n").some((l) => l.endsWith(`/${UNIT}`) || l.includes(`/${UNIT}/`));
|
|
4267
|
+
const mainPid = !o.env.SYSTEMD_EXEC_PID || o.env.SYSTEMD_EXEC_PID === String(o.pid);
|
|
4268
|
+
if (inUnit && mainPid) return "systemd";
|
|
4269
|
+
}
|
|
4270
|
+
return null;
|
|
4271
|
+
}
|
|
4272
|
+
var sleep = (ms) => new Promise((r) => setTimeout(r, ms).unref?.());
|
|
4273
|
+
var AutoUpdater = class {
|
|
4274
|
+
constructor(d) {
|
|
4275
|
+
this.d = d;
|
|
4276
|
+
}
|
|
4277
|
+
d;
|
|
4278
|
+
first = null;
|
|
4279
|
+
every = null;
|
|
4280
|
+
stopped = false;
|
|
4281
|
+
busyWith = null;
|
|
4282
|
+
lastOff = void 0;
|
|
4283
|
+
start() {
|
|
4284
|
+
if (this.record().pending) this.save({ pending: null });
|
|
4285
|
+
this.first = setTimeout(() => void this.tick(), this.d.firstCheckMs ?? 6e4);
|
|
4286
|
+
this.first.unref?.();
|
|
4287
|
+
this.every = setInterval(() => void this.tick(), this.d.tickMs ?? 10 * 6e4);
|
|
4288
|
+
this.every.unref?.();
|
|
4289
|
+
}
|
|
4290
|
+
stop() {
|
|
4291
|
+
this.stopped = true;
|
|
4292
|
+
if (this.first) clearTimeout(this.first);
|
|
4293
|
+
if (this.every) clearInterval(this.every);
|
|
4294
|
+
}
|
|
4295
|
+
record() {
|
|
4296
|
+
return readUpdate(this.d.file);
|
|
4297
|
+
}
|
|
4298
|
+
save(patch) {
|
|
4299
|
+
try {
|
|
4300
|
+
writePrivate(this.d.file, { ...readUpdate(this.d.file), ...patch });
|
|
4301
|
+
} catch {
|
|
4302
|
+
}
|
|
4303
|
+
}
|
|
4304
|
+
/** One look at the clock: check when a check is due and nothing is under way. */
|
|
4305
|
+
async tick() {
|
|
4306
|
+
if (this.stopped || this.busyWith) return;
|
|
4307
|
+
const off2 = this.d.off();
|
|
4308
|
+
if (off2 !== this.lastOff) {
|
|
4309
|
+
if (off2) this.d.log(`auto-update off: ${off2}`);
|
|
4310
|
+
else if (this.lastOff) this.d.log("auto-update on");
|
|
4311
|
+
this.lastOff = off2;
|
|
4312
|
+
this.save({ off: off2 });
|
|
4313
|
+
}
|
|
4314
|
+
if (off2) return;
|
|
4315
|
+
const rec = this.record();
|
|
4316
|
+
if (isDue(rec, this.d.now())) return this.check();
|
|
4317
|
+
const known = rec.latest;
|
|
4318
|
+
if (known && isNewer(known, this.d.version) && rec.failed?.version !== known)
|
|
4319
|
+
return this.guard(() => this.apply(known));
|
|
4320
|
+
}
|
|
4321
|
+
/** One update job at a time; whatever it throws is logged, never left unhandled. */
|
|
4322
|
+
guard(job) {
|
|
4323
|
+
if (this.busyWith) return this.busyWith;
|
|
4324
|
+
this.busyWith = job().catch((e) => {
|
|
4325
|
+
if (!this.stopped) this.d.log(`update: ${String(e?.message || e)}`);
|
|
4326
|
+
}).finally(() => {
|
|
4327
|
+
this.busyWith = null;
|
|
4328
|
+
});
|
|
4329
|
+
return this.busyWith;
|
|
4330
|
+
}
|
|
4331
|
+
/** Ask npm now; when it has a newer release, install it once idle and restart. */
|
|
4332
|
+
check() {
|
|
4333
|
+
return this.guard(async () => {
|
|
4334
|
+
const t0 = this.d.now();
|
|
4335
|
+
let latest;
|
|
4336
|
+
try {
|
|
4337
|
+
latest = await this.d.latest();
|
|
4338
|
+
} catch (e) {
|
|
4339
|
+
const error = String(e?.message || e).slice(0, 200);
|
|
4340
|
+
this.save({ checked_at: new Date(t0).toISOString(), latest: null, error, running: this.d.version });
|
|
4341
|
+
this.d.log(`update check: failed after ${this.d.now() - t0} ms: ${error}`);
|
|
4342
|
+
return;
|
|
4343
|
+
}
|
|
4344
|
+
const ms = this.d.now() - t0;
|
|
4345
|
+
this.save({ checked_at: new Date(t0).toISOString(), latest, error: null, running: this.d.version });
|
|
4346
|
+
if (!isNewer(latest, this.d.version)) {
|
|
4347
|
+
this.save({ pending: null });
|
|
4348
|
+
this.d.log(`update check: ${this.d.version} is current (npm latest ${latest}, ${ms} ms)`);
|
|
4349
|
+
return;
|
|
4350
|
+
}
|
|
4351
|
+
this.d.log(`update check: ${latest} is out (running ${this.d.version}, ${ms} ms); updating once idle`);
|
|
4352
|
+
await this.apply(latest);
|
|
4353
|
+
});
|
|
4354
|
+
}
|
|
4355
|
+
/** Restart onto the copy on disk once idle (`sessionpipe update` installed it). */
|
|
4356
|
+
restartWhenIdle() {
|
|
4357
|
+
if (this.stopped) return false;
|
|
4358
|
+
if (this.busyWith) return true;
|
|
4359
|
+
void this.guard(async () => {
|
|
4360
|
+
const onDisk = await this.d.installed();
|
|
4361
|
+
if (!onDisk || onDisk === this.d.version) {
|
|
4362
|
+
this.d.log(`restart asked: the copy on disk is ${onDisk ?? "unreadable"}, the one running; staying`);
|
|
4363
|
+
return;
|
|
4364
|
+
}
|
|
4365
|
+
await this.quiesce(onDisk, false);
|
|
4366
|
+
await this.resumeOnError(async () => {
|
|
4367
|
+
this.d.log(`restart: ${this.d.version} \u2192 ${onDisk} (installed by hand)`);
|
|
4368
|
+
this.save({ updated: { from: this.d.version, to: onDisk, at: new Date(this.d.now()).toISOString(), ms: 0 } });
|
|
4369
|
+
await this.d.restart(this.d.version, onDisk);
|
|
4370
|
+
});
|
|
4371
|
+
});
|
|
4372
|
+
return true;
|
|
4373
|
+
}
|
|
4374
|
+
/** Wait until the daemon has nothing in hand, then stop its intake. A message that
|
|
4375
|
+
* slipped in between is let through and waited for. An automatic update turned off
|
|
4376
|
+
* meanwhile (`sessionpipe update off`) stops waiting. */
|
|
4377
|
+
async quiesce(target, auto = true) {
|
|
4378
|
+
let said = false;
|
|
4379
|
+
for (; ; ) {
|
|
4380
|
+
let why = this.d.daemon.busy();
|
|
4381
|
+
while (why) {
|
|
4382
|
+
if (this.stopped) throw new Error("stopped");
|
|
4383
|
+
const off2 = auto ? this.d.off() : null;
|
|
4384
|
+
if (off2) {
|
|
4385
|
+
this.save({ pending: null });
|
|
4386
|
+
throw new Error(`${target} not installed: auto-update was turned off (${off2})`);
|
|
4387
|
+
}
|
|
4388
|
+
if (!said) {
|
|
4389
|
+
this.d.log(`update: ${target} waits until the daemon is idle (${why})`);
|
|
4390
|
+
said = true;
|
|
4391
|
+
}
|
|
4392
|
+
await sleep(this.d.idlePollMs ?? 15e3);
|
|
4393
|
+
why = this.d.daemon.busy();
|
|
4394
|
+
}
|
|
4395
|
+
this.d.daemon.pauseIntake();
|
|
4396
|
+
if (!this.d.daemon.busy()) return;
|
|
4397
|
+
this.d.daemon.resumeIntake();
|
|
4398
|
+
}
|
|
4399
|
+
}
|
|
4400
|
+
/** Intake is paused from quiesce() on: whatever goes wrong after it lets it go. */
|
|
4401
|
+
async resumeOnError(fn) {
|
|
4402
|
+
try {
|
|
4403
|
+
await fn();
|
|
4404
|
+
} catch (e) {
|
|
4405
|
+
this.d.daemon.resumeIntake();
|
|
4406
|
+
throw e;
|
|
4407
|
+
}
|
|
4408
|
+
}
|
|
4409
|
+
async apply(target) {
|
|
4410
|
+
this.save({ pending: target });
|
|
4411
|
+
await this.quiesce(target);
|
|
4412
|
+
await this.resumeOnError(() => this.installAndRestart(target));
|
|
4413
|
+
}
|
|
4414
|
+
async installAndRestart(target) {
|
|
4415
|
+
const from = this.d.version;
|
|
4416
|
+
const t0 = this.d.now();
|
|
4417
|
+
const onDisk = await this.d.installed();
|
|
4418
|
+
let note = "";
|
|
4419
|
+
const c = onDisk ? compareVersions(onDisk, target) : null;
|
|
4420
|
+
if (onDisk && c !== null && c >= 0) {
|
|
4421
|
+
target = onDisk;
|
|
4422
|
+
note = " (already on disk)";
|
|
4423
|
+
} else {
|
|
4424
|
+
const r = await this.d.install(target);
|
|
4425
|
+
if (!r.ok) {
|
|
4426
|
+
this.d.daemon.resumeIntake();
|
|
4427
|
+
const ms2 = this.d.now() - t0;
|
|
4428
|
+
this.save({
|
|
4429
|
+
pending: null,
|
|
4430
|
+
failed: { version: target, at: new Date(this.d.now()).toISOString(), error: r.error }
|
|
4431
|
+
});
|
|
4432
|
+
this.d.log(`update: ${from} \u2192 ${target} failed after ${ms2} ms: ${r.error}; still running ${from}`);
|
|
4433
|
+
return;
|
|
4434
|
+
}
|
|
4435
|
+
if (r.hooks) note = `; ${r.hooks}`;
|
|
4436
|
+
}
|
|
4437
|
+
const ms = this.d.now() - t0;
|
|
4438
|
+
if (this.d.daemon.busy()) {
|
|
4439
|
+
this.d.daemon.resumeIntake();
|
|
4440
|
+
await this.quiesce(target, false);
|
|
4441
|
+
}
|
|
4442
|
+
this.save({
|
|
4443
|
+
pending: null,
|
|
4444
|
+
failed: null,
|
|
4445
|
+
updated: { from, to: target, at: new Date(this.d.now()).toISOString(), ms }
|
|
4446
|
+
});
|
|
4447
|
+
this.d.log(`update: ${from} \u2192 ${target} in ${ms} ms${note}; restarting`);
|
|
4448
|
+
await this.d.restart(from, target);
|
|
4449
|
+
}
|
|
4450
|
+
};
|
|
4451
|
+
function supervise(o) {
|
|
4452
|
+
const run = o.spawn ?? spawn3;
|
|
4453
|
+
const exit = o.exit ?? ((c) => process.exit(c));
|
|
4454
|
+
const env = { ...o.env, SESSIONPIPE_SERVICE: `supervisor:${process.pid}` };
|
|
4455
|
+
let child = null;
|
|
4456
|
+
let stopping = false;
|
|
4457
|
+
let later = null;
|
|
4458
|
+
const start = () => {
|
|
4459
|
+
later = null;
|
|
4460
|
+
const c = run(o.node, o.args, { env, stdio: "inherit", windowsHide: true });
|
|
4461
|
+
child = c;
|
|
4462
|
+
let done = false;
|
|
4463
|
+
const ended = (code, signal) => {
|
|
4464
|
+
if (done) return;
|
|
4465
|
+
done = true;
|
|
4466
|
+
child = null;
|
|
4467
|
+
if (stopping) return exit(code ?? 0);
|
|
4468
|
+
if (code === UPDATE_EXIT) {
|
|
4469
|
+
o.log("the daemon is restarting onto its new version");
|
|
4470
|
+
return start();
|
|
4471
|
+
}
|
|
4472
|
+
if (code === 0) return exit(0);
|
|
4473
|
+
o.log(`the daemon exited (${signal ?? `code ${code}`}); starting it again in 5 s`);
|
|
4474
|
+
later = setTimeout(start, o.failDelayMs ?? 5e3);
|
|
4475
|
+
};
|
|
4476
|
+
c.on("exit", ended);
|
|
4477
|
+
c.on("error", (e) => {
|
|
4478
|
+
o.log(`couldn't start the daemon: ${e.message}`);
|
|
4479
|
+
ended(1, null);
|
|
4480
|
+
});
|
|
4481
|
+
};
|
|
4482
|
+
const stop = (sig = "SIGTERM") => {
|
|
4483
|
+
stopping = true;
|
|
4484
|
+
if (later) clearTimeout(later);
|
|
4485
|
+
if (child) child.kill(sig);
|
|
4486
|
+
else exit(0);
|
|
4487
|
+
};
|
|
4488
|
+
start();
|
|
4489
|
+
return { stop };
|
|
4490
|
+
}
|
|
4491
|
+
|
|
4492
|
+
// src/control/cli.ts
|
|
4493
|
+
import { existsSync as existsSync13, realpathSync as realpathSync5, unwatchFile, watchFile } from "node:fs";
|
|
4494
|
+
import os7 from "node:os";
|
|
4495
|
+
import path21 from "node:path";
|
|
4496
|
+
|
|
4497
|
+
// src/control/daemon.ts
|
|
4498
|
+
import { existsSync as existsSync11, mkdirSync as mkdirSync9, realpathSync as realpathSync4, statSync as statSync8, unlinkSync as unlinkSync5 } from "node:fs";
|
|
4499
|
+
import net2 from "node:net";
|
|
4500
|
+
import path19 from "node:path";
|
|
4501
|
+
|
|
4502
|
+
// src/control/reply.ts
|
|
4503
|
+
import { closeSync as closeSync5, openSync as openSync5, readSync as readSync4, statSync as statSync7 } from "node:fs";
|
|
4504
|
+
function transcriptEnd(file) {
|
|
4505
|
+
try {
|
|
4506
|
+
return statSync7(file).size;
|
|
4507
|
+
} catch {
|
|
4508
|
+
return 0;
|
|
4509
|
+
}
|
|
4510
|
+
}
|
|
4511
|
+
function answerIn(lines) {
|
|
4512
|
+
const parts = [];
|
|
4513
|
+
for (const line of lines) {
|
|
4514
|
+
let j;
|
|
4515
|
+
try {
|
|
4516
|
+
j = JSON.parse(line);
|
|
4517
|
+
} catch {
|
|
4518
|
+
continue;
|
|
4519
|
+
}
|
|
4520
|
+
if (j.type !== "assistant" || j.isSidechain) continue;
|
|
4521
|
+
const content = j.message?.content;
|
|
4522
|
+
if (Array.isArray(content)) {
|
|
4523
|
+
for (const c of content)
|
|
4524
|
+
if (c?.type === "text" && typeof c.text === "string" && c.text.trim()) parts.push(c.text.trim());
|
|
4525
|
+
}
|
|
4526
|
+
if (j.message?.stop_reason === "end_turn") return { done: true, text: parts.join("\n\n") };
|
|
4527
|
+
}
|
|
4528
|
+
return { done: false, text: parts.join("\n\n") };
|
|
4529
|
+
}
|
|
4530
|
+
async function awaitAnswer(file, from, o) {
|
|
4531
|
+
const now = o.now ?? Date.now;
|
|
4532
|
+
const sleep3 = o.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms).unref?.()));
|
|
4533
|
+
const until = now() + o.timeoutMs;
|
|
4534
|
+
let offset = from;
|
|
4535
|
+
let partial = "";
|
|
4536
|
+
const lines = [];
|
|
4537
|
+
while (now() < until) {
|
|
4538
|
+
const size = transcriptEnd(file);
|
|
4539
|
+
if (size < offset) return null;
|
|
4540
|
+
if (size > offset) {
|
|
4541
|
+
const fd = openSync5(file, "r");
|
|
4542
|
+
try {
|
|
4543
|
+
const buf = Buffer.alloc(Math.min(size - offset, 4 * 1024 * 1024));
|
|
4544
|
+
const n = readSync4(fd, buf, 0, buf.length, offset);
|
|
4545
|
+
offset += n;
|
|
4546
|
+
const chunk = partial + buf.subarray(0, n).toString("utf8");
|
|
4547
|
+
const cut = chunk.lastIndexOf("\n");
|
|
4548
|
+
partial = cut < 0 ? chunk : chunk.slice(cut + 1);
|
|
4549
|
+
if (cut >= 0) lines.push(...chunk.slice(0, cut).split("\n").filter(Boolean));
|
|
4550
|
+
} finally {
|
|
4551
|
+
closeSync5(fd);
|
|
4552
|
+
}
|
|
4553
|
+
const a = answerIn(lines);
|
|
4554
|
+
if (a.done) return a.text || null;
|
|
4555
|
+
if (a.text) o.onText?.(a.text);
|
|
4556
|
+
}
|
|
4557
|
+
await sleep3(o.pollMs ?? 500);
|
|
4558
|
+
}
|
|
4559
|
+
return null;
|
|
4560
|
+
}
|
|
4561
|
+
|
|
4562
|
+
// src/control/route.ts
|
|
4563
|
+
function routePrompt(s) {
|
|
4564
|
+
if (s.sdk) return { mode: "sdk" };
|
|
4565
|
+
if (s.waiter) return { mode: "waiter" };
|
|
4566
|
+
if (s.midTurn) return { mode: "turn" };
|
|
4567
|
+
if (s.api) return { mode: "api" };
|
|
4568
|
+
if (s.harness !== "claude-code") return { unsupported: `no idle delivery for ${s.harness} yet` };
|
|
4569
|
+
if (!s.known) return { refused: "no_session" };
|
|
4570
|
+
if (!s.live) return { mode: "resume" };
|
|
4571
|
+
if (s.canFork) return { mode: "fork" };
|
|
4572
|
+
return { unsupported: "the session is open and this Claude Code can't copy it (no --fork-session)" };
|
|
4573
|
+
}
|
|
4574
|
+
function kindPath(kind, harness) {
|
|
4575
|
+
switch (kind) {
|
|
4576
|
+
case "prompt":
|
|
4577
|
+
return "prompt";
|
|
4578
|
+
case "permission.answer":
|
|
4579
|
+
return harness === "gemini-cli" ? { unsupported: "Gemini CLI has no permission answer channel" } : "permission";
|
|
4580
|
+
case "cancel":
|
|
4581
|
+
return "cancel";
|
|
4582
|
+
case "start":
|
|
4583
|
+
return harness === "claude-code" ? "start" : { unsupported: `can't start a ${harness} session` };
|
|
4584
|
+
default:
|
|
4585
|
+
return { unsupported: `unknown kind ${String(kind)}` };
|
|
4586
|
+
}
|
|
4587
|
+
}
|
|
4588
|
+
var IN_PLACE_ANSWER = "Answer it right here, as you would answer the person at the keyboard: your reply is sent back to them automatically. Don't run a command or call a tool just to send it.";
|
|
4589
|
+
function frame(text, receiver) {
|
|
4590
|
+
let host = receiver;
|
|
4591
|
+
try {
|
|
4592
|
+
host = new URL(receiver).host;
|
|
4593
|
+
} catch {
|
|
4594
|
+
}
|
|
4595
|
+
return `Message via ${host} (sessionpipe control, signed on the sender's device):
|
|
4596
|
+
|
|
4597
|
+
${text}`;
|
|
4598
|
+
}
|
|
4599
|
+
function frameStart(text, receiver) {
|
|
4600
|
+
let host = receiver;
|
|
4601
|
+
try {
|
|
4602
|
+
host = new URL(receiver).host;
|
|
4603
|
+
} catch {
|
|
4604
|
+
}
|
|
4605
|
+
return `${text}
|
|
4606
|
+
|
|
4607
|
+
(Sent via ${host} with sessionpipe control, signed on the sender's device.)`;
|
|
4608
|
+
}
|
|
4609
|
+
|
|
4125
4610
|
// src/control/stream.ts
|
|
4126
4611
|
function streamJsonPiece(line) {
|
|
4127
4612
|
let j;
|
|
@@ -4212,7 +4697,7 @@ var {
|
|
|
4212
4697
|
recentlyWritten: recentlyWritten2,
|
|
4213
4698
|
sessionFolder: sessionFolder2
|
|
4214
4699
|
} = claude_code_control_exports;
|
|
4215
|
-
var VERSION2 = true ? "0.
|
|
4700
|
+
var VERSION2 = true ? "0.6.1" : "0.0.0";
|
|
4216
4701
|
var MAX_REPLY = 2e4;
|
|
4217
4702
|
var HELLO_EVERY_MS = 10 * 6e4;
|
|
4218
4703
|
var TURN_FALLBACK_MS = 30 * 6e4;
|
|
@@ -4226,6 +4711,15 @@ var CLOSES_ATTENTION = /* @__PURE__ */ new Set([
|
|
|
4226
4711
|
"UserPromptSubmit",
|
|
4227
4712
|
"SessionEnd"
|
|
4228
4713
|
]);
|
|
4714
|
+
var DaemonRunning = class extends Error {
|
|
4715
|
+
constructor(other) {
|
|
4716
|
+
super(
|
|
4717
|
+
`another control daemon already serves this machine (${other.pid ? `pid ${other.pid}, ` : ""}sessionpipe ${other.version ?? "?"})`
|
|
4718
|
+
);
|
|
4719
|
+
this.other = other;
|
|
4720
|
+
}
|
|
4721
|
+
other;
|
|
4722
|
+
};
|
|
4229
4723
|
function sessionToolsFrom(v) {
|
|
4230
4724
|
if (!Array.isArray(v)) return [];
|
|
4231
4725
|
return [
|
|
@@ -4239,9 +4733,9 @@ var ControlDaemon = class {
|
|
|
4239
4733
|
this.dir = dir;
|
|
4240
4734
|
this.sock = sock;
|
|
4241
4735
|
this.deps = deps;
|
|
4242
|
-
this.nonces = new NonceStore(
|
|
4243
|
-
this.acks = new AckOutbox(
|
|
4244
|
-
this.state = readState(
|
|
4736
|
+
this.nonces = new NonceStore(path19.join(dir, "nonces.jsonl"), deps.now);
|
|
4737
|
+
this.acks = new AckOutbox(path19.join(dir, "acks.jsonl"));
|
|
4738
|
+
this.state = readState(path19.join(dir, "state.json"));
|
|
4245
4739
|
}
|
|
4246
4740
|
cfg;
|
|
4247
4741
|
dir;
|
|
@@ -4276,6 +4770,17 @@ var ControlDaemon = class {
|
|
|
4276
4770
|
procWaiters = [];
|
|
4277
4771
|
/** configDir → the last login answer and when (a yes is kept 10 minutes, a no 30 s). */
|
|
4278
4772
|
logins = /* @__PURE__ */ new Map();
|
|
4773
|
+
/** Intake paused for an update: the long-polls end and wait for this. */
|
|
4774
|
+
paused = null;
|
|
4775
|
+
/** Poll answers whose messages are being read and handed out. */
|
|
4776
|
+
receiving = 0;
|
|
4777
|
+
/** The socket file this daemon created (dev:ino): stop() removes only that one, and a
|
|
4778
|
+
* different file at the path means another daemon took over. */
|
|
4779
|
+
sockId = null;
|
|
4780
|
+
ownTimer = null;
|
|
4781
|
+
/** Every open local connection: stop() ends them, or the server's close would wait
|
|
4782
|
+
* on a client that never says anything. */
|
|
4783
|
+
conns = /* @__PURE__ */ new Set();
|
|
4279
4784
|
// --- lifecycle ------------------------------------------------------------------
|
|
4280
4785
|
async start() {
|
|
4281
4786
|
this.running = true;
|
|
@@ -4288,24 +4793,97 @@ var ControlDaemon = class {
|
|
|
4288
4793
|
void this.flushAcks();
|
|
4289
4794
|
const idle = setInterval(() => this.closeIdleSdk(), 6e4);
|
|
4290
4795
|
idle.unref();
|
|
4291
|
-
|
|
4796
|
+
if (this.sockId) {
|
|
4797
|
+
this.ownTimer = setInterval(() => void this.checkSocket(), 1e4);
|
|
4798
|
+
this.ownTimer.unref();
|
|
4799
|
+
}
|
|
4800
|
+
this.deps.log(
|
|
4801
|
+
`control daemon ${VERSION2} up \xB7 pid ${process.pid} \xB7 ${this.cfg.receivers.length} receiver(s) \xB7 socket ${this.sock}`
|
|
4802
|
+
);
|
|
4292
4803
|
}
|
|
4293
|
-
|
|
4804
|
+
/** `leaveSocket`: the socket path is another daemon's now. Closing a Unix-socket
|
|
4805
|
+
* server makes Node unlink its path whoever holds it, so this one's server is left
|
|
4806
|
+
* open for the process's exit to drop. */
|
|
4807
|
+
async stop(o = {}) {
|
|
4294
4808
|
this.running = false;
|
|
4809
|
+
this.paused?.go();
|
|
4295
4810
|
for (const a of this.aborts) a.abort();
|
|
4296
4811
|
if (this.helloTimer) clearInterval(this.helloTimer);
|
|
4297
4812
|
if (this.helloSoon) clearTimeout(this.helloSoon);
|
|
4813
|
+
if (this.ownTimer) clearInterval(this.ownTimer);
|
|
4298
4814
|
for (const s of this.sdkSessions.values()) s.close();
|
|
4299
4815
|
for (const l of this.live.values()) {
|
|
4300
4816
|
l.waiter?.destroy();
|
|
4301
4817
|
for (const a of l.attentions.values()) a.destroy();
|
|
4302
4818
|
}
|
|
4819
|
+
for (const c of this.conns) c.destroy();
|
|
4820
|
+
if (o.leaveSocket) {
|
|
4821
|
+
this.sockId = null;
|
|
4822
|
+
return;
|
|
4823
|
+
}
|
|
4303
4824
|
await new Promise((r) => this.server ? this.server.close(() => r()) : r());
|
|
4304
|
-
|
|
4825
|
+
this.server = null;
|
|
4826
|
+
if (process.platform !== "win32" && this.sockId && socketId(this.sock) === this.sockId)
|
|
4305
4827
|
try {
|
|
4306
4828
|
unlinkSync5(this.sock);
|
|
4307
4829
|
} catch {
|
|
4308
4830
|
}
|
|
4831
|
+
this.sockId = null;
|
|
4832
|
+
}
|
|
4833
|
+
/** Null when nothing is in hand, so a restart would interrupt nothing; otherwise what
|
|
4834
|
+
* is. Parked waiters don't count: a waiter reconnects in silence (CONTROL.md §8). A
|
|
4835
|
+
* session someone is working in mid-turn does (a restart forgets it is mid-turn, so a
|
|
4836
|
+
* message to it would fork rather than wait for its Stop), unless it has been silent
|
|
4837
|
+
* for ten minutes: a session killed mid-turn never sends its Stop. */
|
|
4838
|
+
busy() {
|
|
4839
|
+
if (this.inflight.size) return `${this.inflight.size} message(s) in hand`;
|
|
4840
|
+
if (this.procs || this.ownRuns.size) return "a headless Claude Code run";
|
|
4841
|
+
if (this.startsRunning) return "a session starting";
|
|
4842
|
+
for (const s of this.sdkSessions.values()) if (s.busy) return "an Agent SDK session mid-turn";
|
|
4843
|
+
for (const l of this.live.values()) if (l.attentions.size) return "a permission prompt waiting for an answer";
|
|
4844
|
+
const now = this.deps.now();
|
|
4845
|
+
for (const l of this.live.values())
|
|
4846
|
+
if (l.midTurn && now - l.lastEvent < 10 * 6e4) return "a session on this machine mid-turn";
|
|
4847
|
+
if (this.receiving) return "messages just received";
|
|
4848
|
+
if (this.flushing) return "acks being sent";
|
|
4849
|
+
return null;
|
|
4850
|
+
}
|
|
4851
|
+
/** Stop taking messages: every long-poll ends now and waits for resumeIntake(). The
|
|
4852
|
+
* receiver keeps what arrives meanwhile; the local socket keeps answering. */
|
|
4853
|
+
pauseIntake() {
|
|
4854
|
+
if (this.paused) return;
|
|
4855
|
+
let go;
|
|
4856
|
+
const until = new Promise((r) => {
|
|
4857
|
+
go = r;
|
|
4858
|
+
});
|
|
4859
|
+
this.paused = { until, go };
|
|
4860
|
+
for (const a of this.aborts) a.abort();
|
|
4861
|
+
}
|
|
4862
|
+
resumeIntake() {
|
|
4863
|
+
const p = this.paused;
|
|
4864
|
+
this.paused = null;
|
|
4865
|
+
p?.go();
|
|
4866
|
+
}
|
|
4867
|
+
/** The socket path still holds this daemon's file. Gone: listen again. Replaced:
|
|
4868
|
+
* another daemon took over (two started at once), so this one steps aside. */
|
|
4869
|
+
async checkSocket() {
|
|
4870
|
+
if (!this.running || !this.sockId) return;
|
|
4871
|
+
const now = socketId(this.sock);
|
|
4872
|
+
if (now === this.sockId) return;
|
|
4873
|
+
if (now === null) {
|
|
4874
|
+
this.deps.log(`the socket ${this.sock} vanished; listening again`);
|
|
4875
|
+
this.server?.unref();
|
|
4876
|
+
this.server = null;
|
|
4877
|
+
try {
|
|
4878
|
+
await this.listen();
|
|
4879
|
+
} catch (e) {
|
|
4880
|
+
this.deps.log(`couldn't listen again: ${String(e?.message || e)}`);
|
|
4881
|
+
}
|
|
4882
|
+
return;
|
|
4883
|
+
}
|
|
4884
|
+
this.deps.log("another control daemon took over the socket; this one stops");
|
|
4885
|
+
await this.stop({ leaveSocket: true });
|
|
4886
|
+
this.deps.onLostSocket?.();
|
|
4309
4887
|
}
|
|
4310
4888
|
reload(cfg) {
|
|
4311
4889
|
const known = new Set(this.cfg.receivers.map((r) => r.control + r.machine));
|
|
@@ -4315,6 +4893,7 @@ var ControlDaemon = class {
|
|
|
4315
4893
|
status() {
|
|
4316
4894
|
return {
|
|
4317
4895
|
version: VERSION2,
|
|
4896
|
+
pid: process.pid,
|
|
4318
4897
|
name: this.cfg.name,
|
|
4319
4898
|
receivers: this.cfg.receivers.map((r) => {
|
|
4320
4899
|
const p = this.polling.get(r.control + r.machine);
|
|
@@ -4333,26 +4912,35 @@ var ControlDaemon = class {
|
|
|
4333
4912
|
sdk: [...this.sdkSessions.keys()],
|
|
4334
4913
|
nonces: this.nonces.size,
|
|
4335
4914
|
acks_pending: this.acks.all().length,
|
|
4336
|
-
delivered: { ...this.delivered }
|
|
4915
|
+
delivered: { ...this.delivered },
|
|
4916
|
+
busy: this.busy(),
|
|
4917
|
+
...this.deps.updateStatus ? { update: this.deps.updateStatus() } : {}
|
|
4337
4918
|
};
|
|
4338
4919
|
}
|
|
4339
4920
|
// --- the local socket -----------------------------------------------------------
|
|
4340
|
-
|
|
4921
|
+
/** One daemon per machine: a daemon that answers on the socket keeps it, and this
|
|
4922
|
+
* one doesn't start. A socket file nobody answers on is a dead daemon's; it goes. */
|
|
4923
|
+
async listen() {
|
|
4924
|
+
const other = await ask(this.sock, { op: "status" }, 1500);
|
|
4925
|
+
if (other?.op === "status") throw new DaemonRunning(other.status ?? {});
|
|
4341
4926
|
if (process.platform !== "win32") {
|
|
4342
4927
|
try {
|
|
4343
|
-
if (
|
|
4928
|
+
if (existsSync11(this.sock)) unlinkSync5(this.sock);
|
|
4344
4929
|
} catch {
|
|
4345
4930
|
}
|
|
4346
4931
|
}
|
|
4347
4932
|
this.server = net2.createServer((s) => this.onConn(s));
|
|
4348
|
-
|
|
4933
|
+
await new Promise((resolve, reject) => {
|
|
4349
4934
|
this.server.once("error", reject);
|
|
4350
4935
|
this.server.listen(this.sock, () => resolve());
|
|
4351
4936
|
});
|
|
4937
|
+
if (process.platform !== "win32") this.sockId = socketId(this.sock);
|
|
4352
4938
|
}
|
|
4353
4939
|
onConn(s) {
|
|
4354
4940
|
let buf = "";
|
|
4355
4941
|
let handled = false;
|
|
4942
|
+
this.conns.add(s);
|
|
4943
|
+
s.on("close", () => this.conns.delete(s));
|
|
4356
4944
|
s.on("error", () => {
|
|
4357
4945
|
});
|
|
4358
4946
|
s.on("data", (d) => {
|
|
@@ -4445,6 +5033,11 @@ ${IN_PLACE_ANSWER}`, id: p.msgId });
|
|
|
4445
5033
|
this.onEvent(req);
|
|
4446
5034
|
this.reply(s, { op: "ok" });
|
|
4447
5035
|
return;
|
|
5036
|
+
case "restart":
|
|
5037
|
+
this.reply(s, this.deps.onRestart?.() ? { op: "ok" } : { op: "none" });
|
|
5038
|
+
return;
|
|
5039
|
+
default:
|
|
5040
|
+
s.destroy();
|
|
4448
5041
|
}
|
|
4449
5042
|
}
|
|
4450
5043
|
/** A queued turn message that hasn't expired. */
|
|
@@ -4492,6 +5085,10 @@ ${IN_PLACE_ANSWER}`, id: p.msgId });
|
|
|
4492
5085
|
for (; ; ) {
|
|
4493
5086
|
const r = this.cfg.receivers.find((x) => x.control + x.machine === key);
|
|
4494
5087
|
if (!this.running || !r) break;
|
|
5088
|
+
if (this.paused) {
|
|
5089
|
+
await this.paused.until;
|
|
5090
|
+
continue;
|
|
5091
|
+
}
|
|
4495
5092
|
if (!r.token) {
|
|
4496
5093
|
const t2 = this.deps.token?.(r) ?? null;
|
|
4497
5094
|
if (t2) {
|
|
@@ -4525,16 +5122,21 @@ ${IN_PLACE_ANSWER}`, id: p.msgId });
|
|
|
4525
5122
|
break;
|
|
4526
5123
|
}
|
|
4527
5124
|
if (res.status === 200) {
|
|
4528
|
-
const body = await res.json();
|
|
4529
|
-
const cur = this.cfg.receivers.find((x) => x.control + x.machine === key) ?? r;
|
|
4530
5125
|
let fresh = 0;
|
|
4531
|
-
|
|
4532
|
-
|
|
4533
|
-
|
|
4534
|
-
|
|
4535
|
-
)
|
|
5126
|
+
this.receiving++;
|
|
5127
|
+
try {
|
|
5128
|
+
const body = await res.json();
|
|
5129
|
+
const cur = this.cfg.receivers.find((x) => x.control + x.machine === key) ?? r;
|
|
5130
|
+
for (const m of body.messages ?? []) {
|
|
5131
|
+
if (m && !this.inflight.has(m.id)) fresh++;
|
|
5132
|
+
void this.handle(cur, m).catch(
|
|
5133
|
+
(e) => this.deps.log(`handle ${String(m?.id).slice(-8)}: ${String(e?.message || e)}`)
|
|
5134
|
+
);
|
|
5135
|
+
}
|
|
5136
|
+
} finally {
|
|
5137
|
+
this.receiving--;
|
|
4536
5138
|
}
|
|
4537
|
-
if (!fresh) await
|
|
5139
|
+
if (!fresh) await sleep2(2e3);
|
|
4538
5140
|
st.error = null;
|
|
4539
5141
|
backoff = 0;
|
|
4540
5142
|
} else if (res.status === 204) {
|
|
@@ -4543,13 +5145,14 @@ ${IN_PLACE_ANSWER}`, id: p.msgId });
|
|
|
4543
5145
|
} else {
|
|
4544
5146
|
st.error = `HTTP ${res.status}`;
|
|
4545
5147
|
downSince ||= this.deps.now();
|
|
4546
|
-
await
|
|
5148
|
+
await sleep2(pause(backoff++));
|
|
4547
5149
|
}
|
|
4548
5150
|
} catch (e) {
|
|
4549
5151
|
if (!this.running) break;
|
|
5152
|
+
if (this.paused) continue;
|
|
4550
5153
|
st.error = String(e?.message || e).slice(0, 120);
|
|
4551
5154
|
downSince ||= this.deps.now();
|
|
4552
|
-
await
|
|
5155
|
+
await sleep2(pause(backoff++));
|
|
4553
5156
|
} finally {
|
|
4554
5157
|
clearTimeout(t);
|
|
4555
5158
|
this.aborts.delete(ctl);
|
|
@@ -4657,7 +5260,7 @@ ${IN_PLACE_ANSWER}`, id: p.msgId });
|
|
|
4657
5260
|
const added = now.slice(all.length);
|
|
4658
5261
|
this.acks.keep([...left, ...added]);
|
|
4659
5262
|
if (!left.length && !added.length) return;
|
|
4660
|
-
if (left.length) await
|
|
5263
|
+
if (left.length) await sleep2(2e3 * (round + 1));
|
|
4661
5264
|
}
|
|
4662
5265
|
} finally {
|
|
4663
5266
|
this.flushing = false;
|
|
@@ -4730,10 +5333,10 @@ ${IN_PLACE_ANSWER}`, id: p.msgId });
|
|
|
4730
5333
|
return { folder: ok, transcript: null };
|
|
4731
5334
|
}
|
|
4732
5335
|
allowed(dir) {
|
|
4733
|
-
if (!
|
|
5336
|
+
if (!path19.isAbsolute(dir)) return null;
|
|
4734
5337
|
let r;
|
|
4735
5338
|
try {
|
|
4736
|
-
r =
|
|
5339
|
+
r = realpathSync4.native(dir);
|
|
4737
5340
|
if (!statSync8(r).isDirectory()) return null;
|
|
4738
5341
|
} catch {
|
|
4739
5342
|
return null;
|
|
@@ -4741,10 +5344,10 @@ ${IN_PLACE_ANSWER}`, id: p.msgId });
|
|
|
4741
5344
|
for (const f of this.cfg.folders) {
|
|
4742
5345
|
let rf = f;
|
|
4743
5346
|
try {
|
|
4744
|
-
rf =
|
|
5347
|
+
rf = realpathSync4.native(f);
|
|
4745
5348
|
} catch {
|
|
4746
5349
|
}
|
|
4747
|
-
if (r === rf || r.startsWith(rf.endsWith(
|
|
5350
|
+
if (r === rf || r.startsWith(rf.endsWith(path19.sep) ? rf : rf + path19.sep)) return r;
|
|
4748
5351
|
}
|
|
4749
5352
|
return null;
|
|
4750
5353
|
}
|
|
@@ -4929,7 +5532,7 @@ ${IN_PLACE_ANSWER}`, id: p.msgId });
|
|
|
4929
5532
|
const ran = isUuid2(out2.session) ? out2.session : target;
|
|
4930
5533
|
if (mode === "fork" && ran !== target) {
|
|
4931
5534
|
this.state.copies[ref.id] = ran;
|
|
4932
|
-
writeState(
|
|
5535
|
+
writeState(path19.join(this.dir, "state.json"), this.state);
|
|
4933
5536
|
this.lastEnd.set(ran, this.deps.now());
|
|
4934
5537
|
}
|
|
4935
5538
|
this.finish(
|
|
@@ -5105,7 +5708,7 @@ ${IN_PLACE_ANSWER}`, id: p.msgId });
|
|
|
5105
5708
|
const out3 = await this.slot(async () => {
|
|
5106
5709
|
const s = sdk.start({ session: ref.id, cwd, mode: this.cfg.mode, allowedTools: this.allowedFor(r) });
|
|
5107
5710
|
this.sdkSessions.set(ref.id, s);
|
|
5108
|
-
return s.send(
|
|
5711
|
+
return s.send(frameStart(cmd2.text, r.url));
|
|
5109
5712
|
});
|
|
5110
5713
|
return this.finish(
|
|
5111
5714
|
r,
|
|
@@ -5130,7 +5733,7 @@ ${IN_PLACE_ANSWER}`, id: p.msgId });
|
|
|
5130
5733
|
...this.allowedFor(r).length ? [`--allowedTools=${this.allowedFor(r).join(",")}`] : [],
|
|
5131
5734
|
"--session-id",
|
|
5132
5735
|
ref.id,
|
|
5133
|
-
|
|
5736
|
+
frameStart(cmd2.text, r.url)
|
|
5134
5737
|
],
|
|
5135
5738
|
{
|
|
5136
5739
|
cwd,
|
|
@@ -5158,15 +5761,23 @@ ${IN_PLACE_ANSWER}`, id: p.msgId });
|
|
|
5158
5761
|
}
|
|
5159
5762
|
}
|
|
5160
5763
|
};
|
|
5161
|
-
var
|
|
5764
|
+
var sleep2 = (ms) => new Promise((r) => setTimeout(r, ms).unref?.());
|
|
5765
|
+
function socketId(p) {
|
|
5766
|
+
try {
|
|
5767
|
+
const st = statSync8(p);
|
|
5768
|
+
return `${st.dev}:${st.ino}`;
|
|
5769
|
+
} catch {
|
|
5770
|
+
return null;
|
|
5771
|
+
}
|
|
5772
|
+
}
|
|
5162
5773
|
|
|
5163
5774
|
// src/control/pair.ts
|
|
5164
5775
|
import { execFileSync as execFileSync6 } from "node:child_process";
|
|
5165
5776
|
import { randomBytes as randomBytes3 } from "node:crypto";
|
|
5166
|
-
import { existsSync as
|
|
5777
|
+
import { existsSync as existsSync12, mkdirSync as mkdirSync10, unlinkSync as unlinkSync6, writeFileSync as writeFileSync8 } from "node:fs";
|
|
5167
5778
|
import os6 from "node:os";
|
|
5168
|
-
import
|
|
5169
|
-
var VERSION3 = true ? "0.
|
|
5779
|
+
import path20 from "node:path";
|
|
5780
|
+
var VERSION3 = true ? "0.6.1" : "0.0.0";
|
|
5170
5781
|
async function json(r) {
|
|
5171
5782
|
try {
|
|
5172
5783
|
return await r.json();
|
|
@@ -5191,7 +5802,7 @@ async function discover(url, f) {
|
|
|
5191
5802
|
}
|
|
5192
5803
|
async function pair(o) {
|
|
5193
5804
|
const f = o.fetch ?? fetch;
|
|
5194
|
-
const
|
|
5805
|
+
const sleep3 = o.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
|
|
5195
5806
|
const { control, rpId, open } = await discover(o.url, f);
|
|
5196
5807
|
if (!o.token && !open)
|
|
5197
5808
|
throw new Error(
|
|
@@ -5201,7 +5812,7 @@ async function pair(o) {
|
|
|
5201
5812
|
const cfg = readControl(o.env) ?? { name: o.name, folders: [], mode: o.mode, receivers: [] };
|
|
5202
5813
|
cfg.name = o.name || cfg.name;
|
|
5203
5814
|
cfg.mode = o.mode;
|
|
5204
|
-
cfg.folders = [.../* @__PURE__ */ new Set([...cfg.folders, ...o.folders.map((d) =>
|
|
5815
|
+
cfg.folders = [.../* @__PURE__ */ new Set([...cfg.folders, ...o.folders.map((d) => path20.resolve(d))])];
|
|
5205
5816
|
const existing = cfg.receivers.find((r) => r.control === control);
|
|
5206
5817
|
const auth = { authorization: `Bearer ${o.token ?? pollKey}`, "content-type": "application/json" };
|
|
5207
5818
|
const started = await f(`${control}/pair`, {
|
|
@@ -5243,7 +5854,7 @@ async function pair(o) {
|
|
|
5243
5854
|
const until = Date.parse(String(s.expires_at)) || Date.now() + 10 * 6e4;
|
|
5244
5855
|
for (; ; ) {
|
|
5245
5856
|
if (Date.now() > until) throw new Error("the pairing link expired; run `sessionpipe control pair` again");
|
|
5246
|
-
await
|
|
5857
|
+
await sleep3(2e3);
|
|
5247
5858
|
const r = await f(`${control}/pair?code=${encodeURIComponent(code)}`, { headers: auth }).catch(() => null);
|
|
5248
5859
|
if (!r) continue;
|
|
5249
5860
|
if (r.status === 404) throw new Error("the receiver lost the pairing; run `sessionpipe control pair` again");
|
|
@@ -5301,8 +5912,8 @@ function removeKey(id, env) {
|
|
|
5301
5912
|
writeControl(cfg, env);
|
|
5302
5913
|
return n;
|
|
5303
5914
|
}
|
|
5304
|
-
var
|
|
5305
|
-
var
|
|
5915
|
+
var LABEL2 = "org.sessionpipe.control";
|
|
5916
|
+
var UNIT2 = "sessionpipe-control.service";
|
|
5306
5917
|
function serviceFiles(o) {
|
|
5307
5918
|
const home = o.home ?? os6.homedir();
|
|
5308
5919
|
const xml = (s) => s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
@@ -5310,14 +5921,15 @@ function serviceFiles(o) {
|
|
|
5310
5921
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
5311
5922
|
<plist version="1.0">
|
|
5312
5923
|
<dict>
|
|
5313
|
-
<key>Label</key><string>${
|
|
5924
|
+
<key>Label</key><string>${LABEL2}</string>
|
|
5314
5925
|
<key>ProgramArguments</key>
|
|
5315
5926
|
<array><string>${xml(o.node)}</string><string>${xml(o.cli)}</string><string>control</string><string>run</string></array>
|
|
5927
|
+
<key>EnvironmentVariables</key><dict><key>SESSIONPIPE_SERVICE</key><string>launchd</string></dict>
|
|
5316
5928
|
<key>RunAtLoad</key><true/>
|
|
5317
5929
|
<key>KeepAlive</key><dict><key>SuccessfulExit</key><false/></dict>
|
|
5318
5930
|
<key>ProcessType</key><string>Background</string>
|
|
5319
|
-
<key>StandardOutPath</key><string>${xml(
|
|
5320
|
-
<key>StandardErrorPath</key><string>${xml(
|
|
5931
|
+
<key>StandardOutPath</key><string>${xml(path20.join(o.logDir, "daemon.log"))}</string>
|
|
5932
|
+
<key>StandardErrorPath</key><string>${xml(path20.join(o.logDir, "daemon.log"))}</string>
|
|
5321
5933
|
</dict>
|
|
5322
5934
|
</plist>
|
|
5323
5935
|
`;
|
|
@@ -5328,15 +5940,17 @@ After=network-online.target
|
|
|
5328
5940
|
|
|
5329
5941
|
[Service]
|
|
5330
5942
|
ExecStart=${q(o.node)} ${q(o.cli)} control run
|
|
5943
|
+
Environment=SESSIONPIPE_SERVICE=systemd
|
|
5331
5944
|
Restart=on-failure
|
|
5945
|
+
RestartForceExitStatus=75
|
|
5332
5946
|
RestartSec=5
|
|
5333
5947
|
|
|
5334
5948
|
[Install]
|
|
5335
5949
|
WantedBy=default.target
|
|
5336
5950
|
`;
|
|
5337
5951
|
return {
|
|
5338
|
-
plist: { file:
|
|
5339
|
-
unit: { file:
|
|
5952
|
+
plist: { file: path20.join(home, "Library", "LaunchAgents", `${LABEL2}.plist`), body: plist },
|
|
5953
|
+
unit: { file: path20.join(home, ".config", "systemd", "user", UNIT2), body: unit }
|
|
5340
5954
|
};
|
|
5341
5955
|
}
|
|
5342
5956
|
var execQuiet = (cmd2, args2) => execFileSync6(cmd2, args2, { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], timeout: 15e3 });
|
|
@@ -5366,33 +5980,33 @@ function installService(o) {
|
|
|
5366
5980
|
const run = o.exec ?? execQuiet;
|
|
5367
5981
|
const platform = o.platform ?? process.platform;
|
|
5368
5982
|
if (platform === "darwin") {
|
|
5369
|
-
mkdirSync10(
|
|
5983
|
+
mkdirSync10(path20.dirname(f.plist.file), { recursive: true });
|
|
5370
5984
|
writeFileSync8(f.plist.file, f.plist.body);
|
|
5371
5985
|
const uid = String(process.getuid?.() ?? "");
|
|
5372
5986
|
for (const d of ["gui", "user"])
|
|
5373
5987
|
try {
|
|
5374
|
-
run("launchctl", ["bootout", `${d}/${uid}/${
|
|
5988
|
+
run("launchctl", ["bootout", `${d}/${uid}/${LABEL2}`]);
|
|
5375
5989
|
} catch {
|
|
5376
5990
|
}
|
|
5377
5991
|
try {
|
|
5378
5992
|
run("launchctl", ["bootstrap", `gui/${uid}`, f.plist.file]);
|
|
5379
|
-
return `launchd agent ${
|
|
5993
|
+
return `launchd agent ${LABEL2} (log: ${path20.join(logDir, "daemon.log")})`;
|
|
5380
5994
|
} catch {
|
|
5381
5995
|
}
|
|
5382
5996
|
run("launchctl", ["bootstrap", `user/${uid}`, f.plist.file]);
|
|
5383
|
-
return `launchd agent ${
|
|
5997
|
+
return `launchd agent ${LABEL2} 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: ${path20.join(logDir, "daemon.log")})`;
|
|
5384
5998
|
}
|
|
5385
5999
|
if (platform === "linux") {
|
|
5386
|
-
mkdirSync10(
|
|
6000
|
+
mkdirSync10(path20.dirname(f.unit.file), { recursive: true });
|
|
5387
6001
|
writeFileSync8(f.unit.file, f.unit.body);
|
|
5388
6002
|
try {
|
|
5389
6003
|
run("systemctl", ["--user", "daemon-reload"]);
|
|
5390
|
-
run("systemctl", ["--user", "enable", "--now",
|
|
6004
|
+
run("systemctl", ["--user", "enable", "--now", UNIT2]);
|
|
5391
6005
|
} catch {
|
|
5392
6006
|
return `wrote ${f.unit.file}, but systemd --user isn't reachable here; run \`sessionpipe control run\` yourself (tmux, screen)`;
|
|
5393
6007
|
}
|
|
5394
6008
|
const linger = ensureLinger(os6.userInfo().username, run);
|
|
5395
|
-
return `systemd user unit ${
|
|
6009
|
+
return `systemd user unit ${UNIT2}; ${linger.note} (journalctl --user -u ${UNIT2})`;
|
|
5396
6010
|
}
|
|
5397
6011
|
return "no service manager for this platform yet; run `sessionpipe control run` yourself";
|
|
5398
6012
|
}
|
|
@@ -5404,13 +6018,13 @@ function uninstallService(env) {
|
|
|
5404
6018
|
} catch {
|
|
5405
6019
|
}
|
|
5406
6020
|
};
|
|
5407
|
-
if (process.platform === "darwin" &&
|
|
5408
|
-
run("launchctl", ["bootout", `gui/${process.getuid?.() ?? ""}/${
|
|
5409
|
-
run("launchctl", ["bootout", `user/${process.getuid?.() ?? ""}/${
|
|
6021
|
+
if (process.platform === "darwin" && existsSync12(f.plist.file)) {
|
|
6022
|
+
run("launchctl", ["bootout", `gui/${process.getuid?.() ?? ""}/${LABEL2}`]);
|
|
6023
|
+
run("launchctl", ["bootout", `user/${process.getuid?.() ?? ""}/${LABEL2}`]);
|
|
5410
6024
|
unlinkSync6(f.plist.file);
|
|
5411
6025
|
}
|
|
5412
|
-
if (process.platform === "linux" &&
|
|
5413
|
-
run("systemctl", ["--user", "disable", "--now",
|
|
6026
|
+
if (process.platform === "linux" && existsSync12(f.unit.file)) {
|
|
6027
|
+
run("systemctl", ["--user", "disable", "--now", UNIT2]);
|
|
5414
6028
|
unlinkSync6(f.unit.file);
|
|
5415
6029
|
run("systemctl", ["--user", "daemon-reload"]);
|
|
5416
6030
|
}
|
|
@@ -5613,9 +6227,9 @@ ${CONTROL_HELP}`);
|
|
|
5613
6227
|
out2(
|
|
5614
6228
|
` Mode: ${c.mode} (${c.mode === "safe" ? "nothing that needs approval runs unattended" : "Claude Code's auto mode"})`
|
|
5615
6229
|
);
|
|
5616
|
-
let cli =
|
|
6230
|
+
let cli = path21.join(distDir2, "cli.js");
|
|
5617
6231
|
try {
|
|
5618
|
-
cli =
|
|
6232
|
+
cli = realpathSync5(cli);
|
|
5619
6233
|
} catch {
|
|
5620
6234
|
}
|
|
5621
6235
|
if (argv.includes("--no-service"))
|
|
@@ -5665,12 +6279,15 @@ ${MODE_TEXT}`);
|
|
|
5665
6279
|
return out2(JSON.stringify(r.status, null, 2));
|
|
5666
6280
|
}
|
|
5667
6281
|
case "run":
|
|
5668
|
-
return runDaemon(out2);
|
|
6282
|
+
return runDaemon(out2, distDir2);
|
|
5669
6283
|
default:
|
|
5670
6284
|
return out2(CONTROL_HELP);
|
|
5671
6285
|
}
|
|
5672
6286
|
}
|
|
5673
|
-
|
|
6287
|
+
function enabledHarnesses(harnesses) {
|
|
6288
|
+
return Object.entries(harnesses).filter(([, h]) => h?.enabled !== false).map(([n]) => n).filter((n) => /^[a-z][a-z0-9-]{0,40}$/.test(n));
|
|
6289
|
+
}
|
|
6290
|
+
async function runDaemon(out2, distDir2) {
|
|
5674
6291
|
const cfg = readControl();
|
|
5675
6292
|
if (!cfg?.receivers.length) {
|
|
5676
6293
|
out2("control isn't paired on this machine; nothing to do (`sessionpipe control pair <receiver>`)");
|
|
@@ -5681,11 +6298,12 @@ async function runDaemon(out2) {
|
|
|
5681
6298
|
const claudeNow = () => {
|
|
5682
6299
|
const bin = findClaude2();
|
|
5683
6300
|
if (!bin) return null;
|
|
5684
|
-
const sig = `${bin}:${
|
|
6301
|
+
const sig = `${bin}:${existsSync13(bin) ? realpathSync5(bin) : ""}`;
|
|
5685
6302
|
if (claude?.sig !== sig) claude = { bin, caps: detectCaps2(bin), sig };
|
|
5686
6303
|
return claude;
|
|
5687
6304
|
};
|
|
5688
6305
|
const sdk = await loadSdk({ claudeBin: findClaude2(), env: process.env, log: log2 });
|
|
6306
|
+
let updater = null;
|
|
5689
6307
|
const d = new ControlDaemon(cfg, controlState(), socketPath(stateDir()), {
|
|
5690
6308
|
now: Date.now,
|
|
5691
6309
|
fetch,
|
|
@@ -5698,9 +6316,19 @@ async function runDaemon(out2) {
|
|
|
5698
6316
|
const c = claudeNow();
|
|
5699
6317
|
return c ? claudeLogin2(c.bin, configDir) : { loggedIn: null };
|
|
5700
6318
|
},
|
|
5701
|
-
token: (r) => r.token_in ? secretGet(r.token_in, controlAccount(r.machine)) : null
|
|
6319
|
+
token: (r) => r.token_in ? secretGet(r.token_in, controlAccount(r.machine)) : null,
|
|
6320
|
+
onRestart: () => updater?.restartWhenIdle() ?? false,
|
|
6321
|
+
updateStatus: () => updater ? updater.record() : null,
|
|
6322
|
+
// Two daemons started at once and the other one holds the socket: leave it to it.
|
|
6323
|
+
onLostSocket: () => process.exit(0)
|
|
5702
6324
|
});
|
|
5703
|
-
|
|
6325
|
+
try {
|
|
6326
|
+
await d.start();
|
|
6327
|
+
} catch (e) {
|
|
6328
|
+
if (!(e instanceof DaemonRunning)) throw e;
|
|
6329
|
+
log2(`${e.message}; this one exits`);
|
|
6330
|
+
process.exit(0);
|
|
6331
|
+
}
|
|
5704
6332
|
const drain = setInterval(() => {
|
|
5705
6333
|
const sinks = readConfig().sinks.filter((s) => s.token_in && !s.paused);
|
|
5706
6334
|
if (sinks.length) void flush(stateDir(), sinks, new Outbox(stateDir())).catch(() => {
|
|
@@ -5711,12 +6339,50 @@ async function runDaemon(out2) {
|
|
|
5711
6339
|
const next = readControl();
|
|
5712
6340
|
if (next) d.reload(next);
|
|
5713
6341
|
});
|
|
6342
|
+
let stopping = false;
|
|
5714
6343
|
const bye = async () => {
|
|
6344
|
+
if (stopping) return;
|
|
6345
|
+
stopping = true;
|
|
6346
|
+
updater?.stop();
|
|
5715
6347
|
await d.stop();
|
|
5716
6348
|
process.exit(0);
|
|
5717
6349
|
};
|
|
5718
|
-
|
|
5719
|
-
process.on(
|
|
6350
|
+
const signals = ["SIGTERM", "SIGINT"];
|
|
6351
|
+
for (const sig of signals) process.on(sig, bye);
|
|
6352
|
+
const restart = async (_from, to) => {
|
|
6353
|
+
stopping = true;
|
|
6354
|
+
updater?.stop();
|
|
6355
|
+
clearInterval(drain);
|
|
6356
|
+
unwatchFile(controlFile());
|
|
6357
|
+
for (const sig of signals) process.off(sig, bye);
|
|
6358
|
+
await d.stop();
|
|
6359
|
+
const via = restartVia({ env: process.env, platform: process.platform, pid: process.pid, ppid: process.ppid });
|
|
6360
|
+
if (via) {
|
|
6361
|
+
log2(`exiting ${UPDATE_EXIT} so ${via === "supervisor" ? "the supervising sessionpipe" : via} starts ${to}`);
|
|
6362
|
+
process.exit(UPDATE_EXIT);
|
|
6363
|
+
}
|
|
6364
|
+
let cli = path21.join(distDir2, "cli.js");
|
|
6365
|
+
try {
|
|
6366
|
+
cli = realpathSync5(cli);
|
|
6367
|
+
} catch {
|
|
6368
|
+
}
|
|
6369
|
+
log2(`no service manager restarts this daemon: pid ${process.pid} now runs ${to} as its child`);
|
|
6370
|
+
const sup = supervise({ node: process.execPath, args: [cli, "control", "run"], env: process.env, log: log2 });
|
|
6371
|
+
for (const sig of [...signals, "SIGHUP"]) process.on(sig, () => sup.stop(sig));
|
|
6372
|
+
};
|
|
6373
|
+
updater = new AutoUpdater({
|
|
6374
|
+
now: Date.now,
|
|
6375
|
+
log: log2,
|
|
6376
|
+
version: VERSION,
|
|
6377
|
+
file: updateFile(),
|
|
6378
|
+
off: () => autoUpdateOff({ env: process.env, config: readConfig(), distDir: distDir2 }),
|
|
6379
|
+
latest: () => latestVersion(fetch),
|
|
6380
|
+
installed: () => installedVersion({ node: process.execPath, distDir: distDir2 }),
|
|
6381
|
+
install: (v) => installVersion(v, { node: process.execPath, distDir: distDir2, harnesses: enabledHarnesses(readConfig().harnesses) }),
|
|
6382
|
+
daemon: d,
|
|
6383
|
+
restart
|
|
6384
|
+
});
|
|
6385
|
+
updater.start();
|
|
5720
6386
|
}
|
|
5721
6387
|
async function waitMain(argv, out2) {
|
|
5722
6388
|
const i = argv.indexOf("--session");
|
|
@@ -5740,16 +6406,16 @@ function adoptSinkToken(url, token, tier, file) {
|
|
|
5740
6406
|
}
|
|
5741
6407
|
|
|
5742
6408
|
// src/launcher.ts
|
|
5743
|
-
import { accessSync as
|
|
6409
|
+
import { accessSync as accessSync3, chmodSync as chmodSync3, constants as constants3, mkdirSync as mkdirSync11, readFileSync as readFileSync12, renameSync as renameSync4, rmSync, writeFileSync as writeFileSync9 } from "node:fs";
|
|
5744
6410
|
import os8 from "node:os";
|
|
5745
|
-
import
|
|
6411
|
+
import path22 from "node:path";
|
|
5746
6412
|
var LAUNCHER = "sessionpipe-hook";
|
|
5747
6413
|
var COPIED = ["worker.js", "hook.js"];
|
|
5748
6414
|
function dataDir(env = process.env) {
|
|
5749
|
-
if (env.SESSIONPIPE_DATA) return
|
|
5750
|
-
return
|
|
6415
|
+
if (env.SESSIONPIPE_DATA) return path22.resolve(env.SESSIONPIPE_DATA);
|
|
6416
|
+
return path22.join(env.XDG_DATA_HOME || path22.join(os8.homedir(), ".local", "share"), "sessionpipe");
|
|
5751
6417
|
}
|
|
5752
|
-
var launcherPath = (env = process.env) =>
|
|
6418
|
+
var launcherPath = (env = process.env) => path22.join(dataDir(env), LAUNCHER);
|
|
5753
6419
|
var usesLauncher = (platform = process.platform) => platform !== "win32";
|
|
5754
6420
|
var sq = (s) => `'${s.replace(/'/g, `'\\''`)}'`;
|
|
5755
6421
|
function launcherScript(node, hook, version) {
|
|
@@ -5758,13 +6424,13 @@ function launcherScript(node, hook, version) {
|
|
|
5758
6424
|
# this; it runs hook.js beside it with the node sessionpipe was installed with, or with
|
|
5759
6425
|
# any node on PATH once that one is gone (an nvm uninstall, a Homebrew upgrade).
|
|
5760
6426
|
n=${sq(node)}
|
|
5761
|
-
[ -x "$n" ] || n=$(command -v node) || { [ "$1" = antigravity ] && echo '{}'; exit 0; }
|
|
6427
|
+
[ -x "$n" ] || n=$(command -v node) || { [ "$1" = antigravity ] && if [ "$2" = PreToolUse ]; then echo ${sq(AG_STALE_PRETOOL.trim())}; else echo '{}'; fi; exit 0; }
|
|
5762
6428
|
exec "$n" ${sq(hook)} "$@"
|
|
5763
6429
|
`;
|
|
5764
6430
|
}
|
|
5765
6431
|
function writeIfChanged(file, body, mode) {
|
|
5766
6432
|
try {
|
|
5767
|
-
const cur =
|
|
6433
|
+
const cur = readFileSync12(file);
|
|
5768
6434
|
if (cur.equals(Buffer.isBuffer(body) ? body : Buffer.from(body))) {
|
|
5769
6435
|
chmodSync3(file, mode);
|
|
5770
6436
|
return false;
|
|
@@ -5782,19 +6448,19 @@ function installLauncher(o) {
|
|
|
5782
6448
|
mkdirSync11(dir, { recursive: true, mode: 493 });
|
|
5783
6449
|
let changed = false;
|
|
5784
6450
|
for (const f of COPIED)
|
|
5785
|
-
changed = writeIfChanged(
|
|
5786
|
-
const launcher =
|
|
5787
|
-
changed = writeIfChanged(launcher, launcherScript(o.node,
|
|
6451
|
+
changed = writeIfChanged(path22.join(dir, f), readFileSync12(path22.join(o.distDir, f)), 420) || changed;
|
|
6452
|
+
const launcher = path22.join(dir, LAUNCHER);
|
|
6453
|
+
changed = writeIfChanged(launcher, launcherScript(o.node, path22.join(dir, "hook.js"), o.version), 493) || changed;
|
|
5788
6454
|
return { launcher, changed };
|
|
5789
6455
|
}
|
|
5790
6456
|
function removeLauncher(env = process.env) {
|
|
5791
|
-
for (const f of [LAUNCHER, ...COPIED]) rmSync(
|
|
6457
|
+
for (const f of [LAUNCHER, ...COPIED]) rmSync(path22.join(dataDir(env), f), { force: true });
|
|
5792
6458
|
}
|
|
5793
6459
|
function launcherState(version, env = process.env) {
|
|
5794
6460
|
const file = launcherPath(env);
|
|
5795
6461
|
let text = "";
|
|
5796
6462
|
try {
|
|
5797
|
-
text =
|
|
6463
|
+
text = readFileSync12(file, "utf8");
|
|
5798
6464
|
} catch {
|
|
5799
6465
|
return {
|
|
5800
6466
|
file,
|
|
@@ -5810,7 +6476,7 @@ function launcherState(version, env = process.env) {
|
|
|
5810
6476
|
let nodeOk = false;
|
|
5811
6477
|
try {
|
|
5812
6478
|
if (node) {
|
|
5813
|
-
|
|
6479
|
+
accessSync3(node, constants3.X_OK);
|
|
5814
6480
|
nodeOk = true;
|
|
5815
6481
|
}
|
|
5816
6482
|
} catch {
|
|
@@ -5822,7 +6488,7 @@ function launcherState(version, env = process.env) {
|
|
|
5822
6488
|
);
|
|
5823
6489
|
for (const f of COPIED)
|
|
5824
6490
|
try {
|
|
5825
|
-
|
|
6491
|
+
accessSync3(path22.join(path22.dirname(file), f), constants3.R_OK);
|
|
5826
6492
|
} catch {
|
|
5827
6493
|
problems.push(`${f} is missing beside the hook launcher; run \`sessionpipe install\` again`);
|
|
5828
6494
|
}
|
|
@@ -5849,16 +6515,16 @@ var out = (s = "") => {
|
|
|
5849
6515
|
`);
|
|
5850
6516
|
};
|
|
5851
6517
|
var state = stateDir();
|
|
5852
|
-
var distDir =
|
|
6518
|
+
var distDir = path23.dirname(fileURLToPath(import.meta.url));
|
|
5853
6519
|
function hookCommand() {
|
|
5854
6520
|
if (usesLauncher()) {
|
|
5855
6521
|
const l = launcherPath();
|
|
5856
6522
|
return (a) => ({ argv: [l, ...a], command: `${JSON.stringify(l)} ${a.join(" ")}` });
|
|
5857
6523
|
}
|
|
5858
6524
|
const node = stableNode(process.execPath);
|
|
5859
|
-
let script =
|
|
6525
|
+
let script = path23.join(distDir, "hook.js");
|
|
5860
6526
|
try {
|
|
5861
|
-
script =
|
|
6527
|
+
script = realpathSync6(script);
|
|
5862
6528
|
} catch {
|
|
5863
6529
|
}
|
|
5864
6530
|
return (a) => ({
|
|
@@ -5928,7 +6594,7 @@ function help() {
|
|
|
5928
6594
|
sessionpipe status | doctor [--json]
|
|
5929
6595
|
sessionpipe tail [--session ID] [--tier N] [--harness NAME]
|
|
5930
6596
|
sessionpipe backfill [--days 30] | forget <harness> <session> | replay --sink NAME
|
|
5931
|
-
sessionpipe update
|
|
6597
|
+
sessionpipe update [off|on] (install the latest now; off/on: the daemon's daily update)
|
|
5932
6598
|
${CONTROL_HELP}
|
|
5933
6599
|
|
|
5934
6600
|
Docs: https://sessionpipe.org \xB7 nothing leaves this machine until you add a sink.`);
|
|
@@ -6090,7 +6756,7 @@ async function connect() {
|
|
|
6090
6756
|
out(` \u2713 Keys: in ${storeLabel(store)}${kept.length ? ` (left in the file: ${kept.join(", ")})` : ""}`);
|
|
6091
6757
|
} else
|
|
6092
6758
|
out(
|
|
6093
|
-
` Keys: in ${tilde(
|
|
6759
|
+
` Keys: in ${tilde(path23.dirname(configFile()))} at 0600 \u2014 no unlocked keychain in this session${process.env.SSH_CONNECTION ? " (an ssh shell)" : ""}`
|
|
6094
6760
|
);
|
|
6095
6761
|
if (rows.length) queueBackfill(rows, 30, readConfig());
|
|
6096
6762
|
out("");
|
|
@@ -6362,6 +7028,7 @@ function status() {
|
|
|
6362
7028
|
` 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"}`
|
|
6363
7029
|
);
|
|
6364
7030
|
}
|
|
7031
|
+
out(` update: ${updateLine(updateState(cfg), Date.now())}`);
|
|
6365
7032
|
const t = timing();
|
|
6366
7033
|
if (t)
|
|
6367
7034
|
out(
|
|
@@ -6375,12 +7042,12 @@ function status() {
|
|
|
6375
7042
|
}
|
|
6376
7043
|
}
|
|
6377
7044
|
function hookWall(runs = 8) {
|
|
6378
|
-
const hook =
|
|
6379
|
-
const tmp = mkdtempSync(
|
|
7045
|
+
const hook = path23.join(distDir, "hook.js");
|
|
7046
|
+
const tmp = mkdtempSync(path23.join(os9.tmpdir(), "sessionpipe-doctor-"));
|
|
6380
7047
|
const env = {
|
|
6381
7048
|
...process.env,
|
|
6382
7049
|
SESSIONPIPE_STATE: tmp,
|
|
6383
|
-
SESSIONPIPE_CONFIG:
|
|
7050
|
+
SESSIONPIPE_CONFIG: path23.join(tmp, "config.json"),
|
|
6384
7051
|
SESSIONPIPE_NO_WORKER: "1"
|
|
6385
7052
|
};
|
|
6386
7053
|
const input = JSON.stringify({
|
|
@@ -6408,7 +7075,7 @@ function hookWall(runs = 8) {
|
|
|
6408
7075
|
}
|
|
6409
7076
|
function timing() {
|
|
6410
7077
|
try {
|
|
6411
|
-
const lines =
|
|
7078
|
+
const lines = readFileSync13(path23.join(state, "timing.jsonl"), "utf8").trim().split("\n").slice(-100);
|
|
6412
7079
|
const ms = lines.map((l) => JSON.parse(l).ms).filter((x) => x >= 0).sort((a, b) => a - b);
|
|
6413
7080
|
if (!ms.length) return null;
|
|
6414
7081
|
const q = (p) => Math.round(ms[Math.min(ms.length - 1, Math.floor(p * ms.length))] ?? 0);
|
|
@@ -6431,9 +7098,15 @@ async function doctor() {
|
|
|
6431
7098
|
sinks: [],
|
|
6432
7099
|
timing: timing(),
|
|
6433
7100
|
hook_wall_ms: hookWall(),
|
|
7101
|
+
update: updateState(cfg),
|
|
6434
7102
|
warnings: []
|
|
6435
7103
|
};
|
|
6436
7104
|
const warnings = report.warnings;
|
|
7105
|
+
const upd = report.update;
|
|
7106
|
+
if (upd.failed)
|
|
7107
|
+
warnings.push(
|
|
7108
|
+
`the control daemon couldn't install sessionpipe ${upd.failed.version}: ${upd.failed.error}. It tries again tomorrow; \`sessionpipe update\` tries now.`
|
|
7109
|
+
);
|
|
6437
7110
|
if (usesLauncher()) {
|
|
6438
7111
|
const l = launcherState(VERSION);
|
|
6439
7112
|
report.launcher = { file: tilde(l.file), node: l.node, node_ok: l.nodeOk, version: l.version };
|
|
@@ -6543,6 +7216,7 @@ async function doctor() {
|
|
|
6543
7216
|
out(
|
|
6544
7217
|
` claude ${a.dir.padEnd(26)} ${a.signed_in === true ? "signed in" : a.signed_in === false ? "NOT signed in from here" : "can't tell"}`
|
|
6545
7218
|
);
|
|
7219
|
+
out(` update ${updateLine(upd, Date.now())}`);
|
|
6546
7220
|
const t = report.timing;
|
|
6547
7221
|
out(
|
|
6548
7222
|
w ? ` hook wall clock (what the harness waits): p50 ${w.p50} ms \xB7 p95 ${w.p95} ms over ${w.n} spawns` : " hook wall clock: could not spawn dist/hook.js"
|
|
@@ -6622,37 +7296,67 @@ async function tail() {
|
|
|
6622
7296
|
async function workerFromArgv() {
|
|
6623
7297
|
const f = args[1];
|
|
6624
7298
|
if (!f) return;
|
|
6625
|
-
const job = JSON.parse(
|
|
7299
|
+
const job = JSON.parse(readFileSync13(f, "utf8"));
|
|
6626
7300
|
try {
|
|
6627
7301
|
unlinkSync7(f);
|
|
6628
7302
|
} catch {
|
|
6629
7303
|
}
|
|
6630
7304
|
await runJob(job);
|
|
6631
7305
|
}
|
|
7306
|
+
function updateState(cfg) {
|
|
7307
|
+
return updateSummary({
|
|
7308
|
+
// The shell's own SESSIONPIPE_NO_UPDATE_CHECK says nothing about the daemon's
|
|
7309
|
+
// environment; the daemon records that one itself.
|
|
7310
|
+
off: autoUpdateOff({ env: {}, config: cfg, distDir }),
|
|
7311
|
+
paired: pairedCount() > 0,
|
|
7312
|
+
rec: readUpdate(updateFile())
|
|
7313
|
+
});
|
|
7314
|
+
}
|
|
6632
7315
|
async function update() {
|
|
7316
|
+
const sub = args[1];
|
|
7317
|
+
if (sub === "off" || sub === "on") {
|
|
7318
|
+
const cfg = readConfig();
|
|
7319
|
+
cfg.update_check = sub === "on";
|
|
7320
|
+
writeConfig(cfg);
|
|
7321
|
+
return out(
|
|
7322
|
+
sub === "off" ? " \u2713 The control daemon no longer checks npm or updates itself. `sessionpipe update` still updates by hand; `sessionpipe update on` undoes this." : " \u2713 The control daemon checks npm once a day and installs a new release when it's idle."
|
|
7323
|
+
);
|
|
7324
|
+
}
|
|
6633
7325
|
if (process.env.SESSIONPIPE_NO_UPDATE_CHECK) return out(" update check is off (SESSIONPIPE_NO_UPDATE_CHECK)");
|
|
7326
|
+
let latest;
|
|
6634
7327
|
try {
|
|
6635
|
-
|
|
6636
|
-
const j = await r.json();
|
|
6637
|
-
if (!j.version) throw new Error("no version");
|
|
6638
|
-
if (j.version === VERSION) return out(` sessionpipe ${VERSION} is current`);
|
|
6639
|
-
out(` ${j.version} is available (you run ${VERSION}); installing into this copy's prefix\u2026`);
|
|
6640
|
-
const prefix = prefixOf(distDir);
|
|
6641
|
-
execFileSync7("npm", ["install", "-g", `sessionpipe@${j.version}`, ...prefix ? ["--prefix", prefix] : []], {
|
|
6642
|
-
stdio: "inherit"
|
|
6643
|
-
});
|
|
6644
|
-
out(" re-arming hooks\u2026");
|
|
6645
|
-
const bin = prefix ? path21.join(prefix, "bin", "sessionpipe") : "sessionpipe";
|
|
6646
|
-
const p = spawn2(bin, ["install", "--backfill", "0"], { stdio: "inherit" });
|
|
6647
|
-
await new Promise((r2) => p.on("exit", r2));
|
|
7328
|
+
latest = await latestVersion();
|
|
6648
7329
|
} catch (e) {
|
|
6649
|
-
out(` couldn't check npm: ${e.message}`);
|
|
7330
|
+
return out(` couldn't check npm: ${e.message}`);
|
|
6650
7331
|
}
|
|
6651
|
-
}
|
|
6652
|
-
|
|
6653
|
-
|
|
6654
|
-
|
|
6655
|
-
|
|
7332
|
+
if (!isNewer(latest, VERSION)) return out(` sessionpipe ${VERSION} is current (npm latest: ${latest})`);
|
|
7333
|
+
const prefix = globalPrefix(distDir);
|
|
7334
|
+
if (!prefix)
|
|
7335
|
+
return out(
|
|
7336
|
+
` ${latest} is available (you run ${VERSION}), but this copy isn't a global npm install (npx, a checkout or a dev build): \`npm install -g sessionpipe@${latest}\` installs one.`
|
|
7337
|
+
);
|
|
7338
|
+
out(` ${latest} is available (you run ${VERSION}); installing into ${tilde(prefix)}\u2026`);
|
|
7339
|
+
const r = await installVersion(latest, {
|
|
7340
|
+
node: process.execPath,
|
|
7341
|
+
distDir,
|
|
7342
|
+
harnesses: enabledHarnesses(readConfig().harnesses)
|
|
7343
|
+
});
|
|
7344
|
+
if (!r.ok) {
|
|
7345
|
+
process.exitCode = 1;
|
|
7346
|
+
return out(` \u2717 ${r.error}`);
|
|
7347
|
+
}
|
|
7348
|
+
out(` \u2713 sessionpipe ${latest} installed in ${(r.ms / 1e3).toFixed(1)} s; ${r.hooks}`);
|
|
7349
|
+
const sock = socketPath(state);
|
|
7350
|
+
const st = await ask(sock, { op: "status" }, 2e3);
|
|
7351
|
+
if (st?.op !== "status") return;
|
|
7352
|
+
const running = st.status.version ?? "?";
|
|
7353
|
+
if (running === latest) return;
|
|
7354
|
+
const r2 = await ask(sock, { op: "restart" }, 2e3);
|
|
7355
|
+
if (r2?.op === "ok") out(` The control daemon (${running}) restarts onto ${latest} as soon as nothing is in hand.`);
|
|
7356
|
+
else
|
|
7357
|
+
out(
|
|
7358
|
+
` 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).`
|
|
7359
|
+
);
|
|
6656
7360
|
}
|
|
6657
7361
|
main().catch((e) => {
|
|
6658
7362
|
process.stderr.write(`sessionpipe: ${e.message}
|