@cabane/companion 0.6.160 → 0.6.162

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/launch.js CHANGED
@@ -1,12 +1,12 @@
1
1
  // src/launch.ts
2
- import { readFileSync as readFileSync3 } from "fs";
3
- import { join as join5 } from "path";
2
+ import { readFileSync as readFileSync4 } from "fs";
3
+ import { join as join6 } from "path";
4
4
  import { pathToFileURL } from "url";
5
5
 
6
6
  // src/cabane-command.ts
7
7
  import { accessSync, constants, realpathSync as realpathSync2 } from "fs";
8
- import { homedir as homedir3 } from "os";
9
- import { basename, delimiter, dirname as dirname2, join as join2, sep as sep2 } from "path";
8
+ import { homedir as homedir4 } from "os";
9
+ import { basename as basename2, delimiter, dirname as dirname2, join as join3, sep as sep2 } from "path";
10
10
 
11
11
  // src/install-kind.ts
12
12
  import { realpathSync } from "fs";
@@ -18,9 +18,13 @@ import { fileURLToPath } from "url";
18
18
  import { existsSync } from "fs";
19
19
  import { fileURLToPath as fileURLToPath2 } from "url";
20
20
 
21
- // src/term.ts
22
- import { createInterface } from "readline";
21
+ // src/install-record.ts
22
+ import { existsSync as existsSync2, readFileSync } from "fs";
23
23
  import { homedir as homedir2 } from "os";
24
+ import { basename, join as join2 } from "path";
25
+
26
+ // src/term.ts
27
+ import { homedir as homedir3 } from "os";
24
28
  import pc from "picocolors";
25
29
 
26
30
  // src/cabane-command.ts
@@ -30,14 +34,14 @@ var VERSIONS_SEGMENT = `${sep2}.cabane${sep2}versions${sep2}`;
30
34
  import {
31
35
  chmodSync,
32
36
  mkdirSync,
33
- readFileSync,
37
+ readFileSync as readFileSync2,
34
38
  renameSync,
35
39
  rmSync,
36
40
  writeFileSync,
37
- existsSync as existsSync2
41
+ existsSync as existsSync3
38
42
  } from "fs";
39
- import { homedir as homedir4, userInfo } from "os";
40
- import { dirname as dirname3, join as join3 } from "path";
43
+ import { homedir as homedir5, userInfo } from "os";
44
+ import { dirname as dirname3, join as join4 } from "path";
41
45
  import { z as z2 } from "zod";
42
46
 
43
47
  // src/pairing.ts
@@ -94,6 +98,7 @@ var prepareResultSchema = z.object({
94
98
  });
95
99
 
96
100
  // src/config.ts
101
+ var HARNESS_RUNTIMES = ["claude-code", "codex", "opencode"];
97
102
  var realHomeCache;
98
103
  function realAccountHome() {
99
104
  if (realHomeCache === void 0) {
@@ -106,10 +111,10 @@ function realAccountHome() {
106
111
  return realHomeCache;
107
112
  }
108
113
  function cabaneDir() {
109
- const dir = join3(process.env.CABANE_COMPANION_HOME || homedir4(), ".cabane");
114
+ const dir = join4(process.env.CABANE_COMPANION_HOME || homedir5(), ".cabane");
110
115
  if (process.env.VITEST) {
111
116
  const real = realAccountHome();
112
- if (real && dir === join3(real, ".cabane")) {
117
+ if (real && dir === join4(real, ".cabane")) {
113
118
  throw new Error(
114
119
  `cabaneDir() resolved to the real ${dir} during a test run. Companion tests must swap process.env.HOME to a tmp dir before touching the config dir; this guard prevents wiping the operator's real companion config (see apps/companion/test/setup-home.ts).`
115
120
  );
@@ -178,31 +183,18 @@ var companionConfigSchema = z2.object({
178
183
  autoOpen: z2.boolean().optional(),
179
184
  logLevel: z2.enum(["warn", "info", "debug"]).optional(),
180
185
  logFormat: z2.enum(["human", "json"]).optional(),
181
- // An `opencode serve` the operator runs here (auth via opencode's own
182
- // `/connect`; Cabane never sees provider keys). Set, the device advertises the
183
- // `opencode` runtime and registers its adapter.
186
+ // Where this device's `opencode serve` answers, when it isn't the default
187
+ // address (`DEFAULT_OPENCODE_SERVER_URL`). Detection probes this one; a serve
188
+ // that answers is connected. Auth is opencode's own; Cabane never sees
189
+ // provider keys.
184
190
  opencode: z2.object({
185
191
  serverUrl: z2.string().url()
186
192
  }).strict().optional(),
187
- // Whether the user connected Claude Code on this machine: consent, opted into
188
- // like codex. The manifest advertises claude-code only when this says so AND
189
- // the Agent SDK's bundled executable resolves (not `claude` on PATH, which the
190
- // SDK never runs).
191
- //
192
- // Absent means one of two things, told apart by `harnessesChosen`: a legacy
193
- // config whose user was never asked (`migrateConnectedHarnesses`), or a
194
- // device that has never been offered Claude Code. `enabled: false` is a
195
- // decline, and `start` doesn't offer it again.
196
- //
197
- // Not the per-agent `agents.<key>.claudeCode.autoMemory`, one agent's memory
198
- // switch.
199
- claudeCode: z2.object({
200
- enabled: z2.boolean().optional()
201
- }).strict().optional(),
202
- // Written by every pairing that asks which agents to connect. With it, an
203
- // absent `claudeCode` block means "never offered"; without it, the block's
204
- // absence marks a config from before connectors were chosen.
205
- harnessesChosen: z2.boolean().optional(),
193
+ // The one knob on agents: an allowlist. Agents are detected, never offered
194
+ // (`harness-detect.ts`): whatever is signed in here is connected. Someone with
195
+ // Codex on the device who doesn't want Cabane to see it lists the ones it may:
196
+ // `"harnesses": ["claude-code"]`. Absent means all three.
197
+ harnesses: z2.array(z2.enum(HARNESS_RUNTIMES)).optional(),
206
198
  // Whether this companion keeps itself current: installs the version Cabane names, between
207
199
  // turns, and rolls back one that doesn't come up (`updater.ts`). Absent means never asked,
208
200
  // which `start` offers; off either way until someone says yes.
@@ -212,18 +204,6 @@ var companionConfigSchema = z2.object({
212
204
  // The answer to `start`'s "start at boot?" when it was no, so it isn't asked again.
213
205
  // `cabane service install` still installs it.
214
206
  serviceDeclined: z2.boolean().optional(),
215
- // OpenCode's decline. Its block is an address, so a decline can't live there
216
- // the way `claudeCode`/`codex` `enabled: false` does; without it a declined
217
- // serve would be offered again on every `start`.
218
- opencodeDeclined: z2.boolean().optional(),
219
- // Codex is a local CLI the SDK spawns per turn, so this is an opt-in flag, not
220
- // an address. The operator logs in (`codex login` or `CODEX_API_KEY`; Cabane
221
- // never sees the key). The block's presence enables it; `enabled: false` turns
222
- // it off. Half the answer: advertising also requires the SDK's vendored
223
- // `codex` binary to resolve.
224
- codex: z2.object({
225
- enabled: z2.boolean().optional()
226
- }).strict().optional(),
227
207
  // Run each turn in a child runner process. Absent or `enabled: false` is the
228
208
  // in-process path. `sandbox: 'os'` wraps the runner in an OS sandbox where the
229
209
  // host supports one; `'none'` (the default) is a plain child, supported in its
@@ -267,30 +247,36 @@ var companionConfigSchema = z2.object({
267
247
  policy: z2.unknown().optional()
268
248
  }).strict().optional()
269
249
  });
250
+ var legacyOfferSchema = z2.object({
251
+ claudeCode: z2.object({ enabled: z2.boolean().optional() }).passthrough().optional(),
252
+ codex: z2.object({ enabled: z2.boolean().optional() }).passthrough().optional(),
253
+ harnessesChosen: z2.boolean().optional(),
254
+ opencodeDeclined: z2.boolean().optional()
255
+ });
270
256
 
271
257
  // src/install-layout.ts
272
258
  import {
273
- existsSync as existsSync3,
259
+ existsSync as existsSync4,
274
260
  lstatSync,
275
261
  mkdirSync as mkdirSync2,
276
- readFileSync as readFileSync2,
262
+ readFileSync as readFileSync3,
277
263
  readdirSync,
278
264
  realpathSync as realpathSync3,
279
265
  renameSync as renameSync2,
280
266
  rmSync as rmSync2,
281
267
  symlinkSync
282
268
  } from "fs";
283
- import { dirname as dirname4, join as join4 } from "path";
269
+ import { dirname as dirname4, join as join5 } from "path";
284
270
  import { fileURLToPath as fileURLToPath3 } from "url";
285
271
  function currentLink() {
286
- return join4(cabaneDir(), "current");
272
+ return join5(cabaneDir(), "current");
287
273
  }
288
274
  function ownPackageRoot(moduleUrl = import.meta.url) {
289
275
  return dirname4(dirname4(fileURLToPath3(moduleUrl)));
290
276
  }
291
277
  function packageVersion(root) {
292
278
  try {
293
- const pkg = JSON.parse(readFileSync2(join4(root, "package.json"), "utf8"));
279
+ const pkg = JSON.parse(readFileSync3(join5(root, "package.json"), "utf8"));
294
280
  return typeof pkg.version === "string" ? pkg.version : null;
295
281
  } catch {
296
282
  return null;
@@ -299,7 +285,7 @@ function packageVersion(root) {
299
285
  function currentRoot() {
300
286
  try {
301
287
  const root = realpathSync3(currentLink());
302
- return existsSync3(join4(root, "dist", "cli.js")) ? root : null;
288
+ return existsSync4(join5(root, "dist", "cli.js")) ? root : null;
303
289
  } catch {
304
290
  return null;
305
291
  }
@@ -362,7 +348,7 @@ function parseEnvFile(text) {
362
348
  function loadServiceEnv() {
363
349
  let text;
364
350
  try {
365
- text = readFileSync3(join5(cabaneDir(), "service.env"), "utf8");
351
+ text = readFileSync4(join6(cabaneDir(), "service.env"), "utf8");
366
352
  } catch {
367
353
  return;
368
354
  }
@@ -395,7 +381,7 @@ async function launch(argv = process.argv.slice(2)) {
395
381
  await import("./cli.js");
396
382
  return;
397
383
  }
398
- await import(pathToFileURL(join5(run.root, "dist", "cli.js")).href);
384
+ await import(pathToFileURL(join6(run.root, "dist", "cli.js")).href);
399
385
  }
400
386
  export {
401
387
  chooseInstall,
@@ -1,122 +1,45 @@
1
1
  // src/cabane-command.ts
2
2
  import { accessSync, constants, realpathSync as realpathSync2 } from "fs";
3
- import { homedir as homedir3 } from "os";
4
- import { basename, delimiter, dirname as dirname2, join as join2, sep as sep2 } from "path";
3
+ import { homedir as homedir4 } from "os";
4
+ import { basename as basename2, delimiter, dirname as dirname2, join as join3, sep as sep2 } from "path";
5
5
 
6
6
  // src/install-kind.ts
7
7
  import { realpathSync } from "fs";
8
8
  import { homedir } from "os";
9
9
  import { dirname, join, sep } from "path";
10
10
  import { fileURLToPath } from "url";
11
- function companionInstall(execPath = process.execPath, home = homedir()) {
12
- const real = (path) => {
13
- try {
14
- return realpathSync(path);
15
- } catch {
16
- return path;
17
- }
18
- };
19
- const scriptNode = join(real(home), ".cabane", "node") + sep;
20
- return real(execPath).startsWith(scriptNode) ? "script" : "npm";
21
- }
22
11
 
23
12
  // src/cli-entry.ts
24
13
  import { existsSync } from "fs";
25
14
  import { fileURLToPath as fileURLToPath2 } from "url";
26
- var RELATIVE_CANDIDATES = ["./cli.js", "../dist/cli.js", "../cli.js"];
27
- function companionCliEntry(deps = {}) {
28
- const exists = deps.exists ?? existsSync;
29
- const candidates = deps.candidates ?? RELATIVE_CANDIDATES.map((rel) => fileURLToPath2(new URL(rel, import.meta.url)));
30
- for (const candidate of candidates) {
31
- if (exists(candidate)) return candidate;
32
- }
33
- const argv1 = "argv1" in deps ? deps.argv1 : process.argv[1];
34
- if (argv1 && exists(argv1)) return argv1;
35
- return candidates[0] ?? "";
36
- }
37
15
 
38
- // src/term.ts
39
- import { createInterface } from "readline";
16
+ // src/install-record.ts
17
+ import { existsSync as existsSync2, readFileSync } from "fs";
40
18
  import { homedir as homedir2 } from "os";
19
+ import { basename, join as join2 } from "path";
20
+
21
+ // src/term.ts
22
+ import { homedir as homedir3 } from "os";
41
23
  import pc from "picocolors";
42
- function tildePath(path, home = homedir2()) {
43
- if (home && path.startsWith(home + "/")) return `~${path.slice(home.length)}`;
44
- return path;
45
- }
46
24
 
47
25
  // src/cabane-command.ts
48
- var INVOKED_COMMAND_ENV = "CABANE_INVOKED_COMMAND";
49
26
  var VERSIONS_SEGMENT = `${sep2}.cabane${sep2}versions${sep2}`;
50
- function installedCommandPath(deps = {}) {
51
- const install = deps.install ?? companionInstall();
52
- const home = deps.home ?? homedir3();
53
- if (install === "script") return join2(home, ".cabane", "bin", "cabane");
54
- const entry = deps.cliEntry ?? companionCliEntry();
55
- const own = npmBinFor(entry);
56
- if (own) return own;
57
- if (!entry.includes(VERSIONS_SEGMENT)) return null;
58
- const isExecutable = deps.isExecutable ?? executable;
59
- const invoked = "invoked" in deps ? deps.invoked : process.env[INVOKED_COMMAND_ENV];
60
- if (invoked && isExecutable(invoked)) return invoked;
61
- const besideNode = join2(dirname2(dirname2(deps.execPath ?? process.execPath)), "bin", "cabane");
62
- return isExecutable(besideNode) ? besideNode : null;
63
- }
64
- function npmBinFor(path) {
65
- const marker = `${sep2}lib${sep2}node_modules${sep2}@cabane${sep2}companion${sep2}`;
66
- const at = path.indexOf(marker);
67
- if (at < 0) return null;
68
- return join2(path.slice(0, at), "bin", "cabane");
69
- }
70
- function cabaneCommand(deps = {}) {
71
- const own = installedCommandPath(deps);
72
- if (!own) return "cabane";
73
- const realpath = deps.realpath ?? safeRealpath;
74
- const isExecutable = deps.isExecutable ?? executable;
75
- const ownReal = realpath(own);
76
- if (!ownReal) return "cabane";
77
- const onPath = firstOnPath("cabane", "path" in deps ? deps.path : process.env.PATH, isExecutable);
78
- if (onPath && realpath(onPath) === ownReal) return "cabane";
79
- return tildePath(own, deps.home);
80
- }
81
- function cabane(rest, deps) {
82
- return `${cabaneCommand(deps)} ${rest}`;
83
- }
84
- function firstOnPath(name, path, isExecutable = executable) {
85
- for (const dir of (path ?? "").split(delimiter)) {
86
- if (!dir) continue;
87
- const candidate = join2(dir, name);
88
- if (isExecutable(candidate)) return candidate;
89
- }
90
- return null;
91
- }
92
- function executable(path) {
93
- try {
94
- accessSync(path, constants.X_OK);
95
- return true;
96
- } catch {
97
- return false;
98
- }
99
- }
100
- function safeRealpath(path) {
101
- try {
102
- return realpathSync2(path);
103
- } catch {
104
- return null;
105
- }
27
+ function cabane(rest) {
28
+ return `cabane ${rest}`;
106
29
  }
107
30
 
108
31
  // src/config.ts
109
32
  import {
110
33
  chmodSync,
111
34
  mkdirSync,
112
- readFileSync,
35
+ readFileSync as readFileSync2,
113
36
  renameSync,
114
37
  rmSync,
115
38
  writeFileSync,
116
- existsSync as existsSync2
39
+ existsSync as existsSync3
117
40
  } from "fs";
118
- import { homedir as homedir4, userInfo } from "os";
119
- import { dirname as dirname3, join as join3 } from "path";
41
+ import { homedir as homedir5, userInfo } from "os";
42
+ import { dirname as dirname3, join as join4 } from "path";
120
43
  import { z as z2 } from "zod";
121
44
 
122
45
  // src/errors.ts
@@ -197,6 +120,7 @@ var prepareResultSchema = z.object({
197
120
  });
198
121
 
199
122
  // src/config.ts
123
+ var HARNESS_RUNTIMES = ["claude-code", "codex", "opencode"];
200
124
  var realHomeCache;
201
125
  function realAccountHome() {
202
126
  if (realHomeCache === void 0) {
@@ -209,10 +133,10 @@ function realAccountHome() {
209
133
  return realHomeCache;
210
134
  }
211
135
  function cabaneDir() {
212
- const dir = join3(process.env.CABANE_COMPANION_HOME || homedir4(), ".cabane");
136
+ const dir = join4(process.env.CABANE_COMPANION_HOME || homedir5(), ".cabane");
213
137
  if (process.env.VITEST) {
214
138
  const real = realAccountHome();
215
- if (real && dir === join3(real, ".cabane")) {
139
+ if (real && dir === join4(real, ".cabane")) {
216
140
  throw new Error(
217
141
  `cabaneDir() resolved to the real ${dir} during a test run. Companion tests must swap process.env.HOME to a tmp dir before touching the config dir; this guard prevents wiping the operator's real companion config (see apps/companion/test/setup-home.ts).`
218
142
  );
@@ -221,7 +145,7 @@ function cabaneDir() {
221
145
  return dir;
222
146
  }
223
147
  function configPath() {
224
- return join3(cabaneDir(), "config.json");
148
+ return join4(cabaneDir(), "config.json");
225
149
  }
226
150
  var localAgentConfigSchema = z2.object({
227
151
  cwd: z2.string().optional(),
@@ -284,31 +208,18 @@ var companionConfigSchema = z2.object({
284
208
  autoOpen: z2.boolean().optional(),
285
209
  logLevel: z2.enum(["warn", "info", "debug"]).optional(),
286
210
  logFormat: z2.enum(["human", "json"]).optional(),
287
- // An `opencode serve` the operator runs here (auth via opencode's own
288
- // `/connect`; Cabane never sees provider keys). Set, the device advertises the
289
- // `opencode` runtime and registers its adapter.
211
+ // Where this device's `opencode serve` answers, when it isn't the default
212
+ // address (`DEFAULT_OPENCODE_SERVER_URL`). Detection probes this one; a serve
213
+ // that answers is connected. Auth is opencode's own; Cabane never sees
214
+ // provider keys.
290
215
  opencode: z2.object({
291
216
  serverUrl: z2.string().url()
292
217
  }).strict().optional(),
293
- // Whether the user connected Claude Code on this machine: consent, opted into
294
- // like codex. The manifest advertises claude-code only when this says so AND
295
- // the Agent SDK's bundled executable resolves (not `claude` on PATH, which the
296
- // SDK never runs).
297
- //
298
- // Absent means one of two things, told apart by `harnessesChosen`: a legacy
299
- // config whose user was never asked (`migrateConnectedHarnesses`), or a
300
- // device that has never been offered Claude Code. `enabled: false` is a
301
- // decline, and `start` doesn't offer it again.
302
- //
303
- // Not the per-agent `agents.<key>.claudeCode.autoMemory`, one agent's memory
304
- // switch.
305
- claudeCode: z2.object({
306
- enabled: z2.boolean().optional()
307
- }).strict().optional(),
308
- // Written by every pairing that asks which agents to connect. With it, an
309
- // absent `claudeCode` block means "never offered"; without it, the block's
310
- // absence marks a config from before connectors were chosen.
311
- harnessesChosen: z2.boolean().optional(),
218
+ // The one knob on agents: an allowlist. Agents are detected, never offered
219
+ // (`harness-detect.ts`): whatever is signed in here is connected. Someone with
220
+ // Codex on the device who doesn't want Cabane to see it lists the ones it may:
221
+ // `"harnesses": ["claude-code"]`. Absent means all three.
222
+ harnesses: z2.array(z2.enum(HARNESS_RUNTIMES)).optional(),
312
223
  // Whether this companion keeps itself current: installs the version Cabane names, between
313
224
  // turns, and rolls back one that doesn't come up (`updater.ts`). Absent means never asked,
314
225
  // which `start` offers; off either way until someone says yes.
@@ -318,18 +229,6 @@ var companionConfigSchema = z2.object({
318
229
  // The answer to `start`'s "start at boot?" when it was no, so it isn't asked again.
319
230
  // `cabane service install` still installs it.
320
231
  serviceDeclined: z2.boolean().optional(),
321
- // OpenCode's decline. Its block is an address, so a decline can't live there
322
- // the way `claudeCode`/`codex` `enabled: false` does; without it a declined
323
- // serve would be offered again on every `start`.
324
- opencodeDeclined: z2.boolean().optional(),
325
- // Codex is a local CLI the SDK spawns per turn, so this is an opt-in flag, not
326
- // an address. The operator logs in (`codex login` or `CODEX_API_KEY`; Cabane
327
- // never sees the key). The block's presence enables it; `enabled: false` turns
328
- // it off. Half the answer: advertising also requires the SDK's vendored
329
- // `codex` binary to resolve.
330
- codex: z2.object({
331
- enabled: z2.boolean().optional()
332
- }).strict().optional(),
333
232
  // Run each turn in a child runner process. Absent or `enabled: false` is the
334
233
  // in-process path. `sandbox: 'os'` wraps the runner in an OS sandbox where the
335
234
  // host supports one; `'none'` (the default) is a plain child, supported in its
@@ -373,12 +272,33 @@ var companionConfigSchema = z2.object({
373
272
  policy: z2.unknown().optional()
374
273
  }).strict().optional()
375
274
  });
275
+ var legacyOfferSchema = z2.object({
276
+ claudeCode: z2.object({ enabled: z2.boolean().optional() }).passthrough().optional(),
277
+ codex: z2.object({ enabled: z2.boolean().optional() }).passthrough().optional(),
278
+ harnessesChosen: z2.boolean().optional(),
279
+ opencodeDeclined: z2.boolean().optional()
280
+ });
281
+ function migrateHarnessAllowlist(raw) {
282
+ const legacy = legacyOfferSchema.safeParse(raw);
283
+ if (!legacy.success || !legacy.data.harnessesChosen) return null;
284
+ const declined = /* @__PURE__ */ new Set();
285
+ if (legacy.data.claudeCode?.enabled === false) declined.add("claude-code");
286
+ if (legacy.data.codex?.enabled === false) declined.add("codex");
287
+ if (legacy.data.opencodeDeclined === true) declined.add("opencode");
288
+ if (declined.size === 0) return null;
289
+ return HARNESS_RUNTIMES.filter((runtime) => !declined.has(runtime));
290
+ }
291
+ function carriesLegacyOffer(raw) {
292
+ if (!raw || typeof raw !== "object") return false;
293
+ const obj = raw;
294
+ return "harnessesChosen" in obj || "opencodeDeclined" in obj || typeof obj.claudeCode === "object" && obj.claudeCode !== null && "enabled" in obj.claudeCode || obj.codex !== void 0;
295
+ }
376
296
  function loadConfig() {
377
297
  const path = configPath();
378
- if (!existsSync2(path)) return null;
298
+ if (!existsSync3(path)) return null;
379
299
  let raw;
380
300
  try {
381
- raw = readFileSync(path, "utf8");
301
+ raw = readFileSync2(path, "utf8");
382
302
  } catch (err) {
383
303
  throw new ConfigError(
384
304
  `couldn't read ${path}: ${err instanceof Error ? err.message : String(err)}`
@@ -402,19 +322,28 @@ function loadConfig() {
402
322
  );
403
323
  }
404
324
  throw new ConfigError(
405
- `${path} is from an incompatible or older version of the companion, or was hand-edited. Run \`${cabane("pair")}\` to re-pair, or \`${cabane("logout --purge")}\` to reset.`
325
+ `${path} is from an incompatible or older version of the companion, or was hand-edited. Run \`${cabane("setup")}\` to pair again, or \`${cabane("logout --purge")}\` to reset.`
406
326
  );
407
327
  }
328
+ if (carriesLegacyOffer(parsed)) {
329
+ const allowlist = migrateHarnessAllowlist(parsed);
330
+ const migrated = {
331
+ ...result.data,
332
+ ...allowlist && !result.data.harnesses ? { harnesses: allowlist } : {}
333
+ };
334
+ saveConfig(migrated);
335
+ return migrated;
336
+ }
408
337
  return result.data;
409
338
  }
410
339
  function loadConfigTolerant() {
411
340
  const path = configPath();
412
341
  const empty = {};
413
342
  const fresh = { local: empty, note: null, hadPriorConfig: false };
414
- if (!existsSync2(path)) return fresh;
343
+ if (!existsSync3(path)) return fresh;
415
344
  let raw;
416
345
  try {
417
- raw = readFileSync(path, "utf8");
346
+ raw = readFileSync2(path, "utf8");
418
347
  } catch {
419
348
  return fresh;
420
349
  }
@@ -438,11 +367,8 @@ function loadConfigTolerant() {
438
367
  autoOpen,
439
368
  logLevel,
440
369
  logFormat,
441
- claudeCode: claudeCode2,
442
- codex: codex2,
443
- opencode: opencode2,
444
- opencodeDeclined,
445
- harnessesChosen
370
+ harnesses: harnesses2,
371
+ opencode: opencode2
446
372
  } = strict.data;
447
373
  return {
448
374
  local: {
@@ -452,11 +378,8 @@ function loadConfigTolerant() {
452
378
  ...autoOpen !== void 0 ? { autoOpen } : {},
453
379
  ...logLevel !== void 0 ? { logLevel } : {},
454
380
  ...logFormat !== void 0 ? { logFormat } : {},
455
- ...claudeCode2 !== void 0 ? { claudeCode: claudeCode2 } : {},
456
- ...codex2 !== void 0 ? { codex: codex2 } : {},
457
- ...opencode2 !== void 0 ? { opencode: opencode2 } : {},
458
- ...opencodeDeclined !== void 0 ? { opencodeDeclined } : {},
459
- ...harnessesChosen !== void 0 ? { harnessesChosen } : {}
381
+ ...harnesses2 !== void 0 ? { harnesses: harnesses2 } : {},
382
+ ...opencode2 !== void 0 ? { opencode: opencode2 } : {}
460
383
  },
461
384
  note: null,
462
385
  hadPriorConfig: true
@@ -474,14 +397,10 @@ function loadConfigTolerant() {
474
397
  if (obj.logLevel === "warn" || obj.logLevel === "info" || obj.logLevel === "debug") {
475
398
  local.logLevel = obj.logLevel;
476
399
  }
477
- const claudeCode = companionConfigSchema.shape.claudeCode.safeParse(obj.claudeCode);
478
- if (claudeCode.success && claudeCode.data !== void 0) local.claudeCode = claudeCode.data;
479
- const codex = companionConfigSchema.shape.codex.safeParse(obj.codex);
480
- if (codex.success && codex.data !== void 0) local.codex = codex.data;
400
+ const harnesses = companionConfigSchema.shape.harnesses.safeParse(obj.harnesses);
401
+ if (harnesses.success && harnesses.data !== void 0) local.harnesses = harnesses.data;
481
402
  const opencode = companionConfigSchema.shape.opencode.safeParse(obj.opencode);
482
403
  if (opencode.success && opencode.data !== void 0) local.opencode = opencode.data;
483
- if (obj.opencodeDeclined === true) local.opencodeDeclined = true;
484
- if (obj.harnessesChosen === true) local.harnessesChosen = true;
485
404
  return {
486
405
  local,
487
406
  note: `the existing ${path} was from an older or incompatible companion; re-pairing rewrote it.`,
@@ -628,21 +547,14 @@ function resolvePairBaseUrl(server) {
628
547
  const raw = (server ?? process.env.CABANE_BASE_URL ?? DEFAULT_BASE_URL).trim();
629
548
  if (!isAllowedBaseUrl(raw)) {
630
549
  throw new CompanionError(
631
- `invalid server URL "${raw}": must be an https URL (loopback http is allowed for local dev). Pass it as \`${cabane("start --server https://your-cabane")}\`.`
550
+ `invalid server URL "${raw}": must be an https URL (loopback http is allowed for local dev). Pass it as \`${cabane("setup --server https://your-cabane")}\`.`
632
551
  );
633
552
  }
634
553
  return raw;
635
554
  }
636
555
  function writePairedConfig(paired) {
637
- const { local, note, hadPriorConfig } = loadConfigTolerant();
556
+ const { local, note } = loadConfigTolerant();
638
557
  const config = {
639
- // A device pairs connected to NOTHING, and its agents are chosen from here
640
- // on: `harnessesChosen` tells the harness migration that an absent
641
- // `claudeCode` block means "never offered", not a legacy Claude device.
642
- // Only when there was no config before: stamped on a RE-pair, an upgrading
643
- // user's working device would be disconnected. `local` spreads after,
644
- // keeping a prior answer.
645
- ...hadPriorConfig ? {} : { harnessesChosen: true },
646
558
  ...local,
647
559
  baseUrl: paired.baseUrl,
648
560
  deviceToken: paired.deviceToken,