@cabane/companion 0.6.20 → 0.6.22
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +215 -64
- package/dist/pairing-config.js +47 -9
- package/dist/runtime.js +171 -51
- package/dist/static/app.js +11 -3
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -295,6 +295,26 @@ var companionConfigSchema = z2.object({
|
|
|
295
295
|
opencode: z2.object({
|
|
296
296
|
serverUrl: z2.string().url()
|
|
297
297
|
}).strict().optional(),
|
|
298
|
+
// CT1082: the Claude Code runtime, when the user connected it on this machine.
|
|
299
|
+
// The change this task exists for: Claude Code used to be the one harness a
|
|
300
|
+
// device exposed with no configuration — a `claude --version` exit-0 was taken as
|
|
301
|
+
// consent — and it is now the third config-driven harness, opted into exactly
|
|
302
|
+
// like codex. `claude` on PATH is still required (you can't run what isn't
|
|
303
|
+
// installed), but presence alone no longer exposes anything: the manifest
|
|
304
|
+
// advertises claude-code only when this block says so AND the binary is there.
|
|
305
|
+
//
|
|
306
|
+
// The block's PRESENCE is also the migration marker (see `migrateConnectedHarnesses`).
|
|
307
|
+
// Absent means the config predates this task — a device whose user was never
|
|
308
|
+
// asked — and the migration grandfathers it on first start. So every config
|
|
309
|
+
// written from here on carries the block explicitly, including a freshly-paired
|
|
310
|
+
// one, which starts at `enabled: false`: a new device is connected to nothing
|
|
311
|
+
// until its user says otherwise.
|
|
312
|
+
//
|
|
313
|
+
// Not to be confused with the per-agent `agents.<key>.claudeCode.autoMemory`
|
|
314
|
+
// above — that's one agent's memory switch, this is the device's connected set.
|
|
315
|
+
claudeCode: z2.object({
|
|
316
|
+
enabled: z2.boolean().optional()
|
|
317
|
+
}).strict().optional(),
|
|
298
318
|
// CT481: the codex runtime, when the operator runs Codex on this machine. Unlike
|
|
299
319
|
// opencode (a long-lived `opencode serve` addressed by URL), Codex is a local CLI
|
|
300
320
|
// the `@openai/codex-sdk` spawns per turn — so the config is just an opt-in flag,
|
|
@@ -311,6 +331,13 @@ var companionConfigSchema = z2.object({
|
|
|
311
331
|
function isCodexEnabled(cfg) {
|
|
312
332
|
return !!cfg.codex && cfg.codex.enabled !== false;
|
|
313
333
|
}
|
|
334
|
+
function isClaudeCodeConnected(cfg) {
|
|
335
|
+
return !!cfg.claudeCode && cfg.claudeCode.enabled !== false;
|
|
336
|
+
}
|
|
337
|
+
function migrateConnectedHarnesses(cfg, claudeOnPath2) {
|
|
338
|
+
if (cfg.claudeCode !== void 0) return null;
|
|
339
|
+
return { ...cfg, claudeCode: { enabled: claudeOnPath2 } };
|
|
340
|
+
}
|
|
314
341
|
function localAgentConfig(cfg, agent) {
|
|
315
342
|
const map = cfg.agents ?? {};
|
|
316
343
|
return map[agent.agentId] ?? map[agent.agentUsername] ?? map[`${agent.workspaceSlug}/${agent.agentUsername}`] ?? {};
|
|
@@ -352,31 +379,38 @@ function loadConfig() {
|
|
|
352
379
|
function loadConfigTolerant() {
|
|
353
380
|
const path = configPath();
|
|
354
381
|
const empty = {};
|
|
355
|
-
|
|
382
|
+
const fresh = { local: empty, note: null, hadPriorConfig: false };
|
|
383
|
+
if (!existsSync(path)) return fresh;
|
|
356
384
|
let raw;
|
|
357
385
|
try {
|
|
358
386
|
raw = readFileSync(path, "utf8");
|
|
359
387
|
} catch {
|
|
360
|
-
return
|
|
388
|
+
return fresh;
|
|
361
389
|
}
|
|
362
|
-
if (raw.trim().length === 0) return
|
|
390
|
+
if (raw.trim().length === 0) return fresh;
|
|
363
391
|
let parsed;
|
|
364
392
|
try {
|
|
365
393
|
parsed = JSON.parse(raw);
|
|
366
394
|
} catch {
|
|
367
|
-
return {
|
|
395
|
+
return {
|
|
396
|
+
local: empty,
|
|
397
|
+
note: `${path} was unreadable (invalid JSON) and has been reset.`,
|
|
398
|
+
hadPriorConfig: true
|
|
399
|
+
};
|
|
368
400
|
}
|
|
369
401
|
const strict = companionConfigSchema.safeParse(parsed);
|
|
370
402
|
if (strict.success) {
|
|
371
|
-
const { agents: agents2, dashboardPort, autoOpen, logLevel } = strict.data;
|
|
403
|
+
const { agents: agents2, dashboardPort, autoOpen, logLevel, claudeCode: claudeCode2 } = strict.data;
|
|
372
404
|
return {
|
|
373
405
|
local: {
|
|
374
406
|
...agents2 !== void 0 ? { agents: agents2 } : {},
|
|
375
407
|
...dashboardPort !== void 0 ? { dashboardPort } : {},
|
|
376
408
|
...autoOpen !== void 0 ? { autoOpen } : {},
|
|
377
|
-
...logLevel !== void 0 ? { logLevel } : {}
|
|
409
|
+
...logLevel !== void 0 ? { logLevel } : {},
|
|
410
|
+
...claudeCode2 !== void 0 ? { claudeCode: claudeCode2 } : {}
|
|
378
411
|
},
|
|
379
|
-
note: null
|
|
412
|
+
note: null,
|
|
413
|
+
hadPriorConfig: true
|
|
380
414
|
};
|
|
381
415
|
}
|
|
382
416
|
const obj = parsed && typeof parsed === "object" ? parsed : {};
|
|
@@ -388,9 +422,12 @@ function loadConfigTolerant() {
|
|
|
388
422
|
if (obj.logLevel === "warn" || obj.logLevel === "info" || obj.logLevel === "debug") {
|
|
389
423
|
local.logLevel = obj.logLevel;
|
|
390
424
|
}
|
|
425
|
+
const claudeCode = companionConfigSchema.shape.claudeCode.safeParse(obj.claudeCode);
|
|
426
|
+
if (claudeCode.success && claudeCode.data !== void 0) local.claudeCode = claudeCode.data;
|
|
391
427
|
return {
|
|
392
428
|
local,
|
|
393
|
-
note: `the existing ${path} was from an older or incompatible companion; re-pairing rewrote it
|
|
429
|
+
note: `the existing ${path} was from an older or incompatible companion; re-pairing rewrote it.`,
|
|
430
|
+
hadPriorConfig: true
|
|
394
431
|
};
|
|
395
432
|
}
|
|
396
433
|
function saveConfig(cfg) {
|
|
@@ -592,26 +629,43 @@ async function codexOnPath() {
|
|
|
592
629
|
]);
|
|
593
630
|
return version !== null;
|
|
594
631
|
}
|
|
595
|
-
async function
|
|
632
|
+
async function requireStartConfig(deps = {}) {
|
|
633
|
+
const requireCfg = deps.requireCfg ?? requireConfig;
|
|
634
|
+
const probeClaude = deps.probeClaude ?? claudeOnPath;
|
|
635
|
+
const save2 = deps.save ?? saveConfig;
|
|
636
|
+
const cfg = requireCfg();
|
|
637
|
+
const claudeOnPathResult = await probeClaude();
|
|
638
|
+
const migrated = migrateConnectedHarnesses(cfg, claudeOnPathResult);
|
|
639
|
+
if (!migrated) return { cfg, claudeOnPath: claudeOnPathResult };
|
|
640
|
+
save2(migrated);
|
|
641
|
+
deps.onMigrated?.(migrated, claudeOnPathResult);
|
|
642
|
+
return { cfg: migrated, claudeOnPath: claudeOnPathResult };
|
|
643
|
+
}
|
|
644
|
+
async function warnAboutHarnessReadiness(cfg, deps = {}) {
|
|
596
645
|
const probeClaude = deps.probeClaude ?? claudeOnPath;
|
|
597
646
|
const probeCodex = deps.probeCodex ?? codexOnPath;
|
|
598
647
|
const warn = deps.warn ?? ((message) => process.stderr.write(`${message}
|
|
599
648
|
`));
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
...cfg
|
|
603
|
-
...
|
|
649
|
+
const connected = [
|
|
650
|
+
...isClaudeCodeConnected(cfg) ? ["Claude Code"] : [],
|
|
651
|
+
...isCodexEnabled(cfg) ? ["Codex"] : [],
|
|
652
|
+
...cfg.opencode ? ["opencode"] : []
|
|
604
653
|
];
|
|
605
|
-
if (
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
654
|
+
if (connected.length > 0) {
|
|
655
|
+
if (isClaudeCodeConnected(cfg) && !await probeClaude()) {
|
|
656
|
+
warn(
|
|
657
|
+
"warning: Claude Code is connected on this device but `claude` isn\u2019t on your PATH, so it advertises nothing and a Claude-model agent won\u2019t be routed here. Install it (`npm i -g @anthropic-ai/claude-code`) and log in, or disconnect it."
|
|
658
|
+
);
|
|
659
|
+
}
|
|
610
660
|
return;
|
|
611
661
|
}
|
|
612
|
-
const
|
|
613
|
-
|
|
614
|
-
|
|
662
|
+
const [claudeInstalled, codexInstalled] = await Promise.all([probeClaude(), probeCodex()]);
|
|
663
|
+
const installed = [
|
|
664
|
+
...claudeInstalled ? ["Claude Code"] : [],
|
|
665
|
+
...codexInstalled ? ["Codex"] : []
|
|
666
|
+
];
|
|
667
|
+
warn(
|
|
668
|
+
"No harness is connected on this device yet, so no agent turn can run here. " + (installed.length > 0 ? `We found ${installed.join(" and ")} on this machine \u2014 connect ${installed.length > 1 ? "one" : "it"} in the Companion dashboard (or in cabane, Settings \u2192 Devices) and turns start routing here.` : "Install a harness and sign in \u2014 Claude Code (`npm i -g @anthropic-ai/claude-code`), the Codex CLI (`codex login`), or `opencode serve` \u2014 then connect it in the Companion dashboard.")
|
|
615
669
|
);
|
|
616
670
|
}
|
|
617
671
|
|
|
@@ -709,15 +763,28 @@ function trimSlash(s) {
|
|
|
709
763
|
var STARTUP_TIMEOUT_MS = 8e3;
|
|
710
764
|
var POLL_INTERVAL_MS = 150;
|
|
711
765
|
async function startDaemon(opts = {}, deps = {}) {
|
|
712
|
-
const ensureRuntime = deps.ensureRuntime ??
|
|
713
|
-
const requireCfg = deps.requireCfg ?? requireConfig;
|
|
766
|
+
const ensureRuntime = deps.ensureRuntime ?? warnAboutHarnessReadiness;
|
|
714
767
|
const readState = deps.readState ?? readLiveRuntimeState;
|
|
715
768
|
const verify = deps.verify ?? ((s) => verifyRuntime(s));
|
|
716
769
|
const spawnDetached = deps.spawnDetached ?? defaultSpawnDetached;
|
|
717
770
|
const sleep4 = deps.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
|
|
718
771
|
const now = deps.now ?? (() => Date.now());
|
|
719
|
-
const cfg =
|
|
720
|
-
|
|
772
|
+
const { cfg, claudeOnPath: claudeOnPath2 } = await requireStartConfig({
|
|
773
|
+
// Unset → `requireStartConfig`'s own `requireConfig`, the real one.
|
|
774
|
+
...deps.requireCfg ? { requireCfg: deps.requireCfg } : {},
|
|
775
|
+
...deps.probeClaude ? { probeClaude: deps.probeClaude } : {},
|
|
776
|
+
...deps.save ? { save: deps.save } : {},
|
|
777
|
+
// The child won't log it (the block is written by then), and this process owns
|
|
778
|
+
// the user's terminal — so the one-time record is one line, here.
|
|
779
|
+
onMigrated: (_migrated, onPath) => {
|
|
780
|
+
if (onPath) {
|
|
781
|
+
process.stdout.write(
|
|
782
|
+
"Carried Claude Code over as a connected harness on this device \u2014 connectors are chosen now, not detected.\n"
|
|
783
|
+
);
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
});
|
|
787
|
+
await ensureRuntime(cfg, { probeClaude: async () => claudeOnPath2 });
|
|
721
788
|
const existing = readState();
|
|
722
789
|
if (existing) {
|
|
723
790
|
if (await verify(existing) !== "stale") {
|
|
@@ -992,8 +1059,16 @@ function resolvePairBaseUrl(server) {
|
|
|
992
1059
|
return raw;
|
|
993
1060
|
}
|
|
994
1061
|
function writePairedConfig(paired) {
|
|
995
|
-
const { local, note } = loadConfigTolerant();
|
|
1062
|
+
const { local, note, hadPriorConfig } = loadConfigTolerant();
|
|
996
1063
|
const config = {
|
|
1064
|
+
// CT1082: a device pairs connected to NOTHING — `claudeCode: { enabled: false }`
|
|
1065
|
+
// written explicitly, not left absent, because absence is exactly what marks a
|
|
1066
|
+
// pre-CT1082 config for the grandfathering migration. Omit it and a brand-new
|
|
1067
|
+
// device gets auto-connected on its first start by the behaviour this task
|
|
1068
|
+
// removes; stamp it on a RE-pair and an upgrading user's working device gets
|
|
1069
|
+
// disconnected instead. So it's written only when there was no config here
|
|
1070
|
+
// before. `local` spreads after, so a re-pair keeps the answer already given.
|
|
1071
|
+
...hadPriorConfig ? {} : { claudeCode: { enabled: false } },
|
|
997
1072
|
...local,
|
|
998
1073
|
baseUrl: paired.baseUrl,
|
|
999
1074
|
deviceToken: paired.deviceToken,
|
|
@@ -1390,6 +1465,11 @@ function registerRoutes(app, deps) {
|
|
|
1390
1465
|
app.post("/api/harnesses/enable", async (c) => {
|
|
1391
1466
|
const body = await readJson(c);
|
|
1392
1467
|
const runtime = body.runtime;
|
|
1468
|
+
if (runtime === "claude-code") {
|
|
1469
|
+
const result = await supervisor.enableHarness({ runtime: "claude-code" });
|
|
1470
|
+
if (!result.ok) return c.json({ error: result.error }, 400);
|
|
1471
|
+
return c.json({ ok: true, status: hub.statusJson() });
|
|
1472
|
+
}
|
|
1393
1473
|
if (runtime === "codex") {
|
|
1394
1474
|
const result = await supervisor.enableHarness({ runtime: "codex" });
|
|
1395
1475
|
if (!result.ok) return c.json({ error: result.error }, 400);
|
|
@@ -6754,10 +6834,11 @@ function describeSubAgentError(status2, body) {
|
|
|
6754
6834
|
var Dispatcher = class {
|
|
6755
6835
|
constructor(opts) {
|
|
6756
6836
|
this.opts = opts;
|
|
6837
|
+
this.aborts = opts.aborts ?? /* @__PURE__ */ new Map();
|
|
6757
6838
|
}
|
|
6758
6839
|
opts;
|
|
6759
6840
|
// SJ383: per-(conversation, agent) abort registry.
|
|
6760
|
-
aborts
|
|
6841
|
+
aborts;
|
|
6761
6842
|
notifyStart(info) {
|
|
6762
6843
|
try {
|
|
6763
6844
|
this.opts.observer?.onStart(info);
|
|
@@ -7671,7 +7752,9 @@ var LABELS = {
|
|
|
7671
7752
|
function deriveHarnessSnapshot(signals) {
|
|
7672
7753
|
const advertised = new Set(
|
|
7673
7754
|
buildCompanionManifest({
|
|
7674
|
-
|
|
7755
|
+
// CT1082: connected AND installed — the manifest's own rule, restated here
|
|
7756
|
+
// through the same function rather than re-decided.
|
|
7757
|
+
claudeCode: signals.claudeCodeConnected && signals.claudeOnPath,
|
|
7675
7758
|
opencode: signals.opencodeConfigured,
|
|
7676
7759
|
codex: signals.codexEnabled
|
|
7677
7760
|
}).runtimes.map((r) => r.name)
|
|
@@ -7684,20 +7767,40 @@ function deriveHarnessSnapshot(signals) {
|
|
|
7684
7767
|
return { harnesses, anyExposed: harnesses.some((h) => h.state === "exposed") };
|
|
7685
7768
|
}
|
|
7686
7769
|
function deriveClaudeCode(signals, manifestHas) {
|
|
7687
|
-
const base = { runtime: "claude-code", label: LABELS["claude-code"]
|
|
7770
|
+
const base = { runtime: "claude-code", label: LABELS["claude-code"] };
|
|
7688
7771
|
if (manifestHas) {
|
|
7689
7772
|
return {
|
|
7690
7773
|
...base,
|
|
7691
7774
|
state: "exposed",
|
|
7692
7775
|
version: signals.claudeVersion,
|
|
7693
|
-
detail: "Claude Code is
|
|
7776
|
+
detail: "Claude Code is connected and exposed to Cabane.",
|
|
7777
|
+
enable: null
|
|
7778
|
+
};
|
|
7779
|
+
}
|
|
7780
|
+
if (signals.claudeCodeConnected) {
|
|
7781
|
+
return {
|
|
7782
|
+
...base,
|
|
7783
|
+
state: "needs_attention",
|
|
7784
|
+
version: null,
|
|
7785
|
+
detail: "Connected, but the `claude` CLI isn\u2019t on your PATH. Install it (`npm i -g @anthropic-ai/claude-code`) and sign in, or disconnect it.",
|
|
7786
|
+
enable: null
|
|
7787
|
+
};
|
|
7788
|
+
}
|
|
7789
|
+
if (signals.claudeOnPath) {
|
|
7790
|
+
return {
|
|
7791
|
+
...base,
|
|
7792
|
+
state: "detected_not_exposed",
|
|
7793
|
+
version: signals.claudeVersion,
|
|
7794
|
+
detail: "Claude Code is installed here but not connected yet. Connect it to let Cabane run Claude Code on this device.",
|
|
7795
|
+
enable: "claude-code"
|
|
7694
7796
|
};
|
|
7695
7797
|
}
|
|
7696
7798
|
return {
|
|
7697
7799
|
...base,
|
|
7698
7800
|
state: "not_detected",
|
|
7699
7801
|
version: null,
|
|
7700
|
-
detail: "Not detected. Install Claude Code (`npm i -g @anthropic-ai/claude-code`) and sign in with `claude`, then it
|
|
7802
|
+
detail: "Not detected. Install Claude Code (`npm i -g @anthropic-ai/claude-code`) and sign in with `claude`, then connect it here.",
|
|
7803
|
+
enable: null
|
|
7701
7804
|
};
|
|
7702
7805
|
}
|
|
7703
7806
|
function deriveCodex(signals, manifestHas) {
|
|
@@ -7765,6 +7868,9 @@ function deriveOpencode(signals, manifestHas) {
|
|
|
7765
7868
|
enable: "opencode"
|
|
7766
7869
|
};
|
|
7767
7870
|
}
|
|
7871
|
+
function detectedRuntimesFor(snapshot) {
|
|
7872
|
+
return snapshot.harnesses.filter((h) => h.state === "detected_not_exposed").map((h) => ({ runtime: h.runtime, version: h.version }));
|
|
7873
|
+
}
|
|
7768
7874
|
var PROBE_TIMEOUT_MS2 = 4e3;
|
|
7769
7875
|
async function probeHarnessSignals(cfg, deps = {}) {
|
|
7770
7876
|
const probeClaudePresence = deps.probeClaudePresence ?? claudeOnPath;
|
|
@@ -7781,6 +7887,9 @@ async function probeHarnessSignals(cfg, deps = {}) {
|
|
|
7781
7887
|
return {
|
|
7782
7888
|
claudeOnPath: claudeOnPathResult,
|
|
7783
7889
|
claudeVersion,
|
|
7890
|
+
// CT1082: the user's opt-in. Presence alone exposes nothing now, so this is
|
|
7891
|
+
// the manifest gate and the probe above is only a suggestion.
|
|
7892
|
+
claudeCodeConnected: isClaudeCodeConnected(cfg),
|
|
7784
7893
|
// A parseable `codex --version` is our presence signal (presence alone never
|
|
7785
7894
|
// exposes codex; its config flag is the manifest gate either way).
|
|
7786
7895
|
codexOnPath: codexVersion !== null,
|
|
@@ -8189,6 +8298,10 @@ var CompanionSupervisor = class {
|
|
|
8189
8298
|
// can never disagree with what the manifest advertises. Null until the first
|
|
8190
8299
|
// probe (the heartbeat then falls back to the boot `claudeCode`).
|
|
8191
8300
|
harnessSignals = null;
|
|
8301
|
+
// CT1082: the fresh PATH probe the claude-code connect vets with. Deliberately
|
|
8302
|
+
// NOT the cached beat signal — someone connecting right after installing Claude
|
|
8303
|
+
// Code shouldn't be refused by a snapshot up to a heartbeat old.
|
|
8304
|
+
probeClaudePresence;
|
|
8192
8305
|
exitFn;
|
|
8193
8306
|
reexecFn;
|
|
8194
8307
|
dispatcherFactory;
|
|
@@ -8219,6 +8332,7 @@ var CompanionSupervisor = class {
|
|
|
8219
8332
|
this.log = opts.log;
|
|
8220
8333
|
this.hub = opts.hub;
|
|
8221
8334
|
this.claudeCode = opts.claudeCode ?? true;
|
|
8335
|
+
this.probeClaudePresence = opts.probeClaudePresence ?? claudeOnPath;
|
|
8222
8336
|
this.harnessVersions = opts.harnessVersions ?? emptyHarnessVersions();
|
|
8223
8337
|
this.exitFn = opts.exit ?? ((code) => process.exit(code));
|
|
8224
8338
|
this.reexecFn = opts.reexec ?? defaultReexec;
|
|
@@ -8281,11 +8395,10 @@ var CompanionSupervisor = class {
|
|
|
8281
8395
|
// claude-code when `claude` is on PATH, CT270 opencode when the operator
|
|
8282
8396
|
// configured an `opencode serve`.
|
|
8283
8397
|
manifest: buildCompanionManifest({
|
|
8284
|
-
//
|
|
8285
|
-
// until the first
|
|
8286
|
-
//
|
|
8287
|
-
|
|
8288
|
-
claudeCode: this.claudeCodePresent(),
|
|
8398
|
+
// CT1082: connected AND installed. Presence is the live re-probe (CT586),
|
|
8399
|
+
// falling back to the boot probe until the first one lands; consent is the
|
|
8400
|
+
// user's `claudeCode.enabled`. Claude Code no longer rides presence alone.
|
|
8401
|
+
claudeCode: this.claudeCodeOffered(),
|
|
8289
8402
|
opencode: !!this.config.opencode?.serverUrl,
|
|
8290
8403
|
// CT481: advertise codex when the operator enabled it (config-gated,
|
|
8291
8404
|
// like opencode — the CLI's presence is the operator's responsibility;
|
|
@@ -8303,7 +8416,13 @@ var CompanionSupervisor = class {
|
|
|
8303
8416
|
// CT584: include enumerated models only when the probe SUCCEEDED (non-null).
|
|
8304
8417
|
// A null (probe failed / no opencode) omits the field, and the server then
|
|
8305
8418
|
// leaves this device's stored availability untouched.
|
|
8306
|
-
...opencodeModels !== null ? { models: opencodeModels } : {}
|
|
8419
|
+
...opencodeModels !== null ? { models: opencodeModels } : {},
|
|
8420
|
+
// CT1082: what this machine has that the user hasn't connected, so the web
|
|
8421
|
+
// UI can offer it without a companion round-trip. Sent only once a probe has
|
|
8422
|
+
// actually landed (`harnessSignals` non-null) — an absent field means "we
|
|
8423
|
+
// didn't look this beat" and leaves the server's stored suggestion alone,
|
|
8424
|
+
// the same fail-soft contract `models` keeps.
|
|
8425
|
+
...this.harnessSignals ? { detectedRuntimes: detectedRuntimesFor(deriveHarnessSnapshot(this.harnessSignals)) } : {}
|
|
8307
8426
|
});
|
|
8308
8427
|
this.hub.setDevice({ deviceId: res.deviceId });
|
|
8309
8428
|
this.deviceId = res.deviceId;
|
|
@@ -8469,6 +8588,7 @@ var CompanionSupervisor = class {
|
|
|
8469
8588
|
outbox,
|
|
8470
8589
|
log: this.log
|
|
8471
8590
|
});
|
|
8591
|
+
const aborts = /* @__PURE__ */ new Map();
|
|
8472
8592
|
const dispatcher = this.buildDispatcher({
|
|
8473
8593
|
api,
|
|
8474
8594
|
baseUrl: this.config.baseUrl,
|
|
@@ -8477,7 +8597,8 @@ var CompanionSupervisor = class {
|
|
|
8477
8597
|
agentId: it.agentId,
|
|
8478
8598
|
agentUsername: it.agentUsername,
|
|
8479
8599
|
credential,
|
|
8480
|
-
runConfig
|
|
8600
|
+
runConfig,
|
|
8601
|
+
aborts
|
|
8481
8602
|
});
|
|
8482
8603
|
const drain2 = this.makeDrain(api, it.agentId);
|
|
8483
8604
|
wr.agents.set(it.agentId, {
|
|
@@ -8487,6 +8608,7 @@ var CompanionSupervisor = class {
|
|
|
8487
8608
|
credential,
|
|
8488
8609
|
runConfig,
|
|
8489
8610
|
api,
|
|
8611
|
+
aborts,
|
|
8490
8612
|
dispatcher,
|
|
8491
8613
|
cancelDrain: drain2.cancel,
|
|
8492
8614
|
kickDrain: drain2.kick
|
|
@@ -8508,15 +8630,26 @@ var CompanionSupervisor = class {
|
|
|
8508
8630
|
"companion: stopped agent (unassigned)"
|
|
8509
8631
|
);
|
|
8510
8632
|
}
|
|
8511
|
-
// CT833: is Claude Code on this machine right now?
|
|
8512
|
-
//
|
|
8513
|
-
//
|
|
8514
|
-
// per-beat re-probe), falling back to the boot probe until the first one lands.
|
|
8633
|
+
// CT833: is Claude Code on this machine right now? Live (the per-beat re-probe),
|
|
8634
|
+
// falling back to the boot probe until the first one lands. Presence ONLY —
|
|
8635
|
+
// CT1082 split presence from exposure, so nothing routes on this directly.
|
|
8515
8636
|
claudeCodePresent() {
|
|
8516
8637
|
return this.harnessSignals?.claudeOnPath ?? this.claudeCode;
|
|
8517
8638
|
}
|
|
8639
|
+
// CT1082: does this device OFFER claude-code — connected by its user and actually
|
|
8640
|
+
// installed? The ONE signal both the heartbeat manifest and the dispatcher's
|
|
8641
|
+
// adapter registry read, so what the device advertises and what it can select
|
|
8642
|
+
// can't disagree (the CT833 invariant, now with consent in front of it).
|
|
8643
|
+
claudeCodeOffered() {
|
|
8644
|
+
return isClaudeCodeConnected(this.config) && this.claudeCodePresent();
|
|
8645
|
+
}
|
|
8518
8646
|
buildDispatcher(ctx) {
|
|
8519
|
-
|
|
8647
|
+
const local = localAgentConfig(this.config, {
|
|
8648
|
+
agentId: ctx.agentId,
|
|
8649
|
+
agentUsername: ctx.agentUsername,
|
|
8650
|
+
workspaceSlug: ctx.workspaceSlug
|
|
8651
|
+
});
|
|
8652
|
+
if (this.dispatcherFactory) return this.dispatcherFactory({ ...ctx, local });
|
|
8520
8653
|
return new Dispatcher({
|
|
8521
8654
|
api: ctx.api,
|
|
8522
8655
|
baseUrl: ctx.baseUrl,
|
|
@@ -8525,17 +8658,16 @@ var CompanionSupervisor = class {
|
|
|
8525
8658
|
agentId: ctx.agentId,
|
|
8526
8659
|
agentUsername: ctx.agentUsername,
|
|
8527
8660
|
credential: ctx.credential,
|
|
8528
|
-
local
|
|
8529
|
-
|
|
8530
|
-
agentUsername: ctx.agentUsername,
|
|
8531
|
-
workspaceSlug: ctx.workspaceSlug
|
|
8532
|
-
}),
|
|
8661
|
+
local,
|
|
8662
|
+
aborts: ctx.aborts,
|
|
8533
8663
|
runConfig: ctx.runConfig,
|
|
8534
8664
|
log: this.log,
|
|
8535
|
-
// CT833: register the claude-code adapter only when
|
|
8536
|
-
//
|
|
8537
|
-
// after boot works on the next turn exactly as it
|
|
8538
|
-
|
|
8665
|
+
// CT833: register the claude-code adapter only when this device actually
|
|
8666
|
+
// offers claude-code — read per turn (not captured here), so a harness
|
|
8667
|
+
// installed or connected after boot works on the next turn exactly as it
|
|
8668
|
+
// appears on the next beat. CT1082: "offers" now means connected as well as
|
|
8669
|
+
// installed, so a disconnected harness can't be selected either.
|
|
8670
|
+
claudeCodeAvailable: () => this.claudeCodeOffered(),
|
|
8539
8671
|
// CT270: the opencode server URL (operator-configured), when this device
|
|
8540
8672
|
// offers the opencode runtime. Threaded so an opencode turn selects the
|
|
8541
8673
|
// opencode adapter; unset leaves the device claude-code-only.
|
|
@@ -8797,6 +8929,7 @@ var CompanionSupervisor = class {
|
|
|
8797
8929
|
if (!next) return;
|
|
8798
8930
|
this.config = next;
|
|
8799
8931
|
this.log.level = next.logLevel ?? "debug";
|
|
8932
|
+
this.rebuildDispatchers();
|
|
8800
8933
|
void this.refreshAssignments();
|
|
8801
8934
|
}
|
|
8802
8935
|
// The dashboard "refresh assignments" control — an immediate pull.
|
|
@@ -8841,9 +8974,12 @@ var CompanionSupervisor = class {
|
|
|
8841
8974
|
async recheckHarnesses() {
|
|
8842
8975
|
await this.refreshHarnessStatuses();
|
|
8843
8976
|
}
|
|
8844
|
-
// Friendly enable for the
|
|
8845
|
-
//
|
|
8846
|
-
//
|
|
8977
|
+
// Friendly enable for the config-driven harnesses — flip the flag the app owns in
|
|
8978
|
+
// `~/.cabane/config.json`, no hand-edited JSON. This is enable/expose ONLY: it
|
|
8979
|
+
// never installs a binary and never drives a login (BYO — Decided).
|
|
8980
|
+
// - claude-code (CT1082): set `claudeCode.enabled`, after checking `claude` is
|
|
8981
|
+
// on PATH. This is the callable the terminal offer ("We found Claude Code.
|
|
8982
|
+
// Connect it? [Y/n]") and the web pairing flow both wire to.
|
|
8847
8983
|
// - codex: set `codex.enabled` (the CLI + `codex login` remain the user's).
|
|
8848
8984
|
// - opencode: set `opencode.serverUrl` — but ONLY after health-probing the URL,
|
|
8849
8985
|
// so we never advertise a serve that isn't there. An unreachable URL is a
|
|
@@ -8853,7 +8989,15 @@ var CompanionSupervisor = class {
|
|
|
8853
8989
|
// reachable/valid input).
|
|
8854
8990
|
async enableHarness(input) {
|
|
8855
8991
|
let next;
|
|
8856
|
-
if (input.runtime === "
|
|
8992
|
+
if (input.runtime === "claude-code") {
|
|
8993
|
+
if (!await this.probeClaudePresence()) {
|
|
8994
|
+
return {
|
|
8995
|
+
ok: false,
|
|
8996
|
+
error: "Couldn\u2019t find `claude` on this machine\u2019s PATH. Install Claude Code (`npm i -g @anthropic-ai/claude-code`) and sign in, then connect it."
|
|
8997
|
+
};
|
|
8998
|
+
}
|
|
8999
|
+
next = { ...this.config, claudeCode: { enabled: true } };
|
|
9000
|
+
} else if (input.runtime === "codex") {
|
|
8857
9001
|
next = { ...this.config, codex: { enabled: true } };
|
|
8858
9002
|
} else {
|
|
8859
9003
|
const serverUrl = input.serverUrl.trim();
|
|
@@ -8883,8 +9027,8 @@ var CompanionSupervisor = class {
|
|
|
8883
9027
|
}
|
|
8884
9028
|
// Re-create every running agent's Dispatcher from the CURRENT config, keeping
|
|
8885
9029
|
// the runner (its api/outbox/chains/SSE) intact. Used after a runtime-enabling
|
|
8886
|
-
// config change so
|
|
8887
|
-
//
|
|
9030
|
+
// config change so constructor-captured options — per-agent cwd/prepareHook and
|
|
9031
|
+
// the adapter registry — pick up their new values without an app restart.
|
|
8888
9032
|
rebuildDispatchers() {
|
|
8889
9033
|
for (const wr of this.workspaces.values()) {
|
|
8890
9034
|
for (const runner of wr.agents.values()) {
|
|
@@ -8896,7 +9040,8 @@ var CompanionSupervisor = class {
|
|
|
8896
9040
|
agentId: runner.agentId,
|
|
8897
9041
|
agentUsername: runner.username,
|
|
8898
9042
|
credential: runner.credential,
|
|
8899
|
-
runConfig: runner.runConfig
|
|
9043
|
+
runConfig: runner.runConfig,
|
|
9044
|
+
aborts: runner.aborts
|
|
8900
9045
|
});
|
|
8901
9046
|
}
|
|
8902
9047
|
}
|
|
@@ -9055,13 +9200,17 @@ function clearCrash() {
|
|
|
9055
9200
|
async function createCompanionRuntime(opts = {}) {
|
|
9056
9201
|
const log = getLogger();
|
|
9057
9202
|
installProcessSafetyNet(log);
|
|
9058
|
-
const probeClaude = opts.probeClaude ?? claudeOnPath;
|
|
9059
9203
|
let cfg;
|
|
9060
9204
|
let claudeCode;
|
|
9061
9205
|
try {
|
|
9062
|
-
cfg =
|
|
9063
|
-
|
|
9064
|
-
|
|
9206
|
+
({ cfg, claudeOnPath: claudeCode } = await requireStartConfig({
|
|
9207
|
+
...opts.probeClaude ? { probeClaude: opts.probeClaude } : {},
|
|
9208
|
+
onMigrated: (migrated, onPath) => log.info(
|
|
9209
|
+
{ claudeCode: isClaudeCodeConnected(migrated) },
|
|
9210
|
+
onPath ? "companion: carried Claude Code over as a connected harness on this device (connectors are now chosen, not detected)" : "companion: no Claude Code on PATH, so this device starts with it disconnected (connectors are now chosen, not detected)"
|
|
9211
|
+
)
|
|
9212
|
+
}));
|
|
9213
|
+
await warnAboutHarnessReadiness(cfg, { probeClaude: async () => claudeCode });
|
|
9065
9214
|
} catch (err) {
|
|
9066
9215
|
recordCrash({
|
|
9067
9216
|
reason: err instanceof Error ? err.message : String(err),
|
|
@@ -9073,7 +9222,9 @@ async function createCompanionRuntime(opts = {}) {
|
|
|
9073
9222
|
}
|
|
9074
9223
|
if (cfg.logLevel) log.level = cfg.logLevel;
|
|
9075
9224
|
const harnessVersions = await probeHarnessVersions({
|
|
9076
|
-
|
|
9225
|
+
// CT1082: versions are probed for the runtimes this device OFFERS, and
|
|
9226
|
+
// claude-code is offered only when connected as well as installed.
|
|
9227
|
+
claudeCode: claudeCode && isClaudeCodeConnected(cfg),
|
|
9077
9228
|
opencodeServerUrl: cfg.opencode?.serverUrl,
|
|
9078
9229
|
codex: isCodexEnabled(cfg)
|
|
9079
9230
|
});
|
package/dist/pairing-config.js
CHANGED
|
@@ -161,6 +161,26 @@ var companionConfigSchema = z2.object({
|
|
|
161
161
|
opencode: z2.object({
|
|
162
162
|
serverUrl: z2.string().url()
|
|
163
163
|
}).strict().optional(),
|
|
164
|
+
// CT1082: the Claude Code runtime, when the user connected it on this machine.
|
|
165
|
+
// The change this task exists for: Claude Code used to be the one harness a
|
|
166
|
+
// device exposed with no configuration — a `claude --version` exit-0 was taken as
|
|
167
|
+
// consent — and it is now the third config-driven harness, opted into exactly
|
|
168
|
+
// like codex. `claude` on PATH is still required (you can't run what isn't
|
|
169
|
+
// installed), but presence alone no longer exposes anything: the manifest
|
|
170
|
+
// advertises claude-code only when this block says so AND the binary is there.
|
|
171
|
+
//
|
|
172
|
+
// The block's PRESENCE is also the migration marker (see `migrateConnectedHarnesses`).
|
|
173
|
+
// Absent means the config predates this task — a device whose user was never
|
|
174
|
+
// asked — and the migration grandfathers it on first start. So every config
|
|
175
|
+
// written from here on carries the block explicitly, including a freshly-paired
|
|
176
|
+
// one, which starts at `enabled: false`: a new device is connected to nothing
|
|
177
|
+
// until its user says otherwise.
|
|
178
|
+
//
|
|
179
|
+
// Not to be confused with the per-agent `agents.<key>.claudeCode.autoMemory`
|
|
180
|
+
// above — that's one agent's memory switch, this is the device's connected set.
|
|
181
|
+
claudeCode: z2.object({
|
|
182
|
+
enabled: z2.boolean().optional()
|
|
183
|
+
}).strict().optional(),
|
|
164
184
|
// CT481: the codex runtime, when the operator runs Codex on this machine. Unlike
|
|
165
185
|
// opencode (a long-lived `opencode serve` addressed by URL), Codex is a local CLI
|
|
166
186
|
// the `@openai/codex-sdk` spawns per turn — so the config is just an opt-in flag,
|
|
@@ -211,31 +231,38 @@ function loadConfig() {
|
|
|
211
231
|
function loadConfigTolerant() {
|
|
212
232
|
const path = configPath();
|
|
213
233
|
const empty = {};
|
|
214
|
-
|
|
234
|
+
const fresh = { local: empty, note: null, hadPriorConfig: false };
|
|
235
|
+
if (!existsSync(path)) return fresh;
|
|
215
236
|
let raw;
|
|
216
237
|
try {
|
|
217
238
|
raw = readFileSync(path, "utf8");
|
|
218
239
|
} catch {
|
|
219
|
-
return
|
|
240
|
+
return fresh;
|
|
220
241
|
}
|
|
221
|
-
if (raw.trim().length === 0) return
|
|
242
|
+
if (raw.trim().length === 0) return fresh;
|
|
222
243
|
let parsed;
|
|
223
244
|
try {
|
|
224
245
|
parsed = JSON.parse(raw);
|
|
225
246
|
} catch {
|
|
226
|
-
return {
|
|
247
|
+
return {
|
|
248
|
+
local: empty,
|
|
249
|
+
note: `${path} was unreadable (invalid JSON) and has been reset.`,
|
|
250
|
+
hadPriorConfig: true
|
|
251
|
+
};
|
|
227
252
|
}
|
|
228
253
|
const strict = companionConfigSchema.safeParse(parsed);
|
|
229
254
|
if (strict.success) {
|
|
230
|
-
const { agents: agents2, dashboardPort, autoOpen, logLevel } = strict.data;
|
|
255
|
+
const { agents: agents2, dashboardPort, autoOpen, logLevel, claudeCode: claudeCode2 } = strict.data;
|
|
231
256
|
return {
|
|
232
257
|
local: {
|
|
233
258
|
...agents2 !== void 0 ? { agents: agents2 } : {},
|
|
234
259
|
...dashboardPort !== void 0 ? { dashboardPort } : {},
|
|
235
260
|
...autoOpen !== void 0 ? { autoOpen } : {},
|
|
236
|
-
...logLevel !== void 0 ? { logLevel } : {}
|
|
261
|
+
...logLevel !== void 0 ? { logLevel } : {},
|
|
262
|
+
...claudeCode2 !== void 0 ? { claudeCode: claudeCode2 } : {}
|
|
237
263
|
},
|
|
238
|
-
note: null
|
|
264
|
+
note: null,
|
|
265
|
+
hadPriorConfig: true
|
|
239
266
|
};
|
|
240
267
|
}
|
|
241
268
|
const obj = parsed && typeof parsed === "object" ? parsed : {};
|
|
@@ -247,9 +274,12 @@ function loadConfigTolerant() {
|
|
|
247
274
|
if (obj.logLevel === "warn" || obj.logLevel === "info" || obj.logLevel === "debug") {
|
|
248
275
|
local.logLevel = obj.logLevel;
|
|
249
276
|
}
|
|
277
|
+
const claudeCode = companionConfigSchema.shape.claudeCode.safeParse(obj.claudeCode);
|
|
278
|
+
if (claudeCode.success && claudeCode.data !== void 0) local.claudeCode = claudeCode.data;
|
|
250
279
|
return {
|
|
251
280
|
local,
|
|
252
|
-
note: `the existing ${path} was from an older or incompatible companion; re-pairing rewrote it
|
|
281
|
+
note: `the existing ${path} was from an older or incompatible companion; re-pairing rewrote it.`,
|
|
282
|
+
hadPriorConfig: true
|
|
253
283
|
};
|
|
254
284
|
}
|
|
255
285
|
function saveConfig(cfg) {
|
|
@@ -375,8 +405,16 @@ function resolvePairBaseUrl(server) {
|
|
|
375
405
|
return raw;
|
|
376
406
|
}
|
|
377
407
|
function writePairedConfig(paired) {
|
|
378
|
-
const { local, note } = loadConfigTolerant();
|
|
408
|
+
const { local, note, hadPriorConfig } = loadConfigTolerant();
|
|
379
409
|
const config = {
|
|
410
|
+
// CT1082: a device pairs connected to NOTHING — `claudeCode: { enabled: false }`
|
|
411
|
+
// written explicitly, not left absent, because absence is exactly what marks a
|
|
412
|
+
// pre-CT1082 config for the grandfathering migration. Omit it and a brand-new
|
|
413
|
+
// device gets auto-connected on its first start by the behaviour this task
|
|
414
|
+
// removes; stamp it on a RE-pair and an upgrading user's working device gets
|
|
415
|
+
// disconnected instead. So it's written only when there was no config here
|
|
416
|
+
// before. `local` spreads after, so a re-pair keeps the answer already given.
|
|
417
|
+
...hadPriorConfig ? {} : { claudeCode: { enabled: false } },
|
|
380
418
|
...local,
|
|
381
419
|
baseUrl: paired.baseUrl,
|
|
382
420
|
deviceToken: paired.deviceToken,
|
package/dist/runtime.js
CHANGED
|
@@ -288,6 +288,26 @@ var companionConfigSchema = z2.object({
|
|
|
288
288
|
opencode: z2.object({
|
|
289
289
|
serverUrl: z2.string().url()
|
|
290
290
|
}).strict().optional(),
|
|
291
|
+
// CT1082: the Claude Code runtime, when the user connected it on this machine.
|
|
292
|
+
// The change this task exists for: Claude Code used to be the one harness a
|
|
293
|
+
// device exposed with no configuration — a `claude --version` exit-0 was taken as
|
|
294
|
+
// consent — and it is now the third config-driven harness, opted into exactly
|
|
295
|
+
// like codex. `claude` on PATH is still required (you can't run what isn't
|
|
296
|
+
// installed), but presence alone no longer exposes anything: the manifest
|
|
297
|
+
// advertises claude-code only when this block says so AND the binary is there.
|
|
298
|
+
//
|
|
299
|
+
// The block's PRESENCE is also the migration marker (see `migrateConnectedHarnesses`).
|
|
300
|
+
// Absent means the config predates this task — a device whose user was never
|
|
301
|
+
// asked — and the migration grandfathers it on first start. So every config
|
|
302
|
+
// written from here on carries the block explicitly, including a freshly-paired
|
|
303
|
+
// one, which starts at `enabled: false`: a new device is connected to nothing
|
|
304
|
+
// until its user says otherwise.
|
|
305
|
+
//
|
|
306
|
+
// Not to be confused with the per-agent `agents.<key>.claudeCode.autoMemory`
|
|
307
|
+
// above — that's one agent's memory switch, this is the device's connected set.
|
|
308
|
+
claudeCode: z2.object({
|
|
309
|
+
enabled: z2.boolean().optional()
|
|
310
|
+
}).strict().optional(),
|
|
291
311
|
// CT481: the codex runtime, when the operator runs Codex on this machine. Unlike
|
|
292
312
|
// opencode (a long-lived `opencode serve` addressed by URL), Codex is a local CLI
|
|
293
313
|
// the `@openai/codex-sdk` spawns per turn — so the config is just an opt-in flag,
|
|
@@ -304,6 +324,13 @@ var companionConfigSchema = z2.object({
|
|
|
304
324
|
function isCodexEnabled(cfg) {
|
|
305
325
|
return !!cfg.codex && cfg.codex.enabled !== false;
|
|
306
326
|
}
|
|
327
|
+
function isClaudeCodeConnected(cfg) {
|
|
328
|
+
return !!cfg.claudeCode && cfg.claudeCode.enabled !== false;
|
|
329
|
+
}
|
|
330
|
+
function migrateConnectedHarnesses(cfg, claudeOnPath2) {
|
|
331
|
+
if (cfg.claudeCode !== void 0) return null;
|
|
332
|
+
return { ...cfg, claudeCode: { enabled: claudeOnPath2 } };
|
|
333
|
+
}
|
|
307
334
|
function localAgentConfig(cfg, agent) {
|
|
308
335
|
const map = cfg.agents ?? {};
|
|
309
336
|
return map[agent.agentId] ?? map[agent.agentUsername] ?? map[`${agent.workspaceSlug}/${agent.agentUsername}`] ?? {};
|
|
@@ -739,6 +766,11 @@ function registerRoutes(app, deps) {
|
|
|
739
766
|
app.post("/api/harnesses/enable", async (c) => {
|
|
740
767
|
const body = await readJson(c);
|
|
741
768
|
const runtime = body.runtime;
|
|
769
|
+
if (runtime === "claude-code") {
|
|
770
|
+
const result = await supervisor.enableHarness({ runtime: "claude-code" });
|
|
771
|
+
if (!result.ok) return c.json({ error: result.error }, 400);
|
|
772
|
+
return c.json({ ok: true, status: hub.statusJson() });
|
|
773
|
+
}
|
|
742
774
|
if (runtime === "codex") {
|
|
743
775
|
const result = await supervisor.enableHarness({ runtime: "codex" });
|
|
744
776
|
if (!result.ok) return c.json({ error: result.error }, 400);
|
|
@@ -1012,26 +1044,43 @@ async function codexOnPath() {
|
|
|
1012
1044
|
]);
|
|
1013
1045
|
return version !== null;
|
|
1014
1046
|
}
|
|
1015
|
-
async function
|
|
1047
|
+
async function requireStartConfig(deps = {}) {
|
|
1048
|
+
const requireCfg = deps.requireCfg ?? requireConfig;
|
|
1049
|
+
const probeClaude = deps.probeClaude ?? claudeOnPath;
|
|
1050
|
+
const save2 = deps.save ?? saveConfig;
|
|
1051
|
+
const cfg = requireCfg();
|
|
1052
|
+
const claudeOnPathResult = await probeClaude();
|
|
1053
|
+
const migrated = migrateConnectedHarnesses(cfg, claudeOnPathResult);
|
|
1054
|
+
if (!migrated) return { cfg, claudeOnPath: claudeOnPathResult };
|
|
1055
|
+
save2(migrated);
|
|
1056
|
+
deps.onMigrated?.(migrated, claudeOnPathResult);
|
|
1057
|
+
return { cfg: migrated, claudeOnPath: claudeOnPathResult };
|
|
1058
|
+
}
|
|
1059
|
+
async function warnAboutHarnessReadiness(cfg, deps = {}) {
|
|
1016
1060
|
const probeClaude = deps.probeClaude ?? claudeOnPath;
|
|
1017
1061
|
const probeCodex = deps.probeCodex ?? codexOnPath;
|
|
1018
1062
|
const warn = deps.warn ?? ((message) => process.stderr.write(`${message}
|
|
1019
1063
|
`));
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
...cfg
|
|
1023
|
-
...
|
|
1064
|
+
const connected = [
|
|
1065
|
+
...isClaudeCodeConnected(cfg) ? ["Claude Code"] : [],
|
|
1066
|
+
...isCodexEnabled(cfg) ? ["Codex"] : [],
|
|
1067
|
+
...cfg.opencode ? ["opencode"] : []
|
|
1024
1068
|
];
|
|
1025
|
-
if (
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1069
|
+
if (connected.length > 0) {
|
|
1070
|
+
if (isClaudeCodeConnected(cfg) && !await probeClaude()) {
|
|
1071
|
+
warn(
|
|
1072
|
+
"warning: Claude Code is connected on this device but `claude` isn\u2019t on your PATH, so it advertises nothing and a Claude-model agent won\u2019t be routed here. Install it (`npm i -g @anthropic-ai/claude-code`) and log in, or disconnect it."
|
|
1073
|
+
);
|
|
1074
|
+
}
|
|
1030
1075
|
return;
|
|
1031
1076
|
}
|
|
1032
|
-
const
|
|
1033
|
-
|
|
1034
|
-
|
|
1077
|
+
const [claudeInstalled, codexInstalled] = await Promise.all([probeClaude(), probeCodex()]);
|
|
1078
|
+
const installed = [
|
|
1079
|
+
...claudeInstalled ? ["Claude Code"] : [],
|
|
1080
|
+
...codexInstalled ? ["Codex"] : []
|
|
1081
|
+
];
|
|
1082
|
+
warn(
|
|
1083
|
+
"No harness is connected on this device yet, so no agent turn can run here. " + (installed.length > 0 ? `We found ${installed.join(" and ")} on this machine \u2014 connect ${installed.length > 1 ? "one" : "it"} in the Companion dashboard (or in cabane, Settings \u2192 Devices) and turns start routing here.` : "Install a harness and sign in \u2014 Claude Code (`npm i -g @anthropic-ai/claude-code`), the Codex CLI (`codex login`), or `opencode serve` \u2014 then connect it in the Companion dashboard.")
|
|
1035
1084
|
);
|
|
1036
1085
|
}
|
|
1037
1086
|
|
|
@@ -6407,10 +6456,11 @@ function describeSubAgentError(status, body) {
|
|
|
6407
6456
|
var Dispatcher = class {
|
|
6408
6457
|
constructor(opts) {
|
|
6409
6458
|
this.opts = opts;
|
|
6459
|
+
this.aborts = opts.aborts ?? /* @__PURE__ */ new Map();
|
|
6410
6460
|
}
|
|
6411
6461
|
opts;
|
|
6412
6462
|
// SJ383: per-(conversation, agent) abort registry.
|
|
6413
|
-
aborts
|
|
6463
|
+
aborts;
|
|
6414
6464
|
notifyStart(info) {
|
|
6415
6465
|
try {
|
|
6416
6466
|
this.opts.observer?.onStart(info);
|
|
@@ -7324,7 +7374,9 @@ var LABELS = {
|
|
|
7324
7374
|
function deriveHarnessSnapshot(signals) {
|
|
7325
7375
|
const advertised = new Set(
|
|
7326
7376
|
buildCompanionManifest({
|
|
7327
|
-
|
|
7377
|
+
// CT1082: connected AND installed — the manifest's own rule, restated here
|
|
7378
|
+
// through the same function rather than re-decided.
|
|
7379
|
+
claudeCode: signals.claudeCodeConnected && signals.claudeOnPath,
|
|
7328
7380
|
opencode: signals.opencodeConfigured,
|
|
7329
7381
|
codex: signals.codexEnabled
|
|
7330
7382
|
}).runtimes.map((r) => r.name)
|
|
@@ -7337,20 +7389,40 @@ function deriveHarnessSnapshot(signals) {
|
|
|
7337
7389
|
return { harnesses, anyExposed: harnesses.some((h) => h.state === "exposed") };
|
|
7338
7390
|
}
|
|
7339
7391
|
function deriveClaudeCode(signals, manifestHas) {
|
|
7340
|
-
const base = { runtime: "claude-code", label: LABELS["claude-code"]
|
|
7392
|
+
const base = { runtime: "claude-code", label: LABELS["claude-code"] };
|
|
7341
7393
|
if (manifestHas) {
|
|
7342
7394
|
return {
|
|
7343
7395
|
...base,
|
|
7344
7396
|
state: "exposed",
|
|
7345
7397
|
version: signals.claudeVersion,
|
|
7346
|
-
detail: "Claude Code is
|
|
7398
|
+
detail: "Claude Code is connected and exposed to Cabane.",
|
|
7399
|
+
enable: null
|
|
7400
|
+
};
|
|
7401
|
+
}
|
|
7402
|
+
if (signals.claudeCodeConnected) {
|
|
7403
|
+
return {
|
|
7404
|
+
...base,
|
|
7405
|
+
state: "needs_attention",
|
|
7406
|
+
version: null,
|
|
7407
|
+
detail: "Connected, but the `claude` CLI isn\u2019t on your PATH. Install it (`npm i -g @anthropic-ai/claude-code`) and sign in, or disconnect it.",
|
|
7408
|
+
enable: null
|
|
7409
|
+
};
|
|
7410
|
+
}
|
|
7411
|
+
if (signals.claudeOnPath) {
|
|
7412
|
+
return {
|
|
7413
|
+
...base,
|
|
7414
|
+
state: "detected_not_exposed",
|
|
7415
|
+
version: signals.claudeVersion,
|
|
7416
|
+
detail: "Claude Code is installed here but not connected yet. Connect it to let Cabane run Claude Code on this device.",
|
|
7417
|
+
enable: "claude-code"
|
|
7347
7418
|
};
|
|
7348
7419
|
}
|
|
7349
7420
|
return {
|
|
7350
7421
|
...base,
|
|
7351
7422
|
state: "not_detected",
|
|
7352
7423
|
version: null,
|
|
7353
|
-
detail: "Not detected. Install Claude Code (`npm i -g @anthropic-ai/claude-code`) and sign in with `claude`, then it
|
|
7424
|
+
detail: "Not detected. Install Claude Code (`npm i -g @anthropic-ai/claude-code`) and sign in with `claude`, then connect it here.",
|
|
7425
|
+
enable: null
|
|
7354
7426
|
};
|
|
7355
7427
|
}
|
|
7356
7428
|
function deriveCodex(signals, manifestHas) {
|
|
@@ -7418,6 +7490,9 @@ function deriveOpencode(signals, manifestHas) {
|
|
|
7418
7490
|
enable: "opencode"
|
|
7419
7491
|
};
|
|
7420
7492
|
}
|
|
7493
|
+
function detectedRuntimesFor(snapshot) {
|
|
7494
|
+
return snapshot.harnesses.filter((h) => h.state === "detected_not_exposed").map((h) => ({ runtime: h.runtime, version: h.version }));
|
|
7495
|
+
}
|
|
7421
7496
|
var PROBE_TIMEOUT_MS2 = 4e3;
|
|
7422
7497
|
async function probeHarnessSignals(cfg, deps = {}) {
|
|
7423
7498
|
const probeClaudePresence = deps.probeClaudePresence ?? claudeOnPath;
|
|
@@ -7434,6 +7509,9 @@ async function probeHarnessSignals(cfg, deps = {}) {
|
|
|
7434
7509
|
return {
|
|
7435
7510
|
claudeOnPath: claudeOnPathResult,
|
|
7436
7511
|
claudeVersion,
|
|
7512
|
+
// CT1082: the user's opt-in. Presence alone exposes nothing now, so this is
|
|
7513
|
+
// the manifest gate and the probe above is only a suggestion.
|
|
7514
|
+
claudeCodeConnected: isClaudeCodeConnected(cfg),
|
|
7437
7515
|
// A parseable `codex --version` is our presence signal (presence alone never
|
|
7438
7516
|
// exposes codex; its config flag is the manifest gate either way).
|
|
7439
7517
|
codexOnPath: codexVersion !== null,
|
|
@@ -7842,6 +7920,10 @@ var CompanionSupervisor = class {
|
|
|
7842
7920
|
// can never disagree with what the manifest advertises. Null until the first
|
|
7843
7921
|
// probe (the heartbeat then falls back to the boot `claudeCode`).
|
|
7844
7922
|
harnessSignals = null;
|
|
7923
|
+
// CT1082: the fresh PATH probe the claude-code connect vets with. Deliberately
|
|
7924
|
+
// NOT the cached beat signal — someone connecting right after installing Claude
|
|
7925
|
+
// Code shouldn't be refused by a snapshot up to a heartbeat old.
|
|
7926
|
+
probeClaudePresence;
|
|
7845
7927
|
exitFn;
|
|
7846
7928
|
reexecFn;
|
|
7847
7929
|
dispatcherFactory;
|
|
@@ -7872,6 +7954,7 @@ var CompanionSupervisor = class {
|
|
|
7872
7954
|
this.log = opts.log;
|
|
7873
7955
|
this.hub = opts.hub;
|
|
7874
7956
|
this.claudeCode = opts.claudeCode ?? true;
|
|
7957
|
+
this.probeClaudePresence = opts.probeClaudePresence ?? claudeOnPath;
|
|
7875
7958
|
this.harnessVersions = opts.harnessVersions ?? emptyHarnessVersions();
|
|
7876
7959
|
this.exitFn = opts.exit ?? ((code) => process.exit(code));
|
|
7877
7960
|
this.reexecFn = opts.reexec ?? defaultReexec;
|
|
@@ -7934,11 +8017,10 @@ var CompanionSupervisor = class {
|
|
|
7934
8017
|
// claude-code when `claude` is on PATH, CT270 opencode when the operator
|
|
7935
8018
|
// configured an `opencode serve`.
|
|
7936
8019
|
manifest: buildCompanionManifest({
|
|
7937
|
-
//
|
|
7938
|
-
// until the first
|
|
7939
|
-
//
|
|
7940
|
-
|
|
7941
|
-
claudeCode: this.claudeCodePresent(),
|
|
8020
|
+
// CT1082: connected AND installed. Presence is the live re-probe (CT586),
|
|
8021
|
+
// falling back to the boot probe until the first one lands; consent is the
|
|
8022
|
+
// user's `claudeCode.enabled`. Claude Code no longer rides presence alone.
|
|
8023
|
+
claudeCode: this.claudeCodeOffered(),
|
|
7942
8024
|
opencode: !!this.config.opencode?.serverUrl,
|
|
7943
8025
|
// CT481: advertise codex when the operator enabled it (config-gated,
|
|
7944
8026
|
// like opencode — the CLI's presence is the operator's responsibility;
|
|
@@ -7956,7 +8038,13 @@ var CompanionSupervisor = class {
|
|
|
7956
8038
|
// CT584: include enumerated models only when the probe SUCCEEDED (non-null).
|
|
7957
8039
|
// A null (probe failed / no opencode) omits the field, and the server then
|
|
7958
8040
|
// leaves this device's stored availability untouched.
|
|
7959
|
-
...opencodeModels !== null ? { models: opencodeModels } : {}
|
|
8041
|
+
...opencodeModels !== null ? { models: opencodeModels } : {},
|
|
8042
|
+
// CT1082: what this machine has that the user hasn't connected, so the web
|
|
8043
|
+
// UI can offer it without a companion round-trip. Sent only once a probe has
|
|
8044
|
+
// actually landed (`harnessSignals` non-null) — an absent field means "we
|
|
8045
|
+
// didn't look this beat" and leaves the server's stored suggestion alone,
|
|
8046
|
+
// the same fail-soft contract `models` keeps.
|
|
8047
|
+
...this.harnessSignals ? { detectedRuntimes: detectedRuntimesFor(deriveHarnessSnapshot(this.harnessSignals)) } : {}
|
|
7960
8048
|
});
|
|
7961
8049
|
this.hub.setDevice({ deviceId: res.deviceId });
|
|
7962
8050
|
this.deviceId = res.deviceId;
|
|
@@ -8122,6 +8210,7 @@ var CompanionSupervisor = class {
|
|
|
8122
8210
|
outbox,
|
|
8123
8211
|
log: this.log
|
|
8124
8212
|
});
|
|
8213
|
+
const aborts = /* @__PURE__ */ new Map();
|
|
8125
8214
|
const dispatcher = this.buildDispatcher({
|
|
8126
8215
|
api,
|
|
8127
8216
|
baseUrl: this.config.baseUrl,
|
|
@@ -8130,7 +8219,8 @@ var CompanionSupervisor = class {
|
|
|
8130
8219
|
agentId: it.agentId,
|
|
8131
8220
|
agentUsername: it.agentUsername,
|
|
8132
8221
|
credential,
|
|
8133
|
-
runConfig
|
|
8222
|
+
runConfig,
|
|
8223
|
+
aborts
|
|
8134
8224
|
});
|
|
8135
8225
|
const drain2 = this.makeDrain(api, it.agentId);
|
|
8136
8226
|
wr.agents.set(it.agentId, {
|
|
@@ -8140,6 +8230,7 @@ var CompanionSupervisor = class {
|
|
|
8140
8230
|
credential,
|
|
8141
8231
|
runConfig,
|
|
8142
8232
|
api,
|
|
8233
|
+
aborts,
|
|
8143
8234
|
dispatcher,
|
|
8144
8235
|
cancelDrain: drain2.cancel,
|
|
8145
8236
|
kickDrain: drain2.kick
|
|
@@ -8161,15 +8252,26 @@ var CompanionSupervisor = class {
|
|
|
8161
8252
|
"companion: stopped agent (unassigned)"
|
|
8162
8253
|
);
|
|
8163
8254
|
}
|
|
8164
|
-
// CT833: is Claude Code on this machine right now?
|
|
8165
|
-
//
|
|
8166
|
-
//
|
|
8167
|
-
// per-beat re-probe), falling back to the boot probe until the first one lands.
|
|
8255
|
+
// CT833: is Claude Code on this machine right now? Live (the per-beat re-probe),
|
|
8256
|
+
// falling back to the boot probe until the first one lands. Presence ONLY —
|
|
8257
|
+
// CT1082 split presence from exposure, so nothing routes on this directly.
|
|
8168
8258
|
claudeCodePresent() {
|
|
8169
8259
|
return this.harnessSignals?.claudeOnPath ?? this.claudeCode;
|
|
8170
8260
|
}
|
|
8261
|
+
// CT1082: does this device OFFER claude-code — connected by its user and actually
|
|
8262
|
+
// installed? The ONE signal both the heartbeat manifest and the dispatcher's
|
|
8263
|
+
// adapter registry read, so what the device advertises and what it can select
|
|
8264
|
+
// can't disagree (the CT833 invariant, now with consent in front of it).
|
|
8265
|
+
claudeCodeOffered() {
|
|
8266
|
+
return isClaudeCodeConnected(this.config) && this.claudeCodePresent();
|
|
8267
|
+
}
|
|
8171
8268
|
buildDispatcher(ctx) {
|
|
8172
|
-
|
|
8269
|
+
const local = localAgentConfig(this.config, {
|
|
8270
|
+
agentId: ctx.agentId,
|
|
8271
|
+
agentUsername: ctx.agentUsername,
|
|
8272
|
+
workspaceSlug: ctx.workspaceSlug
|
|
8273
|
+
});
|
|
8274
|
+
if (this.dispatcherFactory) return this.dispatcherFactory({ ...ctx, local });
|
|
8173
8275
|
return new Dispatcher({
|
|
8174
8276
|
api: ctx.api,
|
|
8175
8277
|
baseUrl: ctx.baseUrl,
|
|
@@ -8178,17 +8280,16 @@ var CompanionSupervisor = class {
|
|
|
8178
8280
|
agentId: ctx.agentId,
|
|
8179
8281
|
agentUsername: ctx.agentUsername,
|
|
8180
8282
|
credential: ctx.credential,
|
|
8181
|
-
local
|
|
8182
|
-
|
|
8183
|
-
agentUsername: ctx.agentUsername,
|
|
8184
|
-
workspaceSlug: ctx.workspaceSlug
|
|
8185
|
-
}),
|
|
8283
|
+
local,
|
|
8284
|
+
aborts: ctx.aborts,
|
|
8186
8285
|
runConfig: ctx.runConfig,
|
|
8187
8286
|
log: this.log,
|
|
8188
|
-
// CT833: register the claude-code adapter only when
|
|
8189
|
-
//
|
|
8190
|
-
// after boot works on the next turn exactly as it
|
|
8191
|
-
|
|
8287
|
+
// CT833: register the claude-code adapter only when this device actually
|
|
8288
|
+
// offers claude-code — read per turn (not captured here), so a harness
|
|
8289
|
+
// installed or connected after boot works on the next turn exactly as it
|
|
8290
|
+
// appears on the next beat. CT1082: "offers" now means connected as well as
|
|
8291
|
+
// installed, so a disconnected harness can't be selected either.
|
|
8292
|
+
claudeCodeAvailable: () => this.claudeCodeOffered(),
|
|
8192
8293
|
// CT270: the opencode server URL (operator-configured), when this device
|
|
8193
8294
|
// offers the opencode runtime. Threaded so an opencode turn selects the
|
|
8194
8295
|
// opencode adapter; unset leaves the device claude-code-only.
|
|
@@ -8450,6 +8551,7 @@ var CompanionSupervisor = class {
|
|
|
8450
8551
|
if (!next) return;
|
|
8451
8552
|
this.config = next;
|
|
8452
8553
|
this.log.level = next.logLevel ?? "debug";
|
|
8554
|
+
this.rebuildDispatchers();
|
|
8453
8555
|
void this.refreshAssignments();
|
|
8454
8556
|
}
|
|
8455
8557
|
// The dashboard "refresh assignments" control — an immediate pull.
|
|
@@ -8494,9 +8596,12 @@ var CompanionSupervisor = class {
|
|
|
8494
8596
|
async recheckHarnesses() {
|
|
8495
8597
|
await this.refreshHarnessStatuses();
|
|
8496
8598
|
}
|
|
8497
|
-
// Friendly enable for the
|
|
8498
|
-
//
|
|
8499
|
-
//
|
|
8599
|
+
// Friendly enable for the config-driven harnesses — flip the flag the app owns in
|
|
8600
|
+
// `~/.cabane/config.json`, no hand-edited JSON. This is enable/expose ONLY: it
|
|
8601
|
+
// never installs a binary and never drives a login (BYO — Decided).
|
|
8602
|
+
// - claude-code (CT1082): set `claudeCode.enabled`, after checking `claude` is
|
|
8603
|
+
// on PATH. This is the callable the terminal offer ("We found Claude Code.
|
|
8604
|
+
// Connect it? [Y/n]") and the web pairing flow both wire to.
|
|
8500
8605
|
// - codex: set `codex.enabled` (the CLI + `codex login` remain the user's).
|
|
8501
8606
|
// - opencode: set `opencode.serverUrl` — but ONLY after health-probing the URL,
|
|
8502
8607
|
// so we never advertise a serve that isn't there. An unreachable URL is a
|
|
@@ -8506,7 +8611,15 @@ var CompanionSupervisor = class {
|
|
|
8506
8611
|
// reachable/valid input).
|
|
8507
8612
|
async enableHarness(input) {
|
|
8508
8613
|
let next;
|
|
8509
|
-
if (input.runtime === "
|
|
8614
|
+
if (input.runtime === "claude-code") {
|
|
8615
|
+
if (!await this.probeClaudePresence()) {
|
|
8616
|
+
return {
|
|
8617
|
+
ok: false,
|
|
8618
|
+
error: "Couldn\u2019t find `claude` on this machine\u2019s PATH. Install Claude Code (`npm i -g @anthropic-ai/claude-code`) and sign in, then connect it."
|
|
8619
|
+
};
|
|
8620
|
+
}
|
|
8621
|
+
next = { ...this.config, claudeCode: { enabled: true } };
|
|
8622
|
+
} else if (input.runtime === "codex") {
|
|
8510
8623
|
next = { ...this.config, codex: { enabled: true } };
|
|
8511
8624
|
} else {
|
|
8512
8625
|
const serverUrl = input.serverUrl.trim();
|
|
@@ -8536,8 +8649,8 @@ var CompanionSupervisor = class {
|
|
|
8536
8649
|
}
|
|
8537
8650
|
// Re-create every running agent's Dispatcher from the CURRENT config, keeping
|
|
8538
8651
|
// the runner (its api/outbox/chains/SSE) intact. Used after a runtime-enabling
|
|
8539
|
-
// config change so
|
|
8540
|
-
//
|
|
8652
|
+
// config change so constructor-captured options — per-agent cwd/prepareHook and
|
|
8653
|
+
// the adapter registry — pick up their new values without an app restart.
|
|
8541
8654
|
rebuildDispatchers() {
|
|
8542
8655
|
for (const wr of this.workspaces.values()) {
|
|
8543
8656
|
for (const runner of wr.agents.values()) {
|
|
@@ -8549,7 +8662,8 @@ var CompanionSupervisor = class {
|
|
|
8549
8662
|
agentId: runner.agentId,
|
|
8550
8663
|
agentUsername: runner.username,
|
|
8551
8664
|
credential: runner.credential,
|
|
8552
|
-
runConfig: runner.runConfig
|
|
8665
|
+
runConfig: runner.runConfig,
|
|
8666
|
+
aborts: runner.aborts
|
|
8553
8667
|
});
|
|
8554
8668
|
}
|
|
8555
8669
|
}
|
|
@@ -8708,13 +8822,17 @@ function clearCrash() {
|
|
|
8708
8822
|
async function createCompanionRuntime(opts = {}) {
|
|
8709
8823
|
const log = getLogger();
|
|
8710
8824
|
installProcessSafetyNet(log);
|
|
8711
|
-
const probeClaude = opts.probeClaude ?? claudeOnPath;
|
|
8712
8825
|
let cfg;
|
|
8713
8826
|
let claudeCode;
|
|
8714
8827
|
try {
|
|
8715
|
-
cfg =
|
|
8716
|
-
|
|
8717
|
-
|
|
8828
|
+
({ cfg, claudeOnPath: claudeCode } = await requireStartConfig({
|
|
8829
|
+
...opts.probeClaude ? { probeClaude: opts.probeClaude } : {},
|
|
8830
|
+
onMigrated: (migrated, onPath) => log.info(
|
|
8831
|
+
{ claudeCode: isClaudeCodeConnected(migrated) },
|
|
8832
|
+
onPath ? "companion: carried Claude Code over as a connected harness on this device (connectors are now chosen, not detected)" : "companion: no Claude Code on PATH, so this device starts with it disconnected (connectors are now chosen, not detected)"
|
|
8833
|
+
)
|
|
8834
|
+
}));
|
|
8835
|
+
await warnAboutHarnessReadiness(cfg, { probeClaude: async () => claudeCode });
|
|
8718
8836
|
} catch (err) {
|
|
8719
8837
|
recordCrash({
|
|
8720
8838
|
reason: err instanceof Error ? err.message : String(err),
|
|
@@ -8726,7 +8844,9 @@ async function createCompanionRuntime(opts = {}) {
|
|
|
8726
8844
|
}
|
|
8727
8845
|
if (cfg.logLevel) log.level = cfg.logLevel;
|
|
8728
8846
|
const harnessVersions = await probeHarnessVersions({
|
|
8729
|
-
|
|
8847
|
+
// CT1082: versions are probed for the runtimes this device OFFERS, and
|
|
8848
|
+
// claude-code is offered only when connected as well as installed.
|
|
8849
|
+
claudeCode: claudeCode && isClaudeCodeConnected(cfg),
|
|
8730
8850
|
opencodeServerUrl: cfg.opencode?.serverUrl,
|
|
8731
8851
|
codex: isCodexEnabled(cfg)
|
|
8732
8852
|
});
|
package/dist/static/app.js
CHANGED
|
@@ -137,9 +137,17 @@ function renderHarnesses() {
|
|
|
137
137
|
|
|
138
138
|
if (h.detail) li.append(el('p', 'harness-detail', h.detail));
|
|
139
139
|
|
|
140
|
-
// Friendly enable — never install, never login (BYO).
|
|
141
|
-
// flag
|
|
142
|
-
|
|
140
|
+
// Friendly enable — never install, never login (BYO). Claude Code and Codex are
|
|
141
|
+
// one-click flag flips; opencode takes a URL we validate is reachable before
|
|
142
|
+
// writing. CT1082: Claude Code is connectable here too — it used to be exposed
|
|
143
|
+
// the moment it was installed, with nothing to click.
|
|
144
|
+
if (h.enable === 'claude-code') {
|
|
145
|
+
const btn = el('button', 'primary small', 'Connect Claude Code');
|
|
146
|
+
btn.addEventListener('click', () => void enableHarness({ runtime: 'claude-code' }, btn));
|
|
147
|
+
const wrap = el('div', 'harness-enable');
|
|
148
|
+
wrap.append(btn);
|
|
149
|
+
li.append(wrap);
|
|
150
|
+
} else if (h.enable === 'codex') {
|
|
143
151
|
const btn = el('button', 'primary small', 'Use Codex');
|
|
144
152
|
btn.addEventListener('click', () => void enableHarness({ runtime: 'codex' }, btn));
|
|
145
153
|
const wrap = el('div', 'harness-enable');
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cabane/companion",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.22",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "The Cabane Companion (headless): connect a coding 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",
|