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