@cabane/companion 0.6.162 → 0.6.164

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 +370 -239
  2. package/dist/runtime.js +178 -67
  3. package/package.json +1 -1
package/dist/runtime.js CHANGED
@@ -1678,7 +1678,9 @@ function clearRuntimeStateIfOurs(instanceId) {
1678
1678
  }
1679
1679
  rmSync3(path, { force: true });
1680
1680
  }
1681
- function readLiveRuntimeState() {
1681
+ function readLiveRuntimeState({
1682
+ sweep = true
1683
+ } = {}) {
1682
1684
  const path = runtimePath();
1683
1685
  if (!existsSync6(path)) return null;
1684
1686
  let parsed;
@@ -1691,7 +1693,7 @@ function readLiveRuntimeState() {
1691
1693
  try {
1692
1694
  process.kill(parsed.pid, 0);
1693
1695
  } catch {
1694
- clearRuntimeState();
1696
+ if (sweep) clearRuntimeState();
1695
1697
  return null;
1696
1698
  }
1697
1699
  return parsed;
@@ -2543,15 +2545,20 @@ import {
2543
2545
  rmSync as rmSync4,
2544
2546
  writeFileSync as writeFileSync3
2545
2547
  } from "fs";
2546
- import { dirname as dirname7, join as join11 } from "path";
2548
+ import { join as join11 } from "path";
2547
2549
  import { z as z4 } from "zod";
2548
- function credentialsPath() {
2550
+ var credentialMapSchema = z4.record(z4.string(), z4.string());
2551
+ function credentialsDir() {
2552
+ return join11(cabaneDir(), "credentials");
2553
+ }
2554
+ function legacyPath() {
2549
2555
  return join11(cabaneDir(), "credentials.json");
2550
2556
  }
2551
- var credentialStoreSchema = z4.record(z4.string(), z4.string());
2552
- function load() {
2553
- const path = credentialsPath();
2554
- if (!existsSync7(path)) return {};
2557
+ function devicePath(deviceId) {
2558
+ return join11(credentialsDir(), `${deviceId.replace(/[^A-Za-z0-9_-]/g, "_")}.json`);
2559
+ }
2560
+ function read(path) {
2561
+ if (!existsSync7(path)) return null;
2555
2562
  let raw;
2556
2563
  try {
2557
2564
  raw = readFileSync4(path, "utf8");
@@ -2560,19 +2567,25 @@ function load() {
2560
2567
  }
2561
2568
  if (raw.trim().length === 0) return {};
2562
2569
  try {
2563
- const result = credentialStoreSchema.safeParse(JSON.parse(raw));
2570
+ const result = credentialMapSchema.safeParse(JSON.parse(raw));
2564
2571
  return result.success ? result.data : {};
2565
2572
  } catch {
2566
2573
  return {};
2567
2574
  }
2568
2575
  }
2569
- function save(map) {
2570
- const path = credentialsPath();
2571
- mkdirSync5(dirname7(path), { recursive: true });
2572
- try {
2573
- chmodSync3(cabaneDir(), 448);
2574
- } catch {
2576
+ function load(deviceId) {
2577
+ return read(devicePath(deviceId)) ?? read(legacyPath()) ?? {};
2578
+ }
2579
+ function save(deviceId, map) {
2580
+ const dir2 = credentialsDir();
2581
+ mkdirSync5(dir2, { recursive: true });
2582
+ for (const d of [cabaneDir(), dir2]) {
2583
+ try {
2584
+ chmodSync3(d, 448);
2585
+ } catch {
2586
+ }
2575
2587
  }
2588
+ const path = devicePath(deviceId);
2576
2589
  const tmp = `${path}.${process.pid}.tmp`;
2577
2590
  try {
2578
2591
  writeFileSync3(tmp, JSON.stringify(map, null, 2) + "\n", { mode: 384 });
@@ -2589,26 +2602,23 @@ function save(map) {
2589
2602
  throw err;
2590
2603
  }
2591
2604
  }
2592
- function getCredential(agentId) {
2593
- return load()[agentId] ?? null;
2605
+ function getCredential(deviceId, agentId) {
2606
+ return load(deviceId)[agentId] ?? null;
2594
2607
  }
2595
- function setCredential(agentId, pat) {
2596
- const map = load();
2597
- if (map[agentId] === pat) return;
2608
+ function setCredential(deviceId, agentId, pat) {
2609
+ if (read(devicePath(deviceId))?.[agentId] === pat) return;
2610
+ const map = load(deviceId);
2598
2611
  map[agentId] = pat;
2599
- save(map);
2612
+ save(deviceId, map);
2600
2613
  }
2601
- function pruneCredentials(keepAgentIds) {
2602
- const map = load();
2614
+ function pruneCredentials(deviceId, keepAgentIds) {
2603
2615
  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);
2616
+ if (keep.size === 0) return [];
2617
+ const own = read(devicePath(deviceId));
2618
+ const map = own ?? load(deviceId);
2619
+ const removed = Object.keys(map).filter((id) => !keep.has(id));
2620
+ for (const id of removed) delete map[id];
2621
+ if (removed.length > 0 || own === null) save(deviceId, map);
2612
2622
  return removed;
2613
2623
  }
2614
2624
 
@@ -3922,14 +3932,14 @@ function readRequestContextTokens(msg) {
3922
3932
  return readSdkUsage(msg.message?.usage)?.inputTokens;
3923
3933
  }
3924
3934
  var CONTEXT_READING_TIMEOUT_MS = 2e3;
3925
- async function readHarnessContext(read) {
3926
- if (!read) return void 0;
3935
+ async function readHarnessContext(read2) {
3936
+ if (!read2) return void 0;
3927
3937
  let timer;
3928
3938
  const timeout = new Promise((resolve2) => {
3929
3939
  timer = setTimeout(() => resolve2(void 0), CONTEXT_READING_TIMEOUT_MS);
3930
3940
  });
3931
3941
  try {
3932
- return parseContextUsage(await Promise.race([read(), timeout]));
3942
+ return parseContextUsage(await Promise.race([read2(), timeout]));
3933
3943
  } catch {
3934
3944
  return void 0;
3935
3945
  } finally {
@@ -6136,14 +6146,14 @@ function decideResume3(stored, currentCwd) {
6136
6146
  function readUsageBaseline(raw) {
6137
6147
  if (!raw || typeof raw !== "object") return null;
6138
6148
  const rec = raw;
6139
- const read = (key) => {
6149
+ const read2 = (key) => {
6140
6150
  const v = rec[key];
6141
6151
  return typeof v === "number" && Number.isFinite(v) && v >= 0 ? v : null;
6142
6152
  };
6143
- const inputTokens = read("inputTokens");
6144
- const outputTokens = read("outputTokens");
6145
- const cacheReadTokens = read("cacheReadTokens");
6146
- const cacheCreationTokens = read("cacheCreationTokens");
6153
+ const inputTokens = read2("inputTokens");
6154
+ const outputTokens = read2("outputTokens");
6155
+ const cacheReadTokens = read2("cacheReadTokens");
6156
+ const cacheCreationTokens = read2("cacheCreationTokens");
6147
6157
  if (inputTokens === null || outputTokens === null || cacheReadTokens === null || cacheCreationTokens === null) {
6148
6158
  return null;
6149
6159
  }
@@ -9224,9 +9234,9 @@ function resolveSeqFloor(sources, ctx) {
9224
9234
  );
9225
9235
  return floor;
9226
9236
  }
9227
- function readOutboxFloor(read, turnId, log) {
9237
+ function readOutboxFloor(read2, turnId, log) {
9228
9238
  try {
9229
- return read(turnId);
9239
+ return read2(turnId);
9230
9240
  } catch (err) {
9231
9241
  log.debug(
9232
9242
  { turnId, err: err instanceof Error ? err.message : String(err) },
@@ -9340,7 +9350,7 @@ function clearPrepared(workspaceId, conversationId, agentId) {
9340
9350
 
9341
9351
  // src/transcript-writer.ts
9342
9352
  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";
9353
+ import { basename as basename3, dirname as dirname7, join as join17 } from "path";
9344
9354
  function transcriptsDir() {
9345
9355
  return join17(runnerStateDir(), "transcripts");
9346
9356
  }
@@ -9381,7 +9391,7 @@ var TranscriptWriter = class {
9381
9391
  preserveAnomaly() {
9382
9392
  if (this.broken) return;
9383
9393
  try {
9384
- const dir2 = join17(dirname8(this.path), "anomalies");
9394
+ const dir2 = join17(dirname7(this.path), "anomalies");
9385
9395
  mkdirSync8(dir2, { recursive: true, mode: 448 });
9386
9396
  chmodSync4(dir2, 448);
9387
9397
  const target = join17(dir2, basename3(this.path));
@@ -11302,7 +11312,7 @@ function publishStaged(from, to, move = atomicMove) {
11302
11312
  // src/providers/runner-base.ts
11303
11313
  import { createHash as createHash3 } from "crypto";
11304
11314
  import { existsSync as existsSync14, realpathSync as realpathSync4 } from "fs";
11305
- import { dirname as dirname9, join as join20, sep as sep3 } from "path";
11315
+ import { dirname as dirname8, join as join20, sep as sep3 } from "path";
11306
11316
  import { fileURLToPath as fileURLToPath5 } from "url";
11307
11317
  var COMPANION_MOUNT = "/opt/cabane/companion";
11308
11318
  var BASE_DOCKERFILE = `FROM node:22-slim
@@ -11348,10 +11358,10 @@ async function ensureBaseImage(docker, image, log) {
11348
11358
  return true;
11349
11359
  }
11350
11360
  function ownInstall(moduleFile = fileURLToPath5(import.meta.url)) {
11351
- let dir2 = dirname9(moduleFile);
11361
+ let dir2 = dirname8(moduleFile);
11352
11362
  for (let i = 0; i < 4; i++) {
11353
11363
  if (existsSync14(join20(dir2, "dist", "cli.js")) && existsSync14(join20(dir2, "package.json"))) break;
11354
- dir2 = dirname9(dir2);
11364
+ dir2 = dirname8(dir2);
11355
11365
  }
11356
11366
  if (!existsSync14(join20(dir2, "dist", "cli.js"))) return null;
11357
11367
  const sdk = join20(dir2, "node_modules", "@anthropic-ai", "claude-agent-sdk");
@@ -12411,7 +12421,7 @@ import {
12411
12421
  import { createRequire as createRequire2 } from "module";
12412
12422
  import { isIP } from "net";
12413
12423
  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";
12424
+ import { dirname as dirname9, join as join22, sep as sep4 } from "path";
12415
12425
  import { fileURLToPath as fileURLToPath6 } from "url";
12416
12426
  import { SandboxManager } from "@anthropic-ai/sandbox-runtime";
12417
12427
  function accountHome() {
@@ -12424,9 +12434,9 @@ function accountHome() {
12424
12434
  var STORE_WALK_TIMEOUT_MS = 6e4;
12425
12435
  var STORE_WALK_MAX_AGE_MS = 5 * 6e4;
12426
12436
  function storeWalkRoots(homes, cabaneDir2) {
12427
- const companionHome = dirname10(cabaneDir2);
12428
- const parent = dirname10(companionHome);
12429
- const beside = homes.includes(companionHome) || dirname10(parent) === parent ? [] : [parent];
12437
+ const companionHome = dirname9(cabaneDir2);
12438
+ const parent = dirname9(companionHome);
12439
+ const beside = homes.includes(companionHome) || dirname9(parent) === parent ? [] : [parent];
12430
12440
  const candidates = [.../* @__PURE__ */ new Set([...homes, ...beside])];
12431
12441
  return candidates.filter(
12432
12442
  (root) => !candidates.some((other) => other !== root && isAtOrUnder(root, other))
@@ -13244,7 +13254,7 @@ var SseSubscriber = class {
13244
13254
  // src/updater.ts
13245
13255
  import { spawn as spawn8 } from "child_process";
13246
13256
  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";
13257
+ import { dirname as dirname13, join as join28 } from "path";
13248
13258
 
13249
13259
  // src/boot-rule.ts
13250
13260
  import {
@@ -13256,7 +13266,7 @@ import {
13256
13266
  symlinkSync,
13257
13267
  writeFileSync as writeFileSync9
13258
13268
  } from "fs";
13259
- import { dirname as dirname11, join as join25 } from "path";
13269
+ import { dirname as dirname10, join as join25 } from "path";
13260
13270
  var UNCONFIRMED_WINDOW_MS = 10 * 6e4;
13261
13271
  var MAX_STAGE_ATTEMPTS = 3;
13262
13272
  function superseded(pending, ...running) {
@@ -13289,7 +13299,7 @@ import {
13289
13299
  rmSync as rmSync13,
13290
13300
  symlinkSync as symlinkSync2
13291
13301
  } from "fs";
13292
- import { dirname as dirname12, join as join26 } from "path";
13302
+ import { dirname as dirname11, join as join26 } from "path";
13293
13303
  import { fileURLToPath as fileURLToPath7 } from "url";
13294
13304
  function versionsDir() {
13295
13305
  return join26(cabaneDir(), "versions");
@@ -13304,7 +13314,7 @@ function versionPackageRoot(version) {
13304
13314
  return join26(versionsDir(), version, "node_modules", "@cabane", "companion");
13305
13315
  }
13306
13316
  function ownPackageRoot(moduleUrl = import.meta.url) {
13307
- return dirname12(dirname12(fileURLToPath7(moduleUrl)));
13317
+ return dirname11(dirname11(fileURLToPath7(moduleUrl)));
13308
13318
  }
13309
13319
  function packageVersion(root) {
13310
13320
  try {
@@ -13324,7 +13334,7 @@ function currentRoot() {
13324
13334
  }
13325
13335
  function pointCurrentAt(root) {
13326
13336
  const link = currentLink();
13327
- mkdirSync14(dirname12(link), { recursive: true });
13337
+ mkdirSync14(dirname11(link), { recursive: true });
13328
13338
  const fresh = `${link}.${process.pid}.${Date.now()}`;
13329
13339
  symlinkSync2(root, fresh);
13330
13340
  try {
@@ -13359,7 +13369,7 @@ function compareVersions(a, b) {
13359
13369
 
13360
13370
  // src/update-state.ts
13361
13371
  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";
13372
+ import { dirname as dirname12, join as join27 } from "path";
13363
13373
  import { z as z18 } from "zod";
13364
13374
  var resultSchema = z18.object({
13365
13375
  kind: z18.enum(["updated", "rolled_back", "failed"]),
@@ -13394,7 +13404,7 @@ function readUpdateState(path = updateStatePath()) {
13394
13404
  }
13395
13405
  }
13396
13406
  function writeUpdateState(state, path = updateStatePath()) {
13397
- mkdirSync15(dirname13(path), { recursive: true });
13407
+ mkdirSync15(dirname12(path), { recursive: true });
13398
13408
  const tmp = `${path}.${process.pid}.tmp`;
13399
13409
  writeFileSync10(tmp, `${JSON.stringify(state, null, 2)}
13400
13410
  `, { mode: 384 });
@@ -13435,7 +13445,7 @@ function writeBootGuard(root = ownPackageRoot()) {
13435
13445
  const built = join28(root, "dist", "boot-guard.js");
13436
13446
  if (!existsSync18(built)) return false;
13437
13447
  const target = bootGuardPath();
13438
- mkdirSync16(dirname14(target), { recursive: true });
13448
+ mkdirSync16(dirname13(target), { recursive: true });
13439
13449
  const tmp = `${target}.${process.pid}.tmp`;
13440
13450
  copyFileSync3(built, tmp);
13441
13451
  renameSync11(tmp, target);
@@ -13478,9 +13488,9 @@ async function stageVersion(version, harnesses, deps = {}) {
13478
13488
  if (!await verifyInstall(placed, version, harnesses, { run, execPath: node }))
13479
13489
  return { ok: true, root: placed };
13480
13490
  }
13481
- const npm = join28(dirname14(node), "npm");
13491
+ const npm = join28(dirname13(node), "npm");
13482
13492
  if (!existsSync18(npm))
13483
- return { ok: false, reason: `there is no npm beside Node at ${dirname14(node)}` };
13493
+ return { ok: false, reason: `there is no npm beside Node at ${dirname13(node)}` };
13484
13494
  mkdirSync16(versionsDir(), { recursive: true });
13485
13495
  const staging = join28(versionsDir(), `.${version}.staging-${process.pid}`);
13486
13496
  rmSync14(staging, { recursive: true, force: true });
@@ -13499,7 +13509,7 @@ async function stageVersion(version, harnesses, deps = {}) {
13499
13509
  ],
13500
13510
  {
13501
13511
  // 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 ?? ""}` },
13512
+ env: { ...process.env, PATH: `${dirname13(node)}:${process.env.PATH ?? ""}` },
13503
13513
  timeoutMs: 15 * 6e4
13504
13514
  }
13505
13515
  );
@@ -14162,7 +14172,7 @@ var CompanionSupervisor = class {
14162
14172
  saveConfig(this.config);
14163
14173
  }
14164
14174
  for (const it of items) {
14165
- if (it.credential) setCredential(it.agentId, it.credential);
14175
+ if (it.credential) setCredential(device.id, it.agentId, it.credential);
14166
14176
  }
14167
14177
  const byWorkspace = /* @__PURE__ */ new Map();
14168
14178
  for (const it of items) {
@@ -14173,8 +14183,17 @@ var CompanionSupervisor = class {
14173
14183
  for (const wsId of [...this.workspaces.keys()]) {
14174
14184
  if (!byWorkspace.has(wsId)) await this.removeWorkspace(wsId);
14175
14185
  }
14176
- for (const [wsId, list] of byWorkspace) this.reconcileWorkspace(wsId, list);
14177
- pruneCredentials(items.map((i) => i.agentId));
14186
+ for (const [wsId, list] of byWorkspace) this.reconcileWorkspace(device.id, wsId, list);
14187
+ const pruned = pruneCredentials(
14188
+ device.id,
14189
+ items.map((i) => i.agentId)
14190
+ );
14191
+ if (pruned.length > 0) {
14192
+ this.log.info(
14193
+ { deviceId: device.id, agentIds: pruned },
14194
+ `Removed the saved sign-in of ${pruned.length === 1 ? "agent" : "agents"} ${pruned.join(", ")}: no longer assigned to this device (${device.id}).`
14195
+ );
14196
+ }
14178
14197
  void this.recoverRuns();
14179
14198
  } finally {
14180
14199
  this.refreshing = false;
@@ -14188,7 +14207,7 @@ var CompanionSupervisor = class {
14188
14207
  }
14189
14208
  }
14190
14209
  // ---- reconciliation ----
14191
- reconcileWorkspace(workspaceId, list) {
14210
+ reconcileWorkspace(deviceId, workspaceId, list) {
14192
14211
  const first = list[0];
14193
14212
  let wr = this.workspaces.get(workspaceId);
14194
14213
  if (!wr) {
@@ -14214,7 +14233,7 @@ var CompanionSupervisor = class {
14214
14233
  if (!desired.has(agentId)) this.removeAgent(wr, agentId);
14215
14234
  }
14216
14235
  for (const it of list) {
14217
- const credential = it.credential ?? getCredential(it.agentId);
14236
+ const credential = it.credential ?? getCredential(deviceId, it.agentId);
14218
14237
  const runConfig = parseRunConfig(
14219
14238
  it.runConfig,
14220
14239
  (m) => this.log.warn(
@@ -15297,6 +15316,95 @@ function clearCrash() {
15297
15316
  }
15298
15317
  }
15299
15318
 
15319
+ // src/stray-companion.ts
15320
+ import { spawnSync } from "child_process";
15321
+ import { readFileSync as readFileSync17 } from "fs";
15322
+ import { join as join30 } from "path";
15323
+ var COMPANION_ENTRY = /@cabane\/companion\/|cabane-companion|(^|[\s/])cabane\s|\.cabane\/(versions|current|boot-guard)/;
15324
+ var START = /\sstart(\s|$)/;
15325
+ function companionStarts(psOutput) {
15326
+ const found = [];
15327
+ for (const line of psOutput.split("\n")) {
15328
+ const match = /^\s*(\d+)\s+(.*)$/.exec(line);
15329
+ if (!match) continue;
15330
+ const command = match[2];
15331
+ if (COMPANION_ENTRY.test(command) && START.test(command)) {
15332
+ found.push({ pid: Number(match[1]), command });
15333
+ }
15334
+ }
15335
+ return found;
15336
+ }
15337
+ function stateDirOf(env) {
15338
+ const home = env.CABANE_COMPANION_HOME || env.HOME;
15339
+ return home ? join30(home, ".cabane") : null;
15340
+ }
15341
+ function envFromPsE(output, command) {
15342
+ const line = output.replace(/\n$/, "");
15343
+ if (!line.startsWith(command)) return {};
15344
+ const env = {};
15345
+ for (const entry of line.slice(command.length).trim().split(/ (?=[A-Za-z_][A-Za-z0-9_]*=)/)) {
15346
+ const eq = entry.indexOf("=");
15347
+ if (eq > 0) env[entry.slice(0, eq)] = entry.slice(eq + 1);
15348
+ }
15349
+ return env;
15350
+ }
15351
+ function envFromProc(pid) {
15352
+ try {
15353
+ const env = {};
15354
+ for (const entry of readFileSync17(`/proc/${pid}/environ`, "utf8").split("\0")) {
15355
+ const eq = entry.indexOf("=");
15356
+ if (eq > 0) env[entry.slice(0, eq)] = entry.slice(eq + 1);
15357
+ }
15358
+ return env;
15359
+ } catch {
15360
+ return null;
15361
+ }
15362
+ }
15363
+ function envOf(pid, command) {
15364
+ if (process.platform === "linux") return envFromProc(pid);
15365
+ const result = spawnSync("ps", ["-E", "-ww", "-o", "command=", "-p", String(pid)], {
15366
+ encoding: "utf8",
15367
+ timeout: 5e3
15368
+ });
15369
+ if (result.error || result.status !== 0) return null;
15370
+ return envFromPsE(result.stdout, command);
15371
+ }
15372
+ function findStrayCompanions(exclude = [], deps = {}) {
15373
+ if (!deps.ps && (process.platform === "win32" || typeof process.getuid !== "function")) {
15374
+ return [];
15375
+ }
15376
+ const ps = deps.ps ?? (() => {
15377
+ const result = spawnSync("ps", ["-U", String(process.getuid()), "-o", "pid=,command="], {
15378
+ encoding: "utf8",
15379
+ timeout: 5e3,
15380
+ maxBuffer: 16 * 1024 * 1024
15381
+ });
15382
+ return result.error || typeof result.stdout !== "string" ? null : result.stdout;
15383
+ });
15384
+ const listing = ps();
15385
+ if (!listing) return [];
15386
+ const dir2 = deps.dir ?? cabaneDir();
15387
+ const skip = /* @__PURE__ */ new Set([...deps.self ?? [process.pid, process.ppid], ...exclude]);
15388
+ return companionStarts(listing).filter((c) => {
15389
+ if (skip.has(c.pid)) return false;
15390
+ const env = (deps.env ?? envOf)(c.pid, c.command);
15391
+ return env !== null && stateDirOf(env) === dir2;
15392
+ });
15393
+ }
15394
+ var SERVICE_ENTRY = /\.cabane\/boot-guard/;
15395
+ function strayCompanionLines(strays, retry) {
15396
+ const one = strays.length === 1;
15397
+ return [
15398
+ `Another Cabane companion is running for this computer's ~/.cabane, and ~/.cabane has no record of it:`,
15399
+ ...strays.map((s) => ` pid ${s.pid} ${s.command}`),
15400
+ `Two companions for one ~/.cabane sign each other's agents out. Stop ${one ? "it" : "them"} first:`,
15401
+ ...strays.map(
15402
+ (s) => SERVICE_ENTRY.test(s.command) ? ` pid ${s.pid} is a service, which restarts if killed: cabane service uninstall` : ` kill ${s.pid}`
15403
+ ),
15404
+ `then run ${retry} again.`
15405
+ ];
15406
+ }
15407
+
15300
15408
  // src/runtime.ts
15301
15409
  async function createCompanionRuntime(opts = {}) {
15302
15410
  const log = getLogger();
@@ -15323,6 +15431,8 @@ async function createCompanionRuntime(opts = {}) {
15323
15431
  const startedAt = (/* @__PURE__ */ new Date()).toISOString();
15324
15432
  const pre = readLiveRuntimeState();
15325
15433
  if (pre && await verifyRuntime(pre) === "stale") clearRuntimeState();
15434
+ const strays = findStrayCompanions(pre ? [pre.pid] : []);
15435
+ if (strays.length > 0) return { ok: false, reason: "stray-companion", strays };
15326
15436
  const claim = acquireRuntimeState({
15327
15437
  pid: process.pid,
15328
15438
  startedAt,
@@ -15434,5 +15544,6 @@ async function closeSurfaces(control, dashboard) {
15434
15544
  if (dashboard) await dashboard.close();
15435
15545
  }
15436
15546
  export {
15437
- createCompanionRuntime
15547
+ createCompanionRuntime,
15548
+ strayCompanionLines
15438
15549
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cabane/companion",
3
- "version": "0.6.162",
3
+ "version": "0.6.164",
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",