@cabane/companion 0.6.161 → 0.6.163

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.
Files changed (3) hide show
  1. package/dist/cli.js +367 -232
  2. package/dist/runtime.js +188 -68
  3. package/package.json +1 -1
package/dist/runtime.js CHANGED
@@ -2543,15 +2543,20 @@ import {
2543
2543
  rmSync as rmSync4,
2544
2544
  writeFileSync as writeFileSync3
2545
2545
  } from "fs";
2546
- import { dirname as dirname7, join as join11 } from "path";
2546
+ import { join as join11 } from "path";
2547
2547
  import { z as z4 } from "zod";
2548
- function credentialsPath() {
2548
+ var credentialMapSchema = z4.record(z4.string(), z4.string());
2549
+ function credentialsDir() {
2550
+ return join11(cabaneDir(), "credentials");
2551
+ }
2552
+ function legacyPath() {
2549
2553
  return join11(cabaneDir(), "credentials.json");
2550
2554
  }
2551
- var credentialStoreSchema = z4.record(z4.string(), z4.string());
2552
- function load() {
2553
- const path = credentialsPath();
2554
- if (!existsSync7(path)) return {};
2555
+ function devicePath(deviceId) {
2556
+ return join11(credentialsDir(), `${deviceId.replace(/[^A-Za-z0-9_-]/g, "_")}.json`);
2557
+ }
2558
+ function read(path) {
2559
+ if (!existsSync7(path)) return null;
2555
2560
  let raw;
2556
2561
  try {
2557
2562
  raw = readFileSync4(path, "utf8");
@@ -2560,19 +2565,25 @@ function load() {
2560
2565
  }
2561
2566
  if (raw.trim().length === 0) return {};
2562
2567
  try {
2563
- const result = credentialStoreSchema.safeParse(JSON.parse(raw));
2568
+ const result = credentialMapSchema.safeParse(JSON.parse(raw));
2564
2569
  return result.success ? result.data : {};
2565
2570
  } catch {
2566
2571
  return {};
2567
2572
  }
2568
2573
  }
2569
- function save(map) {
2570
- const path = credentialsPath();
2571
- mkdirSync5(dirname7(path), { recursive: true });
2572
- try {
2573
- chmodSync3(cabaneDir(), 448);
2574
- } catch {
2574
+ function load(deviceId) {
2575
+ return read(devicePath(deviceId)) ?? read(legacyPath()) ?? {};
2576
+ }
2577
+ function save(deviceId, map) {
2578
+ const dir2 = credentialsDir();
2579
+ mkdirSync5(dir2, { recursive: true });
2580
+ for (const d of [cabaneDir(), dir2]) {
2581
+ try {
2582
+ chmodSync3(d, 448);
2583
+ } catch {
2584
+ }
2575
2585
  }
2586
+ const path = devicePath(deviceId);
2576
2587
  const tmp = `${path}.${process.pid}.tmp`;
2577
2588
  try {
2578
2589
  writeFileSync3(tmp, JSON.stringify(map, null, 2) + "\n", { mode: 384 });
@@ -2589,26 +2600,23 @@ function save(map) {
2589
2600
  throw err;
2590
2601
  }
2591
2602
  }
2592
- function getCredential(agentId) {
2593
- return load()[agentId] ?? null;
2603
+ function getCredential(deviceId, agentId) {
2604
+ return load(deviceId)[agentId] ?? null;
2594
2605
  }
2595
- function setCredential(agentId, pat) {
2596
- const map = load();
2597
- if (map[agentId] === pat) return;
2606
+ function setCredential(deviceId, agentId, pat) {
2607
+ if (read(devicePath(deviceId))?.[agentId] === pat) return;
2608
+ const map = load(deviceId);
2598
2609
  map[agentId] = pat;
2599
- save(map);
2610
+ save(deviceId, map);
2600
2611
  }
2601
- function pruneCredentials(keepAgentIds) {
2602
- const map = load();
2612
+ function pruneCredentials(deviceId, keepAgentIds) {
2603
2613
  const keep = new Set(keepAgentIds);
2604
- let removed = 0;
2605
- for (const id of Object.keys(map)) {
2606
- if (!keep.has(id)) {
2607
- delete map[id];
2608
- removed += 1;
2609
- }
2610
- }
2611
- if (removed > 0) save(map);
2614
+ if (keep.size === 0) return [];
2615
+ const own = read(devicePath(deviceId));
2616
+ const map = own ?? load(deviceId);
2617
+ const removed = Object.keys(map).filter((id) => !keep.has(id));
2618
+ for (const id of removed) delete map[id];
2619
+ if (removed.length > 0 || own === null) save(deviceId, map);
2612
2620
  return removed;
2613
2621
  }
2614
2622
 
@@ -3922,14 +3930,14 @@ function readRequestContextTokens(msg) {
3922
3930
  return readSdkUsage(msg.message?.usage)?.inputTokens;
3923
3931
  }
3924
3932
  var CONTEXT_READING_TIMEOUT_MS = 2e3;
3925
- async function readHarnessContext(read) {
3926
- if (!read) return void 0;
3933
+ async function readHarnessContext(read2) {
3934
+ if (!read2) return void 0;
3927
3935
  let timer;
3928
3936
  const timeout = new Promise((resolve2) => {
3929
3937
  timer = setTimeout(() => resolve2(void 0), CONTEXT_READING_TIMEOUT_MS);
3930
3938
  });
3931
3939
  try {
3932
- return parseContextUsage(await Promise.race([read(), timeout]));
3940
+ return parseContextUsage(await Promise.race([read2(), timeout]));
3933
3941
  } catch {
3934
3942
  return void 0;
3935
3943
  } finally {
@@ -6136,14 +6144,14 @@ function decideResume3(stored, currentCwd) {
6136
6144
  function readUsageBaseline(raw) {
6137
6145
  if (!raw || typeof raw !== "object") return null;
6138
6146
  const rec = raw;
6139
- const read = (key) => {
6147
+ const read2 = (key) => {
6140
6148
  const v = rec[key];
6141
6149
  return typeof v === "number" && Number.isFinite(v) && v >= 0 ? v : null;
6142
6150
  };
6143
- const inputTokens = read("inputTokens");
6144
- const outputTokens = read("outputTokens");
6145
- const cacheReadTokens = read("cacheReadTokens");
6146
- const cacheCreationTokens = read("cacheCreationTokens");
6151
+ const inputTokens = read2("inputTokens");
6152
+ const outputTokens = read2("outputTokens");
6153
+ const cacheReadTokens = read2("cacheReadTokens");
6154
+ const cacheCreationTokens = read2("cacheCreationTokens");
6147
6155
  if (inputTokens === null || outputTokens === null || cacheReadTokens === null || cacheCreationTokens === null) {
6148
6156
  return null;
6149
6157
  }
@@ -9224,9 +9232,9 @@ function resolveSeqFloor(sources, ctx) {
9224
9232
  );
9225
9233
  return floor;
9226
9234
  }
9227
- function readOutboxFloor(read, turnId, log) {
9235
+ function readOutboxFloor(read2, turnId, log) {
9228
9236
  try {
9229
- return read(turnId);
9237
+ return read2(turnId);
9230
9238
  } catch (err) {
9231
9239
  log.debug(
9232
9240
  { turnId, err: err instanceof Error ? err.message : String(err) },
@@ -9340,7 +9348,7 @@ function clearPrepared(workspaceId, conversationId, agentId) {
9340
9348
 
9341
9349
  // src/transcript-writer.ts
9342
9350
  import { appendFileSync, chmodSync as chmodSync4, copyFileSync, mkdirSync as mkdirSync8, readdirSync, rmSync as rmSync6 } from "fs";
9343
- import { basename as basename3, dirname as dirname8, join as join17 } from "path";
9351
+ import { basename as basename3, dirname as dirname7, join as join17 } from "path";
9344
9352
  function transcriptsDir() {
9345
9353
  return join17(runnerStateDir(), "transcripts");
9346
9354
  }
@@ -9381,7 +9389,7 @@ var TranscriptWriter = class {
9381
9389
  preserveAnomaly() {
9382
9390
  if (this.broken) return;
9383
9391
  try {
9384
- const dir2 = join17(dirname8(this.path), "anomalies");
9392
+ const dir2 = join17(dirname7(this.path), "anomalies");
9385
9393
  mkdirSync8(dir2, { recursive: true, mode: 448 });
9386
9394
  chmodSync4(dir2, 448);
9387
9395
  const target = join17(dir2, basename3(this.path));
@@ -11302,7 +11310,7 @@ function publishStaged(from, to, move = atomicMove) {
11302
11310
  // src/providers/runner-base.ts
11303
11311
  import { createHash as createHash3 } from "crypto";
11304
11312
  import { existsSync as existsSync14, realpathSync as realpathSync4 } from "fs";
11305
- import { dirname as dirname9, join as join20, sep as sep3 } from "path";
11313
+ import { dirname as dirname8, join as join20, sep as sep3 } from "path";
11306
11314
  import { fileURLToPath as fileURLToPath5 } from "url";
11307
11315
  var COMPANION_MOUNT = "/opt/cabane/companion";
11308
11316
  var BASE_DOCKERFILE = `FROM node:22-slim
@@ -11348,10 +11356,10 @@ async function ensureBaseImage(docker, image, log) {
11348
11356
  return true;
11349
11357
  }
11350
11358
  function ownInstall(moduleFile = fileURLToPath5(import.meta.url)) {
11351
- let dir2 = dirname9(moduleFile);
11359
+ let dir2 = dirname8(moduleFile);
11352
11360
  for (let i = 0; i < 4; i++) {
11353
11361
  if (existsSync14(join20(dir2, "dist", "cli.js")) && existsSync14(join20(dir2, "package.json"))) break;
11354
- dir2 = dirname9(dir2);
11362
+ dir2 = dirname8(dir2);
11355
11363
  }
11356
11364
  if (!existsSync14(join20(dir2, "dist", "cli.js"))) return null;
11357
11365
  const sdk = join20(dir2, "node_modules", "@anthropic-ai", "claude-agent-sdk");
@@ -12411,7 +12419,7 @@ import {
12411
12419
  import { createRequire as createRequire2 } from "module";
12412
12420
  import { isIP } from "net";
12413
12421
  import { homedir as homedir6, networkInterfaces, tmpdir as tmpdir4, userInfo as userInfo2 } from "os";
12414
- import { dirname as dirname10, join as join22, sep as sep4 } from "path";
12422
+ import { dirname as dirname9, join as join22, sep as sep4 } from "path";
12415
12423
  import { fileURLToPath as fileURLToPath6 } from "url";
12416
12424
  import { SandboxManager } from "@anthropic-ai/sandbox-runtime";
12417
12425
  function accountHome() {
@@ -12424,9 +12432,9 @@ function accountHome() {
12424
12432
  var STORE_WALK_TIMEOUT_MS = 6e4;
12425
12433
  var STORE_WALK_MAX_AGE_MS = 5 * 6e4;
12426
12434
  function storeWalkRoots(homes, cabaneDir2) {
12427
- const companionHome = dirname10(cabaneDir2);
12428
- const parent = dirname10(companionHome);
12429
- const beside = homes.includes(companionHome) || dirname10(parent) === parent ? [] : [parent];
12435
+ const companionHome = dirname9(cabaneDir2);
12436
+ const parent = dirname9(companionHome);
12437
+ const beside = homes.includes(companionHome) || dirname9(parent) === parent ? [] : [parent];
12430
12438
  const candidates = [.../* @__PURE__ */ new Set([...homes, ...beside])];
12431
12439
  return candidates.filter(
12432
12440
  (root) => !candidates.some((other) => other !== root && isAtOrUnder(root, other))
@@ -13244,7 +13252,7 @@ var SseSubscriber = class {
13244
13252
  // src/updater.ts
13245
13253
  import { spawn as spawn8 } from "child_process";
13246
13254
  import { copyFileSync as copyFileSync3, existsSync as existsSync18, mkdirSync as mkdirSync16, renameSync as renameSync11, rmSync as rmSync14 } from "fs";
13247
- import { dirname as dirname14, join as join28 } from "path";
13255
+ import { dirname as dirname13, join as join28 } from "path";
13248
13256
 
13249
13257
  // src/boot-rule.ts
13250
13258
  import {
@@ -13256,7 +13264,7 @@ import {
13256
13264
  symlinkSync,
13257
13265
  writeFileSync as writeFileSync9
13258
13266
  } from "fs";
13259
- import { dirname as dirname11, join as join25 } from "path";
13267
+ import { dirname as dirname10, join as join25 } from "path";
13260
13268
  var UNCONFIRMED_WINDOW_MS = 10 * 6e4;
13261
13269
  var MAX_STAGE_ATTEMPTS = 3;
13262
13270
  function superseded(pending, ...running) {
@@ -13289,7 +13297,7 @@ import {
13289
13297
  rmSync as rmSync13,
13290
13298
  symlinkSync as symlinkSync2
13291
13299
  } from "fs";
13292
- import { dirname as dirname12, join as join26 } from "path";
13300
+ import { dirname as dirname11, join as join26 } from "path";
13293
13301
  import { fileURLToPath as fileURLToPath7 } from "url";
13294
13302
  function versionsDir() {
13295
13303
  return join26(cabaneDir(), "versions");
@@ -13304,7 +13312,7 @@ function versionPackageRoot(version) {
13304
13312
  return join26(versionsDir(), version, "node_modules", "@cabane", "companion");
13305
13313
  }
13306
13314
  function ownPackageRoot(moduleUrl = import.meta.url) {
13307
- return dirname12(dirname12(fileURLToPath7(moduleUrl)));
13315
+ return dirname11(dirname11(fileURLToPath7(moduleUrl)));
13308
13316
  }
13309
13317
  function packageVersion(root) {
13310
13318
  try {
@@ -13324,7 +13332,7 @@ function currentRoot() {
13324
13332
  }
13325
13333
  function pointCurrentAt(root) {
13326
13334
  const link = currentLink();
13327
- mkdirSync14(dirname12(link), { recursive: true });
13335
+ mkdirSync14(dirname11(link), { recursive: true });
13328
13336
  const fresh = `${link}.${process.pid}.${Date.now()}`;
13329
13337
  symlinkSync2(root, fresh);
13330
13338
  try {
@@ -13359,7 +13367,7 @@ function compareVersions(a, b) {
13359
13367
 
13360
13368
  // src/update-state.ts
13361
13369
  import { mkdirSync as mkdirSync15, readFileSync as readFileSync15, renameSync as renameSync10, writeFileSync as writeFileSync10 } from "fs";
13362
- import { dirname as dirname13, join as join27 } from "path";
13370
+ import { dirname as dirname12, join as join27 } from "path";
13363
13371
  import { z as z18 } from "zod";
13364
13372
  var resultSchema = z18.object({
13365
13373
  kind: z18.enum(["updated", "rolled_back", "failed"]),
@@ -13394,7 +13402,7 @@ function readUpdateState(path = updateStatePath()) {
13394
13402
  }
13395
13403
  }
13396
13404
  function writeUpdateState(state, path = updateStatePath()) {
13397
- mkdirSync15(dirname13(path), { recursive: true });
13405
+ mkdirSync15(dirname12(path), { recursive: true });
13398
13406
  const tmp = `${path}.${process.pid}.tmp`;
13399
13407
  writeFileSync10(tmp, `${JSON.stringify(state, null, 2)}
13400
13408
  `, { mode: 384 });
@@ -13435,7 +13443,7 @@ function writeBootGuard(root = ownPackageRoot()) {
13435
13443
  const built = join28(root, "dist", "boot-guard.js");
13436
13444
  if (!existsSync18(built)) return false;
13437
13445
  const target = bootGuardPath();
13438
- mkdirSync16(dirname14(target), { recursive: true });
13446
+ mkdirSync16(dirname13(target), { recursive: true });
13439
13447
  const tmp = `${target}.${process.pid}.tmp`;
13440
13448
  copyFileSync3(built, tmp);
13441
13449
  renameSync11(tmp, target);
@@ -13478,9 +13486,9 @@ async function stageVersion(version, harnesses, deps = {}) {
13478
13486
  if (!await verifyInstall(placed, version, harnesses, { run, execPath: node }))
13479
13487
  return { ok: true, root: placed };
13480
13488
  }
13481
- const npm = join28(dirname14(node), "npm");
13489
+ const npm = join28(dirname13(node), "npm");
13482
13490
  if (!existsSync18(npm))
13483
- return { ok: false, reason: `there is no npm beside Node at ${dirname14(node)}` };
13491
+ return { ok: false, reason: `there is no npm beside Node at ${dirname13(node)}` };
13484
13492
  mkdirSync16(versionsDir(), { recursive: true });
13485
13493
  const staging = join28(versionsDir(), `.${version}.staging-${process.pid}`);
13486
13494
  rmSync14(staging, { recursive: true, force: true });
@@ -13499,7 +13507,7 @@ async function stageVersion(version, harnesses, deps = {}) {
13499
13507
  ],
13500
13508
  {
13501
13509
  // npm's install scripts run on this Node, not whichever `node` is first on PATH.
13502
- env: { ...process.env, PATH: `${dirname14(node)}:${process.env.PATH ?? ""}` },
13510
+ env: { ...process.env, PATH: `${dirname13(node)}:${process.env.PATH ?? ""}` },
13503
13511
  timeoutMs: 15 * 6e4
13504
13512
  }
13505
13513
  );
@@ -13699,7 +13707,12 @@ var CompanionUpdater = class {
13699
13707
  const stage = this.deps.stage ?? stageVersion;
13700
13708
  this.staging = (async () => {
13701
13709
  this.deps.log.info({ version }, `Installing Cabane ${version} beside this one`);
13702
- const outcome = await stage(version, this.deps.harnesses());
13710
+ let outcome;
13711
+ try {
13712
+ outcome = await stage(version, this.deps.harnesses());
13713
+ } catch (err) {
13714
+ outcome = { ok: false, reason: err instanceof Error ? err.message : String(err) };
13715
+ }
13703
13716
  if (outcome.ok) {
13704
13717
  this.ready = version;
13705
13718
  this.readyAt = this.now();
@@ -13739,11 +13752,16 @@ var CompanionUpdater = class {
13739
13752
  })().finally(() => {
13740
13753
  this.staging = null;
13741
13754
  });
13742
- void this.staging.then(() => this.trySwap());
13755
+ this.staging.then(() => this.trySwap()).catch(
13756
+ (err) => this.deps.log.warn(
13757
+ { version, err: err instanceof Error ? err.message : String(err) },
13758
+ `Couldn't update to Cabane ${version}; trying again on a later check-in`
13759
+ )
13760
+ );
13743
13761
  }
13744
13762
  // Swap into the ready version: when nothing is running, when it has waited `SWAP_WAIT_MS` for
13745
13763
  // that, when this version is below the server's minimum, or `now` (the person asked). Called on
13746
- // each heartbeat until it does.
13764
+ // each heartbeat until it does, and only while auto-update is on unless the person asked.
13747
13765
  async trySwap(now = false) {
13748
13766
  const to = this.ready;
13749
13767
  if (!to || this.swapping) return;
@@ -13753,6 +13771,7 @@ var CompanionUpdater = class {
13753
13771
  this.deps.log.info({ version: to }, `Cabane ${to} is installed; it runs from the next start`);
13754
13772
  return;
13755
13773
  }
13774
+ if (!now && !this.updatesItself()) return;
13756
13775
  if (!now && !this.deps.isIdle() && !this.waitedLongEnough() && !this.belowMinimum()) return;
13757
13776
  this.swapping = true;
13758
13777
  try {
@@ -14151,7 +14170,7 @@ var CompanionSupervisor = class {
14151
14170
  saveConfig(this.config);
14152
14171
  }
14153
14172
  for (const it of items) {
14154
- if (it.credential) setCredential(it.agentId, it.credential);
14173
+ if (it.credential) setCredential(device.id, it.agentId, it.credential);
14155
14174
  }
14156
14175
  const byWorkspace = /* @__PURE__ */ new Map();
14157
14176
  for (const it of items) {
@@ -14162,8 +14181,17 @@ var CompanionSupervisor = class {
14162
14181
  for (const wsId of [...this.workspaces.keys()]) {
14163
14182
  if (!byWorkspace.has(wsId)) await this.removeWorkspace(wsId);
14164
14183
  }
14165
- for (const [wsId, list] of byWorkspace) this.reconcileWorkspace(wsId, list);
14166
- pruneCredentials(items.map((i) => i.agentId));
14184
+ for (const [wsId, list] of byWorkspace) this.reconcileWorkspace(device.id, wsId, list);
14185
+ const pruned = pruneCredentials(
14186
+ device.id,
14187
+ items.map((i) => i.agentId)
14188
+ );
14189
+ if (pruned.length > 0) {
14190
+ this.log.info(
14191
+ { deviceId: device.id, agentIds: pruned },
14192
+ `Removed the saved sign-in of ${pruned.length === 1 ? "agent" : "agents"} ${pruned.join(", ")}: no longer assigned to this device (${device.id}).`
14193
+ );
14194
+ }
14167
14195
  void this.recoverRuns();
14168
14196
  } finally {
14169
14197
  this.refreshing = false;
@@ -14177,7 +14205,7 @@ var CompanionSupervisor = class {
14177
14205
  }
14178
14206
  }
14179
14207
  // ---- reconciliation ----
14180
- reconcileWorkspace(workspaceId, list) {
14208
+ reconcileWorkspace(deviceId, workspaceId, list) {
14181
14209
  const first = list[0];
14182
14210
  let wr = this.workspaces.get(workspaceId);
14183
14211
  if (!wr) {
@@ -14203,7 +14231,7 @@ var CompanionSupervisor = class {
14203
14231
  if (!desired.has(agentId)) this.removeAgent(wr, agentId);
14204
14232
  }
14205
14233
  for (const it of list) {
14206
- const credential = it.credential ?? getCredential(it.agentId);
14234
+ const credential = it.credential ?? getCredential(deviceId, it.agentId);
14207
14235
  const runConfig = parseRunConfig(
14208
14236
  it.runConfig,
14209
14237
  (m) => this.log.warn(
@@ -15286,6 +15314,95 @@ function clearCrash() {
15286
15314
  }
15287
15315
  }
15288
15316
 
15317
+ // src/stray-companion.ts
15318
+ import { spawnSync } from "child_process";
15319
+ import { readFileSync as readFileSync17 } from "fs";
15320
+ import { join as join30 } from "path";
15321
+ var COMPANION_ENTRY = /@cabane\/companion\/|cabane-companion|(^|[\s/])cabane\s|\.cabane\/(versions|current|boot-guard)/;
15322
+ var START = /\sstart(\s|$)/;
15323
+ function companionStarts(psOutput) {
15324
+ const found = [];
15325
+ for (const line of psOutput.split("\n")) {
15326
+ const match = /^\s*(\d+)\s+(.*)$/.exec(line);
15327
+ if (!match) continue;
15328
+ const command = match[2];
15329
+ if (COMPANION_ENTRY.test(command) && START.test(command)) {
15330
+ found.push({ pid: Number(match[1]), command });
15331
+ }
15332
+ }
15333
+ return found;
15334
+ }
15335
+ function stateDirOf(env) {
15336
+ const home = env.CABANE_COMPANION_HOME || env.HOME;
15337
+ return home ? join30(home, ".cabane") : null;
15338
+ }
15339
+ function envFromPsE(output, command) {
15340
+ const line = output.replace(/\n$/, "");
15341
+ if (!line.startsWith(command)) return {};
15342
+ const env = {};
15343
+ for (const entry of line.slice(command.length).trim().split(/ (?=[A-Za-z_][A-Za-z0-9_]*=)/)) {
15344
+ const eq = entry.indexOf("=");
15345
+ if (eq > 0) env[entry.slice(0, eq)] = entry.slice(eq + 1);
15346
+ }
15347
+ return env;
15348
+ }
15349
+ function envFromProc(pid) {
15350
+ try {
15351
+ const env = {};
15352
+ for (const entry of readFileSync17(`/proc/${pid}/environ`, "utf8").split("\0")) {
15353
+ const eq = entry.indexOf("=");
15354
+ if (eq > 0) env[entry.slice(0, eq)] = entry.slice(eq + 1);
15355
+ }
15356
+ return env;
15357
+ } catch {
15358
+ return null;
15359
+ }
15360
+ }
15361
+ function envOf(pid, command) {
15362
+ if (process.platform === "linux") return envFromProc(pid);
15363
+ const result = spawnSync("ps", ["-E", "-ww", "-o", "command=", "-p", String(pid)], {
15364
+ encoding: "utf8",
15365
+ timeout: 5e3
15366
+ });
15367
+ if (result.error || result.status !== 0) return null;
15368
+ return envFromPsE(result.stdout, command);
15369
+ }
15370
+ function findStrayCompanions(exclude = [], deps = {}) {
15371
+ if (!deps.ps && (process.platform === "win32" || typeof process.getuid !== "function")) {
15372
+ return [];
15373
+ }
15374
+ const ps = deps.ps ?? (() => {
15375
+ const result = spawnSync("ps", ["-U", String(process.getuid()), "-o", "pid=,command="], {
15376
+ encoding: "utf8",
15377
+ timeout: 5e3,
15378
+ maxBuffer: 16 * 1024 * 1024
15379
+ });
15380
+ return result.error || typeof result.stdout !== "string" ? null : result.stdout;
15381
+ });
15382
+ const listing = ps();
15383
+ if (!listing) return [];
15384
+ const dir2 = deps.dir ?? cabaneDir();
15385
+ const skip = /* @__PURE__ */ new Set([...deps.self ?? [process.pid, process.ppid], ...exclude]);
15386
+ return companionStarts(listing).filter((c) => {
15387
+ if (skip.has(c.pid)) return false;
15388
+ const env = (deps.env ?? envOf)(c.pid, c.command);
15389
+ return env !== null && stateDirOf(env) === dir2;
15390
+ });
15391
+ }
15392
+ var SERVICE_ENTRY = /\.cabane\/boot-guard/;
15393
+ function strayCompanionLines(strays, retry) {
15394
+ const one = strays.length === 1;
15395
+ return [
15396
+ `Another Cabane companion is running for this computer's ~/.cabane, and ~/.cabane has no record of it:`,
15397
+ ...strays.map((s) => ` pid ${s.pid} ${s.command}`),
15398
+ `Two companions for one ~/.cabane sign each other's agents out. Stop ${one ? "it" : "them"} first:`,
15399
+ ...strays.map(
15400
+ (s) => SERVICE_ENTRY.test(s.command) ? ` pid ${s.pid} is a service, which restarts if killed: cabane service uninstall` : ` kill ${s.pid}`
15401
+ ),
15402
+ `then run ${retry} again.`
15403
+ ];
15404
+ }
15405
+
15289
15406
  // src/runtime.ts
15290
15407
  async function createCompanionRuntime(opts = {}) {
15291
15408
  const log = getLogger();
@@ -15312,6 +15429,8 @@ async function createCompanionRuntime(opts = {}) {
15312
15429
  const startedAt = (/* @__PURE__ */ new Date()).toISOString();
15313
15430
  const pre = readLiveRuntimeState();
15314
15431
  if (pre && await verifyRuntime(pre) === "stale") clearRuntimeState();
15432
+ const strays = findStrayCompanions(pre ? [pre.pid] : []);
15433
+ if (strays.length > 0) return { ok: false, reason: "stray-companion", strays };
15315
15434
  const claim = acquireRuntimeState({
15316
15435
  pid: process.pid,
15317
15436
  startedAt,
@@ -15423,5 +15542,6 @@ async function closeSurfaces(control, dashboard) {
15423
15542
  if (dashboard) await dashboard.close();
15424
15543
  }
15425
15544
  export {
15426
- createCompanionRuntime
15545
+ createCompanionRuntime,
15546
+ strayCompanionLines
15427
15547
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cabane/companion",
3
- "version": "0.6.161",
3
+ "version": "0.6.163",
4
4
  "type": "module",
5
5
  "description": "The Cabane Companion (headless): connect an agent on your machine to your Cabane workspace as a responder — drive work against your own codebase, files, and MCP servers without putting any of it in Cabane.",
6
6
  "license": "UNLICENSED",