sessionpipe 0.13.2 → 0.13.3

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.13.2 · Apache-2.0 · https://sessionpipe.org
2
+ // sessionpipe 0.13.3 · Apache-2.0 · https://sessionpipe.org
3
3
  var __defProp = Object.defineProperty;
4
4
  var __export = (target2, all) => {
5
5
  for (var name in all)
@@ -1045,6 +1045,14 @@ function claudeAccounts(dirs, home = homeDir()) {
1045
1045
  }
1046
1046
  return out2;
1047
1047
  }
1048
+ function claudeAccountDirs(dirs, home = homeDir()) {
1049
+ const out2 = [];
1050
+ for (const d of dirs) {
1051
+ const l = claudeLogin(d, home);
1052
+ if (l) out2.push({ id: l.id, dir: d });
1053
+ }
1054
+ return out2;
1055
+ }
1048
1056
  var LINK_RE = /^https:\/\/claude\.ai\/code\/session_[A-Za-z0-9]+$/;
1049
1057
  function transcriptLink(records) {
1050
1058
  for (let i = records.length - 1; i >= 0; i--) {
@@ -2740,7 +2748,8 @@ __export(claude_code_control_exports, {
2740
2748
  relaunchMode: () => relaunchMode,
2741
2749
  resolveHome: () => resolveHome,
2742
2750
  runClaude: () => runClaude,
2743
- sessionFolder: () => sessionFolder
2751
+ sessionFolder: () => sessionFolder,
2752
+ versionManagerBins: () => versionManagerBins
2744
2753
  });
2745
2754
  import { execFile, execFileSync as execFileSync3 } from "node:child_process";
2746
2755
  import { closeSync as closeSync2, openSync as openSync2, readdirSync as readdirSync4, readFileSync as readFileSync6, readSync as readSync2, statSync as statSync5 } from "node:fs";
@@ -3298,6 +3307,29 @@ var cmpVersion = (a, b) => {
3298
3307
  if ((pa[i] ?? 0) !== (pb[i] ?? 0)) return (pa[i] ?? 0) - (pb[i] ?? 0);
3299
3308
  return 0;
3300
3309
  };
3310
+ function versionManagerBins(env = process.env, home = os5.homedir()) {
3311
+ const out2 = [];
3312
+ const versions = (dir, tail2) => {
3313
+ let names = [];
3314
+ try {
3315
+ names = readdirSync4(dir).filter((n) => /^v?\d+(\.\d+)*$/.test(n));
3316
+ } catch {
3317
+ return;
3318
+ }
3319
+ for (const n of names.sort((a, b) => cmpVersion(a.replace(/^v/, ""), b.replace(/^v/, ""))).reverse())
3320
+ out2.push(path10.join(dir, n, ...tail2));
3321
+ };
3322
+ versions(path10.join(env.NVM_DIR || path10.join(home, ".nvm"), "versions", "node"), ["bin"]);
3323
+ const fnmDirs = env.FNM_DIR ? [env.FNM_DIR] : [
3324
+ path10.join(home, "Library", "Application Support", "fnm"),
3325
+ path10.join(home, ".local", "share", "fnm"),
3326
+ path10.join(home, ".fnm")
3327
+ ];
3328
+ for (const d of fnmDirs) versions(path10.join(d, "node-versions"), ["installation", "bin"]);
3329
+ out2.push(path10.join(env.VOLTA_HOME || path10.join(home, ".volta"), "bin"));
3330
+ versions(path10.join(env.ASDF_DATA_DIR || path10.join(home, ".asdf"), "installs", "nodejs"), ["bin"]);
3331
+ return out2;
3332
+ }
3301
3333
  function findClaude(env = process.env, home = os5.homedir()) {
3302
3334
  if (env.SESSIONPIPE_CLAUDE && isExe(env.SESSIONPIPE_CLAUDE)) return env.SESSIONPIPE_CLAUDE;
3303
3335
  const names = process.platform === "win32" ? ["claude.exe", "claude.cmd"] : ["claude"];
@@ -3312,6 +3344,7 @@ function findClaude(env = process.env, home = os5.homedir()) {
3312
3344
  "/usr/local/bin/claude"
3313
3345
  ])
3314
3346
  if (isExe(p)) return p;
3347
+ for (const d of versionManagerBins(env, home)) if (isExe(path10.join(d, "claude"))) return path10.join(d, "claude");
3315
3348
  const app = path10.join(home, "Library", "Application Support", "Claude", "claude-code");
3316
3349
  let versions = [];
3317
3350
  try {
@@ -6475,7 +6508,7 @@ var writeState = (file, s) => writePrivate(file, {
6475
6508
  });
6476
6509
 
6477
6510
  // src/control/pair.ts
6478
- var VERSION = true ? "0.13.2" : "0.0.0";
6511
+ var VERSION = true ? "0.13.3" : "0.0.0";
6479
6512
  var RENEW_FOR_MS = 60 * 6e4;
6480
6513
  async function json(r) {
6481
6514
  try {
@@ -6683,8 +6716,16 @@ var spec = (o) => ({
6683
6716
  description: DESCRIPTION,
6684
6717
  argv: [o.node, o.cli, "control", "run"],
6685
6718
  logPath: path22.join(o.logDir, "daemon.log"),
6686
- ...o.home ? { home: o.home } : {}
6719
+ ...o.home ? { home: o.home } : {},
6720
+ ...o.env ? { env: o.env } : {}
6687
6721
  });
6722
+ function serviceEnv(env = process.env) {
6723
+ const out2 = {};
6724
+ if (env.PATH) out2.PATH = env.PATH;
6725
+ const claude = claude_code_control_exports.findClaude(env);
6726
+ if (claude) out2.SESSIONPIPE_CLAUDE = claude;
6727
+ return out2;
6728
+ }
6688
6729
  function installService2(o) {
6689
6730
  const logDir = controlState(o.env);
6690
6731
  mkdirSync7(logDir, { recursive: true, mode: 448 });
@@ -6695,7 +6736,7 @@ function installService2(o) {
6695
6736
  } catch {
6696
6737
  }
6697
6738
  const r = installService({
6698
- ...spec({ node: o.node, cli: o.cli, logDir }),
6739
+ ...spec({ node: o.node, cli: o.cli, logDir, env: serviceEnv(o.env) }),
6699
6740
  ...o.exec ? { exec: o.exec } : {},
6700
6741
  ...o.platform ? { platform: o.platform } : {}
6701
6742
  });
@@ -7069,7 +7110,7 @@ async function tellDaemon(state2, job, session, cwd, transcript) {
7069
7110
  300
7070
7111
  );
7071
7112
  }
7072
- var VERSION2 = true ? "0.13.2" : "0.0.0";
7113
+ var VERSION2 = true ? "0.13.3" : "0.0.0";
7073
7114
  function enqueueJob(state2, job) {
7074
7115
  const jobs = jobsDir(state2);
7075
7116
  mkdirSync11(jobs, { recursive: true, mode: 448 });
@@ -10026,10 +10067,12 @@ var FITS = {
10026
10067
  resume: (s) => HEADLESS.has(s.harness) && s.known && !s.live,
10027
10068
  fork: (s) => HEADLESS.has(s.harness) && s.known && s.live && s.canFork && !s.copied
10028
10069
  };
10070
+ var CLAUDE_NOT_FOUND = "Claude Code not found on this computer: no `claude` on the daemon's PATH or in the usual folders (nvm, fnm, volta, asdf, Homebrew, ~/.local/bin, the desktop app). Run `sessionpipe connect` again from a shell where `claude` works, or set SESSIONPIPE_CLAUDE to its path in the daemon's service file";
10029
10071
  function routePrompt(s) {
10030
10072
  for (const mode of DELIVERY_LADDER) if (FITS[mode](s)) return { mode };
10031
10073
  if (!HEADLESS.has(s.harness)) return { unsupported: `no idle delivery for ${s.harness} yet` };
10032
10074
  if (!s.known) return { refused: "no_session" };
10075
+ if (s.harness === "claude-code" && s.installed === false) return { unsupported: CLAUDE_NOT_FOUND };
10033
10076
  return {
10034
10077
  unsupported: s.harness === "claude-code" ? s.copied ? "the session is open, and so is the copy its messages already go to; a copy is never itself copied" : "the session is open and this Claude Code can't copy it (no --fork-session)" : `the session is open in ${s.harness} right now, and a ${s.harness} session can't be copied: send it again once it has been idle a couple of minutes`
10035
10078
  };
@@ -10068,7 +10111,7 @@ function frameInbox(text, receiver, id) {
10068
10111
 
10069
10112
  ${text}`;
10070
10113
  }
10071
- var COPY_NOTE = "You are a copy of that session, started to answer this message: the conversation above is a replay, the original session is somewhere else, and nothing you do here appears in it. Answer the message and stop; don't carry on the original's work.";
10114
+ var COPY_NOTE = "You are a copy of that session, started because the original is open in a window, and you have its conversation, folder and tools. Do what the message asks the way the original would: edits, commands and publishing all take effect for real. Only this conversation stays here, so the person reads your reply and not the original's window: say what you did, then stop.";
10072
10115
  function frameStart(text, receiver) {
10073
10116
  return `${text}
10074
10117
 
@@ -10834,7 +10877,7 @@ function startNamed(cmd) {
10834
10877
  return null;
10835
10878
  }
10836
10879
  }
10837
- var VERSION3 = true ? "0.13.2" : "0.0.0";
10880
+ var VERSION3 = true ? "0.13.3" : "0.0.0";
10838
10881
  var MAX_REPLY = 2e4;
10839
10882
  var HELLO_EVERY_MS = 10 * 6e4;
10840
10883
  var TURN_FALLBACK_MS = 30 * 6e4;
@@ -10971,6 +11014,8 @@ var ControlDaemon = class {
10971
11014
  procWaiters = [];
10972
11015
  /** configDir → the last login answer and when (a yes is kept 10 minutes, a no 30 s). */
10973
11016
  logins = /* @__PURE__ */ new Map();
11017
+ /** account id → the `signed_in` the log last said, so a change is said once. */
11018
+ signedInSaid = /* @__PURE__ */ new Map();
10974
11019
  /** Intake paused for an update: the long-polls end and wait for this. */
10975
11020
  paused = null;
10976
11021
  /** Poll answers whose messages are being read and handed out. */
@@ -11556,7 +11601,7 @@ ${IN_PLACE_ANSWER}`,
11556
11601
  // Every ACP agent's turn is acked `api` (§6).
11557
11602
  ...harnesses.some((h) => acp_control_exports.acpAgent(h)) ? ["api"] : []
11558
11603
  ];
11559
- const accounts = this.accounts();
11604
+ const accounts = await this.signedInMarked(this.accounts());
11560
11605
  const start = this.startOptions(harnesses);
11561
11606
  for (const r of this.cfg.receivers) {
11562
11607
  const body = {
@@ -11908,6 +11953,47 @@ ${IN_PLACE_ANSWER}`,
11908
11953
  return [];
11909
11954
  }
11910
11955
  }
11956
+ /** Each Claude Code account's `signed_in` for hello: what `claude auth status` says
11957
+ * for its config dir (the answer a resume would get, cached as `signedIn` caches it),
11958
+ * so a receiver can say "run claude auth login on <machine>" before a message is
11959
+ * sent, not in its failed ack. An account signed in from any of its dirs is signed in;
11960
+ * one the daemon can't ask (no claude, no answer) is left unsaid. A change is logged.
11961
+ * (2026-11-05: sessions in the Claude app sign in through the app, and the command
11962
+ * line the daemon resumes with had no login of its own; the first message found out.) */
11963
+ async signedInMarked(accounts) {
11964
+ const login = this.deps.login;
11965
+ if (!login || !accounts.length || !this.deps.claude()) return accounts;
11966
+ const states = /* @__PURE__ */ new Map();
11967
+ for (const { id, dir } of claudeAccountDirs(this.deps.claudeDirs())) {
11968
+ const was = states.get(id);
11969
+ if (was === true) continue;
11970
+ const is = await this.loginState(dir, () => login(dir));
11971
+ if (is !== null || was === void 0) states.set(id, is);
11972
+ }
11973
+ return accounts.map((a) => {
11974
+ const is = a.harness === "claude-code" ? states.get(a.id) : void 0;
11975
+ if (is === void 0 || is === null) return a;
11976
+ const who2 = a.email ?? a.name ?? a.id;
11977
+ if (this.signedInSaid.get(a.id) !== is) {
11978
+ this.signedInSaid.set(a.id, is);
11979
+ this.deps.log(
11980
+ is ? `claude is signed in as ${who2}` : `claude isn't signed in as ${who2} as the daemon sees it: a message to a session of that account would fail until \`claude auth login\` is run here`
11981
+ );
11982
+ }
11983
+ return { ...a, signed_in: is };
11984
+ });
11985
+ }
11986
+ /** One harness account's login as `signedIn` reads it, with the answer itself. */
11987
+ async loginState(key, ask2) {
11988
+ const now = this.deps.now();
11989
+ const hit = this.logins.get(key);
11990
+ let login = hit && now - hit.at < (hit.login.loggedIn === false ? 3e4 : 6e5) ? hit.login : null;
11991
+ if (!login) {
11992
+ login = await ask2().catch(() => ({ loggedIn: null }));
11993
+ this.logins.set(key, { at: now, login });
11994
+ }
11995
+ return login.loggedIn;
11996
+ }
11911
11997
  /** The harnesses this machine can run a message in, for hello: at most
11912
11998
  * HELLO_HARNESSES_MAX, the three with their own drivers first. */
11913
11999
  harnesses() {
@@ -12278,6 +12364,7 @@ ${IN_PLACE_ANSWER}`,
12278
12364
  live,
12279
12365
  known: !!t,
12280
12366
  canFork: !!claude?.caps.fork,
12367
+ installed: !!claude,
12281
12368
  // A message for a session that already has a copy goes to that copy; when the copy
12282
12369
  // is live too the person is told, rather than handed a copy of a copy.
12283
12370
  copied: !!this.state.copies[ref.id],
@@ -12441,14 +12528,7 @@ ${IN_PLACE_ANSWER}`;
12441
12528
  }
12442
12529
  /** One harness account's login, asked at most every 10 minutes (30 s after a no). */
12443
12530
  async signedIn(key, ask2) {
12444
- const now = this.deps.now();
12445
- const hit = this.logins.get(key);
12446
- let login = hit && now - hit.at < (hit.login.loggedIn === false ? 3e4 : 6e5) ? hit.login : null;
12447
- if (!login) {
12448
- login = await ask2().catch(() => ({ loggedIn: null }));
12449
- this.logins.set(key, { at: now, login });
12450
- }
12451
- return login.loggedIn !== false;
12531
+ return await this.loginState(key, ask2) !== false;
12452
12532
  }
12453
12533
  /** Null when a Codex or Antigravity run may go ahead; otherwise what to tell the person. */
12454
12534
  async drivenProblem(drv) {
@@ -12562,8 +12642,7 @@ ${IN_PLACE_ANSWER}`;
12562
12642
  if (acp_control_exports.acpAgent(harness)) return this.resumeAcp(r, id, cmd, place, sent);
12563
12643
  if (harness !== "claude-code") return this.resumeDriven(r, id, cmd, place, sent);
12564
12644
  const claude = this.deps.claude();
12565
- if (!claude)
12566
- return this.finish(r, id, { outcome: "failed", detail: "Claude Code isn't installed on this machine" });
12645
+ if (!claude) return this.finish(r, id, { outcome: "failed", detail: CLAUDE_NOT_FOUND });
12567
12646
  const ref = parseSessionRef(cmd.session);
12568
12647
  const dir = place.transcript?.configDir;
12569
12648
  const home = homeSettings2(dir);
@@ -12856,8 +12935,7 @@ ${COPY_NOTE}` : "");
12856
12935
  detail: "that new session's id is already taken"
12857
12936
  });
12858
12937
  const claude = this.deps.sdk ? null : this.deps.claude();
12859
- if (!this.deps.sdk && !claude)
12860
- return this.finish(r, id, { outcome: "failed", detail: "Claude Code isn't installed on this machine" });
12938
+ if (!this.deps.sdk && !claude) return this.finish(r, id, { outcome: "failed", detail: CLAUDE_NOT_FOUND });
12861
12939
  const signedOut = await this.loginProblem(this.deps.env?.CLAUDE_CONFIG_DIR);
12862
12940
  if (signedOut) return this.finish(r, id, { outcome: "failed", detail: signedOut });
12863
12941
  if (claude) {
@@ -13844,6 +13922,7 @@ async function runDaemon(out2, distDir2) {
13844
13922
  }
13845
13923
  return claude;
13846
13924
  };
13925
+ if (!findClaude2()) log2(CLAUDE_NOT_FOUND);
13847
13926
  const sdk = await loadSdk({ claudeBin: findClaude2(), env: process.env, log: log2 });
13848
13927
  let updater = null;
13849
13928
  d = new ControlDaemon(cfg, controlState(), socketPath(stateDir()), {