@cabane/companion 0.6.236 → 0.6.238

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
@@ -14,6 +14,13 @@ import { realpathSync } from "fs";
14
14
  import { homedir } from "os";
15
15
  import { dirname, join, sep } from "path";
16
16
  import { fileURLToPath } from "url";
17
+ function installScriptCommand(address) {
18
+ return `curl -fsSL ${address.replace(/\/+$/, "")}/install.sh | bash`;
19
+ }
20
+ function installLineWords(install = companionInstall()) {
21
+ const line = install === "script" ? "installer line" : "npm line";
22
+ return `the ${line} from Your settings \u2192 Devices \u2192 Add a device in your Cabane`;
23
+ }
17
24
  function companionInstall(execPath = process.execPath, home = homedir()) {
18
25
  const real = (path) => {
19
26
  try {
@@ -36,11 +43,9 @@ function repairRoute(packageRoot = ownRoot(), install = companionInstall()) {
36
43
  function ownRoot() {
37
44
  return dirname(dirname(fileURLToPath(import.meta.url)));
38
45
  }
39
- var INSTALL_SCRIPT_COMMAND;
40
46
  var init_install_kind = __esm({
41
47
  "src/install-kind.ts"() {
42
48
  "use strict";
43
- INSTALL_SCRIPT_COMMAND = "curl -fsSL https://cabane.ai/install.sh | bash";
44
49
  }
45
50
  });
46
51
 
@@ -222,7 +227,7 @@ var init_cabane_command = __esm({
222
227
  });
223
228
 
224
229
  // src/errors.ts
225
- var CompanionError, ApiError, EdgeBlockError, ConfigError, PrepareHookError;
230
+ var CompanionError, StandaloneError, ApiError, EdgeBlockError, ConfigError, PrepareHookError;
226
231
  var init_errors = __esm({
227
232
  "src/errors.ts"() {
228
233
  "use strict";
@@ -232,6 +237,12 @@ var init_errors = __esm({
232
237
  this.name = "CompanionError";
233
238
  }
234
239
  };
240
+ StandaloneError = class extends CompanionError {
241
+ constructor(message2) {
242
+ super(message2);
243
+ this.name = "StandaloneError";
244
+ }
245
+ };
235
246
  ApiError = class extends CompanionError {
236
247
  constructor(status2, message2, body) {
237
248
  super(message2);
@@ -603,31 +614,13 @@ function loadConfigTolerant() {
603
614
  const strict = companionConfigSchema.safeParse(parsed);
604
615
  if (strict.success) {
605
616
  const {
606
- agents: agents2,
607
- prepareHook: prepareHook2,
608
- root: root2,
609
- dashboardPort,
610
- autoOpen,
611
- logLevel,
612
- logFormat,
613
- harnesses: harnesses2,
614
- opencode: opencode2
617
+ baseUrl: _baseUrl,
618
+ deviceToken: _deviceToken,
619
+ deviceId: _deviceId,
620
+ deviceLabel: _deviceLabel,
621
+ ...local2
615
622
  } = strict.data;
616
- return {
617
- local: {
618
- ...agents2 !== void 0 ? { agents: agents2 } : {},
619
- ...prepareHook2 !== void 0 ? { prepareHook: prepareHook2 } : {},
620
- ...root2 !== void 0 ? { root: root2 } : {},
621
- ...dashboardPort !== void 0 ? { dashboardPort } : {},
622
- ...autoOpen !== void 0 ? { autoOpen } : {},
623
- ...logLevel !== void 0 ? { logLevel } : {},
624
- ...logFormat !== void 0 ? { logFormat } : {},
625
- ...harnesses2 !== void 0 ? { harnesses: harnesses2 } : {},
626
- ...opencode2 !== void 0 ? { opencode: opencode2 } : {}
627
- },
628
- note: null,
629
- hadPriorConfig: true
630
- };
623
+ return { local: local2, note: null, hadPriorConfig: true };
631
624
  }
632
625
  const obj = parsed && typeof parsed === "object" ? parsed : {};
633
626
  const local = {};
@@ -647,6 +640,11 @@ function loadConfigTolerant() {
647
640
  if (harnesses.success && harnesses.data !== void 0) local.harnesses = harnesses.data;
648
641
  const opencode = companionConfigSchema.shape.opencode.safeParse(obj.opencode);
649
642
  if (opencode.success && opencode.data !== void 0) local.opencode = opencode.data;
643
+ const update2 = companionConfigSchema.shape.update.safeParse(obj.update);
644
+ if (update2.success && update2.data !== void 0) local.update = update2.data;
645
+ if (typeof obj.serviceDeclined === "boolean") local.serviceDeclined = obj.serviceDeclined;
646
+ const runner = companionConfigSchema.shape.runner.safeParse(obj.runner);
647
+ if (runner.success && runner.data !== void 0) local.runner = runner.data;
650
648
  return {
651
649
  local,
652
650
  note: `the existing ${path} was from an older or incompatible companion; re-pairing rewrote it.`,
@@ -726,7 +724,7 @@ var init_config = __esm({
726
724
  // the rest of the config; `pair` always writes one.
727
725
  deviceToken: z2.string().optional(),
728
726
  // Identity hints, learned from the device flow and refreshed on the first
729
- // assignments pull. Cosmetic — used for `status`/dashboard display only.
727
+ // assignments pull. Cosmetic — used for `status` display only.
730
728
  deviceId: z2.string().optional(),
731
729
  deviceLabel: z2.string().optional(),
732
730
  // Optional per-agent machine-local overrides (cwd / prepareHook), keyed by
@@ -738,9 +736,8 @@ var init_config = __esm({
738
736
  // silent roster: an agent nobody listed gets no hook and no error, and starts
739
737
  // in the operator's `$HOME`.
740
738
  prepareHook: prepareHookSchema.optional(),
741
- // The CLI ignores both dashboard settings. Desktop uses `dashboardPort`;
742
- // `autoOpen` remains parseable for older config files but no longer controls
743
- // startup. `logLevel` is pino's level, live-editable.
739
+ // Nothing reads either dashboard setting any more; both stay parseable so an
740
+ // older config file still loads. `logLevel` is pino's level, live-editable.
744
741
  dashboardPort: z2.number().int().min(1).max(65535).optional(),
745
742
  autoOpen: z2.boolean().optional(),
746
743
  logLevel: z2.enum(["warn", "info", "debug"]).optional(),
@@ -958,18 +955,32 @@ function resolveBundledBinary(runtime, env = defaultResolveEnv()) {
958
955
  function missingNoun(status2) {
959
956
  return status2 === "executable_missing" ? "executable" : "platform package";
960
957
  }
961
- function repairCommands(route = repairRoute(), spell = cabane) {
958
+ function repairCommands(route = repairRoute(), spell = cabane, address = configuredAddress()) {
962
959
  if (route === "managed") return [spell("stop"), spell("update")];
963
- if (route === "script") return [spell("stop"), INSTALL_SCRIPT_COMMAND];
960
+ if (route === "script")
961
+ return [
962
+ spell("stop"),
963
+ address ? installScriptCommand(address) : { words: installLineWords("script") }
964
+ ];
964
965
  return ["npm i -g @cabane/companion --include=optional"];
965
966
  }
966
- function repairAction(route = repairRoute(), spell = cabane) {
967
- const [first, ...rest] = repairCommands(route, spell);
968
- if (rest.length === 0) return `Reinstall with \`${first}\`.`;
969
- return `Run \`${first}\`, then \`${rest.join("`, then `")}\`. ${STOP_WARNING}`;
967
+ function configuredAddress() {
968
+ try {
969
+ return loadConfig()?.baseUrl ?? null;
970
+ } catch {
971
+ return null;
972
+ }
973
+ }
974
+ function quoteStep(step, quote) {
975
+ return typeof step === "string" ? quote(step) : step.words;
976
+ }
977
+ function repairAction(route = repairRoute(), spell = cabane, address = configuredAddress()) {
978
+ const [first, ...rest] = repairCommands(route, spell, address);
979
+ if (rest.length === 0) return `Reinstall with ${quoteStep(first, backticks)}.`;
980
+ return `Run ${quoteStep(first, backticks)}, then ${rest.map((step) => quoteStep(step, backticks)).join(", then ")}. ${STOP_WARNING}`;
970
981
  }
971
982
  function repairSteps(route = repairRoute()) {
972
- return repairCommands(route).join(", then ");
983
+ return repairCommands(route).map((step) => quoteStep(step, (command) => command)).join(", then ");
973
984
  }
974
985
  function repairCost(route = repairRoute()) {
975
986
  return repairCommands(route).length > 1 ? STOP_WARNING : "";
@@ -981,11 +992,12 @@ function startupWarning(r) {
981
992
  function turnIncompleteCopy(runtime) {
982
993
  return `**This device's ${HARNESS_LABEL[runtime]} runtime is incomplete.** ${repairAction()} Restart it, then try again.`;
983
994
  }
984
- var CLAUDE_SDK, CODEX_SDK, CODEX_CLI, CODEX_PACKAGE_BY_TRIPLE, HARNESS_LABEL, HARNESS_INTENT_WORD, STOP_WARNING;
995
+ var CLAUDE_SDK, CODEX_SDK, CODEX_CLI, CODEX_PACKAGE_BY_TRIPLE, HARNESS_LABEL, HARNESS_INTENT_WORD, backticks, STOP_WARNING;
985
996
  var init_bundled_binaries = __esm({
986
997
  "src/bundled-binaries.ts"() {
987
998
  "use strict";
988
999
  init_cabane_command();
1000
+ init_config();
989
1001
  init_install_kind();
990
1002
  CLAUDE_SDK = "@anthropic-ai/claude-agent-sdk";
991
1003
  CODEX_SDK = "@openai/codex-sdk";
@@ -1006,6 +1018,7 @@ var init_bundled_binaries = __esm({
1006
1018
  "claude-code": "connected",
1007
1019
  codex: "enabled"
1008
1020
  };
1021
+ backticks = (command) => `\`${command}\``;
1009
1022
  STOP_WARNING = "Stopping ends any turn in progress.";
1010
1023
  }
1011
1024
  });
@@ -7420,10 +7433,6 @@ var init_turn_committer = __esm({
7420
7433
  get lastMessageSeq() {
7421
7434
  return this.pump.lastMessageSeq;
7422
7435
  }
7423
- // The turn's final reply text, or null if none landed.
7424
- get replyBody() {
7425
- return this.pump.replyBody;
7426
- }
7427
7436
  get finalSource() {
7428
7437
  return this.pump.finalSource;
7429
7438
  }
@@ -7801,7 +7810,6 @@ var init_turn_execution = __esm({
7801
7810
  );
7802
7811
  }
7803
7812
  const durationMs = Date.now() - startedAt;
7804
- this.supervisor.notifyEnd({ id: payload.messageId, ok: false, durationMs, reason });
7805
7813
  return { ok: false, durationMs, reason, runtime: this.turnContext?.runtime };
7806
7814
  }
7807
7815
  // The admitted turn holds the conversation's lease, so the clear must NAME the
@@ -7840,7 +7848,6 @@ var init_turn_execution = __esm({
7840
7848
  }
7841
7849
  }
7842
7850
  const durationMs = Date.now() - startedAt;
7843
- this.supervisor.notifyEnd({ id: payload.messageId, ok: false, durationMs, reason });
7844
7851
  return { ok: false, durationMs, reason, runtime: this.turnContext?.runtime };
7845
7852
  }
7846
7853
  async fetchContext() {
@@ -7949,11 +7956,7 @@ var init_turn_execution = __esm({
7949
7956
  turnLog.debug({ role: message2.role }, "dispatcher: trigger role not dispatchable \u2014 skipping");
7950
7957
  throw this.concluded("unexpected_role", UNEXPECTED_ROLE_REASON);
7951
7958
  }
7952
- this.supervisor.notifyStart({
7953
- id: this.dispatchId,
7954
- conversationId: payload.conversationId,
7955
- message: message2.body
7956
- });
7959
+ this.supervisor.notifyStart({ id: this.dispatchId, conversationId: payload.conversationId });
7957
7960
  }
7958
7961
  async resolveSecrets() {
7959
7962
  const { turnLog } = this;
@@ -8722,7 +8725,6 @@ var init_turn_execution = __esm({
8722
8725
  const o = this.outcome;
8723
8726
  const { startedAt, turnLog } = this;
8724
8727
  const durationMs = Date.now() - startedAt;
8725
- const finalReplyBody = this.committer.replyBody;
8726
8728
  if (this.transcript) {
8727
8729
  this.transcript.close({
8728
8730
  ok: o.okResult,
@@ -8735,21 +8737,9 @@ var init_turn_execution = __esm({
8735
8737
  }
8736
8738
  if (o.okResult) {
8737
8739
  turnLog.debug({ durationMs }, "dispatcher: turn end (ok)");
8738
- this.supervisor.notifyEnd({
8739
- id: this.dispatchId,
8740
- ok: true,
8741
- durationMs,
8742
- ...finalReplyBody ? { reply: finalReplyBody.trim() } : {}
8743
- });
8744
8740
  return { ok: true, durationMs };
8745
8741
  }
8746
8742
  turnLog.debug({ durationMs, reason: o.resultReason }, "dispatcher: turn end (not ok)");
8747
- this.supervisor.notifyEnd({
8748
- id: this.dispatchId,
8749
- ok: false,
8750
- durationMs,
8751
- ...o.resultReason ? { reason: o.resultReason } : {}
8752
- });
8753
8743
  return {
8754
8744
  ok: false,
8755
8745
  durationMs,
@@ -9255,8 +9245,7 @@ async function runTurn(env, deps) {
9255
9245
  emit({ cabaneRunner: "session_write_notice" });
9256
9246
  return true;
9257
9247
  },
9258
- notifyStart: (info) => emit({ cabaneRunner: "start", ...info }),
9259
- notifyEnd: (info) => emit({ cabaneRunner: "end", ...info })
9248
+ notifyStart: (info) => emit({ cabaneRunner: "start", ...info })
9260
9249
  };
9261
9250
  let outbox;
9262
9251
  const opts = runnerTurnOptions(input, log2, emit, {
@@ -9959,6 +9948,7 @@ import { hostname } from "os";
9959
9948
 
9960
9949
  // src/enrollment.ts
9961
9950
  init_errors();
9951
+ init_install_kind();
9962
9952
  var EnrollmentExpiredError = class extends CompanionError {
9963
9953
  constructor() {
9964
9954
  super("this pairing code expired before it was confirmed.");
@@ -10012,8 +10002,9 @@ async function postJson(baseUrl, path, body, signal) {
10012
10002
  "Too many pairing requests from this network. Wait a few minutes and run cabane setup again."
10013
10003
  );
10014
10004
  if (res.status === 404 && path.endsWith("/code"))
10015
- throw new CompanionError(
10016
- `No pairing endpoint at ${baseUrl}. Either that server is too old for this companion, or it isn't a Cabane API origin: the hosted app is https://app.cabane.ai (pass \`--server <url>\` for your own instance).`
10005
+ throw new StandaloneError(
10006
+ `No pairing endpoint at ${baseUrl}: it isn't a Cabane's address, or that Cabane is too old for this companion.
10007
+ Run ${installLineWords()}.`
10017
10008
  );
10018
10009
  const msg = parsed && typeof parsed === "object" && "error" in parsed ? String(parsed.error) : `${res.status}`;
10019
10010
  throw new ApiError(res.status, `${res.status} ${msg}`, parsed);
@@ -10198,6 +10189,7 @@ async function pairDevice(opts, deps = {}) {
10198
10189
  process.exitCode = 130;
10199
10190
  return null;
10200
10191
  }
10192
+ if (!current) opts.onCode?.();
10201
10193
  current = code;
10202
10194
  const minutes = Math.max(1, Math.round(code.expiresIn / 60));
10203
10195
  const codeLines = ["", ` ${code.userCode}`, ` ${code.verificationUri}`, ""];
@@ -10304,10 +10296,10 @@ init_cabane_command();
10304
10296
  init_config();
10305
10297
  init_pairing();
10306
10298
  init_errors();
10307
- init_errors();
10308
- var DEFAULT_BASE_URL = "https://app.cabane.ai";
10299
+ init_install_kind();
10309
10300
  function resolvePairBaseUrl(server) {
10310
- const raw = (server ?? process.env.CABANE_BASE_URL ?? DEFAULT_BASE_URL).trim();
10301
+ const raw = (server ?? process.env.CABANE_BASE_URL ?? "").trim();
10302
+ if (!raw) throw new NoPairAddressError();
10311
10303
  if (!isAllowedBaseUrl(raw)) {
10312
10304
  throw new CompanionError(
10313
10305
  `invalid server URL "${raw}": must be an https URL (loopback http is allowed for local dev). Pass it as \`${cabane("setup --server https://your-cabane")}\`.`
@@ -10315,6 +10307,20 @@ function resolvePairBaseUrl(server) {
10315
10307
  }
10316
10308
  return raw;
10317
10309
  }
10310
+ var NoPairAddressError = class extends StandaloneError {
10311
+ constructor(install = companionInstall()) {
10312
+ const words = installLineWords(install);
10313
+ super(
10314
+ `Setup doesn't know which Cabane to pair this device with. Run the
10315
+ ${words.slice("the ".length)}.`
10316
+ );
10317
+ this.name = "NoPairAddressError";
10318
+ }
10319
+ };
10320
+ function rememberPairAddress(baseUrl) {
10321
+ const { local } = loadConfigTolerant();
10322
+ saveConfig({ ...local, baseUrl });
10323
+ }
10318
10324
  function writePairedConfig(paired) {
10319
10325
  const { local, note: note2 } = loadConfigTolerant();
10320
10326
  const config = {
@@ -10339,7 +10345,6 @@ function isDevicePaired() {
10339
10345
  init_config();
10340
10346
  init_cabane_command();
10341
10347
  import { randomUUID as randomUUID7 } from "crypto";
10342
- import { fileURLToPath as fileURLToPath5 } from "url";
10343
10348
 
10344
10349
  // src/prereqs.ts
10345
10350
  init_config();
@@ -10356,71 +10361,37 @@ function warnAboutHarnessReadiness(cfg, deps = {}) {
10356
10361
  }
10357
10362
 
10358
10363
  // src/state.ts
10359
- import { EventEmitter } from "events";
10360
- function dispatchToWire(r) {
10361
- return {
10362
- id: r.id,
10363
- timestamp: r.timestamp,
10364
- workspace_slug: r.workspaceSlug,
10365
- ...r.agentUsername ? { agent_username: r.agentUsername } : {},
10366
- message: r.message,
10367
- status: r.status,
10368
- ...r.durationMs !== void 0 ? { duration_ms: r.durationMs } : {},
10369
- ...r.error ? { error: r.error } : {},
10370
- ...r.reply ? { reply: r.reply } : {}
10371
- };
10372
- }
10373
- var MAX_DISPATCHES = 50;
10374
10364
  function today() {
10375
10365
  return (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
10376
10366
  }
10377
10367
  var CompanionStateHub = class {
10378
10368
  constructor(opts) {
10379
10369
  this.opts = opts;
10380
- this.emitter.setMaxListeners(0);
10381
10370
  }
10382
10371
  opts;
10383
- emitter = new EventEmitter();
10384
10372
  workspaces = /* @__PURE__ */ new Map();
10385
- dispatches = [];
10386
- dashboardUrl = null;
10387
10373
  deviceId = null;
10388
10374
  deviceLabel = null;
10389
10375
  deviceError = null;
10390
10376
  // The latest harness snapshot the supervisor probed, or null before the
10391
10377
  // first probe (see CompanionStatusJson.harnesses).
10392
10378
  harnesses = null;
10393
- // ---- subscription (SSE) ----
10394
- on(listener) {
10395
- this.emitter.on("event", listener);
10396
- return () => this.emitter.off("event", listener);
10397
- }
10398
- emit(type, data) {
10399
- this.emitter.emit("event", { type, data });
10400
- }
10401
10379
  // ---- device ----
10402
10380
  setDevice(info) {
10403
10381
  if (info.deviceId !== void 0) this.deviceId = info.deviceId;
10404
10382
  if (info.deviceLabel !== void 0) this.deviceLabel = info.deviceLabel;
10405
- this.emitStatus();
10406
10383
  }
10407
10384
  setDeviceError(message2) {
10408
- if (this.deviceError === message2) return;
10409
10385
  this.deviceError = message2;
10410
- this.emitStatus();
10411
10386
  }
10412
10387
  clearDeviceError() {
10413
- if (this.deviceError === null) return;
10414
10388
  this.deviceError = null;
10415
- this.emitStatus();
10416
10389
  }
10417
10390
  // ---- harnesses ----
10418
- // Replace the harness snapshot and push a status change (so the SSE feed flips
10419
- // the Companion's Harnesses surface live). The supervisor calls this on each
10391
+ // Replace the harness snapshot. The supervisor calls this on each
10420
10392
  // heartbeat-cadence probe and on an on-demand recheck.
10421
10393
  setHarnesses(harnesses) {
10422
10394
  this.harnesses = harnesses;
10423
- this.emitStatus();
10424
10395
  }
10425
10396
  // ---- workspace lifecycle ----
10426
10397
  registerWorkspace(ws) {
@@ -10441,29 +10412,21 @@ var CompanionStateHub = class {
10441
10412
  countDate: today(),
10442
10413
  agents: /* @__PURE__ */ new Map()
10443
10414
  });
10444
- this.emitStatus();
10445
10415
  }
10446
10416
  removeWorkspace(workspaceId) {
10447
- const ws = this.workspaces.get(workspaceId);
10448
- if (!ws) return;
10449
10417
  this.workspaces.delete(workspaceId);
10450
- this.emit("workspace:disconnected", { slug: ws.slug });
10451
- this.emitStatus();
10452
10418
  }
10453
10419
  setConnected(workspaceId, connected) {
10454
10420
  const ws = this.workspaces.get(workspaceId);
10455
10421
  if (!ws || ws.connected === connected) return;
10456
10422
  ws.connected = connected;
10457
10423
  if (connected) ws.authFailed = false;
10458
- this.emitStatus();
10459
10424
  }
10460
10425
  setAuthFailed(workspaceId) {
10461
10426
  const ws = this.workspaces.get(workspaceId);
10462
10427
  if (!ws) return;
10463
- if (ws.authFailed && !ws.connected) return;
10464
10428
  ws.authFailed = true;
10465
10429
  ws.connected = false;
10466
- this.emitStatus();
10467
10430
  }
10468
10431
  recordEvent(workspaceId) {
10469
10432
  const ws = this.workspaces.get(workspaceId);
@@ -10476,22 +10439,15 @@ var CompanionStateHub = class {
10476
10439
  const ws = this.workspaces.get(workspaceId);
10477
10440
  if (!ws) return;
10478
10441
  ws.agents.set(agent.agentId, agent);
10479
- this.emitStatus();
10480
10442
  }
10481
10443
  removeAgent(workspaceId, agentId) {
10482
- const ws = this.workspaces.get(workspaceId);
10483
- if (!ws) return;
10484
- if (ws.agents.delete(agentId)) this.emitStatus();
10444
+ this.workspaces.get(workspaceId)?.agents.delete(agentId);
10485
10445
  }
10486
- // ---- dispatch feed ----
10487
- // A DispatchObserver bound to one (workspace, agent), handed to that agent's
10488
- // Dispatcher by the supervisor.
10489
- observerFor(workspaceId, agentId, slug) {
10490
- const username = () => this.workspaces.get(workspaceId)?.agents.get(agentId)?.username ?? "";
10491
- return {
10492
- onStart: (info) => this.dispatchStarted(workspaceId, slug, username(), info),
10493
- onEnd: (info) => this.dispatchEnded(slug, username(), info)
10494
- };
10446
+ // ---- dispatch count ----
10447
+ // A DispatchObserver bound to one workspace, handed to each of its agents'
10448
+ // Dispatchers by the supervisor: a turn counts toward the day when it starts.
10449
+ observerFor(workspaceId) {
10450
+ return { onStart: () => this.bumpCount(workspaceId) };
10495
10451
  }
10496
10452
  bumpCount(workspaceId) {
10497
10453
  const ws = this.workspaces.get(workspaceId);
@@ -10503,52 +10459,6 @@ var CompanionStateHub = class {
10503
10459
  }
10504
10460
  ws.dispatchCountToday += 1;
10505
10461
  }
10506
- dispatchStarted(workspaceId, slug, agentUsername, info) {
10507
- this.bumpCount(workspaceId);
10508
- const record = {
10509
- id: info.id,
10510
- timestamp: (/* @__PURE__ */ new Date()).toISOString(),
10511
- workspaceSlug: slug,
10512
- ...agentUsername ? { agentUsername } : {},
10513
- message: info.message,
10514
- status: "running"
10515
- };
10516
- this.upsertDispatch(record);
10517
- this.emit("dispatch:started", dispatchToWire(record));
10518
- this.emitStatus();
10519
- }
10520
- dispatchEnded(slug, agentUsername, info) {
10521
- const existing = this.dispatches.find((d) => d.id === info.id);
10522
- const record = {
10523
- id: info.id,
10524
- timestamp: existing?.timestamp ?? (/* @__PURE__ */ new Date()).toISOString(),
10525
- workspaceSlug: existing?.workspaceSlug ?? slug,
10526
- ...existing?.agentUsername ?? agentUsername ? { agentUsername: existing?.agentUsername ?? agentUsername } : {},
10527
- message: existing?.message ?? "",
10528
- status: info.ok ? "replied" : "error",
10529
- durationMs: info.durationMs,
10530
- ...info.reason ? { error: info.reason } : {},
10531
- ...info.reply ? { reply: info.reply } : {}
10532
- };
10533
- this.upsertDispatch(record);
10534
- this.emit(info.ok ? "dispatch:completed" : "dispatch:error", dispatchToWire(record));
10535
- }
10536
- upsertDispatch(record) {
10537
- const idx = this.dispatches.findIndex((d) => d.id === record.id);
10538
- if (idx >= 0) {
10539
- this.dispatches[idx] = record;
10540
- return;
10541
- }
10542
- this.dispatches.unshift(record);
10543
- if (this.dispatches.length > MAX_DISPATCHES) this.dispatches.length = MAX_DISPATCHES;
10544
- }
10545
- // ---- snapshots for the HTTP routes ----
10546
- setDashboardUrl(url) {
10547
- this.dashboardUrl = url;
10548
- }
10549
- recentDispatches(limit = 20) {
10550
- return this.dispatches.slice(0, Math.max(0, limit));
10551
- }
10552
10462
  statusJson() {
10553
10463
  const list = [...this.workspaces.values()].sort((a, b) => a.name.localeCompare(b.name));
10554
10464
  const lastOverall = list.reduce((acc, ws) => {
@@ -10578,15 +10488,11 @@ var CompanionStateHub = class {
10578
10488
  }))
10579
10489
  })),
10580
10490
  last_event_at_overall: lastOverall,
10581
- dashboard_url: this.dashboardUrl,
10582
10491
  companion_version: this.opts.companionVersion,
10583
10492
  instance_id: this.opts.instanceId ?? null,
10584
10493
  harnesses: this.harnesses
10585
10494
  };
10586
10495
  }
10587
- emitStatus() {
10588
- this.emit("status:changed", this.statusJson());
10589
- }
10590
10496
  };
10591
10497
 
10592
10498
  // src/supervisor.ts
@@ -10652,7 +10558,7 @@ function turnFailureCopy(reason, runtime) {
10652
10558
  const stopsFirst = commands.length > 1;
10653
10559
  if (!stopsFirst) commands.push("cabane stop");
10654
10560
  commands.push("cabane start");
10655
- const list = commands.map((command) => `\`${command}\``);
10561
+ const list = commands.map((step) => quoteStep(step, (command) => `\`${command}\``));
10656
10562
  return `${harnessLabel(runtime ?? reason.slice("runtime_incomplete:".length))}'s installation on this device is incomplete. Run ${list.slice(0, -1).join(", ")} and ${list.at(-1)}. ${stopsFirst ? `${STOP_WARNING} ` : ""}Reply in Cabane to try again.`;
10657
10563
  }
10658
10564
  const unavailable = /^(codex|opencode)_unavailable:/.exec(reason ?? "")?.[1] ?? (reason?.startsWith("runtime_unavailable:") ? reason.slice("runtime_unavailable:".length).trim() : void 0);
@@ -11355,7 +11261,6 @@ async function runTurnInRunner(opts, launch, duties, payload, handleOpts) {
11355
11261
  duties.releaseAbort(turnId, controller);
11356
11262
  const durationMs2 = Date.now() - startedAt;
11357
11263
  await settleUnstarted(opts, payload, turnId, minted.errorReason, log2);
11358
- duties.notifyEnd({ id: payload.messageId, ok: false, durationMs: durationMs2, reason: minted.reason });
11359
11264
  return { ok: false, durationMs: durationMs2, reason: minted.reason };
11360
11265
  }
11361
11266
  let result;
@@ -11386,7 +11291,6 @@ async function runTurnInRunner(opts, launch, duties, payload, handleOpts) {
11386
11291
  reason: "provider_failed"
11387
11292
  });
11388
11293
  const durationMs2 = Date.now() - startedAt;
11389
- duties.notifyEnd({ id: payload.messageId, ok: false, durationMs: durationMs2, reason: reason2 });
11390
11294
  return { ok: false, durationMs: durationMs2, reason: reason2 };
11391
11295
  }
11392
11296
  try {
@@ -11450,17 +11354,8 @@ async function runTurnInRunner(opts, launch, duties, payload, handleOpts) {
11450
11354
  }
11451
11355
  return;
11452
11356
  case "start":
11453
- duties.notifyStart({
11454
- id: event.id,
11455
- conversationId: event.conversationId,
11456
- message: event.message
11457
- });
11357
+ duties.notifyStart({ id: event.id, conversationId: event.conversationId });
11458
11358
  return;
11459
- case "end": {
11460
- const { cabaneRunner: _kind, ...info } = event;
11461
- duties.notifyEnd(info);
11462
- return;
11463
- }
11464
11359
  case "session_write_notice":
11465
11360
  duties.markSessionWriteNotice();
11466
11361
  return;
@@ -11564,7 +11459,6 @@ async function runTurnInRunner(opts, launch, duties, payload, handleOpts) {
11564
11459
  if (thrown !== void 0) throw new Error(thrown);
11565
11460
  if (stopped) log2.info({ turnId, code }, "runner: stopped before it could report");
11566
11461
  else log2.warn({ turnId, code }, "runner: exited without reporting a result");
11567
- duties.notifyEnd({ id: payload.messageId, ok: false, durationMs, reason });
11568
11462
  return { ok: false, durationMs, reason };
11569
11463
  }
11570
11464
 
@@ -11591,12 +11485,6 @@ var Dispatcher = class {
11591
11485
  } catch {
11592
11486
  }
11593
11487
  }
11594
- notifyEnd(info) {
11595
- try {
11596
- this.opts.observer?.onEnd(info);
11597
- } catch {
11598
- }
11599
- }
11600
11488
  // The supervisor supplies a stable turnId on both fresh and resumed dispatch.
11601
11489
  // resumed is separate: only replay seeds sequence/speech recovery from the
11602
11490
  // prior span. A direct caller omitting turnId gets a fresh id.
@@ -11624,8 +11512,7 @@ var Dispatcher = class {
11624
11512
  markSessionWriteNotice: () => {
11625
11513
  this.sessionWriteNotified.add(key);
11626
11514
  },
11627
- notifyStart: (info) => this.notifyStart(info),
11628
- notifyEnd: (info) => this.notifyEnd(info)
11515
+ notifyStart: (info) => this.notifyStart(info)
11629
11516
  },
11630
11517
  payload,
11631
11518
  {
@@ -12724,7 +12611,6 @@ var CompanionSupervisor = class {
12724
12611
  runHarnessCheck;
12725
12612
  probeOpencode;
12726
12613
  exitFn;
12727
- reexecFn;
12728
12614
  dispatcherFactory;
12729
12615
  runnerCommand;
12730
12616
  runnerEnv;
@@ -12735,8 +12621,8 @@ var CompanionSupervisor = class {
12735
12621
  host = hostReport();
12736
12622
  deviceApi = null;
12737
12623
  deviceSub = null;
12738
- // Whether the device stream is open, which is what every workspace's link on
12739
- // the dashboard reports.
12624
+ // Whether the device stream is open, which is what every workspace's link in
12625
+ // the status snapshot reports.
12740
12626
  deviceConnected = false;
12741
12627
  // Set when the device stream ended on a 403: the device was deactivated, not
12742
12628
  // unpaired. The next accepted heartbeat proves it was reactivated, and
@@ -12797,7 +12683,6 @@ var CompanionSupervisor = class {
12797
12683
  this.probeOpencode = opts.probeOpencode;
12798
12684
  this.harnessVersions = opts.harnessVersions ?? emptyHarnessVersions();
12799
12685
  this.exitFn = opts.exit ?? ((code) => process.exit(code));
12800
- this.reexecFn = opts.reexec ?? defaultReexec;
12801
12686
  this.restartForUpdate = opts.restartForUpdate ?? (async () => {
12802
12687
  await this.shutdown();
12803
12688
  this.exitFn(0);
@@ -13431,7 +13316,7 @@ var CompanionSupervisor = class {
13431
13316
  locks: this.runnerLocks,
13432
13317
  gzipBodies: this.gzipBodies
13433
13318
  },
13434
- observer: this.hub.observerFor(ctx2.workspaceId, ctx2.agentId, ctx2.workspaceSlug),
13319
+ observer: this.hub.observerFor(ctx2.workspaceId),
13435
13320
  aborts: ctx2.aborts
13436
13321
  });
13437
13322
  }
@@ -13885,36 +13770,9 @@ var CompanionSupervisor = class {
13885
13770
  };
13886
13771
  return { kick, cancel: cancel2 };
13887
13772
  }
13888
- // ---- dashboard-facing operations ----
13889
- // Re-read `~/.cabane/config.json` (the operator may have edited the `agents`
13890
- // overrides or dashboard prefs), apply the log level live, and re-pull
13891
- // assignments so any structural change is reflected. A device-token change
13892
- // means re-pairing, which `pair` + a restart handle.
13893
- reloadConfig() {
13894
- const next = loadConfig();
13895
- if (!next) return;
13896
- this.config = next;
13897
- configureLogger(this.log, next);
13898
- this.rebuildDispatchers();
13899
- void this.refreshAssignments();
13900
- }
13901
- // The dashboard "refresh assignments" control — an immediate pull.
13902
- refresh() {
13903
- void this.refreshAssignments();
13904
- }
13905
- applySettings(patch) {
13906
- const next = {
13907
- ...this.config,
13908
- ...patch.logLevel ? { logLevel: patch.logLevel } : {},
13909
- ...patch.autoOpen !== void 0 ? { autoOpen: patch.autoOpen } : {}
13910
- };
13911
- this.config = next;
13912
- saveConfig(next);
13913
- configureLogger(this.log, next);
13914
- }
13915
13773
  // ---- harnesses ----
13916
13774
  // Look at every agent, in this process: the one whose environment turns run
13917
- // in. Cached for the heartbeat and the dashboard; coalesced, so a re-detect
13775
+ // in. Cached for the heartbeat and the status snapshot; coalesced, so a re-detect
13918
13776
  // during the timer's own run waits for that one. Never throws.
13919
13777
  async detect() {
13920
13778
  if (this.detecting) return this.detecting;
@@ -13942,8 +13800,8 @@ var CompanionSupervisor = class {
13942
13800
  });
13943
13801
  return this.detecting;
13944
13802
  }
13945
- // A re-detect asked for over the control socket (`setup`, `connect`) or the
13946
- // dashboard: read the config file again first — this process owns it, so an
13803
+ // A re-detect asked for over the control socket (`setup`, `connect`): read
13804
+ // the config file again first — this process owns it, so an
13947
13805
  // edit made on disk (the allowlist, the OpenCode address) is adopted here
13948
13806
  // rather than overwritten by its next save — then detect now, and beat at
13949
13807
  // once so Cabane shows it promptly.
@@ -14079,9 +13937,6 @@ var CompanionSupervisor = class {
14079
13937
  await this.shutdown();
14080
13938
  this.exitFn(0);
14081
13939
  }
14082
- requestRestart() {
14083
- this.reexecFn();
14084
- }
14085
13940
  };
14086
13941
  async function waitForBoundedHeartbeat(heartbeat) {
14087
13942
  let timer = null;
@@ -14095,21 +13950,6 @@ async function waitForBoundedHeartbeat(heartbeat) {
14095
13950
  if (timer) clearTimeout(timer);
14096
13951
  }
14097
13952
  }
14098
- function defaultReexec() {
14099
- clearRuntimeState();
14100
- void import("child_process").then(({ spawn: spawn8 }) => {
14101
- try {
14102
- const child = spawn8(process.execPath, process.argv.slice(1), {
14103
- stdio: "inherit",
14104
- detached: false
14105
- });
14106
- child.on("spawn", () => process.exit(0));
14107
- child.on("error", () => {
14108
- });
14109
- } catch {
14110
- }
14111
- });
14112
- }
14113
13953
 
14114
13954
  // src/safety.ts
14115
13955
  var RECOVERABLE_CODES = /* @__PURE__ */ new Set([
@@ -14281,15 +14121,6 @@ function strayCompanionLines(strays, retry) {
14281
14121
  }
14282
14122
 
14283
14123
  // src/runtime.ts
14284
- init_errors();
14285
- function embeddedRunner(host) {
14286
- if (!host.electron) return null;
14287
- const cli = fileURLToPath5(new URL("./cli.js", host.moduleUrl));
14288
- return {
14289
- runnerCommand: [host.execPath, cli, "run-turn"],
14290
- runnerEnv: { ...host.env, ELECTRON_RUN_AS_NODE: "1" }
14291
- };
14292
- }
14293
14124
  async function createCompanionRuntime(opts = {}) {
14294
14125
  const log2 = getLogger();
14295
14126
  installProcessSafetyNet(log2);
@@ -14329,7 +14160,7 @@ async function createCompanionRuntime(opts = {}) {
14329
14160
  const clearRuntimeOnExit = () => clearRuntimeStateIfOurs(instanceId);
14330
14161
  process.on("exit", clearRuntimeOnExit);
14331
14162
  const hub = new CompanionStateHub({
14332
- // The dashboard's connection line shows it.
14163
+ // `status` names the Cabane it is paired with.
14333
14164
  baseUrl: cfg.baseUrl,
14334
14165
  companionVersion: COMPANION_VERSION,
14335
14166
  // Surfaced on `/api/status` so `stop`/`status` can confirm the
@@ -14340,7 +14171,6 @@ async function createCompanionRuntime(opts = {}) {
14340
14171
  };
14341
14172
  let supervisor;
14342
14173
  let control = null;
14343
- let dashboard = null;
14344
14174
  let stopped = false;
14345
14175
  const stop2 = async () => {
14346
14176
  if (stopped) return;
@@ -14349,8 +14179,7 @@ async function createCompanionRuntime(opts = {}) {
14349
14179
  process.removeListener("exit", clearRuntimeOnExit);
14350
14180
  await Promise.allSettled([
14351
14181
  ...supervisor ? [supervisor.shutdown()] : [],
14352
- ...control ? [control.close()] : [],
14353
- ...dashboard ? [dashboard.close()] : []
14182
+ ...control ? [control.close()] : []
14354
14183
  ]);
14355
14184
  };
14356
14185
  try {
@@ -14359,12 +14188,6 @@ async function createCompanionRuntime(opts = {}) {
14359
14188
  log: log2,
14360
14189
  hub,
14361
14190
  ...opts.checkHarness ? { checkHarness: opts.checkHarness } : {},
14362
- ...embeddedRunner({
14363
- electron: Boolean(process.versions.electron),
14364
- execPath: process.execPath,
14365
- moduleUrl: import.meta.url,
14366
- env: process.env
14367
- }) ?? {},
14368
14191
  restartForUpdate: () => restartForUpdate()
14369
14192
  });
14370
14193
  const active2 = supervisor;
@@ -14379,21 +14202,10 @@ async function createCompanionRuntime(opts = {}) {
14379
14202
  update: (auto) => active2.update(auto),
14380
14203
  stop: () => void stop2().then(() => active2.requestStop())
14381
14204
  });
14382
- if (opts.startDashboard) {
14383
- const preferredPort = cfg.dashboardPort;
14384
- dashboard = await opts.startDashboard({
14385
- supervisor,
14386
- hub,
14387
- logPath: companionLogPath(),
14388
- ...preferredPort !== void 0 ? { port: preferredPort } : {}
14389
- });
14390
- hub.setDashboardUrl(dashboard.url);
14391
- }
14392
14205
  if (stopped) throw new Error("The companion was stopped during startup.");
14393
14206
  writeRuntimeState({
14394
14207
  pid: process.pid,
14395
14208
  socket: control.path,
14396
- ...dashboard ? { url: dashboard.url, port: dashboard.port } : {},
14397
14209
  startedAt,
14398
14210
  // The daemon launcher sets this env on the detached child, so the
14399
14211
  // marker records whether this companion is backgrounded (foreground start
@@ -14408,8 +14220,7 @@ async function createCompanionRuntime(opts = {}) {
14408
14220
  process.removeListener("exit", clearRuntimeOnExit);
14409
14221
  await Promise.allSettled([
14410
14222
  ...supervisor ? [supervisor.shutdown()] : [],
14411
- ...control ? [control.close()] : [],
14412
- ...dashboard ? [dashboard.close()] : []
14223
+ ...control ? [control.close()] : []
14413
14224
  ]);
14414
14225
  recordCrash({
14415
14226
  reason: err instanceof Error ? err.message : String(err),
@@ -14433,7 +14244,6 @@ async function createCompanionRuntime(opts = {}) {
14433
14244
  return {
14434
14245
  ok: true,
14435
14246
  runtime: {
14436
- ...dashboard ? { url: dashboard.url, port: dashboard.port } : {},
14437
14247
  socketPath: control.path,
14438
14248
  config: cfg,
14439
14249
  stop: stop2,
@@ -14822,6 +14632,7 @@ function userName() {
14822
14632
  return process.env.USER ?? "";
14823
14633
  }
14824
14634
  }
14635
+ var SCRIPT_WRAPPER_COMMENT = /^# Written by \S+\/install\.sh\. /m;
14825
14636
  function rewriteScriptWrappers(dir2 = join27(cabaneDir(), "bin")) {
14826
14637
  for (const name of ["cabane", "cabane-companion"]) {
14827
14638
  const path = join27(dir2, name);
@@ -14831,7 +14642,7 @@ function rewriteScriptWrappers(dir2 = join27(cabaneDir(), "bin")) {
14831
14642
  } catch {
14832
14643
  continue;
14833
14644
  }
14834
- if (!text.includes("Written by https://cabane.ai/install.sh")) continue;
14645
+ if (!SCRIPT_WRAPPER_COMMENT.test(text)) continue;
14835
14646
  if (!text.includes("/dist/cli.js")) continue;
14836
14647
  writeAtomic(path, text.replace("/dist/cli.js", "/dist/bin.js"));
14837
14648
  chmodSync5(path, 493);
@@ -15168,6 +14979,9 @@ async function runAttached(runtime) {
15168
14979
  });
15169
14980
  }
15170
14981
 
14982
+ // src/commands/setup.ts
14983
+ init_errors();
14984
+
15171
14985
  // src/prompts.ts
15172
14986
  init_term();
15173
14987
  import * as clack from "@clack/prompts";
@@ -15188,7 +15002,8 @@ function terminalUi() {
15188
15002
  },
15189
15003
  spinner: () => railSpinner(),
15190
15004
  outro: (lines2) => clack.outro(lines2.join("\n ")),
15191
- cancel: (message2) => clack.cancel(message2)
15005
+ // Clack draws only the first line on the rail's end; the rest align under its text.
15006
+ cancel: (message2) => clack.cancel(message2.split("\n").join("\n "))
15192
15007
  };
15193
15008
  }
15194
15009
  var FRAMES = ["\u25D2", "\u25D0", "\u25D3", "\u25D1"];
@@ -15244,7 +15059,10 @@ function plainUi() {
15244
15059
  outro: (lines2) => {
15245
15060
  for (const line of lines2) write(`${INDENT}${line}`);
15246
15061
  },
15247
- cancel: (message2) => write(`${INDENT}${message2}`)
15062
+ // A closing message can run to more than one line; each is indented like the first.
15063
+ cancel: (message2) => {
15064
+ for (const line of message2.split("\n")) write(`${INDENT}${line}`);
15065
+ }
15248
15066
  };
15249
15067
  }
15250
15068
 
@@ -15256,9 +15074,13 @@ async function setup(opts = {}, deps = {}) {
15256
15074
  try {
15257
15075
  await setupScript(opts, deps);
15258
15076
  } catch (err) {
15259
- for (const line of pathLines(reloadLine())) write(line);
15260
- write(err instanceof Error ? err.message : String(err));
15261
- write(`Check the server connection, then run ${cabane("setup")} again.`);
15077
+ if (err instanceof StandaloneError) {
15078
+ write(err.message);
15079
+ } else {
15080
+ for (const line of pathLines(reloadLine())) write(line);
15081
+ write(err instanceof Error ? err.message : String(err));
15082
+ write(`Check the server connection, then run ${cabane("setup")} again.`);
15083
+ }
15262
15084
  process.exitCode = 1;
15263
15085
  } finally {
15264
15086
  setConsoleLogging(true);
@@ -15286,8 +15108,21 @@ async function setupScript(opts, deps) {
15286
15108
  }
15287
15109
  let justPaired = false;
15288
15110
  if (!paired) {
15289
- const baseUrl = resolvePairBaseUrl(opts.server ?? cfg?.baseUrl);
15290
- const device = await (deps.pair ?? pairDevice)({ baseUrl, interactive, ui });
15111
+ let baseUrl;
15112
+ try {
15113
+ baseUrl = resolvePairBaseUrl(opts.server ?? cfg?.baseUrl);
15114
+ } catch (err) {
15115
+ if (!(err instanceof NoPairAddressError)) throw err;
15116
+ ui.cancel(err.message);
15117
+ process.exitCode = 1;
15118
+ return;
15119
+ }
15120
+ const device = await (deps.pair ?? pairDevice)({
15121
+ baseUrl,
15122
+ interactive,
15123
+ ui,
15124
+ onCode: () => rememberPairAddress(baseUrl)
15125
+ });
15291
15126
  if (!device) return;
15292
15127
  writePairedConfig(device);
15293
15128
  justPaired = true;
@@ -16378,7 +16213,7 @@ init_cabane_command();
16378
16213
  rememberInvokedCommand();
16379
16214
  var program = new Command();
16380
16215
  program.name("cabane").description("Run your Cabane agents on this device, with Claude Code, Codex or OpenCode.").version(COMPANION_VERSION);
16381
- program.command("setup").argument("[code]", "not taken: run cabane setup and enter the code it shows in Cabane").description("set up this device: find your agents, pair it with Cabane, and keep it running.").allowExcessArguments(false).option("--server <url>", "the Cabane instance to pair with (default https://app.cabane.ai)").addOption(
16216
+ program.command("setup").argument("[code]", "not taken: run cabane setup and enter the code it shows in Cabane").description("set up this device: find your agents, pair it with Cabane, and keep it running.").allowExcessArguments(false).option("--server <url>", "the address of the Cabane to pair with").addOption(
16382
16217
  new Option("--log-level <level>", "info (default), warn, or debug").choices([
16383
16218
  "info",
16384
16219
  "warn",