@cabane/companion 0.6.40 → 0.6.41
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 +82 -74
- package/dist/runtime.js +2 -11
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -819,7 +819,7 @@ async function warnAboutHarnessReadiness(cfg, deps = {}) {
|
|
|
819
819
|
var HARNESS_LABELS = {
|
|
820
820
|
"claude-code": "Claude Code",
|
|
821
821
|
codex: "Codex",
|
|
822
|
-
opencode: "
|
|
822
|
+
opencode: "OpenCode"
|
|
823
823
|
};
|
|
824
824
|
var LABELS = HARNESS_LABELS;
|
|
825
825
|
var OFFER_ORDER = ["claude-code", "codex", "opencode"];
|
|
@@ -949,9 +949,6 @@ function deriveOpencode(signals, manifestHas) {
|
|
|
949
949
|
enable: "opencode"
|
|
950
950
|
};
|
|
951
951
|
}
|
|
952
|
-
function detectedRuntimesFor(snapshot) {
|
|
953
|
-
return snapshot.harnesses.filter((h) => h.state === "detected_not_exposed").map((h) => ({ runtime: h.runtime, version: h.version }));
|
|
954
|
-
}
|
|
955
952
|
var PROBE_TIMEOUT_MS = 4e3;
|
|
956
953
|
async function probeHarnessSignals(cfg, deps = {}) {
|
|
957
954
|
const probeClaudePresence = deps.probeClaudePresence ?? claudeOnPath;
|
|
@@ -1198,8 +1195,6 @@ function blank() {
|
|
|
1198
1195
|
process.stdout.write("\n");
|
|
1199
1196
|
}
|
|
1200
1197
|
var tick = (rest) => `${pc.green("\u2713")} ${rest}`;
|
|
1201
|
-
var arrow = (rest) => `${pc.yellow("\u2192")} ${rest}`;
|
|
1202
|
-
var bang = (rest) => `${pc.yellow("!")} ${rest}`;
|
|
1203
1198
|
function tildePath(path) {
|
|
1204
1199
|
const home = homedir2();
|
|
1205
1200
|
if (home && path.startsWith(home + "/")) return `~${path.slice(home.length)}`;
|
|
@@ -9140,13 +9135,7 @@ var CompanionSupervisor = class {
|
|
|
9140
9135
|
// CT584: include enumerated models only when the probe SUCCEEDED (non-null).
|
|
9141
9136
|
// A null (probe failed / no opencode) omits the field, and the server then
|
|
9142
9137
|
// leaves this device's stored availability untouched.
|
|
9143
|
-
...opencodeModels !== null ? { models: opencodeModels } : {}
|
|
9144
|
-
// CT1082: what this machine has that the user hasn't connected, so the web
|
|
9145
|
-
// UI can offer it without a companion round-trip. Sent only once a probe has
|
|
9146
|
-
// actually landed (`harnessSignals` non-null) — an absent field means "we
|
|
9147
|
-
// didn't look this beat" and leaves the server's stored suggestion alone,
|
|
9148
|
-
// the same fail-soft contract `models` keeps.
|
|
9149
|
-
...this.harnessSignals ? { detectedRuntimes: detectedRuntimesFor(deriveHarnessSnapshot(this.harnessSignals)) } : {}
|
|
9138
|
+
...opencodeModels !== null ? { models: opencodeModels } : {}
|
|
9150
9139
|
});
|
|
9151
9140
|
this.hub.setDevice({ deviceId: res.deviceId });
|
|
9152
9141
|
this.deviceId = res.deviceId;
|
|
@@ -10241,6 +10230,7 @@ async function startScript(opts, interactive) {
|
|
|
10241
10230
|
return;
|
|
10242
10231
|
}
|
|
10243
10232
|
blank();
|
|
10233
|
+
const held = !isDevicePaired() ? await collectHarnessChoices(interactive) : [];
|
|
10244
10234
|
let paired = null;
|
|
10245
10235
|
if (!isDevicePaired()) {
|
|
10246
10236
|
paired = await pairHere(opts, interactive);
|
|
@@ -10262,12 +10252,18 @@ async function startScript(opts, interactive) {
|
|
|
10262
10252
|
return;
|
|
10263
10253
|
}
|
|
10264
10254
|
const runtime = result.runtime;
|
|
10255
|
+
const connected = [];
|
|
10256
|
+
for (const choice of held) {
|
|
10257
|
+
if (!choice.accepted) continue;
|
|
10258
|
+
const outcome = await runtime.connectHarness(choice.runtime, choice.serverUrl);
|
|
10259
|
+
if (outcome.ok) connected.push(HARNESS_LABELS[choice.runtime]);
|
|
10260
|
+
else write(`${INDENT}${outcome.error}`);
|
|
10261
|
+
}
|
|
10265
10262
|
await runtime.heartbeatNow();
|
|
10266
|
-
const offered = await runConnectorOffer(runtime, { interactive, justPaired });
|
|
10267
10263
|
if (interactive && !opts.foreground) {
|
|
10268
10264
|
await handOffToBackground(runtime, {
|
|
10269
10265
|
justPaired,
|
|
10270
|
-
|
|
10266
|
+
connected
|
|
10271
10267
|
});
|
|
10272
10268
|
return;
|
|
10273
10269
|
}
|
|
@@ -10279,7 +10275,7 @@ async function startScript(opts, interactive) {
|
|
|
10279
10275
|
write(`${INDENT}Listening for messages\u2026`);
|
|
10280
10276
|
setConsoleLogging(true);
|
|
10281
10277
|
} else {
|
|
10282
|
-
|
|
10278
|
+
blank();
|
|
10283
10279
|
write(
|
|
10284
10280
|
`${INDENT}Cabane companion is running. No terminal attached, so it stays in the foreground.`
|
|
10285
10281
|
);
|
|
@@ -10301,11 +10297,60 @@ function reportAlreadyRunning(pid) {
|
|
|
10301
10297
|
write("Stop it first with `cabane-companion stop` if you want to relaunch.");
|
|
10302
10298
|
write("Connect a harness to the running companion: cabane-companion connect claude-code");
|
|
10303
10299
|
}
|
|
10300
|
+
async function collectHarnessChoices(interactive) {
|
|
10301
|
+
const opencodeUrl = "http://127.0.0.1:4096";
|
|
10302
|
+
const [claudePresent, claudeVersion, codexVersion, opencodeVersion] = await Promise.all([
|
|
10303
|
+
claudeOnPath().catch(() => false),
|
|
10304
|
+
probeCliVersion("claude").catch(() => null),
|
|
10305
|
+
probeCliVersion("codex").catch(() => null),
|
|
10306
|
+
probeOpencodeVersion(opencodeUrl).catch(() => null)
|
|
10307
|
+
]);
|
|
10308
|
+
const found = [
|
|
10309
|
+
...claudePresent ? [{ runtime: "claude-code", version: claudeVersion }] : [],
|
|
10310
|
+
...codexVersion ? [{ runtime: "codex", version: codexVersion }] : [],
|
|
10311
|
+
...opencodeVersion ? [{ runtime: "opencode", version: opencodeVersion, serverUrl: opencodeUrl }] : []
|
|
10312
|
+
].sort((a, b) => OFFER_ORDER.indexOf(a.runtime) - OFFER_ORDER.indexOf(b.runtime));
|
|
10313
|
+
if (found.length === 0) {
|
|
10314
|
+
write(
|
|
10315
|
+
`${INDENT}No coding agent found on this machine yet \u2014 you can pair now and add one later.`
|
|
10316
|
+
);
|
|
10317
|
+
blank();
|
|
10318
|
+
return [];
|
|
10319
|
+
}
|
|
10320
|
+
write(`${INDENT}Welcome to Cabane. Let's set up this machine \u2014 two steps, both right here:`);
|
|
10321
|
+
blank();
|
|
10322
|
+
write(`${DATA_INDENT}1. Choose which coding agents Cabane can use on this machine`);
|
|
10323
|
+
write(`${DATA_INDENT}2. Pair this machine with your Cabane account`);
|
|
10324
|
+
blank();
|
|
10325
|
+
const choices = [];
|
|
10326
|
+
for (const harness of found) {
|
|
10327
|
+
const phrase = prePairFoundPhrase(harness);
|
|
10328
|
+
if (!interactive) {
|
|
10329
|
+
write(`${INDENT}${phrase}. Add it with: ${connectCommand(harness.runtime)}`);
|
|
10330
|
+
choices.push({ ...harness, accepted: false });
|
|
10331
|
+
continue;
|
|
10332
|
+
}
|
|
10333
|
+
const accepted = await confirm(`${phrase}. Make it available in Cabane?`);
|
|
10334
|
+
choices.push({ ...harness, accepted });
|
|
10335
|
+
if (accepted) {
|
|
10336
|
+
write(
|
|
10337
|
+
INDENT + tick(`${HARNESS_LABELS[harness.runtime]} \u2014 available once this device is paired.`)
|
|
10338
|
+
);
|
|
10339
|
+
} else {
|
|
10340
|
+
write(`${INDENT}Skipped. Add it any time with: ${connectCommand(harness.runtime)}`);
|
|
10341
|
+
}
|
|
10342
|
+
blank();
|
|
10343
|
+
}
|
|
10344
|
+
return choices;
|
|
10345
|
+
}
|
|
10346
|
+
function prePairFoundPhrase(harness) {
|
|
10347
|
+
if (harness.runtime === "opencode") return `We found OpenCode at ${harness.serverUrl}`;
|
|
10348
|
+
const label = HARNESS_LABELS[harness.runtime];
|
|
10349
|
+
return harness.version ? `We found ${label} on this machine (${harness.version})` : `We found ${label} on this machine`;
|
|
10350
|
+
}
|
|
10304
10351
|
async function pairHere(opts, interactive) {
|
|
10305
10352
|
const baseUrl = resolvePairBaseUrl(opts.server);
|
|
10306
10353
|
const label = deviceLabelFromHostname(hostname2());
|
|
10307
|
-
write(`${INDENT}Not paired yet.`);
|
|
10308
|
-
blank();
|
|
10309
10354
|
const aborter = new AbortController();
|
|
10310
10355
|
const onSigint = () => aborter.abort();
|
|
10311
10356
|
process.on("SIGINT", onSigint);
|
|
@@ -10318,7 +10363,7 @@ async function pairHere(opts, interactive) {
|
|
|
10318
10363
|
if (attempt === 1) printFirstCode(code);
|
|
10319
10364
|
else printFreshCode(code);
|
|
10320
10365
|
spinner.start(
|
|
10321
|
-
`Waiting for you to confirm
|
|
10366
|
+
`Waiting for you to confirm in Cabane\u2026 (the code is good for ${minutes} minutes)`
|
|
10322
10367
|
);
|
|
10323
10368
|
try {
|
|
10324
10369
|
const device = await awaitEnrollment(baseUrl, code, { signal: aborter.signal });
|
|
@@ -10328,7 +10373,7 @@ async function pairHere(opts, interactive) {
|
|
|
10328
10373
|
if (err instanceof EnrollmentCancelledError) {
|
|
10329
10374
|
spinner.clear();
|
|
10330
10375
|
blank();
|
|
10331
|
-
write(`${INDENT}Pairing cancelled. Run
|
|
10376
|
+
write(`${INDENT}Pairing cancelled. Run cabane-companion start when you're ready.`);
|
|
10332
10377
|
return null;
|
|
10333
10378
|
}
|
|
10334
10379
|
if (!(err instanceof EnrollmentExpiredError)) throw err;
|
|
@@ -10339,8 +10384,9 @@ async function pairHere(opts, interactive) {
|
|
|
10339
10384
|
}
|
|
10340
10385
|
spinner.freeze();
|
|
10341
10386
|
blank();
|
|
10342
|
-
write(
|
|
10343
|
-
|
|
10387
|
+
write(
|
|
10388
|
+
`${INDENT}Still not confirmed after ${minutesWaited} minutes \u2014 stopping here. Run cabane-companion start again when you're ready.`
|
|
10389
|
+
);
|
|
10344
10390
|
return null;
|
|
10345
10391
|
}
|
|
10346
10392
|
}
|
|
@@ -10350,11 +10396,11 @@ async function pairHere(opts, interactive) {
|
|
|
10350
10396
|
}
|
|
10351
10397
|
}
|
|
10352
10398
|
function printFirstCode(code) {
|
|
10353
|
-
write(`${INDENT}Enter this code in Cabane:`);
|
|
10399
|
+
write(`${INDENT}Now let's pair this machine. Enter this code in Cabane:`);
|
|
10354
10400
|
blank();
|
|
10355
10401
|
write(`${DATA_INDENT}${code.userCode}`);
|
|
10356
10402
|
blank();
|
|
10357
|
-
write(`${INDENT}or
|
|
10403
|
+
write(`${INDENT}or, if Cabane isn\u2019t open:`);
|
|
10358
10404
|
blank();
|
|
10359
10405
|
write(`${DATA_INDENT}${code.verificationUriComplete}`);
|
|
10360
10406
|
blank();
|
|
@@ -10369,54 +10415,6 @@ function printFreshCode(code) {
|
|
|
10369
10415
|
function possessive(ownerName) {
|
|
10370
10416
|
return ownerName ? `${ownerName}'s` : "your Cabane";
|
|
10371
10417
|
}
|
|
10372
|
-
async function runConnectorOffer(runtime, ctx) {
|
|
10373
|
-
const snapshot = runtime.harnesses();
|
|
10374
|
-
const detected = OFFER_ORDER.map((r) => snapshot.find((h) => h.runtime === r)).filter(
|
|
10375
|
-
(h) => !!h && h.state === "detected_not_exposed"
|
|
10376
|
-
);
|
|
10377
|
-
if (detected.length === 0) {
|
|
10378
|
-
if (!ctx.justPaired) return { printedSomething: false };
|
|
10379
|
-
blank();
|
|
10380
|
-
write(
|
|
10381
|
-
INDENT + bang(
|
|
10382
|
-
"No coding agent found on this machine yet \u2014 install Claude Code, Codex or opencode and sign in,"
|
|
10383
|
-
)
|
|
10384
|
-
);
|
|
10385
|
-
write(`${INDENT} then: cabane-companion connect claude-code (or codex, or opencode)`);
|
|
10386
|
-
return { printedSomething: true };
|
|
10387
|
-
}
|
|
10388
|
-
blank();
|
|
10389
|
-
if (!ctx.interactive) {
|
|
10390
|
-
for (const h of detected) {
|
|
10391
|
-
write(
|
|
10392
|
-
`${INDENT}${foundPhrase(h, runtime.config)} but it isn't connected. Connect it: ${connectCommand(h.runtime)}`
|
|
10393
|
-
);
|
|
10394
|
-
}
|
|
10395
|
-
return { printedSomething: true };
|
|
10396
|
-
}
|
|
10397
|
-
for (const h of detected) {
|
|
10398
|
-
const yes = await confirm(`${foundPhrase(h, runtime.config)}. Connect it to Cabane?`);
|
|
10399
|
-
if (!yes) {
|
|
10400
|
-
write(`${INDENT}Skipped. Connect it later with: ${connectCommand(h.runtime)}`);
|
|
10401
|
-
continue;
|
|
10402
|
-
}
|
|
10403
|
-
const outcome = await runtime.connectHarness(h.runtime);
|
|
10404
|
-
if (!outcome.ok) {
|
|
10405
|
-
write(`${INDENT}${outcome.error}`);
|
|
10406
|
-
continue;
|
|
10407
|
-
}
|
|
10408
|
-
write(INDENT + tick(outcome.message));
|
|
10409
|
-
}
|
|
10410
|
-
return { printedSomething: true };
|
|
10411
|
-
}
|
|
10412
|
-
function foundPhrase(h, cfg) {
|
|
10413
|
-
const label = HARNESS_LABELS[h.runtime];
|
|
10414
|
-
if (h.runtime === "opencode") {
|
|
10415
|
-
const url = cfg.opencode?.serverUrl;
|
|
10416
|
-
return url ? `We found ${label} at ${url}` : `We found ${label} on this machine`;
|
|
10417
|
-
}
|
|
10418
|
-
return h.version ? `We found ${label} on this machine (${h.version})` : `We found ${label} on this machine`;
|
|
10419
|
-
}
|
|
10420
10418
|
function connectCommand(runtime) {
|
|
10421
10419
|
return `cabane-companion connect ${runtime}`;
|
|
10422
10420
|
}
|
|
@@ -10426,12 +10424,22 @@ async function handOffToBackground(runtime, ctx) {
|
|
|
10426
10424
|
if (!outcome.started) {
|
|
10427
10425
|
return;
|
|
10428
10426
|
}
|
|
10429
|
-
if (ctx.
|
|
10427
|
+
if (ctx.justPaired) {
|
|
10428
|
+
for (const label of ctx.connected) write(INDENT + tick(`${label} connected.`));
|
|
10429
|
+
if (ctx.connected.length === 0) {
|
|
10430
|
+
write(
|
|
10431
|
+
`${INDENT}Add a coding agent any time: cabane-companion connect claude-code (or codex, or opencode)`
|
|
10432
|
+
);
|
|
10433
|
+
}
|
|
10434
|
+
}
|
|
10430
10435
|
write(INDENT + tick("Cabane companion is running in the background."));
|
|
10431
10436
|
write(` Stop: cabane-companion stop Logs: ${tildePath(companionLogPath())}`);
|
|
10432
10437
|
if (ctx.justPaired) {
|
|
10433
10438
|
blank();
|
|
10434
|
-
write(INDENT
|
|
10439
|
+
write(`${INDENT}! It won't restart on its own \u2014 after a reboot, or if it ever stops,`);
|
|
10440
|
+
write(`${INDENT} just run cabane-companion start again.`);
|
|
10441
|
+
blank();
|
|
10442
|
+
write(`${INDENT}All set on this side. Finish up in Cabane.`);
|
|
10435
10443
|
}
|
|
10436
10444
|
}
|
|
10437
10445
|
async function runAttached(runtime) {
|
package/dist/runtime.js
CHANGED
|
@@ -1297,7 +1297,7 @@ async function warnAboutHarnessReadiness(cfg, deps = {}) {
|
|
|
1297
1297
|
var HARNESS_LABELS = {
|
|
1298
1298
|
"claude-code": "Claude Code",
|
|
1299
1299
|
codex: "Codex",
|
|
1300
|
-
opencode: "
|
|
1300
|
+
opencode: "OpenCode"
|
|
1301
1301
|
};
|
|
1302
1302
|
var LABELS = HARNESS_LABELS;
|
|
1303
1303
|
function deriveHarnessSnapshot(signals) {
|
|
@@ -1419,9 +1419,6 @@ function deriveOpencode(signals, manifestHas) {
|
|
|
1419
1419
|
enable: "opencode"
|
|
1420
1420
|
};
|
|
1421
1421
|
}
|
|
1422
|
-
function detectedRuntimesFor(snapshot) {
|
|
1423
|
-
return snapshot.harnesses.filter((h) => h.state === "detected_not_exposed").map((h) => ({ runtime: h.runtime, version: h.version }));
|
|
1424
|
-
}
|
|
1425
1422
|
var PROBE_TIMEOUT_MS = 4e3;
|
|
1426
1423
|
async function probeHarnessSignals(cfg, deps = {}) {
|
|
1427
1424
|
const probeClaudePresence = deps.probeClaudePresence ?? claudeOnPath;
|
|
@@ -8647,13 +8644,7 @@ var CompanionSupervisor = class {
|
|
|
8647
8644
|
// CT584: include enumerated models only when the probe SUCCEEDED (non-null).
|
|
8648
8645
|
// A null (probe failed / no opencode) omits the field, and the server then
|
|
8649
8646
|
// leaves this device's stored availability untouched.
|
|
8650
|
-
...opencodeModels !== null ? { models: opencodeModels } : {}
|
|
8651
|
-
// CT1082: what this machine has that the user hasn't connected, so the web
|
|
8652
|
-
// UI can offer it without a companion round-trip. Sent only once a probe has
|
|
8653
|
-
// actually landed (`harnessSignals` non-null) — an absent field means "we
|
|
8654
|
-
// didn't look this beat" and leaves the server's stored suggestion alone,
|
|
8655
|
-
// the same fail-soft contract `models` keeps.
|
|
8656
|
-
...this.harnessSignals ? { detectedRuntimes: detectedRuntimesFor(deriveHarnessSnapshot(this.harnessSignals)) } : {}
|
|
8647
|
+
...opencodeModels !== null ? { models: opencodeModels } : {}
|
|
8657
8648
|
});
|
|
8658
8649
|
this.hub.setDevice({ deviceId: res.deviceId });
|
|
8659
8650
|
this.deviceId = res.deviceId;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cabane/companion",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.41",
|
|
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",
|