@cabane/companion 0.6.214 → 0.6.216

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
@@ -778,7 +778,10 @@ var init_config = __esm({
778
778
  // credential and secrets on its stdin (`runner/launch.ts`, `wrapped`). The README lists
779
779
  // what a wrapper can rely on.
780
780
  runner: z2.object({
781
- command: z2.array(z2.string().min(1)).min(1)
781
+ command: z2.array(z2.string().min(1)).min(1),
782
+ // The external wrapper supplies only broker placeholders and removes
783
+ // credential mounts. Do not hand provider logins to that runner on stdin.
784
+ brokeredCredentials: z2.boolean().optional()
782
785
  }).strict().optional()
783
786
  });
784
787
  RETIRED_ROW_KEYS = ["sandbox"];
@@ -1418,7 +1421,7 @@ function pathForLog(path) {
1418
1421
  const q = path.indexOf("?");
1419
1422
  return q === -1 ? path : path.slice(0, q);
1420
1423
  }
1421
- function sleep(ms, signal) {
1424
+ function sleep2(ms, signal) {
1422
1425
  return new Promise((resolve2) => {
1423
1426
  if (signal?.aborted) return resolve2();
1424
1427
  const timer = setTimeout(() => {
@@ -1521,7 +1524,7 @@ var init_api = __esm({
1521
1524
  },
1522
1525
  "companion: API call failed transiently; retrying"
1523
1526
  );
1524
- await sleep(delayMs, signal);
1527
+ await sleep2(delayMs, signal);
1525
1528
  if (signal?.aborted) throw err;
1526
1529
  }
1527
1530
  }
@@ -7510,7 +7513,7 @@ var init_opencode_lock = __esm({
7510
7513
  });
7511
7514
 
7512
7515
  // src/turn-containment.ts
7513
- import { execFile, execFileSync, spawn as spawn3 } from "child_process";
7516
+ import { execFile, execFileSync, spawn as spawn4 } from "child_process";
7514
7517
  import { readFileSync as readFileSync6 } from "fs";
7515
7518
  import { promisify as promisify2 } from "util";
7516
7519
  function turnScopeUnit(turnId, attempt) {
@@ -7639,7 +7642,7 @@ function createTurnContainment(turnId, deps = {}) {
7639
7642
  command: options.command
7640
7643
  });
7641
7644
  return asContained(
7642
- spawn3("systemd-run", [...scopeArgs(unit), options.command, ...options.args], {
7645
+ spawn4("systemd-run", [...scopeArgs(unit), options.command, ...options.args], {
7643
7646
  ...options.cwd ? { cwd: options.cwd } : {},
7644
7647
  env: options.env,
7645
7648
  stdio: ["pipe", "pipe", "pipe"]
@@ -7647,7 +7650,7 @@ function createTurnContainment(turnId, deps = {}) {
7647
7650
  );
7648
7651
  }
7649
7652
  function spawnInGroup(options) {
7650
- const child = spawn3(options.command, options.args, {
7653
+ const child = spawn4(options.command, options.args, {
7651
7654
  ...options.cwd ? { cwd: options.cwd } : {},
7652
7655
  env: options.env,
7653
7656
  stdio: ["pipe", "pipe", "pipe"],
@@ -7729,7 +7732,7 @@ function createTurnContainment(turnId, deps = {}) {
7729
7732
  } catch {
7730
7733
  }
7731
7734
  }
7732
- await sleep2(fallbackGraceMs);
7735
+ await sleep3(fallbackGraceMs);
7733
7736
  const afterTerm = processGroupMembers(pgid);
7734
7737
  if (!afterTerm.ok) {
7735
7738
  result.failures.push({
@@ -7792,7 +7795,7 @@ function createTurnContainment(turnId, deps = {}) {
7792
7795
  }
7793
7796
  };
7794
7797
  }
7795
- var execFileAsync, asContained, CONTAINMENT_STOP_GRACE_SEC, FALLBACK_GRACE_MS, CAPABILITY_PROBE_TIMEOUT_MS, OOM_READ_TIMEOUT_MS, sleep2, MANAGER_ENV_KEYS, managerEnvKey;
7798
+ var execFileAsync, asContained, CONTAINMENT_STOP_GRACE_SEC, FALLBACK_GRACE_MS, CAPABILITY_PROBE_TIMEOUT_MS, OOM_READ_TIMEOUT_MS, sleep3, MANAGER_ENV_KEYS, managerEnvKey;
7796
7799
  var init_turn_containment = __esm({
7797
7800
  "src/turn-containment.ts"() {
7798
7801
  "use strict";
@@ -7802,7 +7805,7 @@ var init_turn_containment = __esm({
7802
7805
  FALLBACK_GRACE_MS = CONTAINMENT_STOP_GRACE_SEC * 1e3;
7803
7806
  CAPABILITY_PROBE_TIMEOUT_MS = 4e3;
7804
7807
  OOM_READ_TIMEOUT_MS = 1500;
7805
- sleep2 = (ms) => new Promise((resolve2) => setTimeout(resolve2, ms));
7808
+ sleep3 = (ms) => new Promise((resolve2) => setTimeout(resolve2, ms));
7806
7809
  MANAGER_ENV_KEYS = ["DBUS_SESSION_BUS_ADDRESS", "XDG_RUNTIME_DIR", "PATH"];
7807
7810
  managerEnvKey = (env) => MANAGER_ENV_KEYS.map((k) => `${k}=${env[k] ?? ""}`).join("\0");
7808
7811
  }
@@ -8464,7 +8467,7 @@ var init_build_options = __esm({
8464
8467
  import { readFile as readFile2 } from "fs/promises";
8465
8468
  import { homedir as homedir7 } from "os";
8466
8469
  import { join as join16 } from "path";
8467
- import { parse } from "smol-toml";
8470
+ import { parse as parse2 } from "smol-toml";
8468
8471
  function codexConfigPath(env = process.env, home = homedir7()) {
8469
8472
  return join16(env.CODEX_HOME || join16(home, ".codex"), "config.toml");
8470
8473
  }
@@ -8479,7 +8482,7 @@ async function readCodexConfigSetsSandboxMode(path = codexConfigPath()) {
8479
8482
  }
8480
8483
  function configSetsSandboxMode(text) {
8481
8484
  try {
8482
- return parse(text).sandbox_mode !== void 0;
8485
+ return parse2(text).sandbox_mode !== void 0;
8483
8486
  } catch {
8484
8487
  return false;
8485
8488
  }
@@ -11729,7 +11732,6 @@ async function liveCompanionState() {
11729
11732
  // src/commands/setup.ts
11730
11733
  init_cabane_command();
11731
11734
  init_config();
11732
- import { hostname as hostname2 } from "os";
11733
11735
 
11734
11736
  // src/logger.ts
11735
11737
  init_config();
@@ -11904,61 +11906,346 @@ function getLogger() {
11904
11906
  return cached;
11905
11907
  }
11906
11908
 
11909
+ // src/pair-flow.ts
11910
+ init_cabane_command();
11911
+ import { spawn as spawn3 } from "child_process";
11912
+ import { hostname } from "os";
11913
+
11907
11914
  // src/enrollment.ts
11908
11915
  init_errors();
11909
- async function claimSetupCode(baseUrl, input) {
11916
+ var EnrollmentExpiredError = class extends CompanionError {
11917
+ constructor() {
11918
+ super("this pairing code expired before it was confirmed.");
11919
+ this.name = "EnrollmentExpiredError";
11920
+ }
11921
+ };
11922
+ var EnrollmentCancelledError = class extends CompanionError {
11923
+ constructor() {
11924
+ super("pairing was cancelled.");
11925
+ this.name = "EnrollmentCancelledError";
11926
+ }
11927
+ };
11928
+ function trimBase(baseUrl) {
11929
+ return baseUrl.endsWith("/") ? baseUrl.slice(0, -1) : baseUrl;
11930
+ }
11931
+ async function postJson(baseUrl, path, body, signal) {
11910
11932
  let res;
11911
11933
  try {
11912
- res = await fetch(`${baseUrl.replace(/\/$/, "")}/api/device-enrollment/claim`, {
11934
+ res = await fetch(`${trimBase(baseUrl)}${path}`, {
11913
11935
  method: "POST",
11914
- headers: {
11915
- "content-type": "application/json",
11916
- ...input.deviceToken ? { authorization: `Bearer ${input.deviceToken}` } : {}
11917
- },
11918
- body: JSON.stringify({
11919
- userCode: input.userCode,
11920
- label: input.label,
11921
- platform: process.platform,
11922
- arch: process.arch,
11923
- nodeMajor: Number.parseInt(process.versions.node.split(".")[0], 10)
11924
- })
11936
+ headers: { "content-type": "application/json", accept: "application/json" },
11937
+ body: JSON.stringify(body),
11938
+ ...signal ? { signal } : {}
11925
11939
  });
11926
11940
  } catch {
11941
+ if (signal?.aborted) throw new EnrollmentCancelledError();
11927
11942
  throw new CompanionError(
11928
11943
  `Couldn't reach Cabane at ${baseUrl}. Check the server URL and your connection.`
11929
11944
  );
11930
11945
  }
11931
- if (res.status === 429)
11932
- throw new CompanionError(
11933
- "Too many attempts from this network. Wait a few minutes and paste the setup line again."
11934
- );
11935
- if (res.status === 404 && input.deviceToken)
11936
- throw new CompanionError(
11937
- "The setup code or saved device credential is unavailable. If this device was removed, run cabane logout, then take a fresh setup line from your workspace\u2019s Settings \u2192 Devices."
11938
- );
11939
- if (res.status === 404 || res.status === 410)
11946
+ let raw;
11947
+ try {
11948
+ raw = await res.text();
11949
+ } catch {
11950
+ if (signal?.aborted) throw new EnrollmentCancelledError();
11940
11951
  throw new CompanionError(
11941
- "That setup code is unavailable. Take a fresh setup line from your workspace\u2019s Settings \u2192 Devices."
11952
+ `Couldn't reach Cabane at ${baseUrl}. Check the server URL and your connection.`
11942
11953
  );
11943
- if (!res.ok) throw new ApiError(res.status, `Setup failed (${res.status}).`);
11944
- const body = await res.json();
11945
- if (!body || typeof body !== "object" || !("baseUrl" in body) || typeof body.baseUrl !== "string" || !("deviceId" in body) || typeof body.deviceId !== "string")
11946
- throw new CompanionError("Cabane returned an incomplete setup response.");
11947
- const token = "deviceToken" in body && typeof body.deviceToken === "string" ? body.deviceToken : input.deviceToken;
11948
- if (!token) throw new CompanionError("Cabane returned no device credential.");
11954
+ }
11955
+ let parsed = void 0;
11956
+ if (raw.length > 0) {
11957
+ try {
11958
+ parsed = JSON.parse(raw);
11959
+ } catch {
11960
+ parsed = raw;
11961
+ }
11962
+ }
11963
+ if (res.status >= 400) {
11964
+ if (res.status === 429)
11965
+ throw new CompanionError(
11966
+ "Too many pairing requests from this network. Wait a few minutes and run cabane setup again."
11967
+ );
11968
+ if (res.status === 404 && path.endsWith("/code"))
11969
+ throw new CompanionError(
11970
+ `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).`
11971
+ );
11972
+ const msg = parsed && typeof parsed === "object" && "error" in parsed ? String(parsed.error) : `${res.status}`;
11973
+ throw new ApiError(res.status, `${res.status} ${msg}`, parsed);
11974
+ }
11975
+ return parsed;
11976
+ }
11977
+ function requestEnrollmentCode(baseUrl, opts = {}) {
11978
+ return postJson(
11979
+ baseUrl,
11980
+ "/api/device-enrollment/code",
11981
+ { ...opts.label ? { label: opts.label } : {}, ...hostFacts() },
11982
+ opts.signal
11983
+ );
11984
+ }
11985
+ function hostFacts() {
11986
+ const nodeMajor = Number.parseInt(process.versions.node.split(".")[0] ?? "", 10);
11949
11987
  return {
11950
- baseUrl: body.baseUrl,
11951
- deviceToken: token,
11952
- deviceId: body.deviceId,
11953
- ..."workspace" in body && body.workspace && typeof body.workspace === "object" && "name" in body.workspace && typeof body.workspace.name === "string" && "slug" in body.workspace && typeof body.workspace.slug === "string" ? { workspace: { name: body.workspace.name, slug: body.workspace.slug } } : {},
11954
- ..."devicesUrl" in body && typeof body.devicesUrl === "string" ? { devicesUrl: body.devicesUrl } : {},
11955
- ..."deviceLabel" in body && typeof body.deviceLabel === "string" ? { deviceLabel: body.deviceLabel } : {}
11988
+ platform: process.platform,
11989
+ arch: process.arch,
11990
+ ...Number.isInteger(nodeMajor) ? { nodeMajor } : {}
11956
11991
  };
11957
11992
  }
11958
11993
  function deviceLabelFromHostname(hostname3) {
11959
11994
  const trimmed = hostname3.trim().replace(/\.local$/i, "");
11960
11995
  return trimmed.length ? trimmed.slice(0, 120) : void 0;
11961
11996
  }
11997
+ function pollOnce(baseUrl, deviceCode, signal) {
11998
+ return postJson(baseUrl, "/api/device-enrollment/poll", { deviceCode }, signal);
11999
+ }
12000
+ var sleep = (ms) => new Promise((resolve2) => setTimeout(resolve2, ms));
12001
+ async function awaitEnrollment(baseUrl, code, opts = {}) {
12002
+ const now = opts.now ?? (() => Date.now());
12003
+ const wait = opts.sleepMs ?? sleep;
12004
+ const deadline = now() + code.expiresIn * 1e3;
12005
+ let intervalMs = Math.max(1, code.interval) * 1e3;
12006
+ while (now() < deadline) {
12007
+ await wait(intervalMs);
12008
+ if (opts.signal?.aborted) throw new EnrollmentCancelledError();
12009
+ const res = await pollOnce(baseUrl, code.deviceCode, opts.signal);
12010
+ if (opts.signal?.aborted) throw new EnrollmentCancelledError();
12011
+ if (res.status === "complete") {
12012
+ if (!res.deviceToken || !res.baseUrl)
12013
+ throw new CompanionError("Cabane reported the pairing complete but returned no token.");
12014
+ return {
12015
+ baseUrl: res.baseUrl,
12016
+ deviceToken: res.deviceToken,
12017
+ ...res.deviceId ? { deviceId: res.deviceId } : {},
12018
+ ...res.deviceLabel ? { deviceLabel: res.deviceLabel } : {}
12019
+ };
12020
+ }
12021
+ if (res.status === "expired") throw new EnrollmentExpiredError();
12022
+ if (res.status === "slow_down") intervalMs += 1e3;
12023
+ }
12024
+ throw new EnrollmentExpiredError();
12025
+ }
12026
+
12027
+ // src/terminal-copy.ts
12028
+ init_cabane_command();
12029
+ var UNPAIRED = `This device isn't paired yet. Run ${cabane("setup")} on it.`;
12030
+ var lease = DEFAULT_DEVICE_OFFLINE_THRESHOLD_MS;
12031
+ function age(at, now) {
12032
+ const seconds = Math.max(0, Math.floor((now - Date.parse(at)) / 1e3));
12033
+ return seconds < 60 ? `${seconds}s` : seconds < 3600 ? `${Math.floor(seconds / 60)}m` : `${Math.floor(seconds / 3600)}h`;
12034
+ }
12035
+ function deviceLine(f, now = Date.now()) {
12036
+ if (!f.running) return `not running \u2014 start it with ${cabane("start")}`;
12037
+ if (f.lastHeartbeatRefused)
12038
+ return `refused by the server \u2014 it may be deactivated; see ${f.devicesUrl}`;
12039
+ if (!f.lastHeartbeatAckAt) return "starting \u2014 hasn't checked in yet";
12040
+ const since = age(f.lastHeartbeatAckAt, now);
12041
+ if (now - Date.parse(f.lastHeartbeatAckAt) > lease)
12042
+ return `can't reach the server \u2014 last checked in ${since} ago`;
12043
+ const view = f.lastAcknowledgedDevice;
12044
+ if (!view) return `checked in ${since} ago`;
12045
+ const word = view.state === "online" && view.health ? view.health === "checking" ? "online, checking harnesses\u2026" : DEVICE_HEALTH_LABEL[view.health] : DEVICE_STATE_LABEL[view.state];
12046
+ return `${word.toLowerCase()} \u2014 checked in ${since} ago`;
12047
+ }
12048
+ function pairingLine(pair) {
12049
+ return `\u2713 Paired as "${pair.deviceLabel ?? "Computer"}".`;
12050
+ }
12051
+ function pathLines(reload) {
12052
+ return reload ? [` To use cabane in this window: ${reload}`, ""] : [];
12053
+ }
12054
+ function agentLines(detection, opts) {
12055
+ const rows = agentRows(detection, opts);
12056
+ const names = rows.map((row3) => row3.usable ? `\u2713 ${row3.label}` : row3.label);
12057
+ const width = Math.max(...names.map((name) => name.length)) + 3;
12058
+ return rows.map((row3, i) => `${names[i].padEnd(width)}${row3.text}`);
12059
+ }
12060
+ var modeLines = {
12061
+ background: "Cabane is running in the background",
12062
+ service: "Cabane is running as a service",
12063
+ attached: "Cabane is running in this process",
12064
+ existing: "Cabane was already running"
12065
+ };
12066
+ function closingBlock(input, now = Date.now()) {
12067
+ const { mode, facts } = input;
12068
+ const device = deviceLine(facts, now);
12069
+ const noAck = !facts.lastHeartbeatAckAt && !facts.lastHeartbeatRefused;
12070
+ const stale = facts.lastHeartbeatAckAt !== null && now - Date.parse(facts.lastHeartbeatAckAt) > lease;
12071
+ const modeLine = modeLines[mode];
12072
+ const sentence = noAck ? `${mode === "background" ? "Cabane started in the background" : modeLine} but hasn't checked in yet.` : `${modeLine}. ${facts.lastHeartbeatRefused || stale ? device[0].toUpperCase() + device.slice(1) : `This device ${device.startsWith("checked in") || device.startsWith("needs attention") ? "" : "is "}${device}`}.`;
12073
+ const lines2 = [` ${sentence}`, "", ...pathLines(input.reload)];
12074
+ if (noAck || stale || facts.lastHeartbeatRefused) {
12075
+ lines2.push(
12076
+ ` See what it's doing: ${cabane("status")}${facts.lastHeartbeatRefused ? "" : ` Log: ${cabane("logs")}`}`
12077
+ );
12078
+ } else {
12079
+ lines2.push(" Harnesses on this device");
12080
+ lines2.push(
12081
+ ...(input.detection ? agentLines(input.detection, { signInHint: true }) : ["Claude Code checking", "Codex checking", "OpenCode checking"]).map((line) => ` ${line}`)
12082
+ );
12083
+ lines2.push(
12084
+ "",
12085
+ ` Status ${cabane("status")}${mode === "attached" ? "" : ` Stop ${cabane("stop")}`} Log ${cabane("logs")}`
12086
+ );
12087
+ if (!input.service && input.servicePlatform) {
12088
+ lines2.push(
12089
+ ` Start at login and keep Cabane updating itself: ${cabane("service install")}${input.servicePlatform === "launchd" ? " (macOS will ask you to allow a background item)" : ""}`
12090
+ );
12091
+ lines2.push(
12092
+ ` Updates itself only as a service; to turn that off: ${cabane("update --auto off")}`
12093
+ );
12094
+ } else if (input.service && input.autoUpdate)
12095
+ lines2.push(` Updating itself; to turn that off: ${cabane("update --auto off")}`);
12096
+ }
12097
+ if (input.address) lines2.push("", ` In Cabane: ${input.address}`);
12098
+ if (mode === "attached") lines2.push("", " Listening for messages\u2026");
12099
+ return lines2;
12100
+ }
12101
+ function startFailure(message2, reload) {
12102
+ return [
12103
+ ...pathLines(reload),
12104
+ `\u2717 Cabane couldn't start: ${message2}`,
12105
+ "",
12106
+ ` Log: ${cabane("logs")} Try again: ${cabane("start")}`
12107
+ ];
12108
+ }
12109
+
12110
+ // src/pair-flow.ts
12111
+ var MAX_CODES = 3;
12112
+ async function pairDevice(opts, deps = {}) {
12113
+ const requestCode = deps.requestCode ?? requestEnrollmentCode;
12114
+ const wait = deps.await ?? awaitEnrollment;
12115
+ const open2 = deps.open ?? openLink;
12116
+ const { ui, interactive } = opts;
12117
+ const label = deviceLabelFromHostname(hostname());
12118
+ const aborter = new AbortController();
12119
+ const onSigint = () => aborter.abort();
12120
+ process.on("SIGINT", onSigint);
12121
+ const spinner = ui.spinner();
12122
+ let current = null;
12123
+ const keys = interactive ? watchReturn({
12124
+ onReturn: () => {
12125
+ if (!current) return;
12126
+ const link = current.verificationUriComplete;
12127
+ void open2(link).then((opened) => {
12128
+ if (!opened)
12129
+ spinner.message(
12130
+ `Couldn't open a browser here. Open this page on any device: ${link}`
12131
+ );
12132
+ });
12133
+ },
12134
+ onInterrupt: () => aborter.abort(),
12135
+ ...deps.input ? { input: deps.input } : {}
12136
+ }) : null;
12137
+ let minutesWaited = 0;
12138
+ let waiting = false;
12139
+ try {
12140
+ for (let attempt = 1; attempt <= MAX_CODES; attempt += 1) {
12141
+ let code;
12142
+ try {
12143
+ code = await requestCode(opts.baseUrl, {
12144
+ ...label ? { label } : {},
12145
+ signal: aborter.signal
12146
+ });
12147
+ } catch (err) {
12148
+ if (!(err instanceof EnrollmentCancelledError)) throw err;
12149
+ ui.cancel(`Pairing cancelled. Run ${cabane("setup")} when you're ready.`);
12150
+ process.exitCode = 130;
12151
+ return null;
12152
+ }
12153
+ current = code;
12154
+ const minutes = Math.max(1, Math.round(code.expiresIn / 60));
12155
+ const codeLines = ["", ` ${code.userCode}`, ` ${code.verificationUriComplete}`, ""];
12156
+ if (attempt === 1) {
12157
+ ui.step([
12158
+ "Pair this device",
12159
+ interactive ? "Enter this code in Cabane, or press Return to open the page:" : "Enter this code in Cabane:",
12160
+ ...codeLines
12161
+ ]);
12162
+ } else {
12163
+ ui.step(codeLines.slice(1));
12164
+ }
12165
+ spinner.start(
12166
+ `Waiting for you to confirm in Cabane\u2026 (the code is good for ${minutes} minutes)`
12167
+ );
12168
+ waiting = true;
12169
+ try {
12170
+ const device = await wait(opts.baseUrl, code, { signal: aborter.signal });
12171
+ waiting = false;
12172
+ spinner.stop(pairingLine(device));
12173
+ return device;
12174
+ } catch (err) {
12175
+ waiting = false;
12176
+ if (err instanceof EnrollmentCancelledError) {
12177
+ spinner.stop("Pairing stopped.");
12178
+ ui.cancel(`Pairing cancelled. Run ${cabane("setup")} when you're ready.`);
12179
+ process.exitCode = 130;
12180
+ return null;
12181
+ }
12182
+ if (!(err instanceof EnrollmentExpiredError)) {
12183
+ spinner.stop("Pairing stopped.");
12184
+ throw err;
12185
+ }
12186
+ minutesWaited += minutes;
12187
+ if (attempt < MAX_CODES) {
12188
+ spinner.stop("That code expired. Here's a fresh one:");
12189
+ continue;
12190
+ }
12191
+ spinner.stop(
12192
+ `Nothing confirmed in ${minutesWaited} minutes. Run ${cabane("setup")} when you're ready.`
12193
+ );
12194
+ process.exitCode = 1;
12195
+ return null;
12196
+ }
12197
+ }
12198
+ return null;
12199
+ } finally {
12200
+ if (waiting) spinner.stop("Pairing stopped.");
12201
+ keys?.release();
12202
+ process.removeListener("SIGINT", onSigint);
12203
+ }
12204
+ }
12205
+ function watchReturn(handlers) {
12206
+ const input = handlers.input ?? process.stdin;
12207
+ if (!input.isTTY || typeof input.setRawMode !== "function") return { release: () => {
12208
+ } };
12209
+ const wasRaw = input.isRaw;
12210
+ const onData = (chunk) => {
12211
+ const text = chunk.toString();
12212
+ if (text.includes("")) handlers.onInterrupt();
12213
+ else if (text.includes("\r") || text.includes("\n")) handlers.onReturn();
12214
+ };
12215
+ input.setRawMode(true);
12216
+ input.on("data", onData);
12217
+ input.resume();
12218
+ return {
12219
+ release: () => {
12220
+ input.removeListener("data", onData);
12221
+ input.setRawMode(wasRaw);
12222
+ input.pause();
12223
+ }
12224
+ };
12225
+ }
12226
+ function pageOpener(platform, link) {
12227
+ if (platform === "darwin") return { command: "open", args: [link] };
12228
+ if (platform === "win32") return { command: "cmd", args: ["/c", "start", '""', `"${link}"`] };
12229
+ return { command: "xdg-open", args: [link] };
12230
+ }
12231
+ function openLink(link) {
12232
+ return new Promise((resolve2) => {
12233
+ const { command, args } = pageOpener(process.platform, link);
12234
+ try {
12235
+ const child = spawn3(command, args, {
12236
+ stdio: "ignore",
12237
+ detached: true,
12238
+ // `cmd` parses its own line: the quoting above is the one it needs, as written.
12239
+ ...process.platform === "win32" ? { windowsVerbatimArguments: true } : {}
12240
+ });
12241
+ child.once("error", () => resolve2(false));
12242
+ child.once("exit", (code) => resolve2(code === 0));
12243
+ child.unref();
12244
+ } catch {
12245
+ resolve2(false);
12246
+ }
12247
+ });
12248
+ }
11962
12249
 
11963
12250
  // src/commands/start.ts
11964
12251
  init_cabane_command();
@@ -12501,7 +12788,7 @@ var emptyHeartbeatStatus = () => ({
12501
12788
 
12502
12789
  // src/supervisor.ts
12503
12790
  init_cabane_command();
12504
- import { hostname } from "os";
12791
+ import { hostname as hostname2 } from "os";
12505
12792
 
12506
12793
  // src/log-copy.ts
12507
12794
  init_failure();
@@ -12971,6 +13258,32 @@ init_boot_id();
12971
13258
  init_turn_containment();
12972
13259
  init_protocol();
12973
13260
  import { createInterface } from "readline";
13261
+
13262
+ // src/credential-broker/client-config.ts
13263
+ import { parse, stringify } from "smol-toml";
13264
+ var BROKERED_CREDENTIAL_NAMES = /* @__PURE__ */ new Set([
13265
+ "ANTHROPIC_API_KEY",
13266
+ "ANTHROPIC_AUTH_TOKEN",
13267
+ "CLAUDE_CODE_OAUTH_TOKEN",
13268
+ "CLAUDE_CODE_OAUTH_REFRESH_TOKEN",
13269
+ "CLAUDE_CODE_SESSION_ACCESS_TOKEN",
13270
+ "CLAUDE_CODE_OAUTH_TOKEN_FILE_DESCRIPTOR",
13271
+ "CLAUDE_CODE_HOST_CREDS_FILE",
13272
+ "CODEX_API_KEY",
13273
+ "OPENAI_API_KEY",
13274
+ "OPENAI_ACCESS_TOKEN",
13275
+ "GH_TOKEN",
13276
+ "GITHUB_TOKEN",
13277
+ "GH_ENTERPRISE_TOKEN",
13278
+ "GITHUB_ENTERPRISE_TOKEN"
13279
+ ]);
13280
+ function withoutBrokeredCredentials(values) {
13281
+ return Object.fromEntries(
13282
+ Object.entries(values).filter(([name]) => !BROKERED_CREDENTIAL_NAMES.has(name))
13283
+ );
13284
+ }
13285
+
13286
+ // src/runner/launch.ts
12974
13287
  function gzipBodiesSignal() {
12975
13288
  let on = false;
12976
13289
  const listeners = /* @__PURE__ */ new Set();
@@ -13233,12 +13546,22 @@ async function runTurnInRunner(opts, launch, duties, payload, handleOpts) {
13233
13546
  }
13234
13547
  try {
13235
13548
  const wrapped = launch.wrapped === true;
13236
- const env = runnerExecEnv(launch.env ?? process.env, wrapped);
13237
- const grants = minted.deviceGrants.length > 0 ? launch.grants?.(minted.deviceGrants) ?? {} : {};
13549
+ if (launch.brokeredCredentials && !wrapped)
13550
+ throw new Error("credential broker requires a wrapped runner");
13551
+ const inherited = runnerExecEnv(launch.env ?? process.env, wrapped);
13552
+ const env = launch.brokeredCredentials ? withoutBrokeredCredentials(inherited) : inherited;
13553
+ const directGrants = minted.deviceGrants.length > 0 ? launch.grants?.(minted.deviceGrants) ?? {} : {};
13554
+ const grants = launch.brokeredCredentials ? Object.fromEntries(
13555
+ Object.entries(directGrants).map(([key, values]) => [
13556
+ key,
13557
+ withoutBrokeredCredentials(values)
13558
+ ])
13559
+ ) : directGrants;
13560
+ const secrets = wrapped ? launch.secrets?.() ?? {} : void 0;
13238
13561
  const input = {
13239
13562
  baseUrl: opts.baseUrl,
13240
13563
  credential: minted.credential,
13241
- ...wrapped ? { secrets: launch.secrets?.() ?? {} } : {},
13564
+ ...secrets ? { secrets: launch.brokeredCredentials ? withoutBrokeredCredentials(secrets) : secrets } : {},
13242
13565
  ...Object.keys(grants).length > 0 ? { grants } : {},
13243
13566
  dispatch: payload,
13244
13567
  turnId,
@@ -13541,7 +13864,7 @@ function hostReport() {
13541
13864
  }
13542
13865
 
13543
13866
  // src/providers/local-process.ts
13544
- import { spawn as spawn4 } from "child_process";
13867
+ import { spawn as spawn5 } from "child_process";
13545
13868
  import { constants as constants2 } from "os";
13546
13869
  var DEFAULT_KILL_GRACE_MS = 3e4;
13547
13870
  var DEFAULT_REAP_GRACE_MS = 3e3;
@@ -13552,7 +13875,7 @@ function exitCode(code, signal) {
13552
13875
  }
13553
13876
  function createLocalProcessProvider(opts = {}) {
13554
13877
  const killGraceMs = opts.killGraceMs ?? DEFAULT_KILL_GRACE_MS;
13555
- const spawnFn = opts.spawnFn ?? spawn4;
13878
+ const spawnFn = opts.spawnFn ?? spawn5;
13556
13879
  const reapGraceMs = opts.reapGraceMs ?? DEFAULT_REAP_GRACE_MS;
13557
13880
  const killFn = opts.killFn ?? ((pid, signal) => process.kill(pid, signal));
13558
13881
  const groupAlive = (pgid) => {
@@ -13928,7 +14251,7 @@ var SseSubscriber = class {
13928
14251
  init_transcript_writer();
13929
14252
 
13930
14253
  // src/updater.ts
13931
- import { spawn as spawn5 } from "child_process";
14254
+ import { spawn as spawn6 } from "child_process";
13932
14255
  import { copyFileSync as copyFileSync2, existsSync as existsSync16, mkdirSync as mkdirSync14, renameSync as renameSync9, rmSync as rmSync10 } from "fs";
13933
14256
  import { dirname as dirname11, join as join26 } from "path";
13934
14257
 
@@ -14143,7 +14466,7 @@ var runProcess = (command, args, opts = {}) => new Promise((resolve2) => {
14143
14466
  let stderr = "";
14144
14467
  let child;
14145
14468
  try {
14146
- child = spawn5(command, [...args], {
14469
+ child = spawn6(command, [...args], {
14147
14470
  stdio: ["ignore", "pipe", "pipe"],
14148
14471
  env: opts.env ?? process.env
14149
14472
  });
@@ -14647,7 +14970,7 @@ var CompanionSupervisor = class {
14647
14970
  if (this.stopped) return;
14648
14971
  this.log.info(
14649
14972
  { protocolVersion: TURN_PROTOCOL_VERSION, version: COMPANION_VERSION },
14650
- `Companion ${COMPANION_VERSION} starting on ${this.config.deviceLabel ?? hostname()}`
14973
+ `Companion ${COMPANION_VERSION} starting on ${this.config.deviceLabel ?? hostname2()}`
14651
14974
  );
14652
14975
  const retired = retiredConfigKeysOnDisk();
14653
14976
  if (retired.length > 0) {
@@ -14682,9 +15005,7 @@ var CompanionSupervisor = class {
14682
15005
  this.detectTimer = setInterval(() => void this.detect(), DETECT_INTERVAL_MS);
14683
15006
  this.detectTimer.unref?.();
14684
15007
  if (!this.deviceApi || !this.config.deviceToken) {
14685
- this.log.warn(
14686
- "This device isn't paired. Get a setup line from a workspace's Settings \u2192 Devices."
14687
- );
15008
+ this.log.warn(`This device isn't paired. Run ${cabane("setup")} on it.`);
14688
15009
  return;
14689
15010
  }
14690
15011
  this.deviceSub = new SseSubscriber({
@@ -14695,7 +15016,7 @@ var CompanionSupervisor = class {
14695
15016
  log: this.log,
14696
15017
  lastEventId: null,
14697
15018
  onOpen: () => {
14698
- this.log.info(`Connected to Cabane as ${this.hub.statusJson().device_label ?? hostname()}`);
15019
+ this.log.info(`Connected to Cabane as ${this.hub.statusJson().device_label ?? hostname2()}`);
14699
15020
  this.deviceConnected = true;
14700
15021
  for (const wr of this.workspaces.values()) {
14701
15022
  this.hub.setConnected(wr.workspaceId, true);
@@ -15244,6 +15565,7 @@ var CompanionSupervisor = class {
15244
15565
  ),
15245
15566
  ...this.config.runner?.command ? {
15246
15567
  wrapped: true,
15568
+ brokeredCredentials: this.config.runner.brokeredCredentials,
15247
15569
  secrets: () => loadSecretValuesTolerant(
15248
15570
  (m) => this.log.warn({ err: m }, "Could not read the secrets file for a wrapped turn.")
15249
15571
  )
@@ -15928,9 +16250,9 @@ async function waitForBoundedHeartbeat(heartbeat) {
15928
16250
  }
15929
16251
  function defaultReexec() {
15930
16252
  clearRuntimeState();
15931
- void import("child_process").then(({ spawn: spawn7 }) => {
16253
+ void import("child_process").then(({ spawn: spawn8 }) => {
15932
16254
  try {
15933
- const child = spawn7(process.execPath, process.argv.slice(1), {
16255
+ const child = spawn8(process.execPath, process.argv.slice(1), {
15934
16256
  stdio: "inherit",
15935
16257
  detached: false
15936
16258
  });
@@ -16279,13 +16601,13 @@ async function observeStart(deps = {}) {
16279
16601
  const read2 = deps.readState ?? readLiveRuntimeState;
16280
16602
  const verify = deps.verify ?? verifyRuntime;
16281
16603
  const now = deps.now ?? Date.now;
16282
- const sleep3 = deps.sleep ?? ((ms) => new Promise((resolve2) => setTimeout(resolve2, ms)));
16604
+ const sleep4 = deps.sleep ?? ((ms) => new Promise((resolve2) => setTimeout(resolve2, ms)));
16283
16605
  const deadline = now() + 8e3;
16284
16606
  for (; ; ) {
16285
16607
  const state = await read2();
16286
16608
  if (state?.socket && await verify(state) === "ours") return state;
16287
16609
  if (now() >= deadline) return null;
16288
- await sleep3(Math.min(150, Math.max(0, deadline - now())));
16610
+ await sleep4(Math.min(150, Math.max(0, deadline - now())));
16289
16611
  }
16290
16612
  }
16291
16613
  function markLaunchRequester() {
@@ -16703,7 +17025,7 @@ async function controlService(action, deps) {
16703
17025
  init_cabane_command();
16704
17026
  init_cli_entry();
16705
17027
  init_config();
16706
- import { spawn as spawn6 } from "child_process";
17028
+ import { spawn as spawn7 } from "child_process";
16707
17029
  import { closeSync as closeSync3, mkdirSync as mkdirSync17, openSync as openSync4 } from "fs";
16708
17030
  init_config();
16709
17031
  async function startDaemon(opts = {}, deps = {}) {
@@ -16785,7 +17107,7 @@ function defaultSpawnDetached(args) {
16785
17107
  mkdirSync17(cabaneDir(), { recursive: true });
16786
17108
  const logFd = openSync4(companionLogPath(), "a");
16787
17109
  try {
16788
- const child = spawn6(process.execPath, [cliPath, ...args], {
17110
+ const child = spawn7(process.execPath, [cliPath, ...args], {
16789
17111
  detached: true,
16790
17112
  stdio: ["ignore", logFd, logFd],
16791
17113
  env: { ...process.env, CABANE_COMPANION_DAEMON: "1" }
@@ -16803,89 +17125,6 @@ function defaultSpawnDetached(args) {
16803
17125
  }
16804
17126
  }
16805
17127
 
16806
- // src/terminal-copy.ts
16807
- init_cabane_command();
16808
- var UNPAIRED = "This device isn't paired yet. Get a setup line from a workspace's Settings \u2192 Devices and paste it here.";
16809
- var lease = DEFAULT_DEVICE_OFFLINE_THRESHOLD_MS;
16810
- function age(at, now) {
16811
- const seconds = Math.max(0, Math.floor((now - Date.parse(at)) / 1e3));
16812
- return seconds < 60 ? `${seconds}s` : seconds < 3600 ? `${Math.floor(seconds / 60)}m` : `${Math.floor(seconds / 3600)}h`;
16813
- }
16814
- function deviceLine(f, now = Date.now()) {
16815
- if (!f.running) return `not running \u2014 start it with ${cabane("start")}`;
16816
- if (f.lastHeartbeatRefused)
16817
- return `refused by the server \u2014 it may be deactivated; see ${f.devicesUrl}`;
16818
- if (!f.lastHeartbeatAckAt) return "starting \u2014 hasn't checked in yet";
16819
- const since = age(f.lastHeartbeatAckAt, now);
16820
- if (now - Date.parse(f.lastHeartbeatAckAt) > lease)
16821
- return `can't reach the server \u2014 last checked in ${since} ago`;
16822
- const view = f.lastAcknowledgedDevice;
16823
- if (!view) return `checked in ${since} ago`;
16824
- const word = view.state === "online" && view.health ? view.health === "checking" ? "online, checking harnesses\u2026" : DEVICE_HEALTH_LABEL[view.health] : DEVICE_STATE_LABEL[view.state];
16825
- return `${word.toLowerCase()} \u2014 checked in ${since} ago`;
16826
- }
16827
- function pairingLine(pair) {
16828
- return `\u2713 Paired as "${pair.deviceLabel ?? "Computer"}" with ${pair.workspace?.name ?? "the workspace"}.`;
16829
- }
16830
- function pathLines(reload) {
16831
- return reload ? [` To use cabane in this window: ${reload}`, ""] : [];
16832
- }
16833
- function agentLines(detection, opts) {
16834
- const rows = agentRows(detection, opts);
16835
- const names = rows.map((row3) => row3.usable ? `\u2713 ${row3.label}` : row3.label);
16836
- const width = Math.max(...names.map((name) => name.length)) + 3;
16837
- return rows.map((row3, i) => `${names[i].padEnd(width)}${row3.text}`);
16838
- }
16839
- var modeLines = {
16840
- background: "Cabane is running in the background",
16841
- service: "Cabane is running as a service",
16842
- attached: "Cabane is running in this process",
16843
- existing: "Cabane was already running"
16844
- };
16845
- function closingBlock(input, now = Date.now()) {
16846
- const { mode, facts } = input;
16847
- const device = deviceLine(facts, now);
16848
- const noAck = !facts.lastHeartbeatAckAt && !facts.lastHeartbeatRefused;
16849
- const stale = facts.lastHeartbeatAckAt !== null && now - Date.parse(facts.lastHeartbeatAckAt) > lease;
16850
- const modeLine = modeLines[mode];
16851
- const sentence = noAck ? `${mode === "background" ? "Cabane started in the background" : modeLine} but hasn't checked in yet.` : `${modeLine}. ${facts.lastHeartbeatRefused || stale ? device[0].toUpperCase() + device.slice(1) : `This device ${device.startsWith("checked in") || device.startsWith("needs attention") ? "" : "is "}${device}`}.`;
16852
- const lines2 = [` ${sentence}`, "", ...pathLines(input.reload)];
16853
- if (noAck || stale || facts.lastHeartbeatRefused) {
16854
- lines2.push(
16855
- ` See what it's doing: ${cabane("status")}${facts.lastHeartbeatRefused ? "" : ` Log: ${cabane("logs")}`}`
16856
- );
16857
- } else {
16858
- lines2.push(" Harnesses on this device");
16859
- lines2.push(
16860
- ...(input.detection ? agentLines(input.detection, { signInHint: true }) : ["Claude Code checking", "Codex checking", "OpenCode checking"]).map((line) => ` ${line}`)
16861
- );
16862
- lines2.push(
16863
- "",
16864
- ` Status ${cabane("status")}${mode === "attached" ? "" : ` Stop ${cabane("stop")}`} Log ${cabane("logs")}`
16865
- );
16866
- if (!input.service && input.servicePlatform) {
16867
- lines2.push(
16868
- ` Start at login and keep Cabane updating itself: ${cabane("service install")}${input.servicePlatform === "launchd" ? " (macOS will ask you to allow a background item)" : ""}`
16869
- );
16870
- lines2.push(
16871
- ` Updates itself only as a service; to turn that off: ${cabane("update --auto off")}`
16872
- );
16873
- } else if (input.service && input.autoUpdate)
16874
- lines2.push(` Updating itself; to turn that off: ${cabane("update --auto off")}`);
16875
- }
16876
- if (input.address) lines2.push("", ` In Cabane: ${input.address}`);
16877
- if (mode === "attached") lines2.push("", " Listening for messages\u2026");
16878
- return lines2;
16879
- }
16880
- function startFailure(message2, reload) {
16881
- return [
16882
- ...pathLines(reload),
16883
- `\u2717 Cabane couldn't start: ${message2}`,
16884
- "",
16885
- ` Log: ${cabane("logs")} Try again: ${cabane("start")}`
16886
- ];
16887
- }
16888
-
16889
17128
  // src/terminal-status.ts
16890
17129
  async function readTerminalStatus(socket, timeoutMs = 1e3) {
16891
17130
  if (!socket) return emptyHeartbeatStatus();
@@ -17024,18 +17263,17 @@ async function startScript(opts, deps) {
17024
17263
  if (service2.state !== "running" || service2.pid !== current.pid) mode = "existing";
17025
17264
  }
17026
17265
  }
17027
- const personalUrl = `${cfg.baseUrl.replace(/\/$/, "")}/settings/devices`;
17028
- const address = opts.pairing ? opts.pairing.devicesUrl : personalUrl;
17266
+ const address = `${cfg.baseUrl.replace(/\/$/, "")}/settings/devices`;
17029
17267
  if (!daemon)
17030
17268
  for (const line of closingBlock({
17031
17269
  mode,
17032
- facts: { ...status2, running: true, devicesUrl: address ?? personalUrl },
17270
+ facts: { ...status2, running: true, devicesUrl: address },
17033
17271
  detection,
17034
17272
  reload: reloadLine(),
17035
17273
  service: installed,
17036
17274
  servicePlatform: servicePlatform(),
17037
17275
  autoUpdate: cfg.update?.auto !== false,
17038
- ...address ? { address } : {}
17276
+ address
17039
17277
  }))
17040
17278
  write(line);
17041
17279
  if (runtime) {
@@ -17171,22 +17409,22 @@ async function setup(opts = {}, deps = {}) {
17171
17409
  } catch (err) {
17172
17410
  for (const line of pathLines(reloadLine())) write(line);
17173
17411
  write(err instanceof Error ? err.message : String(err));
17174
- write(
17175
- "Check the server connection, then take a fresh setup line from the workspace's Settings \u2192 Devices."
17176
- );
17412
+ write(`Check the server connection, then run ${cabane("setup")} again.`);
17177
17413
  process.exitCode = 1;
17178
17414
  } finally {
17179
17415
  setConsoleLogging(true);
17180
17416
  }
17181
17417
  }
17182
17418
  async function setupScript(opts, deps) {
17183
- const interactive = deps.interactive ?? isInteractive();
17184
- const paired = isDevicePaired();
17185
- if (!opts.code && !paired) {
17186
- write(UNPAIRED);
17419
+ if (opts.code) {
17420
+ write(
17421
+ `Setup codes come from this device now: run ${cabane("setup")} and enter the code it shows in Cabane.`
17422
+ );
17187
17423
  process.exitCode = 1;
17188
17424
  return;
17189
17425
  }
17426
+ const interactive = deps.interactive ?? isInteractive();
17427
+ const paired = isDevicePaired();
17190
17428
  const ui = deps.ui ?? (interactive ? terminalUi() : plainUi());
17191
17429
  const running = await (deps.live ?? liveCompanionState)();
17192
17430
  const cfg = safeLoadConfig();
@@ -17197,32 +17435,16 @@ async function setupScript(opts, deps) {
17197
17435
  process.exitCode = 1;
17198
17436
  return;
17199
17437
  }
17200
- let device;
17201
- if (opts.code) {
17438
+ let justPaired = false;
17439
+ if (!paired) {
17202
17440
  const baseUrl = resolvePairBaseUrl(opts.server ?? cfg?.baseUrl);
17203
- if (paired && cfg && new URL(cfg.baseUrl).origin !== new URL(baseUrl).origin) {
17204
- for (const line of pathLines(reloadLine())) write(line);
17205
- write(
17206
- "This device is paired to another Cabane server. Log out before setting it up on a different server."
17207
- );
17208
- write(`Run ${cabane("logout")} before using the new setup line.`);
17209
- process.exitCode = 1;
17210
- return;
17211
- }
17212
- device = await (deps.claim ?? claimSetupCode)(baseUrl, {
17213
- userCode: opts.code,
17214
- label: deviceLabelFromHostname(hostname2()) ?? "Computer",
17215
- ...paired && cfg?.deviceToken ? { deviceToken: cfg.deviceToken } : {}
17216
- });
17441
+ const device = await (deps.pair ?? pairDevice)({ baseUrl, interactive, ui });
17442
+ if (!device) return;
17217
17443
  writePairedConfig(device);
17218
- write(pairingLine(device));
17444
+ justPaired = true;
17219
17445
  }
17220
17446
  const detection = await start(
17221
- {
17222
- ...opts,
17223
- ...device ? { pairing: device } : {},
17224
- ...!interactive && device ? { foreground: true } : {}
17225
- },
17447
+ { ...opts, ...!interactive && justPaired ? { foreground: true } : {} },
17226
17448
  { ...deps, interactive }
17227
17449
  );
17228
17450
  if (interactive && detection && !anyUsable(detection)) {
@@ -17443,7 +17665,7 @@ async function stop(deps = {}) {
17443
17665
  const readState = deps.readState ?? readLiveRuntimeState;
17444
17666
  const verify = deps.verify ?? ((s) => verifyRuntime(s));
17445
17667
  const kill = deps.kill ?? ((pid2, signal) => process.kill(pid2, signal));
17446
- const sleep3 = deps.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
17668
+ const sleep4 = deps.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
17447
17669
  const now = deps.now ?? (() => Date.now());
17448
17670
  const state = readState();
17449
17671
  if (!state) {
@@ -17471,7 +17693,7 @@ async function stop(deps = {}) {
17471
17693
  process.stdout.write("companion: stopped.\n");
17472
17694
  return;
17473
17695
  }
17474
- await sleep3(POLL_INTERVAL_MS);
17696
+ await sleep4(POLL_INTERVAL_MS);
17475
17697
  }
17476
17698
  process.stdout.write(
17477
17699
  `companion: didn't exit within ${TERM_GRACE_MS / 1e3}s, sending SIGKILL.
@@ -17497,7 +17719,7 @@ function isAlive(kill, pid) {
17497
17719
  var STARTUP_WAIT_MS = 2e4;
17498
17720
  async function installAndStartService(deps = {}) {
17499
17721
  const readState = deps.readState ?? readLiveRuntimeState;
17500
- const sleep3 = deps.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
17722
+ const sleep4 = deps.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
17501
17723
  const now = deps.now ?? Date.now;
17502
17724
  const before = await serviceState(deps);
17503
17725
  const running = readState();
@@ -17513,7 +17735,7 @@ async function installAndStartService(deps = {}) {
17513
17735
  while (now() < deadline) {
17514
17736
  const state = readState();
17515
17737
  if (state?.socket) return true;
17516
- await sleep3(250);
17738
+ await sleep4(250);
17517
17739
  }
17518
17740
  write(
17519
17741
  `${INDENT}The service is installed but Cabane hasn't come up yet. ${cabane("service status")} and ${cabane("logs")} say why.`
@@ -17643,9 +17865,7 @@ async function status(deps = {}) {
17643
17865
  cfg = null;
17644
17866
  }
17645
17867
  if (!cfg || !cfg.deviceToken) {
17646
- out(
17647
- " Not paired yet. Get a setup line from a workspace's Settings \u2192 Devices and paste it here."
17648
- );
17868
+ out(` Not paired yet. Run ${cabane("setup")} on this device.`);
17649
17869
  for (const line of strays) out(line);
17650
17870
  process.exitCode = 1;
17651
17871
  return;
@@ -18308,7 +18528,7 @@ init_cabane_command();
18308
18528
  rememberInvokedCommand();
18309
18529
  var program = new Command();
18310
18530
  program.name("cabane").description("Run your Cabane agents on this device, with Claude Code, Codex or OpenCode.").version(COMPANION_VERSION);
18311
- program.command("setup [code]").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(
18531
+ 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(
18312
18532
  new Option("--log-level <level>", "info (default), warn, or debug").choices([
18313
18533
  "info",
18314
18534
  "warn",