@cabane/companion 0.6.174 → 0.6.175
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 +68 -56
- package/dist/runtime.js +39 -41
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -12117,11 +12117,60 @@ init_config();
|
|
|
12117
12117
|
init_bundled_binaries();
|
|
12118
12118
|
init_cabane_command();
|
|
12119
12119
|
init_config();
|
|
12120
|
+
import { homedir as homedir6 } from "os";
|
|
12121
|
+
import { join as join7 } from "path";
|
|
12122
|
+
|
|
12123
|
+
// src/device-state.ts
|
|
12124
|
+
var HARNESS_STATE_LABEL = {
|
|
12125
|
+
checking: "checking",
|
|
12126
|
+
connected: "connected",
|
|
12127
|
+
needs_sign_in: "needs sign-in",
|
|
12128
|
+
limited: "limited",
|
|
12129
|
+
problem: "problem",
|
|
12130
|
+
off: "off",
|
|
12131
|
+
gone: "gone"
|
|
12132
|
+
};
|
|
12133
|
+
|
|
12134
|
+
// src/harness-state.ts
|
|
12135
|
+
init_config();
|
|
12136
|
+
function harnessEntry(runtime, state) {
|
|
12137
|
+
switch (state.state) {
|
|
12138
|
+
case "usable":
|
|
12139
|
+
return {
|
|
12140
|
+
runtime,
|
|
12141
|
+
state: "connected",
|
|
12142
|
+
reason: state.via,
|
|
12143
|
+
version: state.version,
|
|
12144
|
+
...state.url ? { url: state.url } : {}
|
|
12145
|
+
};
|
|
12146
|
+
case "signed_out":
|
|
12147
|
+
return { runtime, state: "needs_sign_in", reason: "signed_out", version: state.version };
|
|
12148
|
+
case "not_running":
|
|
12149
|
+
return { runtime, state: "problem", reason: "not_running", url: state.url };
|
|
12150
|
+
case "off":
|
|
12151
|
+
return { runtime, state: "off" };
|
|
12152
|
+
case "problem":
|
|
12153
|
+
return { runtime, state: "problem", reason: checkReason(state.check) };
|
|
12154
|
+
}
|
|
12155
|
+
}
|
|
12156
|
+
function harnessEntriesFromDetection(detection) {
|
|
12157
|
+
return HARNESS_RUNTIMES.map((runtime) => harnessEntry(runtime, detection[runtime]));
|
|
12158
|
+
}
|
|
12159
|
+
function checkReason(check) {
|
|
12160
|
+
switch (check.status) {
|
|
12161
|
+
case "bundle_missing":
|
|
12162
|
+
return check.bundled;
|
|
12163
|
+
case "check_failed":
|
|
12164
|
+
return `check_failed:${check.reason}`;
|
|
12165
|
+
default:
|
|
12166
|
+
return check.status;
|
|
12167
|
+
}
|
|
12168
|
+
}
|
|
12169
|
+
|
|
12170
|
+
// src/harness-copy.ts
|
|
12120
12171
|
init_install_kind();
|
|
12121
12172
|
init_install_record();
|
|
12122
12173
|
init_term();
|
|
12123
|
-
import { homedir as homedir6 } from "os";
|
|
12124
|
-
import { join as join7 } from "path";
|
|
12125
12174
|
var HARNESS_LABELS = {
|
|
12126
12175
|
"claude-code": "Claude Code",
|
|
12127
12176
|
codex: "Codex",
|
|
@@ -12176,23 +12225,23 @@ function agentRows(detection, opts = {}) {
|
|
|
12176
12225
|
});
|
|
12177
12226
|
}
|
|
12178
12227
|
function agentRowText(runtime, state, opts = {}) {
|
|
12179
|
-
const
|
|
12228
|
+
const word = HARNESS_STATE_LABEL[harnessEntry(runtime, state).state];
|
|
12180
12229
|
switch (state.state) {
|
|
12181
12230
|
case "usable": {
|
|
12182
12231
|
const version = state.version ? ` (${state.version})` : "";
|
|
12183
|
-
if (state.via === "api_key") return
|
|
12184
|
-
if (state.via === "container") return
|
|
12185
|
-
if (state.via === "serve") return
|
|
12186
|
-
return
|
|
12232
|
+
if (state.via === "api_key") return `${word} \u2014 API key configured${version}`;
|
|
12233
|
+
if (state.via === "container") return `${word} \u2014 turns run in Docker`;
|
|
12234
|
+
if (state.via === "serve") return `${word} \u2014 running at ${state.url ?? ""}`;
|
|
12235
|
+
return `${word} \u2014 signed in${version}`;
|
|
12187
12236
|
}
|
|
12188
12237
|
case "signed_out":
|
|
12189
|
-
return opts.signInHint && runtime !== "opencode" ?
|
|
12238
|
+
return opts.signInHint && runtime !== "opencode" ? `${word} \u2014 run ${loginCommand(runtime)}` : word;
|
|
12190
12239
|
case "not_running":
|
|
12191
|
-
return
|
|
12240
|
+
return `${word} \u2014 not running`;
|
|
12192
12241
|
case "off":
|
|
12193
|
-
return
|
|
12242
|
+
return `${word} \u2014 turned off in config`;
|
|
12194
12243
|
case "problem":
|
|
12195
|
-
return runtime === "opencode" ?
|
|
12244
|
+
return runtime === "opencode" ? `${word} \u2014 not running` : `${word} \u2014 ${unusableReason(runtime, state.check) ?? "could not be checked"}`;
|
|
12196
12245
|
}
|
|
12197
12246
|
}
|
|
12198
12247
|
var AGENT_CHOICES = [
|
|
@@ -12281,7 +12330,7 @@ function nothingUsableLines(detection) {
|
|
|
12281
12330
|
if (reason) lines2.push(reason);
|
|
12282
12331
|
}
|
|
12283
12332
|
}
|
|
12284
|
-
lines2.push(`Cabane can run while you sign in to a
|
|
12333
|
+
lines2.push(`Cabane can run while you sign in to a harness.`);
|
|
12285
12334
|
return lines2;
|
|
12286
12335
|
}
|
|
12287
12336
|
function indent(line) {
|
|
@@ -13460,7 +13509,7 @@ function turnFailureCopy(reason, runtime) {
|
|
|
13460
13509
|
if (reason?.startsWith("runtime_incomplete:"))
|
|
13461
13510
|
return `The agent installation is incomplete. ${repairAction()} Then restart the companion.`;
|
|
13462
13511
|
if (/^(runtime|codex|opencode)_unavailable:/.test(reason ?? ""))
|
|
13463
|
-
return "The
|
|
13512
|
+
return "The harness is off on this device. Enable it here, or choose another harness or device in Cabane.";
|
|
13464
13513
|
if (reason?.startsWith("model_unavailable:"))
|
|
13465
13514
|
return "The selected model is unavailable. Choose a different model in the agent settings in Cabane.";
|
|
13466
13515
|
if (reason?.startsWith("prepare_failed:"))
|
|
@@ -13839,7 +13888,6 @@ init_install_kind();
|
|
|
13839
13888
|
init_src();
|
|
13840
13889
|
var ConnectorHealthStore = class {
|
|
13841
13890
|
byRuntime = /* @__PURE__ */ new Map();
|
|
13842
|
-
sources = /* @__PURE__ */ new Map();
|
|
13843
13891
|
// Record a settled turn's outcome for `runtime`. A success heals to `ok`; a
|
|
13844
13892
|
// classified failure sets `limited`(+reset) / `auth_expired` with the reason as
|
|
13845
13893
|
// `last_error`; anything else (a user Stop, a timeout, an unclassified error)
|
|
@@ -13848,7 +13896,6 @@ var ConnectorHealthStore = class {
|
|
|
13848
13896
|
recordSettle(runtime, outcome) {
|
|
13849
13897
|
if (outcome.ok) {
|
|
13850
13898
|
this.byRuntime.set(runtime, { quotaState: "ok", limitedUntil: null, lastError: null });
|
|
13851
|
-
this.sources.set(runtime, "turn");
|
|
13852
13899
|
return;
|
|
13853
13900
|
}
|
|
13854
13901
|
const failure = decodeFailureReason(outcome.errorReason);
|
|
@@ -13860,26 +13907,6 @@ var ConnectorHealthStore = class {
|
|
|
13860
13907
|
limitedUntil: quota.limitedUntil,
|
|
13861
13908
|
lastError: outcome.errorReason ?? null
|
|
13862
13909
|
});
|
|
13863
|
-
this.sources.set(runtime, "turn");
|
|
13864
|
-
}
|
|
13865
|
-
// Record what the companion's own sign-in check found for a connected
|
|
13866
|
-
// runtime at `start` or `connect`, so the heartbeat carries "sign-in needed"
|
|
13867
|
-
// before any turn fails. Signed out seeds `auth_expired` unless a turn has
|
|
13868
|
-
// already spoken; usable clears only a seed of our own.
|
|
13869
|
-
recordLocalCheck(runtime, usable) {
|
|
13870
|
-
const source = this.sources.get(runtime);
|
|
13871
|
-
if (!usable) {
|
|
13872
|
-
if (source === "turn" && this.byRuntime.get(runtime)?.quotaState !== "ok") return;
|
|
13873
|
-
this.byRuntime.set(runtime, {
|
|
13874
|
-
quotaState: "auth_expired",
|
|
13875
|
-
limitedUntil: null,
|
|
13876
|
-
lastError: encodeFailureReason({ kind: "auth_expired" })
|
|
13877
|
-
});
|
|
13878
|
-
this.sources.set(runtime, "local");
|
|
13879
|
-
return;
|
|
13880
|
-
}
|
|
13881
|
-
if (source !== "local") return;
|
|
13882
|
-
this.byRuntime.set(runtime, { quotaState: "ok", limitedUntil: null, lastError: null });
|
|
13883
13910
|
}
|
|
13884
13911
|
// The store's current opinion for one runtime, or undefined when it has
|
|
13885
13912
|
// no observation. Read by the dispatcher at idle-timeout fire: an idle reap
|
|
@@ -16364,6 +16391,9 @@ var CompanionSupervisor = class {
|
|
|
16364
16391
|
// Omitted until the first observed failure or heal, so the server's
|
|
16365
16392
|
// status-less manifest synthesis still runs for a fresh companion.
|
|
16366
16393
|
...connectorReports.length > 0 ? { connectors: connectorReports } : {},
|
|
16394
|
+
// The complete harness inventory in the machine's words, once detection
|
|
16395
|
+
// has run; omitted before it, which the server reads as "nothing yet".
|
|
16396
|
+
...this.detection ? { harnesses: harnessEntriesFromDetection(this.detection) } : {},
|
|
16367
16397
|
...opencodeModels !== null ? { models: opencodeModels } : {},
|
|
16368
16398
|
...this.updater ? { update: this.updater.report() } : {}
|
|
16369
16399
|
});
|
|
@@ -17316,7 +17346,6 @@ var CompanionSupervisor = class {
|
|
|
17316
17346
|
...this.probeOpencode ? { probeOpencode: this.probeOpencode } : {}
|
|
17317
17347
|
});
|
|
17318
17348
|
this.detection = detection;
|
|
17319
|
-
this.seedSignInHealth(previous, detection);
|
|
17320
17349
|
this.logBundledTransitions();
|
|
17321
17350
|
this.harnessVersions = {
|
|
17322
17351
|
claudeCode: detectedVersion(detection["claude-code"]),
|
|
@@ -17374,23 +17403,6 @@ var CompanionSupervisor = class {
|
|
|
17374
17403
|
const bare = advertisedRuntimes(this.detection).filter((runtime) => !named.has(runtime)).map((runtime) => ({ runtime }));
|
|
17375
17404
|
return [...health, ...bare];
|
|
17376
17405
|
}
|
|
17377
|
-
// Seed connector health from what detection saw of a sign-in, so Cabane says
|
|
17378
|
-
// "sign-in needed" before a turn fails. Only a LAPSE seeds it — an agent that
|
|
17379
|
-
// was usable at an earlier look and is signed out now — because a device
|
|
17380
|
-
// that never signed in to Codex has nothing to report about Codex. Usable
|
|
17381
|
-
// again clears a seed of our own.
|
|
17382
|
-
seedSignInHealth(previous, detection) {
|
|
17383
|
-
for (const runtime of ["claude-code", "codex"]) {
|
|
17384
|
-
const now = detection[runtime];
|
|
17385
|
-
if (now.state === "usable") {
|
|
17386
|
-
if (now.via !== "container") this.connectorHealth.recordLocalCheck(runtime, true);
|
|
17387
|
-
continue;
|
|
17388
|
-
}
|
|
17389
|
-
if (now.state === "signed_out" && previous?.[runtime].state === "usable") {
|
|
17390
|
-
this.connectorHealth.recordLocalCheck(runtime, false);
|
|
17391
|
-
}
|
|
17392
|
-
}
|
|
17393
|
-
}
|
|
17394
17406
|
// Beat NOW and wait for it to land. `start` calls this before asking the
|
|
17395
17407
|
// person anything, so the browser's connect step already shows what this
|
|
17396
17408
|
// machine has rather than sitting blank while the terminal waits on an answer.
|
|
@@ -18363,7 +18375,7 @@ async function startScript(opts, interactive) {
|
|
|
18363
18375
|
}
|
|
18364
18376
|
if (!isDevicePaired()) {
|
|
18365
18377
|
refuse([
|
|
18366
|
-
`This device isn't set up yet. Run ${cabane("setup")} to
|
|
18378
|
+
`This device isn't set up yet. Run ${cabane("setup")} to sign in to a harness and pair it.`
|
|
18367
18379
|
]);
|
|
18368
18380
|
return;
|
|
18369
18381
|
}
|
|
@@ -18814,7 +18826,7 @@ async function setupScript(opts, deps) {
|
|
|
18814
18826
|
const detection = await detect(cfg, running, deps);
|
|
18815
18827
|
ui.intro("Cabane setup");
|
|
18816
18828
|
const usable = anyUsable(detection);
|
|
18817
|
-
ui.step(["
|
|
18829
|
+
ui.step(["Harnesses on this device", ...agentLines(detection, { signInHint: !usable })]);
|
|
18818
18830
|
if (!usable && hasProblem(detection)) {
|
|
18819
18831
|
ui.cancel(`Fix the above, then run ${cabane("setup")} again.`);
|
|
18820
18832
|
process.exitCode = 1;
|
|
@@ -19214,7 +19226,7 @@ function strayStatusLines(running, find = findStrayCompanions) {
|
|
|
19214
19226
|
}
|
|
19215
19227
|
async function agentLines2(cfg, running) {
|
|
19216
19228
|
const detection = running ?? await detectHarnesses(cfg);
|
|
19217
|
-
return agentRows(detection
|
|
19229
|
+
return agentRows(detection).map((agent) => row2(agent.label, agent.text));
|
|
19218
19230
|
}
|
|
19219
19231
|
function sandboxStatusLines(cfg) {
|
|
19220
19232
|
const rows = agentSandboxRows(cfg);
|
package/dist/runtime.js
CHANGED
|
@@ -1860,7 +1860,7 @@ function turnFailureCopy(reason, runtime) {
|
|
|
1860
1860
|
if (reason?.startsWith("runtime_incomplete:"))
|
|
1861
1861
|
return `The agent installation is incomplete. ${repairAction()} Then restart the companion.`;
|
|
1862
1862
|
if (/^(runtime|codex|opencode)_unavailable:/.test(reason ?? ""))
|
|
1863
|
-
return "The
|
|
1863
|
+
return "The harness is off on this device. Enable it here, or choose another harness or device in Cabane.";
|
|
1864
1864
|
if (reason?.startsWith("model_unavailable:"))
|
|
1865
1865
|
return "The selected model is unavailable. Choose a different model in the agent settings in Cabane.";
|
|
1866
1866
|
if (reason?.startsWith("prepare_failed:"))
|
|
@@ -7761,7 +7761,6 @@ import { createSdkMcpServer, tool } from "@anthropic-ai/claude-agent-sdk";
|
|
|
7761
7761
|
// src/connector-health.ts
|
|
7762
7762
|
var ConnectorHealthStore = class {
|
|
7763
7763
|
byRuntime = /* @__PURE__ */ new Map();
|
|
7764
|
-
sources = /* @__PURE__ */ new Map();
|
|
7765
7764
|
// Record a settled turn's outcome for `runtime`. A success heals to `ok`; a
|
|
7766
7765
|
// classified failure sets `limited`(+reset) / `auth_expired` with the reason as
|
|
7767
7766
|
// `last_error`; anything else (a user Stop, a timeout, an unclassified error)
|
|
@@ -7770,7 +7769,6 @@ var ConnectorHealthStore = class {
|
|
|
7770
7769
|
recordSettle(runtime, outcome) {
|
|
7771
7770
|
if (outcome.ok) {
|
|
7772
7771
|
this.byRuntime.set(runtime, { quotaState: "ok", limitedUntil: null, lastError: null });
|
|
7773
|
-
this.sources.set(runtime, "turn");
|
|
7774
7772
|
return;
|
|
7775
7773
|
}
|
|
7776
7774
|
const failure = decodeFailureReason(outcome.errorReason);
|
|
@@ -7782,26 +7780,6 @@ var ConnectorHealthStore = class {
|
|
|
7782
7780
|
limitedUntil: quota.limitedUntil,
|
|
7783
7781
|
lastError: outcome.errorReason ?? null
|
|
7784
7782
|
});
|
|
7785
|
-
this.sources.set(runtime, "turn");
|
|
7786
|
-
}
|
|
7787
|
-
// Record what the companion's own sign-in check found for a connected
|
|
7788
|
-
// runtime at `start` or `connect`, so the heartbeat carries "sign-in needed"
|
|
7789
|
-
// before any turn fails. Signed out seeds `auth_expired` unless a turn has
|
|
7790
|
-
// already spoken; usable clears only a seed of our own.
|
|
7791
|
-
recordLocalCheck(runtime, usable) {
|
|
7792
|
-
const source = this.sources.get(runtime);
|
|
7793
|
-
if (!usable) {
|
|
7794
|
-
if (source === "turn" && this.byRuntime.get(runtime)?.quotaState !== "ok") return;
|
|
7795
|
-
this.byRuntime.set(runtime, {
|
|
7796
|
-
quotaState: "auth_expired",
|
|
7797
|
-
limitedUntil: null,
|
|
7798
|
-
lastError: encodeFailureReason({ kind: "auth_expired" })
|
|
7799
|
-
});
|
|
7800
|
-
this.sources.set(runtime, "local");
|
|
7801
|
-
return;
|
|
7802
|
-
}
|
|
7803
|
-
if (source !== "local") return;
|
|
7804
|
-
this.byRuntime.set(runtime, { quotaState: "ok", limitedUntil: null, lastError: null });
|
|
7805
7783
|
}
|
|
7806
7784
|
// The store's current opinion for one runtime, or undefined when it has
|
|
7807
7785
|
// no observation. Read by the dispatcher at idle-timeout fire: an idle reap
|
|
@@ -12767,6 +12745,41 @@ function runtimeIssuesFor(gates) {
|
|
|
12767
12745
|
return issues;
|
|
12768
12746
|
}
|
|
12769
12747
|
|
|
12748
|
+
// src/harness-state.ts
|
|
12749
|
+
function harnessEntry(runtime, state) {
|
|
12750
|
+
switch (state.state) {
|
|
12751
|
+
case "usable":
|
|
12752
|
+
return {
|
|
12753
|
+
runtime,
|
|
12754
|
+
state: "connected",
|
|
12755
|
+
reason: state.via,
|
|
12756
|
+
version: state.version,
|
|
12757
|
+
...state.url ? { url: state.url } : {}
|
|
12758
|
+
};
|
|
12759
|
+
case "signed_out":
|
|
12760
|
+
return { runtime, state: "needs_sign_in", reason: "signed_out", version: state.version };
|
|
12761
|
+
case "not_running":
|
|
12762
|
+
return { runtime, state: "problem", reason: "not_running", url: state.url };
|
|
12763
|
+
case "off":
|
|
12764
|
+
return { runtime, state: "off" };
|
|
12765
|
+
case "problem":
|
|
12766
|
+
return { runtime, state: "problem", reason: checkReason(state.check) };
|
|
12767
|
+
}
|
|
12768
|
+
}
|
|
12769
|
+
function harnessEntriesFromDetection(detection) {
|
|
12770
|
+
return HARNESS_RUNTIMES.map((runtime) => harnessEntry(runtime, detection[runtime]));
|
|
12771
|
+
}
|
|
12772
|
+
function checkReason(check) {
|
|
12773
|
+
switch (check.status) {
|
|
12774
|
+
case "bundle_missing":
|
|
12775
|
+
return check.bundled;
|
|
12776
|
+
case "check_failed":
|
|
12777
|
+
return `check_failed:${check.reason}`;
|
|
12778
|
+
default:
|
|
12779
|
+
return check.status;
|
|
12780
|
+
}
|
|
12781
|
+
}
|
|
12782
|
+
|
|
12770
12783
|
// src/harness-versions.ts
|
|
12771
12784
|
var EMPTY = { claudeCode: null, opencode: null, codex: null };
|
|
12772
12785
|
function parseVersionToken(raw) {
|
|
@@ -14301,6 +14314,9 @@ var CompanionSupervisor = class {
|
|
|
14301
14314
|
// Omitted until the first observed failure or heal, so the server's
|
|
14302
14315
|
// status-less manifest synthesis still runs for a fresh companion.
|
|
14303
14316
|
...connectorReports.length > 0 ? { connectors: connectorReports } : {},
|
|
14317
|
+
// The complete harness inventory in the machine's words, once detection
|
|
14318
|
+
// has run; omitted before it, which the server reads as "nothing yet".
|
|
14319
|
+
...this.detection ? { harnesses: harnessEntriesFromDetection(this.detection) } : {},
|
|
14304
14320
|
...opencodeModels !== null ? { models: opencodeModels } : {},
|
|
14305
14321
|
...this.updater ? { update: this.updater.report() } : {}
|
|
14306
14322
|
});
|
|
@@ -15253,7 +15269,6 @@ var CompanionSupervisor = class {
|
|
|
15253
15269
|
...this.probeOpencode ? { probeOpencode: this.probeOpencode } : {}
|
|
15254
15270
|
});
|
|
15255
15271
|
this.detection = detection;
|
|
15256
|
-
this.seedSignInHealth(previous, detection);
|
|
15257
15272
|
this.logBundledTransitions();
|
|
15258
15273
|
this.harnessVersions = {
|
|
15259
15274
|
claudeCode: detectedVersion(detection["claude-code"]),
|
|
@@ -15311,23 +15326,6 @@ var CompanionSupervisor = class {
|
|
|
15311
15326
|
const bare = advertisedRuntimes(this.detection).filter((runtime) => !named.has(runtime)).map((runtime) => ({ runtime }));
|
|
15312
15327
|
return [...health, ...bare];
|
|
15313
15328
|
}
|
|
15314
|
-
// Seed connector health from what detection saw of a sign-in, so Cabane says
|
|
15315
|
-
// "sign-in needed" before a turn fails. Only a LAPSE seeds it — an agent that
|
|
15316
|
-
// was usable at an earlier look and is signed out now — because a device
|
|
15317
|
-
// that never signed in to Codex has nothing to report about Codex. Usable
|
|
15318
|
-
// again clears a seed of our own.
|
|
15319
|
-
seedSignInHealth(previous, detection) {
|
|
15320
|
-
for (const runtime of ["claude-code", "codex"]) {
|
|
15321
|
-
const now = detection[runtime];
|
|
15322
|
-
if (now.state === "usable") {
|
|
15323
|
-
if (now.via !== "container") this.connectorHealth.recordLocalCheck(runtime, true);
|
|
15324
|
-
continue;
|
|
15325
|
-
}
|
|
15326
|
-
if (now.state === "signed_out" && previous?.[runtime].state === "usable") {
|
|
15327
|
-
this.connectorHealth.recordLocalCheck(runtime, false);
|
|
15328
|
-
}
|
|
15329
|
-
}
|
|
15330
|
-
}
|
|
15331
15329
|
// Beat NOW and wait for it to land. `start` calls this before asking the
|
|
15332
15330
|
// person anything, so the browser's connect step already shows what this
|
|
15333
15331
|
// machine has rather than sitting blank while the terminal waits on an answer.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cabane/companion",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.175",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "The Cabane Companion (headless): connect an agent on your machine to your Cabane workspace as a responder — drive work against your own codebase, files, and MCP servers without putting any of it in Cabane.",
|
|
6
6
|
"license": "UNLICENSED",
|