@cabane/companion 0.6.34 → 0.6.35

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
@@ -1010,7 +1010,7 @@ async function shakeOutHarness(runtime, cfg, deps = {}) {
1010
1010
  try {
1011
1011
  if (runtime === "opencode") {
1012
1012
  const url = cfg.opencode?.serverUrl;
1013
- if (!url) return "failed";
1013
+ if (!url) return "absent";
1014
1014
  return await probeOpencode(url) !== null ? "ok" : "failed";
1015
1015
  }
1016
1016
  const { auth, presence } = runtime === "codex" ? {
@@ -1020,11 +1020,13 @@ async function shakeOutHarness(runtime, cfg, deps = {}) {
1020
1020
  auth: ["claude", ["auth", "status"]],
1021
1021
  presence: ["claude", ["--version"]]
1022
1022
  };
1023
+ const presenceRun = await run(presence[0], [...presence[1]]);
1024
+ if (presenceRun.error === "spawn") return "absent";
1025
+ if (presenceRun.code !== 0) return "unverified";
1023
1026
  const authRun = await run(auth[0], [...auth[1]]);
1024
1027
  if (authRun.code === 0) return "ok";
1025
1028
  if (looksUnsupported(authRun.output)) {
1026
- const presenceRun = await run(presence[0], [...presence[1]]);
1027
- return presenceRun.code === 0 ? "unverified" : "failed";
1029
+ return "unverified";
1028
1030
  }
1029
1031
  return "failed";
1030
1032
  } catch {
@@ -1032,6 +1034,7 @@ async function shakeOutHarness(runtime, cfg, deps = {}) {
1032
1034
  }
1033
1035
  }
1034
1036
  function connectedLine(runtime, verdict) {
1037
+ if (verdict === "absent") throw new Error("an absent harness cannot be connected");
1035
1038
  const label = HARNESS_LABELS[runtime];
1036
1039
  if (verdict !== "failed") return `${label} connected.`;
1037
1040
  return `${label} connected \u2014 ${FAILED_SUFFIX[runtime]}`;
@@ -1041,6 +1044,14 @@ var FAILED_SUFFIX = {
1041
1044
  codex: "it doesn\u2019t look signed in yet. Run `codex login` once, then it\u2019s ready.",
1042
1045
  opencode: "its server isn\u2019t answering. Start `opencode serve`, then it\u2019s ready."
1043
1046
  };
1047
+ function absentLine(runtime) {
1048
+ if (runtime === "opencode") {
1049
+ return "No opencode server is configured \u2014 run `opencode serve` and connect with `--url`.";
1050
+ }
1051
+ const label = HARNESS_LABELS[runtime];
1052
+ const login = runtime === "codex" ? "`codex login`" : "`claude`";
1053
+ return `${label} isn\u2019t installed on this machine \u2014 install it and sign in (${login}), then run this again.`;
1054
+ }
1044
1055
  function looksUnsupported(output) {
1045
1056
  return /unrecognized|unknown (sub)?command|unexpected argument|invalid (sub)?command|no such (sub)?command|usage:|did you mean/i.test(
1046
1057
  output
@@ -1049,17 +1060,17 @@ function looksUnsupported(output) {
1049
1060
  function runBounded(command, args) {
1050
1061
  return new Promise((resolve) => {
1051
1062
  let settled = false;
1052
- const done = (code, output) => {
1063
+ const done = (result) => {
1053
1064
  if (settled) return;
1054
1065
  settled = true;
1055
1066
  clearTimeout(timer);
1056
- resolve({ code, output });
1067
+ resolve(result);
1057
1068
  };
1058
1069
  let child;
1059
1070
  try {
1060
1071
  child = spawn4(command, args, { stdio: ["ignore", "pipe", "pipe"] });
1061
1072
  } catch {
1062
- resolve({ code: null, output: "" });
1073
+ resolve({ code: null, output: "", error: "spawn" });
1063
1074
  return;
1064
1075
  }
1065
1076
  let out = "";
@@ -1070,11 +1081,11 @@ function runBounded(command, args) {
1070
1081
  child.stderr?.on("data", capture);
1071
1082
  const timer = setTimeout(() => {
1072
1083
  child.kill("SIGKILL");
1073
- done(null, out);
1084
+ done({ code: null, output: out, error: "timeout" });
1074
1085
  }, CHECK_TIMEOUT_MS);
1075
1086
  timer.unref?.();
1076
- child.once("error", () => done(null, out));
1077
- child.once("exit", (code) => done(code, out));
1087
+ child.once("error", () => done({ code: null, output: out, error: "spawn" }));
1088
+ child.once("exit", (code) => done({ code, output: out }));
1078
1089
  });
1079
1090
  }
1080
1091
 
@@ -1271,11 +1282,23 @@ async function connect2(raw, opts = {}) {
1271
1282
  }
1272
1283
  const live = await liveSocket();
1273
1284
  if (live) {
1274
- const result = await controlRequest(live, {
1275
- cmd: "connect",
1276
- runtime,
1277
- ...opts.serverUrl ? { serverUrl: opts.serverUrl } : {}
1278
- });
1285
+ let result;
1286
+ try {
1287
+ result = await controlRequest(
1288
+ live,
1289
+ {
1290
+ cmd: "connect",
1291
+ runtime,
1292
+ ...opts.serverUrl ? { serverUrl: opts.serverUrl } : {}
1293
+ },
1294
+ 1e4
1295
+ );
1296
+ } catch (err) {
1297
+ const detail = err instanceof Error ? err.message : String(err);
1298
+ throw new CompanionError(
1299
+ `Couldn\u2019t reach the running companion \u2014 ${detail}; try again, or \`cabane-companion stop\` and \`start\`.`
1300
+ );
1301
+ }
1279
1302
  if (!result.ok) throw new CompanionError(result.message);
1280
1303
  write(INDENT + tick(result.message));
1281
1304
  return;
@@ -1307,8 +1330,9 @@ async function connect2(raw, opts = {}) {
1307
1330
  }
1308
1331
  next = { ...cfg, opencode: { serverUrl } };
1309
1332
  }
1310
- saveConfig(next);
1311
1333
  const verdict = await shakeOutHarness(runtime, next);
1334
+ if (verdict === "absent") throw new CompanionError(absentLine(runtime));
1335
+ saveConfig(next);
1312
1336
  write(INDENT + tick(connectedLine(runtime, verdict)));
1313
1337
  write(`${INDENT}Not running yet \u2014 start it with: cabane-companion start`);
1314
1338
  }
@@ -1692,21 +1716,39 @@ function consoleMessageFormat(log, messageKey) {
1692
1716
  return short ? `${short} ${msg}` : msg;
1693
1717
  }
1694
1718
  var cached = null;
1695
- function getLogger() {
1696
- if (cached) return cached;
1719
+ var consoleLogging = true;
1720
+ function setConsoleLogging(enabled) {
1721
+ consoleLogging = enabled;
1722
+ }
1723
+ function createLogger(destinations = {}) {
1697
1724
  const path = companionLogPath();
1698
- mkdirSync5(dirname3(path), { recursive: true });
1725
+ if (!destinations.file) mkdirSync5(dirname3(path), { recursive: true });
1699
1726
  const streams = [];
1700
1727
  if (process.env.CABANE_COMPANION_DAEMON !== "1") {
1701
1728
  const consoleStream = pretty({
1702
1729
  colorize: true,
1703
1730
  ignore: CONSOLE_IGNORE,
1704
- messageFormat: (log, messageKey) => consoleMessageFormat(log, messageKey)
1731
+ messageFormat: (log, messageKey) => consoleMessageFormat(log, messageKey),
1732
+ ...destinations.console ? { destination: destinations.console } : {}
1733
+ });
1734
+ streams.push({
1735
+ level: "info",
1736
+ stream: {
1737
+ write(chunk) {
1738
+ if (consoleLogging) consoleStream.write(chunk);
1739
+ }
1740
+ }
1705
1741
  });
1706
- streams.push({ level: "info", stream: consoleStream });
1707
1742
  }
1708
- streams.push({ level: "debug", stream: createWriteStream(path, { flags: "a" }) });
1709
- cached = pino({ level: "debug" }, pino.multistream(streams));
1743
+ streams.push({
1744
+ level: "debug",
1745
+ stream: destinations.file ?? createWriteStream(path, { flags: "a" })
1746
+ });
1747
+ return pino({ level: "debug" }, pino.multistream(streams));
1748
+ }
1749
+ function getLogger() {
1750
+ if (cached) return cached;
1751
+ cached = createLogger();
1710
1752
  return cached;
1711
1753
  }
1712
1754
 
@@ -10201,11 +10243,13 @@ async function createCompanionRuntime(opts = {}) {
10201
10243
  });
10202
10244
  await supervisor.start();
10203
10245
  const connectHarness = async (runtime, serverUrl) => {
10246
+ const candidate = runtime === "opencode" ? { ...supervisor.currentConfig(), opencode: { serverUrl: serverUrl ?? "" } } : supervisor.currentConfig();
10247
+ const verdict = await shakeOutHarness(runtime, candidate);
10248
+ if (verdict === "absent") return { ok: false, error: absentLine(runtime) };
10204
10249
  const result = await supervisor.enableHarness(
10205
10250
  runtime === "opencode" ? { runtime: "opencode", serverUrl: serverUrl ?? "" } : { runtime }
10206
10251
  );
10207
10252
  if (!result.ok) return { ok: false, error: result.error };
10208
- const verdict = await shakeOutHarness(runtime, supervisor.currentConfig());
10209
10253
  return { ok: true, message: connectedLine(runtime, verdict) };
10210
10254
  };
10211
10255
  const control = await startControlServer({
@@ -10430,6 +10474,15 @@ var DEPLOY_GRACE_MS = 6.5 * 60 * 60 * 1e3;
10430
10474
  var MAX_CODES = 3;
10431
10475
  async function start(opts = {}) {
10432
10476
  const interactive = isInteractive();
10477
+ if (!interactive) return startScript(opts, false);
10478
+ setConsoleLogging(false);
10479
+ try {
10480
+ await startScript(opts, true);
10481
+ } finally {
10482
+ setConsoleLogging(true);
10483
+ }
10484
+ }
10485
+ async function startScript(opts, interactive) {
10433
10486
  const running = await liveCompanion();
10434
10487
  if (running) {
10435
10488
  reportAlreadyRunning(running.pid);
@@ -10439,7 +10492,9 @@ async function start(opts = {}) {
10439
10492
  let paired = null;
10440
10493
  if (!isDevicePaired()) {
10441
10494
  paired = await pairHere(opts, interactive);
10442
- if (!paired) return;
10495
+ if (!paired) {
10496
+ return;
10497
+ }
10443
10498
  const { note } = writePairedConfig(paired);
10444
10499
  if (note) getLogger().warn({ note }, "companion: salvaged an older config on pairing");
10445
10500
  }
@@ -10470,6 +10525,7 @@ async function start(opts = {}) {
10470
10525
  write(` Stop: Ctrl-C Logs: ${tildePath(companionLogPath())}`);
10471
10526
  blank();
10472
10527
  write(`${INDENT}Listening for messages\u2026`);
10528
+ setConsoleLogging(true);
10473
10529
  } else {
10474
10530
  if (!offered.printedSomething) blank();
10475
10531
  write(
@@ -11242,7 +11298,12 @@ program.parseAsync(process.argv).catch((err) => {
11242
11298
  process.exitCode = 130;
11243
11299
  return;
11244
11300
  }
11245
- process.stderr.write(`${err instanceof Error ? err.stack ?? err.message : String(err)}
11301
+ setConsoleLogging(false);
11302
+ try {
11303
+ getLogger().error({ err }, "companion: command failed");
11304
+ } catch {
11305
+ }
11306
+ process.stderr.write(`${err instanceof Error ? err.message : String(err)}
11246
11307
  `);
11247
11308
  process.exitCode = 1;
11248
11309
  });
package/dist/runtime.js CHANGED
@@ -461,21 +461,36 @@ function consoleMessageFormat(log, messageKey) {
461
461
  return short ? `${short} ${msg}` : msg;
462
462
  }
463
463
  var cached = null;
464
- function getLogger() {
465
- if (cached) return cached;
464
+ var consoleLogging = true;
465
+ function createLogger(destinations = {}) {
466
466
  const path = companionLogPath();
467
- mkdirSync2(dirname2(path), { recursive: true });
467
+ if (!destinations.file) mkdirSync2(dirname2(path), { recursive: true });
468
468
  const streams = [];
469
469
  if (process.env.CABANE_COMPANION_DAEMON !== "1") {
470
470
  const consoleStream = pretty({
471
471
  colorize: true,
472
472
  ignore: CONSOLE_IGNORE,
473
- messageFormat: (log, messageKey) => consoleMessageFormat(log, messageKey)
473
+ messageFormat: (log, messageKey) => consoleMessageFormat(log, messageKey),
474
+ ...destinations.console ? { destination: destinations.console } : {}
475
+ });
476
+ streams.push({
477
+ level: "info",
478
+ stream: {
479
+ write(chunk) {
480
+ if (consoleLogging) consoleStream.write(chunk);
481
+ }
482
+ }
474
483
  });
475
- streams.push({ level: "info", stream: consoleStream });
476
484
  }
477
- streams.push({ level: "debug", stream: createWriteStream(path, { flags: "a" }) });
478
- cached = pino({ level: "debug" }, pino.multistream(streams));
485
+ streams.push({
486
+ level: "debug",
487
+ stream: destinations.file ?? createWriteStream(path, { flags: "a" })
488
+ });
489
+ return pino({ level: "debug" }, pino.multistream(streams));
490
+ }
491
+ function getLogger() {
492
+ if (cached) return cached;
493
+ cached = createLogger();
479
494
  return cached;
480
495
  }
481
496
 
@@ -1465,7 +1480,7 @@ async function shakeOutHarness(runtime, cfg, deps = {}) {
1465
1480
  try {
1466
1481
  if (runtime === "opencode") {
1467
1482
  const url = cfg.opencode?.serverUrl;
1468
- if (!url) return "failed";
1483
+ if (!url) return "absent";
1469
1484
  return await probeOpencode(url) !== null ? "ok" : "failed";
1470
1485
  }
1471
1486
  const { auth, presence } = runtime === "codex" ? {
@@ -1475,11 +1490,13 @@ async function shakeOutHarness(runtime, cfg, deps = {}) {
1475
1490
  auth: ["claude", ["auth", "status"]],
1476
1491
  presence: ["claude", ["--version"]]
1477
1492
  };
1493
+ const presenceRun = await run(presence[0], [...presence[1]]);
1494
+ if (presenceRun.error === "spawn") return "absent";
1495
+ if (presenceRun.code !== 0) return "unverified";
1478
1496
  const authRun = await run(auth[0], [...auth[1]]);
1479
1497
  if (authRun.code === 0) return "ok";
1480
1498
  if (looksUnsupported(authRun.output)) {
1481
- const presenceRun = await run(presence[0], [...presence[1]]);
1482
- return presenceRun.code === 0 ? "unverified" : "failed";
1499
+ return "unverified";
1483
1500
  }
1484
1501
  return "failed";
1485
1502
  } catch {
@@ -1487,6 +1504,7 @@ async function shakeOutHarness(runtime, cfg, deps = {}) {
1487
1504
  }
1488
1505
  }
1489
1506
  function connectedLine(runtime, verdict) {
1507
+ if (verdict === "absent") throw new Error("an absent harness cannot be connected");
1490
1508
  const label = HARNESS_LABELS[runtime];
1491
1509
  if (verdict !== "failed") return `${label} connected.`;
1492
1510
  return `${label} connected \u2014 ${FAILED_SUFFIX[runtime]}`;
@@ -1496,6 +1514,14 @@ var FAILED_SUFFIX = {
1496
1514
  codex: "it doesn\u2019t look signed in yet. Run `codex login` once, then it\u2019s ready.",
1497
1515
  opencode: "its server isn\u2019t answering. Start `opencode serve`, then it\u2019s ready."
1498
1516
  };
1517
+ function absentLine(runtime) {
1518
+ if (runtime === "opencode") {
1519
+ return "No opencode server is configured \u2014 run `opencode serve` and connect with `--url`.";
1520
+ }
1521
+ const label = HARNESS_LABELS[runtime];
1522
+ const login = runtime === "codex" ? "`codex login`" : "`claude`";
1523
+ return `${label} isn\u2019t installed on this machine \u2014 install it and sign in (${login}), then run this again.`;
1524
+ }
1499
1525
  function looksUnsupported(output) {
1500
1526
  return /unrecognized|unknown (sub)?command|unexpected argument|invalid (sub)?command|no such (sub)?command|usage:|did you mean/i.test(
1501
1527
  output
@@ -1504,17 +1530,17 @@ function looksUnsupported(output) {
1504
1530
  function runBounded(command, args) {
1505
1531
  return new Promise((resolve) => {
1506
1532
  let settled = false;
1507
- const done = (code, output) => {
1533
+ const done = (result) => {
1508
1534
  if (settled) return;
1509
1535
  settled = true;
1510
1536
  clearTimeout(timer);
1511
- resolve({ code, output });
1537
+ resolve(result);
1512
1538
  };
1513
1539
  let child;
1514
1540
  try {
1515
1541
  child = spawn4(command, args, { stdio: ["ignore", "pipe", "pipe"] });
1516
1542
  } catch {
1517
- resolve({ code: null, output: "" });
1543
+ resolve({ code: null, output: "", error: "spawn" });
1518
1544
  return;
1519
1545
  }
1520
1546
  let out = "";
@@ -1525,11 +1551,11 @@ function runBounded(command, args) {
1525
1551
  child.stderr?.on("data", capture);
1526
1552
  const timer = setTimeout(() => {
1527
1553
  child.kill("SIGKILL");
1528
- done(null, out);
1554
+ done({ code: null, output: out, error: "timeout" });
1529
1555
  }, CHECK_TIMEOUT_MS);
1530
1556
  timer.unref?.();
1531
- child.once("error", () => done(null, out));
1532
- child.once("exit", (code) => done(code, out));
1557
+ child.once("error", () => done({ code: null, output: out, error: "spawn" }));
1558
+ child.once("exit", (code) => done({ code, output: out }));
1533
1559
  });
1534
1560
  }
1535
1561
 
@@ -9268,11 +9294,13 @@ async function createCompanionRuntime(opts = {}) {
9268
9294
  });
9269
9295
  await supervisor.start();
9270
9296
  const connectHarness = async (runtime, serverUrl) => {
9297
+ const candidate = runtime === "opencode" ? { ...supervisor.currentConfig(), opencode: { serverUrl: serverUrl ?? "" } } : supervisor.currentConfig();
9298
+ const verdict = await shakeOutHarness(runtime, candidate);
9299
+ if (verdict === "absent") return { ok: false, error: absentLine(runtime) };
9271
9300
  const result = await supervisor.enableHarness(
9272
9301
  runtime === "opencode" ? { runtime: "opencode", serverUrl: serverUrl ?? "" } : { runtime }
9273
9302
  );
9274
9303
  if (!result.ok) return { ok: false, error: result.error };
9275
- const verdict = await shakeOutHarness(runtime, supervisor.currentConfig());
9276
9304
  return { ok: true, message: connectedLine(runtime, verdict) };
9277
9305
  };
9278
9306
  const control = await startControlServer({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cabane/companion",
3
- "version": "0.6.34",
3
+ "version": "0.6.35",
4
4
  "type": "module",
5
5
  "description": "The Cabane Companion (headless): connect a coding 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",