@cabane/companion 0.6.159 → 0.6.161

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