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/dist/cli.js CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- // sessionpipe 0.5.0 · Apache-2.0 · https://sessionpipe.org
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, spawn as spawn2, spawnSync as spawnSync2 } from "node:child_process";
11
- import { mkdtempSync, readdirSync as readdirSync8, readFileSync as readFileSync12, realpathSync as realpathSync5, rmSync as rmSync2, statSync as statSync9, unlinkSync as unlinkSync7 } from "node:fs";
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 path21 from "node:path";
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", "PreToolUse", "PostToolUse", "Stop"];
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, lean) {
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, opts = {}) {
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, opts.lean);
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, opts = {}) {
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, opts.lean)) ? "current" : "stale"
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
- if (!id) return { session: { id: "" }, events: [], stdout: "{}\n" };
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: "{}\n" };
494
+ return { session, events: [], stdout };
494
495
  }
495
- const r = { session, events, stdout: "{}\n", hints: { workspaces } };
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/cli.ts
2946
- import { existsSync as existsSync12, realpathSync as realpathSync4, watchFile } from "node:fs";
2947
- import os7 from "node:os";
2948
- import path19 from "node:path";
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.5.0" : "0.0.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/control/daemon.ts
3518
- import { existsSync as existsSync10, mkdirSync as mkdirSync9, realpathSync as realpathSync3, statSync as statSync8, unlinkSync as unlinkSync5 } from "node:fs";
3519
- import net2 from "node:net";
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
- import {
3956
- appendFileSync as appendFileSync4,
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(path16.dirname(file), { recursive: true, mode: 448 });
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(path16.dirname(this.file), { recursive: true, mode: 448 });
4064
- const fd = openSync5(this.file, "a", 384);
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
- closeSync5(fd);
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 (!existsSync9(this.file) && !this.seenAt.size) return;
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(path16.dirname(this.file), { recursive: true, mode: 448 });
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(path16.dirname(this.file), { recursive: true, mode: 448 });
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(path16.dirname(this.file), { recursive: true, mode: 448 });
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.5.0" : "0.0.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(path17.join(dir, "nonces.jsonl"), deps.now);
4243
- this.acks = new AckOutbox(path17.join(dir, "acks.jsonl"));
4244
- this.state = readState(path17.join(dir, "state.json"));
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
- this.deps.log(`control daemon ${VERSION2} up \xB7 ${this.cfg.receivers.length} receiver(s) \xB7 socket ${this.sock}`);
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
- async stop() {
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
- if (process.platform !== "win32")
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
- listen() {
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 (existsSync10(this.sock)) unlinkSync5(this.sock);
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
- return new Promise((resolve, reject) => {
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
- for (const m of body.messages ?? []) {
4532
- if (m && !this.inflight.has(m.id)) fresh++;
4533
- void this.handle(cur, m).catch(
4534
- (e) => this.deps.log(`handle ${String(m?.id).slice(-8)}: ${String(e?.message || e)}`)
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 sleep(2e3);
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 sleep(pause(backoff++));
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 sleep(pause(backoff++));
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 sleep(2e3 * (round + 1));
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 (!path17.isAbsolute(dir)) return null;
5336
+ if (!path19.isAbsolute(dir)) return null;
4734
5337
  let r;
4735
5338
  try {
4736
- r = realpathSync3.native(dir);
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 = realpathSync3.native(f);
5347
+ rf = realpathSync4.native(f);
4745
5348
  } catch {
4746
5349
  }
4747
- if (r === rf || r.startsWith(rf.endsWith(path17.sep) ? rf : rf + path17.sep)) return r;
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(path17.join(this.dir, "state.json"), this.state);
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(frame(cmd2.text, r.url));
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
- frame(cmd2.text, r.url)
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 sleep = (ms) => new Promise((r) => setTimeout(r, ms).unref?.());
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 existsSync11, mkdirSync as mkdirSync10, unlinkSync as unlinkSync6, writeFileSync as writeFileSync8 } from "node:fs";
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 path18 from "node:path";
5169
- var VERSION3 = true ? "0.5.0" : "0.0.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 sleep2 = o.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
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) => path18.resolve(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 sleep2(2e3);
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 LABEL = "org.sessionpipe.control";
5305
- var UNIT = "sessionpipe-control.service";
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, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
@@ -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>${LABEL}</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(path18.join(o.logDir, "daemon.log"))}</string>
5320
- <key>StandardErrorPath</key><string>${xml(path18.join(o.logDir, "daemon.log"))}</string>
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: path18.join(home, "Library", "LaunchAgents", `${LABEL}.plist`), body: plist },
5339
- unit: { file: path18.join(home, ".config", "systemd", "user", UNIT), body: unit }
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(path18.dirname(f.plist.file), { recursive: true });
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}/${LABEL}`]);
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 ${LABEL} (log: ${path18.join(logDir, "daemon.log")})`;
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 ${LABEL} 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: ${path18.join(logDir, "daemon.log")})`;
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(path18.dirname(f.unit.file), { recursive: true });
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", UNIT]);
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 ${UNIT}; ${linger.note} (journalctl --user -u ${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" && existsSync11(f.plist.file)) {
5408
- run("launchctl", ["bootout", `gui/${process.getuid?.() ?? ""}/${LABEL}`]);
5409
- run("launchctl", ["bootout", `user/${process.getuid?.() ?? ""}/${LABEL}`]);
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" && existsSync11(f.unit.file)) {
5413
- run("systemctl", ["--user", "disable", "--now", UNIT]);
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 = path19.join(distDir2, "cli.js");
6230
+ let cli = path21.join(distDir2, "cli.js");
5617
6231
  try {
5618
- cli = realpathSync4(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
- async function runDaemon(out2) {
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}:${existsSync12(bin) ? realpathSync4(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
- await d.start();
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
- process.on("SIGTERM", bye);
5719
- process.on("SIGINT", bye);
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 accessSync2, chmodSync as chmodSync3, constants as constants2, mkdirSync as mkdirSync11, readFileSync as readFileSync11, renameSync as renameSync4, rmSync, writeFileSync as writeFileSync9 } from "node:fs";
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 path20 from "node:path";
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 path20.resolve(env.SESSIONPIPE_DATA);
5750
- return path20.join(env.XDG_DATA_HOME || path20.join(os8.homedir(), ".local", "share"), "sessionpipe");
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) => path20.join(dataDir(env), LAUNCHER);
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 = readFileSync11(file);
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(path20.join(dir, f), readFileSync11(path20.join(o.distDir, f)), 420) || changed;
5786
- const launcher = path20.join(dir, LAUNCHER);
5787
- changed = writeIfChanged(launcher, launcherScript(o.node, path20.join(dir, "hook.js"), o.version), 493) || changed;
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(path20.join(dataDir(env), f), { force: true });
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 = readFileSync11(file, "utf8");
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
- accessSync2(node, constants2.X_OK);
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
- accessSync2(path20.join(path20.dirname(file), f), constants2.R_OK);
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 = path21.dirname(fileURLToPath(import.meta.url));
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 = path21.join(distDir, "hook.js");
6525
+ let script = path23.join(distDir, "hook.js");
5860
6526
  try {
5861
- script = realpathSync5(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(path21.dirname(configFile()))} at 0600 \u2014 no unlocked keychain in this session${process.env.SSH_CONNECTION ? " (an ssh shell)" : ""}`
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 = path21.join(distDir, "hook.js");
6379
- const tmp = mkdtempSync(path21.join(os9.tmpdir(), "sessionpipe-doctor-"));
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: path21.join(tmp, "config.json"),
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 = readFileSync12(path21.join(state, "timing.jsonl"), "utf8").trim().split("\n").slice(-100);
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(readFileSync12(f, "utf8"));
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
- const r = await fetch("https://registry.npmjs.org/sessionpipe/latest", { signal: AbortSignal.timeout(8e3) });
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
- function prefixOf(p) {
6653
- const re = process.platform === "win32" ? /^(.*?)[\\/]node_modules[\\/]sessionpipe[\\/]/ : /^(.*)\/lib\/node_modules\/sessionpipe\//;
6654
- const m = re.exec(p);
6655
- return m?.[1] ?? null;
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}