@cabane/companion 0.6.232 → 0.6.234

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/dist/cli.js +386 -327
  2. package/dist/runtime.js +151 -128
  3. package/package.json +3 -3
package/dist/cli.js CHANGED
@@ -1010,6 +1010,317 @@ var init_bundled_binaries = __esm({
1010
1010
  }
1011
1011
  });
1012
1012
 
1013
+ // src/device-state.ts
1014
+ var DEVICE_STATES, DEFAULT_DEVICE_OFFLINE_THRESHOLD_MS, HARNESS_STATES, DEVICE_HEALTH_STATES, DEVICE_STATE_LABEL, DEVICE_HEALTH_LABEL, HARNESS_STATE_LABEL;
1015
+ var init_device_state = __esm({
1016
+ "src/device-state.ts"() {
1017
+ "use strict";
1018
+ DEVICE_STATES = ["waiting", "online", "offline", "deactivated"];
1019
+ DEFAULT_DEVICE_OFFLINE_THRESHOLD_MS = 9e4;
1020
+ HARNESS_STATES = [
1021
+ "checking",
1022
+ "connected",
1023
+ "needs_sign_in",
1024
+ "limited",
1025
+ "problem",
1026
+ "off",
1027
+ "gone"
1028
+ ];
1029
+ DEVICE_HEALTH_STATES = ["checking", "ready", "degraded"];
1030
+ DEVICE_STATE_LABEL = {
1031
+ waiting: "Waiting for its first check-in",
1032
+ online: "Online",
1033
+ offline: "Offline",
1034
+ deactivated: "Deactivated"
1035
+ };
1036
+ DEVICE_HEALTH_LABEL = {
1037
+ checking: "Checking harnesses\u2026",
1038
+ ready: "Ready",
1039
+ degraded: "Needs attention"
1040
+ };
1041
+ HARNESS_STATE_LABEL = {
1042
+ checking: "checking",
1043
+ connected: "connected",
1044
+ needs_sign_in: "needs sign-in",
1045
+ limited: "limited",
1046
+ problem: "problem",
1047
+ off: "disabled",
1048
+ gone: "gone"
1049
+ };
1050
+ }
1051
+ });
1052
+
1053
+ // src/harness-state.ts
1054
+ function harnessEntry(runtime, state) {
1055
+ switch (state.state) {
1056
+ case "usable":
1057
+ return {
1058
+ runtime,
1059
+ state: "connected",
1060
+ reason: state.via,
1061
+ version: state.version,
1062
+ ...state.url ? { url: state.url } : {}
1063
+ };
1064
+ case "signed_out":
1065
+ return { runtime, state: "needs_sign_in", reason: "signed_out", version: state.version };
1066
+ case "not_running":
1067
+ return { runtime, state: "problem", reason: "not_running", url: state.url };
1068
+ case "off":
1069
+ return { runtime, state: "off" };
1070
+ case "problem":
1071
+ return { runtime, state: "problem", reason: checkReason(state.check) };
1072
+ }
1073
+ }
1074
+ function harnessEntriesFromDetection(detection) {
1075
+ return HARNESS_RUNTIMES.map((runtime) => harnessEntry(runtime, detection[runtime]));
1076
+ }
1077
+ function checkReason(check) {
1078
+ switch (check.status) {
1079
+ case "bundle_missing":
1080
+ return check.bundled;
1081
+ case "check_failed":
1082
+ return `check_failed:${check.reason}`;
1083
+ default:
1084
+ return check.status;
1085
+ }
1086
+ }
1087
+ var init_harness_state = __esm({
1088
+ "src/harness-state.ts"() {
1089
+ "use strict";
1090
+ init_config();
1091
+ init_device_state();
1092
+ }
1093
+ });
1094
+
1095
+ // src/harness-copy.ts
1096
+ import { homedir as homedir6 } from "os";
1097
+ import { join as join7, win32 } from "path";
1098
+ function harnessLabel(runtime) {
1099
+ const known = HARNESS_RUNTIMES.find((candidate) => candidate === runtime);
1100
+ return known ? HARNESS_LABELS[known] : "The harness";
1101
+ }
1102
+ function ctx(c = {}) {
1103
+ const path = "path" in c ? c.path : process.env.PATH;
1104
+ const home = c.home ?? homedir6();
1105
+ return {
1106
+ path,
1107
+ home,
1108
+ install: c.install ?? companionInstall(),
1109
+ profile: "profile" in c ? c.profile ?? null : profileFile({ home }),
1110
+ reload: "reload" in c ? c.reload ?? null : reloadLine(),
1111
+ hasNpm: c.hasNpm ?? firstOnPath("npm", path) !== null,
1112
+ windows: (c.platform ?? process.platform) === "win32"
1113
+ };
1114
+ }
1115
+ function loginCommand(runtime) {
1116
+ return runtime === "codex" ? "codex login" : "claude auth login";
1117
+ }
1118
+ function failureText(runtime, reason) {
1119
+ const label = HARNESS_LABELS[runtime];
1120
+ switch (reason) {
1121
+ case "timeout":
1122
+ return `Couldn't check whether ${label} is signed in: the check didn't answer in time. Try again.`;
1123
+ case "spawn":
1124
+ return `Couldn't check whether ${label} is signed in: Cabane's bundled ${label} wouldn't start. Repair it with ${repairSteps()} and try again.${spaced(repairCost())}`;
1125
+ case "config":
1126
+ return `Couldn't check whether ${label} is signed in: its configuration file didn't load${runtime === "codex" ? ` (${tildePath(join7(homedir6(), ".codex", "config.toml"))})` : ""}. Fix it, then try again.`;
1127
+ case "unreadable":
1128
+ return `Couldn't check whether ${label} is signed in: it answered in a way Cabane couldn't read. Try again, and if it keeps happening, update Cabane.`;
1129
+ }
1130
+ }
1131
+ function bundleMissingText(runtime) {
1132
+ const label = HARNESS_LABELS[runtime];
1133
+ return `This Cabane install is missing its bundled ${label}. Repair it with ${repairSteps()} and try again.${spaced(repairCost())}`;
1134
+ }
1135
+ function unusableReason(runtime, check) {
1136
+ if (check.status === "bundle_missing") return bundleMissingText(runtime);
1137
+ if (check.status === "check_failed") return failureText(runtime, check.reason);
1138
+ return null;
1139
+ }
1140
+ function agentRows(detection, opts = {}) {
1141
+ return HARNESS_RUNTIMES.map((runtime) => {
1142
+ const state = detection[runtime];
1143
+ return {
1144
+ runtime,
1145
+ label: HARNESS_LABELS[runtime],
1146
+ text: agentRowText(runtime, state, opts),
1147
+ usable: state.state === "usable"
1148
+ };
1149
+ });
1150
+ }
1151
+ function agentRowText(runtime, state, opts = {}) {
1152
+ const word = HARNESS_STATE_LABEL[harnessEntry(runtime, state).state];
1153
+ switch (state.state) {
1154
+ case "usable": {
1155
+ const version = state.version ? ` (${state.version})` : "";
1156
+ if (state.via === "api_key") return `${word} \u2014 API key configured${version}`;
1157
+ if (state.via === "serve") return `${word} \u2014 running at ${state.url ?? ""}`;
1158
+ return `${word} \u2014 signed in${version}`;
1159
+ }
1160
+ case "signed_out":
1161
+ return opts.signInHint && runtime !== "opencode" ? `${word} \u2014 run ${loginCommand(runtime)}` : word;
1162
+ case "not_running":
1163
+ return `${word} \u2014 not running`;
1164
+ case "off":
1165
+ return `${word} \u2014 disabled in config`;
1166
+ case "problem":
1167
+ return runtime === "opencode" ? `${word} \u2014 not running` : `${word} \u2014 ${unusableReason(runtime, state.check) ?? "could not be checked"}`;
1168
+ }
1169
+ }
1170
+ function agentNote(runtime, c = {}) {
1171
+ const k = ctx(c);
1172
+ if (k.windows) return windowsAgentNote(runtime, k);
1173
+ if (runtime === "claude-code") {
1174
+ const commands = ["curl -fsSL https://claude.ai/install.sh | bash"];
1175
+ const localBin = join7(k.home, ".local", "bin");
1176
+ const needsPath = !dirOnPath(localBin, k.path);
1177
+ const profile = k.profile ? tildePath(k.profile, k.home) : null;
1178
+ let sourced = null;
1179
+ if (needsPath) {
1180
+ if (profile) {
1181
+ commands.push(`echo 'export PATH="$HOME/.local/bin:$PATH"' >> ${profile}`);
1182
+ sourced = `source ${profile}`;
1183
+ commands.push(sourced);
1184
+ } else {
1185
+ commands.push('export PATH="$HOME/.local/bin:$PATH"');
1186
+ }
1187
+ }
1188
+ if (k.reload && k.reload !== sourced) commands.push(k.reload);
1189
+ commands.push("claude auth login");
1190
+ return {
1191
+ title: "Set up Claude Code",
1192
+ lines: [
1193
+ "Run these lines here. The last one opens your browser",
1194
+ "to sign in with your Claude account.",
1195
+ "",
1196
+ ...commands.map(indent),
1197
+ "",
1198
+ SETUP_AGAIN
1199
+ ]
1200
+ };
1201
+ }
1202
+ if (runtime === "codex") {
1203
+ const own = k.hasNpm || k.install !== "script";
1204
+ const on = own ? "" : 'PATH="$HOME/.cabane/node/bin:$PATH" ';
1205
+ const commands = [
1206
+ `${on}npm i -g @openai/codex`,
1207
+ ...k.reload ? [k.reload] : [],
1208
+ `${on}codex login`
1209
+ ];
1210
+ return {
1211
+ title: "Set up Codex",
1212
+ lines: [
1213
+ "Run these lines here. The last one opens your browser",
1214
+ "to sign in with your ChatGPT account.",
1215
+ "",
1216
+ ...commands.map(indent),
1217
+ "",
1218
+ SETUP_AGAIN
1219
+ ]
1220
+ };
1221
+ }
1222
+ const here = [...k.reload ? [k.reload] : [], "curl -fsSL https://opencode.ai/install | bash"];
1223
+ return {
1224
+ title: "Set up OpenCode",
1225
+ lines: [
1226
+ `Run the ${here.length > 1 ? "first two lines" : "first line"} here. Then open a second terminal`,
1227
+ "window, run the last line there, and leave that window open.",
1228
+ "",
1229
+ ...here.map(indent),
1230
+ indent("opencode serve --port 4096"),
1231
+ "",
1232
+ `Then ${cabane("status")} shows it signed in; nothing else to run.`
1233
+ ]
1234
+ };
1235
+ }
1236
+ function windowsAgentNote(runtime, k) {
1237
+ if (runtime === "claude-code") {
1238
+ const commands = ["irm https://claude.ai/install.ps1 | iex"];
1239
+ if (!windowsDirOnPath(win32.join(k.home, ".local", "bin"), k.path)) {
1240
+ commands.push(
1241
+ `$bin = "$env:USERPROFILE\\.local\\bin"`,
1242
+ `$p = [Environment]::GetEnvironmentVariable('PATH', 'User')`,
1243
+ `[Environment]::SetEnvironmentVariable('PATH', "$p;$bin", 'User')`,
1244
+ `$env:PATH += ";$bin"`
1245
+ );
1246
+ }
1247
+ commands.push("claude auth login");
1248
+ return {
1249
+ title: "Set up Claude Code",
1250
+ lines: [
1251
+ "Run these lines in PowerShell. The last one opens your browser",
1252
+ "to sign in with your Claude account.",
1253
+ "",
1254
+ ...commands.map(indent),
1255
+ "",
1256
+ SETUP_AGAIN
1257
+ ]
1258
+ };
1259
+ }
1260
+ if (runtime === "codex") {
1261
+ return {
1262
+ title: "Set up Codex",
1263
+ lines: [
1264
+ "Run these lines here. The last one opens your browser",
1265
+ "to sign in with your ChatGPT account.",
1266
+ "",
1267
+ indent("npm i -g @openai/codex"),
1268
+ indent("codex login"),
1269
+ "",
1270
+ SETUP_AGAIN
1271
+ ]
1272
+ };
1273
+ }
1274
+ return {
1275
+ title: "Set up OpenCode",
1276
+ lines: [
1277
+ "Run the first line here. Then open a second terminal",
1278
+ "window, run the last line there, and leave that window open.",
1279
+ "",
1280
+ indent("npm i -g opencode-ai"),
1281
+ indent("opencode serve --port 4096"),
1282
+ "",
1283
+ `Then ${cabane("status")} shows it signed in; nothing else to run.`
1284
+ ]
1285
+ };
1286
+ }
1287
+ function windowsDirOnPath(dir2, path) {
1288
+ const norm = (entry) => entry.replace(/[\\/]+$/, "").toLowerCase();
1289
+ return (path ?? "").split(";").some((entry) => norm(entry) === norm(dir2));
1290
+ }
1291
+ function indent(line) {
1292
+ return ` ${line}`;
1293
+ }
1294
+ function spaced(sentence) {
1295
+ return sentence ? ` ${sentence}` : "";
1296
+ }
1297
+ var HARNESS_LABELS, AGENT_CHOICES, CHOOSE_AGENT, SETUP_AGAIN;
1298
+ var init_harness_copy = __esm({
1299
+ "src/harness-copy.ts"() {
1300
+ "use strict";
1301
+ init_bundled_binaries();
1302
+ init_cabane_command();
1303
+ init_config();
1304
+ init_device_state();
1305
+ init_harness_state();
1306
+ init_install_kind();
1307
+ init_install_record();
1308
+ init_term();
1309
+ HARNESS_LABELS = {
1310
+ "claude-code": "Claude Code",
1311
+ codex: "Codex",
1312
+ opencode: "OpenCode"
1313
+ };
1314
+ AGENT_CHOICES = [
1315
+ { value: "claude-code", label: "Claude Code", hint: "a Claude Pro or Max plan" },
1316
+ { value: "codex", label: "Codex", hint: "a ChatGPT plan" },
1317
+ { value: "opencode", label: "OpenCode" }
1318
+ ];
1319
+ CHOOSE_AGENT = "Cabane needs one of them. Which will you set up?";
1320
+ SETUP_AGAIN = `Then ${cabane("status")} shows it signed in; nothing else to run.`;
1321
+ }
1322
+ });
1323
+
1013
1324
  // src/harness-versions.ts
1014
1325
  function parseVersionToken(raw) {
1015
1326
  if (!raw) return null;
@@ -7204,9 +7515,12 @@ var init_turn_runtime_integrity = __esm({
7204
7515
  // src/turn-execution.ts
7205
7516
  import { createHash as createHash2, randomUUID as randomUUID4 } from "crypto";
7206
7517
  import { existsSync as existsSync11, mkdirSync as mkdirSync9 } from "fs";
7207
- function harnessUnavailableNotice(deviceId, deviceLabel) {
7518
+ function harnessUnavailableNotice(runtime, agentUsername, deviceId, deviceLabel) {
7208
7519
  const name = deviceId && deviceLabel !== void 0 ? ` ([${deviceLabel.replace(/([\\[\]])/g, "\\$1")}](/settings/devices/${encodeURIComponent(deviceId)}))` : "";
7209
- return "**This harness isn't available on the companion.** Check the harness on the device" + name + ", or choose another harness in the agent's settings. Then reply to try again.";
7520
+ const agent = `\\@${agentUsername}`;
7521
+ return `${harnessLabel(runtime)} isn't connected on ${agent}'s device${name}, so they can't reply. Run \`cabane setup\` in a terminal, or choose a different harness in ${agent}'s settings.
7522
+
7523
+ Reply to try again.`;
7210
7524
  }
7211
7525
  function resolveIdleTimeoutMs(override) {
7212
7526
  return override ?? DEFAULT_AGENT_IDLE_TIMEOUT_MS;
@@ -7273,6 +7587,7 @@ var init_turn_execution = __esm({
7273
7587
  init_turn_containment();
7274
7588
  init_turn_committer();
7275
7589
  init_turn_runtime_integrity();
7590
+ init_harness_copy();
7276
7591
  PREPARING_TOOL_NAME = "preparing";
7277
7592
  NO_TURN_TOKEN_REASON = "no_turn_token: the server issued no turn credential for this turn";
7278
7593
  UNEXPECTED_ROLE_REASON = `refused the wake trigger (role "system" isn't dispatchable on this companion) \u2014 the companion is likely running outdated code; refresh it, then re-address the agent`;
@@ -7900,7 +8215,10 @@ var init_turn_execution = __esm({
7900
8215
  async selectAdapter() {
7901
8216
  const { payload, workspaceId, turnId, turnLog } = this;
7902
8217
  await guardBundledBinary(this.integrityContext(), (r, e) => this.concluded(r, e), this.outcome);
7903
- const onWarn = (msg, meta) => turnLog.debug(meta ?? {}, msg);
8218
+ const onWarn = (msg, meta) => turnLog.warn(
8219
+ { ...meta, ...typeof meta?.thrown === "string" ? { err: meta.thrown } : {} },
8220
+ msg
8221
+ );
7904
8222
  const adapters = [];
7905
8223
  if (this.opts.claudeCodeAvailable?.() ?? true) {
7906
8224
  this.containment = createTurnContainment(this.turnId, {
@@ -7969,7 +8287,12 @@ var init_turn_execution = __esm({
7969
8287
  );
7970
8288
  try {
7971
8289
  await this.opts.api.postTurnMessage(workspaceId, payload.conversationId, {
7972
- body: harnessUnavailableNotice(this.opts.deviceId, this.opts.deviceLabel),
8290
+ body: harnessUnavailableNotice(
8291
+ err.runtime,
8292
+ this.opts.agentUsername,
8293
+ this.opts.deviceId,
8294
+ this.opts.deviceLabel
8295
+ ),
7973
8296
  kind: "runtime_notice",
7974
8297
  turnId,
7975
8298
  parentMessageId: payload.messageId,
@@ -9168,295 +9491,7 @@ init_errors();
9168
9491
 
9169
9492
  // src/harness-status.ts
9170
9493
  init_config();
9171
-
9172
- // src/harness-copy.ts
9173
- init_bundled_binaries();
9174
- init_cabane_command();
9175
- init_config();
9176
- import { homedir as homedir6 } from "os";
9177
- import { join as join7, win32 } from "path";
9178
-
9179
- // src/device-state.ts
9180
- var DEVICE_STATES = ["waiting", "online", "offline", "deactivated"];
9181
- var DEFAULT_DEVICE_OFFLINE_THRESHOLD_MS = 9e4;
9182
- var HARNESS_STATES = [
9183
- "checking",
9184
- "connected",
9185
- "needs_sign_in",
9186
- "limited",
9187
- "problem",
9188
- "off",
9189
- "gone"
9190
- ];
9191
- var DEVICE_HEALTH_STATES = ["checking", "ready", "degraded"];
9192
- var DEVICE_STATE_LABEL = {
9193
- waiting: "Waiting for its first check-in",
9194
- online: "Online",
9195
- offline: "Offline",
9196
- deactivated: "Deactivated"
9197
- };
9198
- var DEVICE_HEALTH_LABEL = {
9199
- checking: "Checking harnesses\u2026",
9200
- ready: "Ready",
9201
- degraded: "Needs attention"
9202
- };
9203
- var HARNESS_STATE_LABEL = {
9204
- checking: "checking",
9205
- connected: "connected",
9206
- needs_sign_in: "needs sign-in",
9207
- limited: "limited",
9208
- problem: "problem",
9209
- off: "disabled",
9210
- gone: "gone"
9211
- };
9212
-
9213
- // src/harness-state.ts
9214
- init_config();
9215
- function harnessEntry(runtime, state) {
9216
- switch (state.state) {
9217
- case "usable":
9218
- return {
9219
- runtime,
9220
- state: "connected",
9221
- reason: state.via,
9222
- version: state.version,
9223
- ...state.url ? { url: state.url } : {}
9224
- };
9225
- case "signed_out":
9226
- return { runtime, state: "needs_sign_in", reason: "signed_out", version: state.version };
9227
- case "not_running":
9228
- return { runtime, state: "problem", reason: "not_running", url: state.url };
9229
- case "off":
9230
- return { runtime, state: "off" };
9231
- case "problem":
9232
- return { runtime, state: "problem", reason: checkReason(state.check) };
9233
- }
9234
- }
9235
- function harnessEntriesFromDetection(detection) {
9236
- return HARNESS_RUNTIMES.map((runtime) => harnessEntry(runtime, detection[runtime]));
9237
- }
9238
- function checkReason(check) {
9239
- switch (check.status) {
9240
- case "bundle_missing":
9241
- return check.bundled;
9242
- case "check_failed":
9243
- return `check_failed:${check.reason}`;
9244
- default:
9245
- return check.status;
9246
- }
9247
- }
9248
-
9249
- // src/harness-copy.ts
9250
- init_install_kind();
9251
- init_install_record();
9252
- init_term();
9253
- var HARNESS_LABELS = {
9254
- "claude-code": "Claude Code",
9255
- codex: "Codex",
9256
- opencode: "OpenCode"
9257
- };
9258
- function ctx(c = {}) {
9259
- const path = "path" in c ? c.path : process.env.PATH;
9260
- const home = c.home ?? homedir6();
9261
- return {
9262
- path,
9263
- home,
9264
- install: c.install ?? companionInstall(),
9265
- profile: "profile" in c ? c.profile ?? null : profileFile({ home }),
9266
- reload: "reload" in c ? c.reload ?? null : reloadLine(),
9267
- hasNpm: c.hasNpm ?? firstOnPath("npm", path) !== null,
9268
- windows: (c.platform ?? process.platform) === "win32"
9269
- };
9270
- }
9271
- function loginCommand(runtime) {
9272
- return runtime === "codex" ? "codex login" : "claude auth login";
9273
- }
9274
- function failureText(runtime, reason) {
9275
- const label = HARNESS_LABELS[runtime];
9276
- switch (reason) {
9277
- case "timeout":
9278
- return `Couldn't check whether ${label} is signed in: the check didn't answer in time. Try again.`;
9279
- case "spawn":
9280
- return `Couldn't check whether ${label} is signed in: Cabane's bundled ${label} wouldn't start. Repair it with ${repairSteps()} and try again.${spaced(repairCost())}`;
9281
- case "config":
9282
- return `Couldn't check whether ${label} is signed in: its configuration file didn't load${runtime === "codex" ? ` (${tildePath(join7(homedir6(), ".codex", "config.toml"))})` : ""}. Fix it, then try again.`;
9283
- case "unreadable":
9284
- return `Couldn't check whether ${label} is signed in: it answered in a way Cabane couldn't read. Try again, and if it keeps happening, update Cabane.`;
9285
- }
9286
- }
9287
- function bundleMissingText(runtime) {
9288
- const label = HARNESS_LABELS[runtime];
9289
- return `This Cabane install is missing its bundled ${label}. Repair it with ${repairSteps()} and try again.${spaced(repairCost())}`;
9290
- }
9291
- function unusableReason(runtime, check) {
9292
- if (check.status === "bundle_missing") return bundleMissingText(runtime);
9293
- if (check.status === "check_failed") return failureText(runtime, check.reason);
9294
- return null;
9295
- }
9296
- function agentRows(detection, opts = {}) {
9297
- return HARNESS_RUNTIMES.map((runtime) => {
9298
- const state = detection[runtime];
9299
- return {
9300
- runtime,
9301
- label: HARNESS_LABELS[runtime],
9302
- text: agentRowText(runtime, state, opts),
9303
- usable: state.state === "usable"
9304
- };
9305
- });
9306
- }
9307
- function agentRowText(runtime, state, opts = {}) {
9308
- const word = HARNESS_STATE_LABEL[harnessEntry(runtime, state).state];
9309
- switch (state.state) {
9310
- case "usable": {
9311
- const version = state.version ? ` (${state.version})` : "";
9312
- if (state.via === "api_key") return `${word} \u2014 API key configured${version}`;
9313
- if (state.via === "serve") return `${word} \u2014 running at ${state.url ?? ""}`;
9314
- return `${word} \u2014 signed in${version}`;
9315
- }
9316
- case "signed_out":
9317
- return opts.signInHint && runtime !== "opencode" ? `${word} \u2014 run ${loginCommand(runtime)}` : word;
9318
- case "not_running":
9319
- return `${word} \u2014 not running`;
9320
- case "off":
9321
- return `${word} \u2014 disabled in config`;
9322
- case "problem":
9323
- return runtime === "opencode" ? `${word} \u2014 not running` : `${word} \u2014 ${unusableReason(runtime, state.check) ?? "could not be checked"}`;
9324
- }
9325
- }
9326
- var AGENT_CHOICES = [
9327
- { value: "claude-code", label: "Claude Code", hint: "a Claude Pro or Max plan" },
9328
- { value: "codex", label: "Codex", hint: "a ChatGPT plan" },
9329
- { value: "opencode", label: "OpenCode" }
9330
- ];
9331
- var CHOOSE_AGENT = "Cabane needs one of them. Which will you set up?";
9332
- var SETUP_AGAIN = `Then ${cabane("status")} shows it signed in; nothing else to run.`;
9333
- function agentNote(runtime, c = {}) {
9334
- const k = ctx(c);
9335
- if (k.windows) return windowsAgentNote(runtime, k);
9336
- if (runtime === "claude-code") {
9337
- const commands = ["curl -fsSL https://claude.ai/install.sh | bash"];
9338
- const localBin = join7(k.home, ".local", "bin");
9339
- const needsPath = !dirOnPath(localBin, k.path);
9340
- const profile = k.profile ? tildePath(k.profile, k.home) : null;
9341
- let sourced = null;
9342
- if (needsPath) {
9343
- if (profile) {
9344
- commands.push(`echo 'export PATH="$HOME/.local/bin:$PATH"' >> ${profile}`);
9345
- sourced = `source ${profile}`;
9346
- commands.push(sourced);
9347
- } else {
9348
- commands.push('export PATH="$HOME/.local/bin:$PATH"');
9349
- }
9350
- }
9351
- if (k.reload && k.reload !== sourced) commands.push(k.reload);
9352
- commands.push("claude auth login");
9353
- return {
9354
- title: "Set up Claude Code",
9355
- lines: [
9356
- "Run these lines here. The last one opens your browser",
9357
- "to sign in with your Claude account.",
9358
- "",
9359
- ...commands.map(indent),
9360
- "",
9361
- SETUP_AGAIN
9362
- ]
9363
- };
9364
- }
9365
- if (runtime === "codex") {
9366
- const own = k.hasNpm || k.install !== "script";
9367
- const on = own ? "" : 'PATH="$HOME/.cabane/node/bin:$PATH" ';
9368
- const commands = [
9369
- `${on}npm i -g @openai/codex`,
9370
- ...k.reload ? [k.reload] : [],
9371
- `${on}codex login`
9372
- ];
9373
- return {
9374
- title: "Set up Codex",
9375
- lines: [
9376
- "Run these lines here. The last one opens your browser",
9377
- "to sign in with your ChatGPT account.",
9378
- "",
9379
- ...commands.map(indent),
9380
- "",
9381
- SETUP_AGAIN
9382
- ]
9383
- };
9384
- }
9385
- const here = [...k.reload ? [k.reload] : [], "curl -fsSL https://opencode.ai/install | bash"];
9386
- return {
9387
- title: "Set up OpenCode",
9388
- lines: [
9389
- `Run the ${here.length > 1 ? "first two lines" : "first line"} here. Then open a second terminal`,
9390
- "window, run the last line there, and leave that window open.",
9391
- "",
9392
- ...here.map(indent),
9393
- indent("opencode serve --port 4096"),
9394
- "",
9395
- `Then ${cabane("status")} shows it signed in; nothing else to run.`
9396
- ]
9397
- };
9398
- }
9399
- function windowsAgentNote(runtime, k) {
9400
- if (runtime === "claude-code") {
9401
- const commands = ["irm https://claude.ai/install.ps1 | iex"];
9402
- if (!windowsDirOnPath(win32.join(k.home, ".local", "bin"), k.path)) {
9403
- commands.push(
9404
- `$bin = "$env:USERPROFILE\\.local\\bin"`,
9405
- `$p = [Environment]::GetEnvironmentVariable('PATH', 'User')`,
9406
- `[Environment]::SetEnvironmentVariable('PATH', "$p;$bin", 'User')`,
9407
- `$env:PATH += ";$bin"`
9408
- );
9409
- }
9410
- commands.push("claude auth login");
9411
- return {
9412
- title: "Set up Claude Code",
9413
- lines: [
9414
- "Run these lines in PowerShell. The last one opens your browser",
9415
- "to sign in with your Claude account.",
9416
- "",
9417
- ...commands.map(indent),
9418
- "",
9419
- SETUP_AGAIN
9420
- ]
9421
- };
9422
- }
9423
- if (runtime === "codex") {
9424
- return {
9425
- title: "Set up Codex",
9426
- lines: [
9427
- "Run these lines here. The last one opens your browser",
9428
- "to sign in with your ChatGPT account.",
9429
- "",
9430
- indent("npm i -g @openai/codex"),
9431
- indent("codex login"),
9432
- "",
9433
- SETUP_AGAIN
9434
- ]
9435
- };
9436
- }
9437
- return {
9438
- title: "Set up OpenCode",
9439
- lines: [
9440
- "Run the first line here. Then open a second terminal",
9441
- "window, run the last line there, and leave that window open.",
9442
- "",
9443
- indent("npm i -g opencode-ai"),
9444
- indent("opencode serve --port 4096"),
9445
- "",
9446
- `Then ${cabane("status")} shows it signed in; nothing else to run.`
9447
- ]
9448
- };
9449
- }
9450
- function windowsDirOnPath(dir2, path) {
9451
- const norm = (entry) => entry.replace(/[\\/]+$/, "").toLowerCase();
9452
- return (path ?? "").split(";").some((entry) => norm(entry) === norm(dir2));
9453
- }
9454
- function indent(line) {
9455
- return ` ${line}`;
9456
- }
9457
- function spaced(sentence) {
9458
- return sentence ? ` ${sentence}` : "";
9459
- }
9494
+ init_harness_copy();
9460
9495
 
9461
9496
  // src/harness-detect.ts
9462
9497
  init_config();
@@ -9734,6 +9769,7 @@ async function liveCompanionState() {
9734
9769
  // src/commands/setup.ts
9735
9770
  init_cabane_command();
9736
9771
  init_config();
9772
+ init_harness_copy();
9737
9773
 
9738
9774
  // src/logger.ts
9739
9775
  init_config();
@@ -10028,6 +10064,8 @@ async function awaitEnrollment(baseUrl, code, opts = {}) {
10028
10064
 
10029
10065
  // src/terminal-copy.ts
10030
10066
  init_cabane_command();
10067
+ init_device_state();
10068
+ init_harness_copy();
10031
10069
  var UNPAIRED = `This device isn't paired yet. Run ${cabane("setup")} on it.`;
10032
10070
  var lease = DEFAULT_DEVICE_OFFLINE_THRESHOLD_MS;
10033
10071
  function age(at, now) {
@@ -10547,6 +10585,7 @@ var CompanionStateHub = class {
10547
10585
  init_errors();
10548
10586
 
10549
10587
  // src/heartbeat-status.ts
10588
+ init_device_state();
10550
10589
  import { z as z3 } from "zod";
10551
10590
  var acknowledgedDeviceSchema = z3.object({
10552
10591
  state: z3.enum(DEVICE_STATES),
@@ -10571,72 +10610,90 @@ import { hostname as hostname2 } from "os";
10571
10610
  // src/log-copy.ts
10572
10611
  init_failure();
10573
10612
  init_bundled_binaries();
10574
- var RESTART = "then `cabane stop` and `cabane start` to restart the companion";
10613
+ init_harness_copy();
10614
+ var RESTART = "`cabane stop` and `cabane start`";
10575
10615
  function turnFailureCopy(reason, runtime) {
10576
10616
  const failure = decodeFailureReason(reason);
10617
+ const harness = harnessLabel(runtime);
10577
10618
  if (failure?.kind === "auth_expired") {
10578
10619
  if (runtime === "claude-code")
10579
- return `Claude Code is signed out on this device. To sign back in, run \`claude auth login\`, ${RESTART}. Then reply in Cabane to try again.`;
10620
+ return `Claude Code is signed out on this device. To sign back in, run \`claude auth login\`, ${RESTART}. Reply in Cabane to try again.`;
10580
10621
  if (runtime === "codex")
10581
- return `Codex is signed out on this device. To sign back in, run \`codex login\`, ${RESTART}. Then reply in Cabane to try again.`;
10582
- return `The agent is signed out on this device. Sign in to their harness, ${RESTART}. Then reply in Cabane to try again.`;
10622
+ return `Codex is signed out on this device. To sign back in, run \`codex login\`, ${RESTART}. Reply in Cabane to try again.`;
10623
+ return `${harness} is signed out on this device. Sign in to ${runtime === "opencode" ? "OpenCode" : "it"}, then run ${RESTART}. Reply in Cabane to try again.`;
10583
10624
  }
10584
10625
  if (failure?.kind === "usage_capped")
10585
- return failure.resetsAt ? `The usage limit is reached; it resets around ${new Date(failure.resetsAt).toLocaleTimeString()}. Reply in Cabane after it resets.` : "The usage limit is reached. Try again later in Cabane.";
10626
+ return failure.resetsAt ? `The usage limit is reached and resets around ${new Date(failure.resetsAt).toLocaleTimeString([], { timeStyle: "short" })}. Reply in Cabane after reset to try again.` : "The usage limit is reached. Reply in Cabane later to try again.";
10586
10627
  if (failure?.kind === "rate_limited")
10587
10628
  return "The provider is limiting requests. Reply in Cabane to try again.";
10588
10629
  if (failure?.kind === "server_error")
10589
10630
  return "The provider had a temporary error. Reply in Cabane to try again.";
10590
- if (reason?.startsWith("missing_secret:"))
10591
- return `A required secret is missing: ${humanText(reason.slice(15))}. Add it to ~/.cabane/secrets.json, then reply in Cabane to try again.`;
10592
- if (reason?.startsWith("runtime_incomplete:"))
10593
- return `The agent installation is incomplete. ${repairAction()} Then restart the companion.`;
10594
- if (/^(runtime|codex|opencode)_unavailable:/.test(reason ?? ""))
10595
- return "This harness isn't available on the companion. Check the harness on this device, or choose another harness in the agent's settings in Cabane. Then reply in Cabane to try again.";
10631
+ if (reason?.startsWith("missing_secret:")) {
10632
+ const tokens = humanText(reason.slice(15)).split(",").map((name) => name.trim()).filter(Boolean);
10633
+ const more = tokens.reduce(
10634
+ (n, token) => /^\+\d+$/.test(token) ? n + Number(token.slice(1)) : n,
10635
+ 0
10636
+ );
10637
+ const names = tokens.filter((token) => !/^\+\d+$/.test(token)).map((name) => `\`${name}\``);
10638
+ const count = names.length + more;
10639
+ const needs = names.length === 0 ? count > 1 ? `${count} secrets` : "a secret" : `${count > 1 ? "secrets" : "a secret"} called ${more > 0 ? `${names.join(", ")} and ${more} more` : names.length > 1 ? `${names.slice(0, -1).join(", ")} and ${names.at(-1)}` : names[0]}`;
10640
+ return `The agent's tools need ${needs}, which this device doesn't have. Add ${count > 1 ? "them" : "it"} to the secrets file, \`~/.cabane/secrets.json\`. Reply in Cabane to try again.`;
10641
+ }
10642
+ if (reason?.startsWith("runtime_incomplete:")) {
10643
+ const commands = repairCommands(void 0, (rest) => `cabane ${rest}`);
10644
+ const stopsFirst = commands.length > 1;
10645
+ if (!stopsFirst) commands.push("cabane stop");
10646
+ commands.push("cabane start");
10647
+ const list = commands.map((command) => `\`${command}\``);
10648
+ 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.`;
10649
+ }
10650
+ const unavailable = /^(codex|opencode)_unavailable:/.exec(reason ?? "")?.[1] ?? (reason?.startsWith("runtime_unavailable:") ? reason.slice("runtime_unavailable:".length).trim() : void 0);
10651
+ if (unavailable !== void 0)
10652
+ return `${harnessLabel(runtime ?? unavailable)} isn't connected on this device. Run \`cabane setup\`, or choose a different harness in the agent's settings in Cabane. Reply in Cabane to try again.`;
10596
10653
  if (reason?.startsWith("model_unavailable:"))
10597
- return "The selected model is unavailable. Choose a different model in the agent settings in Cabane.";
10654
+ return "The agent's model isn't available to the harness on this device. Choose a different model in the agent's settings in Cabane. Reply in Cabane to try again.";
10598
10655
  if (reason?.startsWith("prepare_failed:"))
10599
- return `Could not prepare the working environment (${humanText(reason.slice(15))}). Check the prepare command in ~/.cabane/config.json.`;
10656
+ return `This device couldn't get a folder ready for the agent's turn. It reported: "${humanText(reason.slice(15))}". Check the agent's prepare command in \`~/.cabane/config.json\`. Reply in Cabane to try again.`;
10600
10657
  if (reason?.startsWith("fetch_failed:"))
10601
- return `Could not load the conversation (${humanText(reason.slice(13))}). Reply in Cabane to try again.`;
10658
+ return `This device couldn't load the conversation (${humanText(reason.slice(13))}), so the turn didn't run. Reply in Cabane to try again.`;
10602
10659
  if (reason === "timeout_idle")
10603
- return "The agent stopped responding. Reply in Cabane to try again.";
10660
+ return "The agent went quiet for too long, so the turn was stopped. Reply in Cabane to try again.";
10604
10661
  if (reason === "timeout_total")
10605
- return "The turn ran longer than expected and was stopped. Reply in Cabane to continue.";
10662
+ return "The turn ran too long and was stopped. Reply in Cabane to continue.";
10606
10663
  if (reason === "result_error:error_max_turns")
10607
10664
  return "The agent reached their step limit. Reply in Cabane to continue.";
10608
- if (reason === "no_result" || reason === "empty_result" || reason === "empty_result_unverified")
10609
- return "The agent finished without a response. Reply in Cabane to try again.";
10665
+ if (reason === "empty_result" || reason === "empty_result_unverified")
10666
+ return `${harness} finished the turn without a reply. Reply in Cabane to try again.`;
10610
10667
  if (reason === "session_start_failed")
10611
- return "The agent could not start a session. Restart the companion, then reply in Cabane to try again.";
10612
- if (reason === "no_terminal")
10613
- return "The agent stopped before confirming the turn was complete. Reply in Cabane to continue.";
10668
+ return `${harness} closed before the turn could start, so the turn didn't run. Reply in Cabane to try again.`;
10669
+ if (reason === "no_terminal" || reason === "no_result")
10670
+ return `${harness} stopped before the turn finished. Reply in Cabane to try again.`;
10614
10671
  if (reason === "lease_unconfirmed")
10615
- return "Could not confirm with Cabane that the turn could start. Check this device\u2019s connection, then reply in Cabane to try again.";
10672
+ return "Before a turn starts, this device checks with Cabane that it should run it. That check didn't go through, so nothing ran. Check this device's connection. Reply in Cabane to try again.";
10616
10673
  if (reason === "workspace_tools_missing")
10617
- return "The Cabane workspace tools did not load. Reply in Cabane to try again.";
10674
+ return "The agent's Cabane workspace tools didn't load, so they couldn't reply. Reply in Cabane to try again.";
10618
10675
  if (reason?.startsWith("seq_floor_unavailable:"))
10619
- return "The interrupted turn could not safely continue. Reply in Cabane to continue.";
10676
+ return "The turn was interrupted, and it couldn't safely pick back up where it left off. Reply in Cabane to continue.";
10620
10677
  if (reason === "result_error:error_max_budget_usd")
10621
- return "The agent reached their spending limit. Check their budget settings before trying again in Cabane.";
10678
+ return "The agent reached their spending limit. Check their budget settings. Reply in Cabane to try again.";
10622
10679
  if (reason === "result_error:error_max_structured_output_retries")
10623
- return "The agent could not produce the requested response format. Check the requested format, then reply in Cabane to try again.";
10680
+ return "The agent couldn't produce the response format they were asked for. Check that format. Reply in Cabane to try again.";
10624
10681
  if (reason?.startsWith("result_error:"))
10625
- return "The agent stopped with an error while running. Check their transcript, then reply in Cabane to try again.";
10682
+ return `${harness} ended the turn with an error. Check the agent's transcript. Reply in Cabane to try again.`;
10626
10683
  if (reason === "setup_failed")
10627
- return "The turn could not be set up. Check the agent setup on this device, then reply in Cabane to try again.";
10684
+ return "The companion couldn't set up this turn, so it didn't run. Check the agent's setup on this device. Reply in Cabane to try again.";
10628
10685
  if (reason === "unexpected_role")
10629
- return "The message could not start an agent response. Send a new message in Cabane to try again.";
10686
+ return "That message couldn't start a turn for the agent. Send a new message in Cabane.";
10630
10687
  if (reason === "lease_lost" || reason?.startsWith("lease_refused:"))
10631
- return "The turn could not keep its connection to Cabane. Reply in Cabane to try again.";
10688
+ return "The turn lost its connection to Cabane. Reply in Cabane to try again.";
10632
10689
  const detail = humanText(reason?.replace(/^error:/, "") ?? "").slice(0, 400);
10633
10690
  if (/\b(EACCES|EPERM)\b|permission denied/i.test(detail)) {
10634
10691
  const readable = detail.replace(/\b(EACCES|EPERM)\b/g, "permission denied");
10635
- return `The agent was denied access: ${readable}. Check access permissions for the reported file or command, then reply in Cabane to try again.`;
10692
+ return `The agent was denied access: ${readable}. Check the permissions on that file or command. Reply in Cabane to try again.`;
10636
10693
  }
10637
10694
  if (detail)
10638
- return `The agent reported: ${detail}. Check the agent setup on this device, then reply in Cabane to try again.`;
10639
- return "The agent could not finish this turn. Reply in Cabane to try again.";
10695
+ return `The agent reported an error: ${detail}. Check the agent's setup on this device. Reply in Cabane to try again.`;
10696
+ return "The agent couldn't finish this turn. Reply in Cabane to try again.";
10640
10697
  }
10641
10698
 
10642
10699
  // src/supervisor.ts
@@ -11823,6 +11880,7 @@ function runtimeIssuesFor(gates) {
11823
11880
  }
11824
11881
 
11825
11882
  // src/supervisor.ts
11883
+ init_harness_state();
11826
11884
  init_harness_versions();
11827
11885
  init_harness_usable();
11828
11886
 
@@ -13664,7 +13722,7 @@ var CompanionSupervisor = class {
13664
13722
  attempt,
13665
13723
  cap: MAX_RESUME_ATTEMPTS
13666
13724
  },
13667
- "An interrupted turn couldn't finish after several attempts. Reply in Cabane to try again."
13725
+ "The turn was interrupted, and it couldn't pick back up after several attempts. Reply in Cabane to continue."
13668
13726
  );
13669
13727
  try {
13670
13728
  await agent.api.setActiveRun(workspaceId, payload.conversationId, payload.agentId, {
@@ -15419,6 +15477,7 @@ import { readdirSync as readdirSync4, realpathSync as realpathSync6, existsSync
15419
15477
  import { homedir as homedir10 } from "os";
15420
15478
  import { dirname as dirname12, join as join28 } from "path";
15421
15479
  init_config();
15480
+ init_harness_copy();
15422
15481
  init_secrets();
15423
15482
  init_term();
15424
15483
  init_transcript_writer();
package/dist/runtime.js CHANGED
@@ -1500,73 +1500,166 @@ var NEGATED_CAP = /not (your|a) usage limit/g;
1500
1500
  var RATE_PATTERNS = [/rate[_ ]?limit/, /\b429\b/, /too many requests/];
1501
1501
  var BARE_LIMIT = /\blimit (reached|exceeded)\b/;
1502
1502
 
1503
+ // src/harness-copy.ts
1504
+ import { homedir as homedir6 } from "os";
1505
+ import { join as join9, win32 } from "path";
1506
+
1507
+ // src/harness-state.ts
1508
+ function harnessEntry(runtime, state) {
1509
+ switch (state.state) {
1510
+ case "usable":
1511
+ return {
1512
+ runtime,
1513
+ state: "connected",
1514
+ reason: state.via,
1515
+ version: state.version,
1516
+ ...state.url ? { url: state.url } : {}
1517
+ };
1518
+ case "signed_out":
1519
+ return { runtime, state: "needs_sign_in", reason: "signed_out", version: state.version };
1520
+ case "not_running":
1521
+ return { runtime, state: "problem", reason: "not_running", url: state.url };
1522
+ case "off":
1523
+ return { runtime, state: "off" };
1524
+ case "problem":
1525
+ return { runtime, state: "problem", reason: checkReason(state.check) };
1526
+ }
1527
+ }
1528
+ function harnessEntriesFromDetection(detection) {
1529
+ return HARNESS_RUNTIMES.map((runtime) => harnessEntry(runtime, detection[runtime]));
1530
+ }
1531
+ function checkReason(check) {
1532
+ switch (check.status) {
1533
+ case "bundle_missing":
1534
+ return check.bundled;
1535
+ case "check_failed":
1536
+ return `check_failed:${check.reason}`;
1537
+ default:
1538
+ return check.status;
1539
+ }
1540
+ }
1541
+
1542
+ // src/harness-copy.ts
1543
+ var HARNESS_LABELS = {
1544
+ "claude-code": "Claude Code",
1545
+ codex: "Codex",
1546
+ opencode: "OpenCode"
1547
+ };
1548
+ function harnessLabel(runtime) {
1549
+ const known = HARNESS_RUNTIMES.find((candidate) => candidate === runtime);
1550
+ return known ? HARNESS_LABELS[known] : "The harness";
1551
+ }
1552
+ function failureText(runtime, reason) {
1553
+ const label = HARNESS_LABELS[runtime];
1554
+ switch (reason) {
1555
+ case "timeout":
1556
+ return `Couldn't check whether ${label} is signed in: the check didn't answer in time. Try again.`;
1557
+ case "spawn":
1558
+ return `Couldn't check whether ${label} is signed in: Cabane's bundled ${label} wouldn't start. Repair it with ${repairSteps()} and try again.${spaced(repairCost())}`;
1559
+ case "config":
1560
+ return `Couldn't check whether ${label} is signed in: its configuration file didn't load${runtime === "codex" ? ` (${tildePath(join9(homedir6(), ".codex", "config.toml"))})` : ""}. Fix it, then try again.`;
1561
+ case "unreadable":
1562
+ return `Couldn't check whether ${label} is signed in: it answered in a way Cabane couldn't read. Try again, and if it keeps happening, update Cabane.`;
1563
+ }
1564
+ }
1565
+ function bundleMissingText(runtime) {
1566
+ const label = HARNESS_LABELS[runtime];
1567
+ return `This Cabane install is missing its bundled ${label}. Repair it with ${repairSteps()} and try again.${spaced(repairCost())}`;
1568
+ }
1569
+ function unusableReason(runtime, check) {
1570
+ if (check.status === "bundle_missing") return bundleMissingText(runtime);
1571
+ if (check.status === "check_failed") return failureText(runtime, check.reason);
1572
+ return null;
1573
+ }
1574
+ var SETUP_AGAIN = `Then ${cabane("status")} shows it signed in; nothing else to run.`;
1575
+ function spaced(sentence) {
1576
+ return sentence ? ` ${sentence}` : "";
1577
+ }
1578
+
1503
1579
  // src/log-copy.ts
1504
- var RESTART = "then `cabane stop` and `cabane start` to restart the companion";
1580
+ var RESTART = "`cabane stop` and `cabane start`";
1505
1581
  function turnFailureCopy(reason, runtime) {
1506
1582
  const failure = decodeFailureReason(reason);
1583
+ const harness = harnessLabel(runtime);
1507
1584
  if (failure?.kind === "auth_expired") {
1508
1585
  if (runtime === "claude-code")
1509
- return `Claude Code is signed out on this device. To sign back in, run \`claude auth login\`, ${RESTART}. Then reply in Cabane to try again.`;
1586
+ return `Claude Code is signed out on this device. To sign back in, run \`claude auth login\`, ${RESTART}. Reply in Cabane to try again.`;
1510
1587
  if (runtime === "codex")
1511
- return `Codex is signed out on this device. To sign back in, run \`codex login\`, ${RESTART}. Then reply in Cabane to try again.`;
1512
- return `The agent is signed out on this device. Sign in to their harness, ${RESTART}. Then reply in Cabane to try again.`;
1588
+ return `Codex is signed out on this device. To sign back in, run \`codex login\`, ${RESTART}. Reply in Cabane to try again.`;
1589
+ return `${harness} is signed out on this device. Sign in to ${runtime === "opencode" ? "OpenCode" : "it"}, then run ${RESTART}. Reply in Cabane to try again.`;
1513
1590
  }
1514
1591
  if (failure?.kind === "usage_capped")
1515
- return failure.resetsAt ? `The usage limit is reached; it resets around ${new Date(failure.resetsAt).toLocaleTimeString()}. Reply in Cabane after it resets.` : "The usage limit is reached. Try again later in Cabane.";
1592
+ return failure.resetsAt ? `The usage limit is reached and resets around ${new Date(failure.resetsAt).toLocaleTimeString([], { timeStyle: "short" })}. Reply in Cabane after reset to try again.` : "The usage limit is reached. Reply in Cabane later to try again.";
1516
1593
  if (failure?.kind === "rate_limited")
1517
1594
  return "The provider is limiting requests. Reply in Cabane to try again.";
1518
1595
  if (failure?.kind === "server_error")
1519
1596
  return "The provider had a temporary error. Reply in Cabane to try again.";
1520
- if (reason?.startsWith("missing_secret:"))
1521
- return `A required secret is missing: ${humanText(reason.slice(15))}. Add it to ~/.cabane/secrets.json, then reply in Cabane to try again.`;
1522
- if (reason?.startsWith("runtime_incomplete:"))
1523
- return `The agent installation is incomplete. ${repairAction()} Then restart the companion.`;
1524
- if (/^(runtime|codex|opencode)_unavailable:/.test(reason ?? ""))
1525
- return "This harness isn't available on the companion. Check the harness on this device, or choose another harness in the agent's settings in Cabane. Then reply in Cabane to try again.";
1597
+ if (reason?.startsWith("missing_secret:")) {
1598
+ const tokens = humanText(reason.slice(15)).split(",").map((name) => name.trim()).filter(Boolean);
1599
+ const more = tokens.reduce(
1600
+ (n, token) => /^\+\d+$/.test(token) ? n + Number(token.slice(1)) : n,
1601
+ 0
1602
+ );
1603
+ const names = tokens.filter((token) => !/^\+\d+$/.test(token)).map((name) => `\`${name}\``);
1604
+ const count = names.length + more;
1605
+ const needs = names.length === 0 ? count > 1 ? `${count} secrets` : "a secret" : `${count > 1 ? "secrets" : "a secret"} called ${more > 0 ? `${names.join(", ")} and ${more} more` : names.length > 1 ? `${names.slice(0, -1).join(", ")} and ${names.at(-1)}` : names[0]}`;
1606
+ return `The agent's tools need ${needs}, which this device doesn't have. Add ${count > 1 ? "them" : "it"} to the secrets file, \`~/.cabane/secrets.json\`. Reply in Cabane to try again.`;
1607
+ }
1608
+ if (reason?.startsWith("runtime_incomplete:")) {
1609
+ const commands = repairCommands(void 0, (rest) => `cabane ${rest}`);
1610
+ const stopsFirst = commands.length > 1;
1611
+ if (!stopsFirst) commands.push("cabane stop");
1612
+ commands.push("cabane start");
1613
+ const list = commands.map((command) => `\`${command}\``);
1614
+ 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.`;
1615
+ }
1616
+ const unavailable = /^(codex|opencode)_unavailable:/.exec(reason ?? "")?.[1] ?? (reason?.startsWith("runtime_unavailable:") ? reason.slice("runtime_unavailable:".length).trim() : void 0);
1617
+ if (unavailable !== void 0)
1618
+ return `${harnessLabel(runtime ?? unavailable)} isn't connected on this device. Run \`cabane setup\`, or choose a different harness in the agent's settings in Cabane. Reply in Cabane to try again.`;
1526
1619
  if (reason?.startsWith("model_unavailable:"))
1527
- return "The selected model is unavailable. Choose a different model in the agent settings in Cabane.";
1620
+ return "The agent's model isn't available to the harness on this device. Choose a different model in the agent's settings in Cabane. Reply in Cabane to try again.";
1528
1621
  if (reason?.startsWith("prepare_failed:"))
1529
- return `Could not prepare the working environment (${humanText(reason.slice(15))}). Check the prepare command in ~/.cabane/config.json.`;
1622
+ return `This device couldn't get a folder ready for the agent's turn. It reported: "${humanText(reason.slice(15))}". Check the agent's prepare command in \`~/.cabane/config.json\`. Reply in Cabane to try again.`;
1530
1623
  if (reason?.startsWith("fetch_failed:"))
1531
- return `Could not load the conversation (${humanText(reason.slice(13))}). Reply in Cabane to try again.`;
1624
+ return `This device couldn't load the conversation (${humanText(reason.slice(13))}), so the turn didn't run. Reply in Cabane to try again.`;
1532
1625
  if (reason === "timeout_idle")
1533
- return "The agent stopped responding. Reply in Cabane to try again.";
1626
+ return "The agent went quiet for too long, so the turn was stopped. Reply in Cabane to try again.";
1534
1627
  if (reason === "timeout_total")
1535
- return "The turn ran longer than expected and was stopped. Reply in Cabane to continue.";
1628
+ return "The turn ran too long and was stopped. Reply in Cabane to continue.";
1536
1629
  if (reason === "result_error:error_max_turns")
1537
1630
  return "The agent reached their step limit. Reply in Cabane to continue.";
1538
- if (reason === "no_result" || reason === "empty_result" || reason === "empty_result_unverified")
1539
- return "The agent finished without a response. Reply in Cabane to try again.";
1631
+ if (reason === "empty_result" || reason === "empty_result_unverified")
1632
+ return `${harness} finished the turn without a reply. Reply in Cabane to try again.`;
1540
1633
  if (reason === "session_start_failed")
1541
- return "The agent could not start a session. Restart the companion, then reply in Cabane to try again.";
1542
- if (reason === "no_terminal")
1543
- return "The agent stopped before confirming the turn was complete. Reply in Cabane to continue.";
1634
+ return `${harness} closed before the turn could start, so the turn didn't run. Reply in Cabane to try again.`;
1635
+ if (reason === "no_terminal" || reason === "no_result")
1636
+ return `${harness} stopped before the turn finished. Reply in Cabane to try again.`;
1544
1637
  if (reason === "lease_unconfirmed")
1545
- return "Could not confirm with Cabane that the turn could start. Check this device\u2019s connection, then reply in Cabane to try again.";
1638
+ return "Before a turn starts, this device checks with Cabane that it should run it. That check didn't go through, so nothing ran. Check this device's connection. Reply in Cabane to try again.";
1546
1639
  if (reason === "workspace_tools_missing")
1547
- return "The Cabane workspace tools did not load. Reply in Cabane to try again.";
1640
+ return "The agent's Cabane workspace tools didn't load, so they couldn't reply. Reply in Cabane to try again.";
1548
1641
  if (reason?.startsWith("seq_floor_unavailable:"))
1549
- return "The interrupted turn could not safely continue. Reply in Cabane to continue.";
1642
+ return "The turn was interrupted, and it couldn't safely pick back up where it left off. Reply in Cabane to continue.";
1550
1643
  if (reason === "result_error:error_max_budget_usd")
1551
- return "The agent reached their spending limit. Check their budget settings before trying again in Cabane.";
1644
+ return "The agent reached their spending limit. Check their budget settings. Reply in Cabane to try again.";
1552
1645
  if (reason === "result_error:error_max_structured_output_retries")
1553
- return "The agent could not produce the requested response format. Check the requested format, then reply in Cabane to try again.";
1646
+ return "The agent couldn't produce the response format they were asked for. Check that format. Reply in Cabane to try again.";
1554
1647
  if (reason?.startsWith("result_error:"))
1555
- return "The agent stopped with an error while running. Check their transcript, then reply in Cabane to try again.";
1648
+ return `${harness} ended the turn with an error. Check the agent's transcript. Reply in Cabane to try again.`;
1556
1649
  if (reason === "setup_failed")
1557
- return "The turn could not be set up. Check the agent setup on this device, then reply in Cabane to try again.";
1650
+ return "The companion couldn't set up this turn, so it didn't run. Check the agent's setup on this device. Reply in Cabane to try again.";
1558
1651
  if (reason === "unexpected_role")
1559
- return "The message could not start an agent response. Send a new message in Cabane to try again.";
1652
+ return "That message couldn't start a turn for the agent. Send a new message in Cabane.";
1560
1653
  if (reason === "lease_lost" || reason?.startsWith("lease_refused:"))
1561
- return "The turn could not keep its connection to Cabane. Reply in Cabane to try again.";
1654
+ return "The turn lost its connection to Cabane. Reply in Cabane to try again.";
1562
1655
  const detail = humanText(reason?.replace(/^error:/, "") ?? "").slice(0, 400);
1563
1656
  if (/\b(EACCES|EPERM)\b|permission denied/i.test(detail)) {
1564
1657
  const readable = detail.replace(/\b(EACCES|EPERM)\b/g, "permission denied");
1565
- return `The agent was denied access: ${readable}. Check access permissions for the reported file or command, then reply in Cabane to try again.`;
1658
+ return `The agent was denied access: ${readable}. Check the permissions on that file or command. Reply in Cabane to try again.`;
1566
1659
  }
1567
1660
  if (detail)
1568
- return `The agent reported: ${detail}. Check the agent setup on this device, then reply in Cabane to try again.`;
1569
- return "The agent could not finish this turn. Reply in Cabane to try again.";
1661
+ return `The agent reported an error: ${detail}. Check the agent's setup on this device. Reply in Cabane to try again.`;
1662
+ return "The agent couldn't finish this turn. Reply in Cabane to try again.";
1570
1663
  }
1571
1664
 
1572
1665
  // src/api-error-shape.ts
@@ -2304,17 +2397,17 @@ import {
2304
2397
  rmSync as rmSync4,
2305
2398
  writeFileSync as writeFileSync3
2306
2399
  } from "fs";
2307
- import { join as join9 } from "path";
2400
+ import { join as join10 } from "path";
2308
2401
  import { z as z5 } from "zod";
2309
2402
  var credentialMapSchema = z5.record(z5.string(), z5.string());
2310
2403
  function credentialsDir() {
2311
- return join9(cabaneDir(), "credentials");
2404
+ return join10(cabaneDir(), "credentials");
2312
2405
  }
2313
2406
  function legacyPath() {
2314
- return join9(cabaneDir(), "credentials.json");
2407
+ return join10(cabaneDir(), "credentials.json");
2315
2408
  }
2316
2409
  function devicePath(deviceId) {
2317
- return join9(credentialsDir(), `${deviceId.replace(/[^A-Za-z0-9_-]/g, "_")}.json`);
2410
+ return join10(credentialsDir(), `${deviceId.replace(/[^A-Za-z0-9_-]/g, "_")}.json`);
2318
2411
  }
2319
2412
  function read(path) {
2320
2413
  if (!existsSync6(path)) return null;
@@ -2383,13 +2476,13 @@ function pruneCredentials(deviceId, keepAgentIds) {
2383
2476
 
2384
2477
  // src/dispatch-dedupe.ts
2385
2478
  import { mkdirSync as mkdirSync6, readFileSync as readFileSync5, writeFileSync as writeFileSync4, renameSync as renameSync4, existsSync as existsSync7 } from "fs";
2386
- import { join as join10 } from "path";
2479
+ import { join as join11 } from "path";
2387
2480
  var MAX_IDS = 256;
2388
2481
  function dir(log) {
2389
- return join10(cabaneDir(), log);
2482
+ return join11(cabaneDir(), log);
2390
2483
  }
2391
2484
  function pathFor(log, workspaceId) {
2392
- return join10(dir(log), encodeURIComponent(workspaceId));
2485
+ return join11(dir(log), encodeURIComponent(workspaceId));
2393
2486
  }
2394
2487
  function readIds(log, workspaceId) {
2395
2488
  const path = pathFor(log, workspaceId);
@@ -2426,10 +2519,10 @@ function markCompleted(workspaceId, eventId) {
2426
2519
  }
2427
2520
  var MAX_RESUME_ATTEMPTS = 3;
2428
2521
  function resumeDir() {
2429
- return join10(cabaneDir(), "resume-attempts");
2522
+ return join11(cabaneDir(), "resume-attempts");
2430
2523
  }
2431
2524
  function resumePathFor(workspaceId) {
2432
- return join10(resumeDir(), encodeURIComponent(workspaceId));
2525
+ return join11(resumeDir(), encodeURIComponent(workspaceId));
2433
2526
  }
2434
2527
  function readResumeCounts(workspaceId) {
2435
2528
  const out = /* @__PURE__ */ new Map();
@@ -2465,10 +2558,10 @@ function bumpResumeAttempt(workspaceId, eventId) {
2465
2558
  return next;
2466
2559
  }
2467
2560
  function turnDir() {
2468
- return join10(cabaneDir(), "turns");
2561
+ return join11(cabaneDir(), "turns");
2469
2562
  }
2470
2563
  function turnPathFor(workspaceId) {
2471
- return join10(turnDir(), encodeURIComponent(workspaceId));
2564
+ return join11(turnDir(), encodeURIComponent(workspaceId));
2472
2565
  }
2473
2566
  var TURN_ID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
2474
2567
  function readTurnIds(workspaceId) {
@@ -6053,7 +6146,7 @@ import { existsSync as existsSync11, mkdirSync as mkdirSync9 } from "fs";
6053
6146
 
6054
6147
  // src/grants.ts
6055
6148
  import { existsSync as existsSync8, readFileSync as readFileSync7 } from "fs";
6056
- import { join as join11 } from "path";
6149
+ import { join as join12 } from "path";
6057
6150
  import { z as z14 } from "zod";
6058
6151
  var VARIABLE_NAME_RE = /^[A-Z_][A-Z0-9_]*$/;
6059
6152
  function allowedGrantVariable(name) {
@@ -6063,7 +6156,7 @@ function environmentSafe(value) {
6063
6156
  return !value.includes("\0");
6064
6157
  }
6065
6158
  function grantsPath() {
6066
- return join11(cabaneDir(), "grants.json");
6159
+ return join12(cabaneDir(), "grants.json");
6067
6160
  }
6068
6161
  var grantsFileSchema = z14.record(z14.string(), z14.record(z14.string(), z14.string()));
6069
6162
  function readGrants(keys, report) {
@@ -6112,7 +6205,7 @@ function loadGrantsFile(report) {
6112
6205
 
6113
6206
  // src/work-dir.ts
6114
6207
  import { statSync as statSync3 } from "fs";
6115
- import { join as join12 } from "path";
6208
+ import { join as join13 } from "path";
6116
6209
 
6117
6210
  // src/turn-control-tools.ts
6118
6211
  import { randomUUID as randomUUID3 } from "crypto";
@@ -6130,33 +6223,33 @@ var CANCEL_WAKE_TOOL_NAME = `mcp__${COMPANION_LOCAL_MCP_SERVER}__${CANCEL_WAKE_T
6130
6223
 
6131
6224
  // src/codex-config.ts
6132
6225
  import { readFile } from "fs/promises";
6133
- import { homedir as homedir6 } from "os";
6134
- import { join as join13 } from "path";
6226
+ import { homedir as homedir7 } from "os";
6227
+ import { join as join14 } from "path";
6135
6228
  import { parse as parse2 } from "smol-toml";
6136
6229
 
6137
6230
  // src/codex-images.ts
6138
6231
  import { mkdtemp, rm, writeFile } from "fs/promises";
6139
6232
  import { tmpdir } from "os";
6140
- import { join as join14 } from "path";
6233
+ import { join as join15 } from "path";
6141
6234
 
6142
6235
  // src/codex-compactions.ts
6143
6236
  import { createReadStream } from "fs";
6144
6237
  import { readdir } from "fs/promises";
6145
- import { homedir as homedir7 } from "os";
6146
- import { join as join15 } from "path";
6238
+ import { homedir as homedir8 } from "os";
6239
+ import { join as join16 } from "path";
6147
6240
  import { createInterface as createInterface2 } from "readline";
6148
6241
 
6149
6242
  // src/prepared.ts
6150
6243
  import { mkdirSync as mkdirSync7, readFileSync as readFileSync8, rmSync as rmSync5, writeFileSync as writeFileSync5, existsSync as existsSync9 } from "fs";
6151
- import { join as join16 } from "path";
6244
+ import { join as join17 } from "path";
6152
6245
 
6153
6246
  // src/secrets.ts
6154
6247
  import { existsSync as existsSync10, readFileSync as readFileSync9 } from "fs";
6155
- import { join as join17 } from "path";
6248
+ import { join as join18 } from "path";
6156
6249
  import { z as z16 } from "zod";
6157
6250
  var PLACEHOLDER_RE = /\$\{([A-Za-z_][A-Za-z0-9_]*)\}/g;
6158
6251
  function secretsPath() {
6159
- return join17(cabaneDir(), "secrets.json");
6252
+ return join18(cabaneDir(), "secrets.json");
6160
6253
  }
6161
6254
  var secretStoreSchema = z16.record(z16.string(), z16.string());
6162
6255
  function loadSecretValues() {
@@ -6346,9 +6439,9 @@ function runBounded(path, args, env) {
6346
6439
 
6347
6440
  // src/transcript-writer.ts
6348
6441
  import { appendFileSync, chmodSync as chmodSync4, copyFileSync, mkdirSync as mkdirSync8, readdirSync, rmSync as rmSync6 } from "fs";
6349
- import { basename as basename3, dirname as dirname6, join as join18 } from "path";
6442
+ import { basename as basename3, dirname as dirname6, join as join19 } from "path";
6350
6443
  function transcriptsDir() {
6351
- return join18(cabaneDir(), "transcripts");
6444
+ return join19(cabaneDir(), "transcripts");
6352
6445
  }
6353
6446
 
6354
6447
  // src/turn-execution.ts
@@ -6673,76 +6766,6 @@ function runtimeIssuesFor(gates) {
6673
6766
  return issues;
6674
6767
  }
6675
6768
 
6676
- // src/harness-state.ts
6677
- function harnessEntry(runtime, state) {
6678
- switch (state.state) {
6679
- case "usable":
6680
- return {
6681
- runtime,
6682
- state: "connected",
6683
- reason: state.via,
6684
- version: state.version,
6685
- ...state.url ? { url: state.url } : {}
6686
- };
6687
- case "signed_out":
6688
- return { runtime, state: "needs_sign_in", reason: "signed_out", version: state.version };
6689
- case "not_running":
6690
- return { runtime, state: "problem", reason: "not_running", url: state.url };
6691
- case "off":
6692
- return { runtime, state: "off" };
6693
- case "problem":
6694
- return { runtime, state: "problem", reason: checkReason(state.check) };
6695
- }
6696
- }
6697
- function harnessEntriesFromDetection(detection) {
6698
- return HARNESS_RUNTIMES.map((runtime) => harnessEntry(runtime, detection[runtime]));
6699
- }
6700
- function checkReason(check) {
6701
- switch (check.status) {
6702
- case "bundle_missing":
6703
- return check.bundled;
6704
- case "check_failed":
6705
- return `check_failed:${check.reason}`;
6706
- default:
6707
- return check.status;
6708
- }
6709
- }
6710
-
6711
- // src/harness-copy.ts
6712
- import { homedir as homedir8 } from "os";
6713
- import { join as join19, win32 } from "path";
6714
- var HARNESS_LABELS = {
6715
- "claude-code": "Claude Code",
6716
- codex: "Codex",
6717
- opencode: "OpenCode"
6718
- };
6719
- function failureText(runtime, reason) {
6720
- const label = HARNESS_LABELS[runtime];
6721
- switch (reason) {
6722
- case "timeout":
6723
- return `Couldn't check whether ${label} is signed in: the check didn't answer in time. Try again.`;
6724
- case "spawn":
6725
- return `Couldn't check whether ${label} is signed in: Cabane's bundled ${label} wouldn't start. Repair it with ${repairSteps()} and try again.${spaced(repairCost())}`;
6726
- case "config":
6727
- return `Couldn't check whether ${label} is signed in: its configuration file didn't load${runtime === "codex" ? ` (${tildePath(join19(homedir8(), ".codex", "config.toml"))})` : ""}. Fix it, then try again.`;
6728
- case "unreadable":
6729
- return `Couldn't check whether ${label} is signed in: it answered in a way Cabane couldn't read. Try again, and if it keeps happening, update Cabane.`;
6730
- }
6731
- }
6732
- function bundleMissingText(runtime) {
6733
- const label = HARNESS_LABELS[runtime];
6734
- return `This Cabane install is missing its bundled ${label}. Repair it with ${repairSteps()} and try again.${spaced(repairCost())}`;
6735
- }
6736
- function unusableReason(runtime, check) {
6737
- if (check.status === "bundle_missing") return bundleMissingText(runtime);
6738
- if (check.status === "check_failed") return failureText(runtime, check.reason);
6739
- return null;
6740
- }
6741
- var SETUP_AGAIN = `Then ${cabane("status")} shows it signed in; nothing else to run.`;
6742
- function spaced(sentence) {
6743
- return sentence ? ` ${sentence}` : "";
6744
- }
6745
-
6746
6769
  // src/harness-detect.ts
6747
6770
  var DEFAULT_OPENCODE_SERVER_URL = "http://127.0.0.1:4096";
6748
6771
  var DEFAULT_OPENCODE_PROBE_MS = 1e3;
@@ -8893,7 +8916,7 @@ var CompanionSupervisor = class {
8893
8916
  attempt,
8894
8917
  cap: MAX_RESUME_ATTEMPTS
8895
8918
  },
8896
- "An interrupted turn couldn't finish after several attempts. Reply in Cabane to try again."
8919
+ "The turn was interrupted, and it couldn't pick back up after several attempts. Reply in Cabane to continue."
8897
8920
  );
8898
8921
  try {
8899
8922
  await agent.api.setActiveRun(workspaceId, payload.conversationId, payload.agentId, {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cabane/companion",
3
- "version": "0.6.232",
3
+ "version": "0.6.234",
4
4
  "type": "module",
5
5
  "description": "The Cabane Companion (headless): connect an agent on your machine to your Cabane workspace as a responder — drive work against your own codebase, files, and MCP servers without putting any of it in Cabane.",
6
6
  "license": "UNLICENSED",
@@ -38,9 +38,9 @@
38
38
  "prepublishOnly": "pnpm build"
39
39
  },
40
40
  "dependencies": {
41
- "@anthropic-ai/claude-agent-sdk": "0.3.316",
41
+ "@anthropic-ai/claude-agent-sdk": "0.3.319",
42
42
  "@clack/prompts": "1.8.1",
43
- "@openai/codex-sdk": "0.166.0",
43
+ "@openai/codex-sdk": "0.167.0",
44
44
  "commander": "12.1.0",
45
45
  "eventsource-parser": "3.0.8",
46
46
  "graphql": "16.11.0",