@cabane/companion 0.6.30 → 0.6.32
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 +30 -56
- package/dist/runtime.js +26 -52
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -5514,9 +5514,13 @@ function sealHeld2(held, terminal) {
|
|
|
5514
5514
|
import { z as z11 } from "zod";
|
|
5515
5515
|
function codexToolPolicy(policy) {
|
|
5516
5516
|
return policy.hostFs ? {
|
|
5517
|
-
|
|
5517
|
+
sandboxMode: "danger-full-access",
|
|
5518
|
+
// Headless: nothing to approve, because nothing is denied.
|
|
5518
5519
|
approvalPolicy: "never",
|
|
5519
|
-
|
|
5520
|
+
// Full access means the command network is open regardless of the grant —
|
|
5521
|
+
// recorded as `true` so the value never claims a restriction that isn't
|
|
5522
|
+
// enforced.
|
|
5523
|
+
networkAccessEnabled: true
|
|
5520
5524
|
} : {
|
|
5521
5525
|
sandboxMode: "read-only",
|
|
5522
5526
|
// Headless: the sandbox is the boundary; never pause for a human.
|
|
@@ -5636,7 +5640,6 @@ function buildConfig(req, baseInstructionsFile) {
|
|
|
5636
5640
|
default_tools_approval_mode: "approve"
|
|
5637
5641
|
};
|
|
5638
5642
|
}
|
|
5639
|
-
const policy = codexToolPolicy(req.policy);
|
|
5640
5643
|
const tmpDir = req.local.env?.TMPDIR;
|
|
5641
5644
|
const shellEnv = {};
|
|
5642
5645
|
if (tmpDir) shellEnv.TMPDIR = tmpDir;
|
|
@@ -5644,7 +5647,6 @@ function buildConfig(req, baseInstructionsFile) {
|
|
|
5644
5647
|
const value = req.local.env?.[key];
|
|
5645
5648
|
if (value !== void 0) shellEnv[key] = value;
|
|
5646
5649
|
}
|
|
5647
|
-
const gateLockDir = req.local.env?.CABANE_GATE_LOCK_DIR;
|
|
5648
5650
|
return {
|
|
5649
5651
|
mcp_servers,
|
|
5650
5652
|
experimental_use_rmcp_client: true,
|
|
@@ -5652,28 +5654,7 @@ function buildConfig(req, baseInstructionsFile) {
|
|
|
5652
5654
|
// becomes Codex's base instructions, and Codex's own environment preamble goes
|
|
5653
5655
|
// with the harness prompt it belonged to.
|
|
5654
5656
|
...baseInstructionsFile ? { model_instructions_file: baseInstructionsFile, include_environment_context: false } : {},
|
|
5655
|
-
...Object.keys(shellEnv).length ? { shell_environment_policy: { set: shellEnv } } : {}
|
|
5656
|
-
...policy.permissionProfile ? {
|
|
5657
|
-
// CT733: named permission profiles are Codex's split-filesystem path.
|
|
5658
|
-
// `:root = read` preserves coding-mode host reads; the one explicit
|
|
5659
|
-
// workspace-root write grants the checkout, and the more-specific
|
|
5660
|
-
// `.git` write reopens the metadata Codex protects by default. Neither
|
|
5661
|
-
// rule grants an adjacent directory. Do not combine this with legacy `sandbox_mode` /
|
|
5662
|
-
// `sandbox_workspace_write`, which would restore the `.git` carve-out.
|
|
5663
|
-
approval_policy: policy.approvalPolicy,
|
|
5664
|
-
default_permissions: policy.permissionProfile,
|
|
5665
|
-
permissions: {
|
|
5666
|
-
[policy.permissionProfile]: {
|
|
5667
|
-
filesystem: {
|
|
5668
|
-
":root": "read",
|
|
5669
|
-
":workspace_roots": { ".": "write", ".git": "write" },
|
|
5670
|
-
// The one absolute-path grant — see the CT612-fallout note above.
|
|
5671
|
-
...gateLockDir ? { [gateLockDir]: "write" } : {}
|
|
5672
|
-
},
|
|
5673
|
-
network: { enabled: policy.networkAccessEnabled, mode: "full" }
|
|
5674
|
-
}
|
|
5675
|
-
}
|
|
5676
|
-
} : {}
|
|
5657
|
+
...Object.keys(shellEnv).length ? { shell_environment_policy: { set: shellEnv } } : {}
|
|
5677
5658
|
};
|
|
5678
5659
|
}
|
|
5679
5660
|
function isStringRecord2(v) {
|
|
@@ -5687,11 +5668,10 @@ import {
|
|
|
5687
5668
|
function buildSdkThreadOptions(spec) {
|
|
5688
5669
|
return {
|
|
5689
5670
|
...spec.model ? { model: spec.model } : {},
|
|
5690
|
-
|
|
5671
|
+
sandboxMode: spec.policy.sandboxMode,
|
|
5691
5672
|
workingDirectory: spec.directory,
|
|
5692
5673
|
skipGitRepoCheck: spec.skipGitRepoCheck,
|
|
5693
|
-
|
|
5694
|
-
...spec.policy.sandboxMode === "workspace-write" ? { networkAccessEnabled: spec.policy.networkAccessEnabled } : {},
|
|
5674
|
+
approvalPolicy: spec.policy.approvalPolicy,
|
|
5695
5675
|
...spec.modelReasoningEffort ? { modelReasoningEffort: spec.modelReasoningEffort } : {}
|
|
5696
5676
|
};
|
|
5697
5677
|
}
|
|
@@ -6475,7 +6455,7 @@ var ConnectorHealthStore = class {
|
|
|
6475
6455
|
|
|
6476
6456
|
// src/dispatcher.ts
|
|
6477
6457
|
import { randomUUID } from "crypto";
|
|
6478
|
-
import { appendFileSync as appendFileSync2, existsSync as existsSync11, mkdirSync as mkdirSync11, statSync } from "fs";
|
|
6458
|
+
import { appendFileSync as appendFileSync2, existsSync as existsSync11, mkdirSync as mkdirSync11, readdirSync as readdirSync2, statSync } from "fs";
|
|
6479
6459
|
import { join as join15 } from "path";
|
|
6480
6460
|
|
|
6481
6461
|
// src/summon.ts
|
|
@@ -7371,10 +7351,19 @@ var DEFAULT_PREPARING_ROW_DELAY_MS = 1500;
|
|
|
7371
7351
|
var DEFAULT_AGENT_IDLE_TIMEOUT_MS = 10 * 6e4;
|
|
7372
7352
|
var DEFAULT_AGENT_TOTAL_TIMEOUT_MS = 6 * 60 * 6e4;
|
|
7373
7353
|
function checkoutState(cwd) {
|
|
7374
|
-
if (!cwd) return { ok: false, reason: "no
|
|
7375
|
-
if (!existsSync11(cwd)) return { ok: false, reason: `the
|
|
7354
|
+
if (!cwd) return { ok: false, reason: "no working directory was resolved for this turn" };
|
|
7355
|
+
if (!existsSync11(cwd)) return { ok: false, reason: `the working directory is gone (${cwd})` };
|
|
7356
|
+
let entries;
|
|
7357
|
+
try {
|
|
7358
|
+
entries = readdirSync2(cwd);
|
|
7359
|
+
} catch (error) {
|
|
7360
|
+
return { ok: false, reason: `${cwd} is unreadable (${error.message})` };
|
|
7361
|
+
}
|
|
7362
|
+
if (entries.length === 0) {
|
|
7363
|
+
return { ok: false, reason: `${cwd} is empty \u2014 a recreated shell, not a prepared directory` };
|
|
7364
|
+
}
|
|
7376
7365
|
const gitPath = join15(cwd, ".git");
|
|
7377
|
-
if (!existsSync11(gitPath)) return { ok:
|
|
7366
|
+
if (!existsSync11(gitPath)) return { ok: true, reason: "usable" };
|
|
7378
7367
|
let stat;
|
|
7379
7368
|
try {
|
|
7380
7369
|
stat = statSync(gitPath);
|
|
@@ -7708,22 +7697,7 @@ ${reason}`,
|
|
|
7708
7697
|
}
|
|
7709
7698
|
}
|
|
7710
7699
|
}
|
|
7711
|
-
|
|
7712
|
-
if (effectiveCwd && turnContext.runtime === "codex") {
|
|
7713
|
-
const tmpDir = join15(effectiveCwd, "node_modules", ".cache", "cabane-tmp", turnId);
|
|
7714
|
-
try {
|
|
7715
|
-
mkdirSync11(tmpDir, { recursive: true });
|
|
7716
|
-
turnEnv = { ...hookEnv, TMPDIR: tmpDir };
|
|
7717
|
-
} catch (err) {
|
|
7718
|
-
turnLog.warn(
|
|
7719
|
-
{ err: err instanceof Error ? err.message : String(err), tmpDir },
|
|
7720
|
-
"dispatcher: failed to create per-turn TMPDIR \u2014 proceeding with the inherited temp dir"
|
|
7721
|
-
);
|
|
7722
|
-
}
|
|
7723
|
-
}
|
|
7724
|
-
if (turnContext.runtime === "codex" && process.env.CABANE_GATE_LOCK_DIR) {
|
|
7725
|
-
turnEnv = { ...turnEnv, CABANE_GATE_LOCK_DIR: process.env.CABANE_GATE_LOCK_DIR };
|
|
7726
|
-
}
|
|
7700
|
+
const turnEnv = hookEnv;
|
|
7727
7701
|
const key = runKey(payload.conversationId, payload.agentId);
|
|
7728
7702
|
const abortController = new AbortController();
|
|
7729
7703
|
this.aborts.set(key, abortController);
|
|
@@ -8588,7 +8562,7 @@ async function enumerateOpencodeModels(serverUrl, fetchImpl = fetch) {
|
|
|
8588
8562
|
import {
|
|
8589
8563
|
existsSync as existsSync12,
|
|
8590
8564
|
mkdirSync as mkdirSync12,
|
|
8591
|
-
readdirSync as
|
|
8565
|
+
readdirSync as readdirSync3,
|
|
8592
8566
|
readFileSync as readFileSync9,
|
|
8593
8567
|
renameSync as renameSync3,
|
|
8594
8568
|
rmSync as rmSync7,
|
|
@@ -8645,7 +8619,7 @@ var Outbox = class {
|
|
|
8645
8619
|
if (!existsSync12(dir2)) return [];
|
|
8646
8620
|
let names;
|
|
8647
8621
|
try {
|
|
8648
|
-
names =
|
|
8622
|
+
names = readdirSync3(dir2);
|
|
8649
8623
|
} catch {
|
|
8650
8624
|
return [];
|
|
8651
8625
|
}
|
|
@@ -8680,7 +8654,7 @@ var Outbox = class {
|
|
|
8680
8654
|
const dir2 = this.dir();
|
|
8681
8655
|
if (!existsSync12(dir2)) return 0;
|
|
8682
8656
|
try {
|
|
8683
|
-
return
|
|
8657
|
+
return readdirSync3(dir2).filter((n) => n.endsWith(".json")).length;
|
|
8684
8658
|
} catch {
|
|
8685
8659
|
return 0;
|
|
8686
8660
|
}
|
|
@@ -10058,7 +10032,7 @@ companion: deploy drain requested (${graceMs}ms grace)\u2026
|
|
|
10058
10032
|
}
|
|
10059
10033
|
|
|
10060
10034
|
// src/commands/status.ts
|
|
10061
|
-
import { readdirSync as
|
|
10035
|
+
import { readdirSync as readdirSync4 } from "fs";
|
|
10062
10036
|
async function status(deps = {}) {
|
|
10063
10037
|
const readService = deps.service ?? serviceStatus;
|
|
10064
10038
|
const cfg = loadConfig();
|
|
@@ -10129,7 +10103,7 @@ function transcriptsLine() {
|
|
|
10129
10103
|
let count = 0;
|
|
10130
10104
|
let newest;
|
|
10131
10105
|
try {
|
|
10132
|
-
const files =
|
|
10106
|
+
const files = readdirSync4(dir2).filter((f) => f.endsWith(".jsonl")).sort();
|
|
10133
10107
|
count = files.length;
|
|
10134
10108
|
newest = files[files.length - 1];
|
|
10135
10109
|
} catch {
|
|
@@ -10229,7 +10203,7 @@ function isAlive(kill, pid) {
|
|
|
10229
10203
|
}
|
|
10230
10204
|
|
|
10231
10205
|
// src/commands/transcript.ts
|
|
10232
|
-
import { existsSync as existsSync14, readFileSync as readFileSync11, readdirSync as
|
|
10206
|
+
import { existsSync as existsSync14, readFileSync as readFileSync11, readdirSync as readdirSync5 } from "fs";
|
|
10233
10207
|
import { isAbsolute, join as join18 } from "path";
|
|
10234
10208
|
async function transcript(opts = {}) {
|
|
10235
10209
|
const dir2 = transcriptsDir();
|
|
@@ -10355,7 +10329,7 @@ async function followTranscripts(dir2) {
|
|
|
10355
10329
|
}
|
|
10356
10330
|
function listFiles(dir2) {
|
|
10357
10331
|
try {
|
|
10358
|
-
return
|
|
10332
|
+
return readdirSync5(dir2).filter((f) => f.endsWith(".jsonl")).sort().reverse();
|
|
10359
10333
|
} catch {
|
|
10360
10334
|
return [];
|
|
10361
10335
|
}
|
package/dist/runtime.js
CHANGED
|
@@ -4626,9 +4626,13 @@ function sealHeld2(held, terminal) {
|
|
|
4626
4626
|
import { z as z11 } from "zod";
|
|
4627
4627
|
function codexToolPolicy(policy) {
|
|
4628
4628
|
return policy.hostFs ? {
|
|
4629
|
-
|
|
4629
|
+
sandboxMode: "danger-full-access",
|
|
4630
|
+
// Headless: nothing to approve, because nothing is denied.
|
|
4630
4631
|
approvalPolicy: "never",
|
|
4631
|
-
|
|
4632
|
+
// Full access means the command network is open regardless of the grant —
|
|
4633
|
+
// recorded as `true` so the value never claims a restriction that isn't
|
|
4634
|
+
// enforced.
|
|
4635
|
+
networkAccessEnabled: true
|
|
4632
4636
|
} : {
|
|
4633
4637
|
sandboxMode: "read-only",
|
|
4634
4638
|
// Headless: the sandbox is the boundary; never pause for a human.
|
|
@@ -4748,7 +4752,6 @@ function buildConfig(req, baseInstructionsFile) {
|
|
|
4748
4752
|
default_tools_approval_mode: "approve"
|
|
4749
4753
|
};
|
|
4750
4754
|
}
|
|
4751
|
-
const policy = codexToolPolicy(req.policy);
|
|
4752
4755
|
const tmpDir = req.local.env?.TMPDIR;
|
|
4753
4756
|
const shellEnv = {};
|
|
4754
4757
|
if (tmpDir) shellEnv.TMPDIR = tmpDir;
|
|
@@ -4756,7 +4759,6 @@ function buildConfig(req, baseInstructionsFile) {
|
|
|
4756
4759
|
const value = req.local.env?.[key];
|
|
4757
4760
|
if (value !== void 0) shellEnv[key] = value;
|
|
4758
4761
|
}
|
|
4759
|
-
const gateLockDir = req.local.env?.CABANE_GATE_LOCK_DIR;
|
|
4760
4762
|
return {
|
|
4761
4763
|
mcp_servers,
|
|
4762
4764
|
experimental_use_rmcp_client: true,
|
|
@@ -4764,28 +4766,7 @@ function buildConfig(req, baseInstructionsFile) {
|
|
|
4764
4766
|
// becomes Codex's base instructions, and Codex's own environment preamble goes
|
|
4765
4767
|
// with the harness prompt it belonged to.
|
|
4766
4768
|
...baseInstructionsFile ? { model_instructions_file: baseInstructionsFile, include_environment_context: false } : {},
|
|
4767
|
-
...Object.keys(shellEnv).length ? { shell_environment_policy: { set: shellEnv } } : {}
|
|
4768
|
-
...policy.permissionProfile ? {
|
|
4769
|
-
// CT733: named permission profiles are Codex's split-filesystem path.
|
|
4770
|
-
// `:root = read` preserves coding-mode host reads; the one explicit
|
|
4771
|
-
// workspace-root write grants the checkout, and the more-specific
|
|
4772
|
-
// `.git` write reopens the metadata Codex protects by default. Neither
|
|
4773
|
-
// rule grants an adjacent directory. Do not combine this with legacy `sandbox_mode` /
|
|
4774
|
-
// `sandbox_workspace_write`, which would restore the `.git` carve-out.
|
|
4775
|
-
approval_policy: policy.approvalPolicy,
|
|
4776
|
-
default_permissions: policy.permissionProfile,
|
|
4777
|
-
permissions: {
|
|
4778
|
-
[policy.permissionProfile]: {
|
|
4779
|
-
filesystem: {
|
|
4780
|
-
":root": "read",
|
|
4781
|
-
":workspace_roots": { ".": "write", ".git": "write" },
|
|
4782
|
-
// The one absolute-path grant — see the CT612-fallout note above.
|
|
4783
|
-
...gateLockDir ? { [gateLockDir]: "write" } : {}
|
|
4784
|
-
},
|
|
4785
|
-
network: { enabled: policy.networkAccessEnabled, mode: "full" }
|
|
4786
|
-
}
|
|
4787
|
-
}
|
|
4788
|
-
} : {}
|
|
4769
|
+
...Object.keys(shellEnv).length ? { shell_environment_policy: { set: shellEnv } } : {}
|
|
4789
4770
|
};
|
|
4790
4771
|
}
|
|
4791
4772
|
function isStringRecord2(v) {
|
|
@@ -4799,11 +4780,10 @@ import {
|
|
|
4799
4780
|
function buildSdkThreadOptions(spec) {
|
|
4800
4781
|
return {
|
|
4801
4782
|
...spec.model ? { model: spec.model } : {},
|
|
4802
|
-
|
|
4783
|
+
sandboxMode: spec.policy.sandboxMode,
|
|
4803
4784
|
workingDirectory: spec.directory,
|
|
4804
4785
|
skipGitRepoCheck: spec.skipGitRepoCheck,
|
|
4805
|
-
|
|
4806
|
-
...spec.policy.sandboxMode === "workspace-write" ? { networkAccessEnabled: spec.policy.networkAccessEnabled } : {},
|
|
4786
|
+
approvalPolicy: spec.policy.approvalPolicy,
|
|
4807
4787
|
...spec.modelReasoningEffort ? { modelReasoningEffort: spec.modelReasoningEffort } : {}
|
|
4808
4788
|
};
|
|
4809
4789
|
}
|
|
@@ -5587,7 +5567,7 @@ var ConnectorHealthStore = class {
|
|
|
5587
5567
|
|
|
5588
5568
|
// src/dispatcher.ts
|
|
5589
5569
|
import { randomUUID } from "crypto";
|
|
5590
|
-
import { appendFileSync as appendFileSync2, existsSync as existsSync9, mkdirSync as mkdirSync9, statSync } from "fs";
|
|
5570
|
+
import { appendFileSync as appendFileSync2, existsSync as existsSync9, mkdirSync as mkdirSync9, readdirSync as readdirSync2, statSync } from "fs";
|
|
5591
5571
|
import { join as join13 } from "path";
|
|
5592
5572
|
|
|
5593
5573
|
// src/summon.ts
|
|
@@ -6483,10 +6463,19 @@ var DEFAULT_PREPARING_ROW_DELAY_MS = 1500;
|
|
|
6483
6463
|
var DEFAULT_AGENT_IDLE_TIMEOUT_MS = 10 * 6e4;
|
|
6484
6464
|
var DEFAULT_AGENT_TOTAL_TIMEOUT_MS = 6 * 60 * 6e4;
|
|
6485
6465
|
function checkoutState(cwd) {
|
|
6486
|
-
if (!cwd) return { ok: false, reason: "no
|
|
6487
|
-
if (!existsSync9(cwd)) return { ok: false, reason: `the
|
|
6466
|
+
if (!cwd) return { ok: false, reason: "no working directory was resolved for this turn" };
|
|
6467
|
+
if (!existsSync9(cwd)) return { ok: false, reason: `the working directory is gone (${cwd})` };
|
|
6468
|
+
let entries;
|
|
6469
|
+
try {
|
|
6470
|
+
entries = readdirSync2(cwd);
|
|
6471
|
+
} catch (error) {
|
|
6472
|
+
return { ok: false, reason: `${cwd} is unreadable (${error.message})` };
|
|
6473
|
+
}
|
|
6474
|
+
if (entries.length === 0) {
|
|
6475
|
+
return { ok: false, reason: `${cwd} is empty \u2014 a recreated shell, not a prepared directory` };
|
|
6476
|
+
}
|
|
6488
6477
|
const gitPath = join13(cwd, ".git");
|
|
6489
|
-
if (!existsSync9(gitPath)) return { ok:
|
|
6478
|
+
if (!existsSync9(gitPath)) return { ok: true, reason: "usable" };
|
|
6490
6479
|
let stat;
|
|
6491
6480
|
try {
|
|
6492
6481
|
stat = statSync(gitPath);
|
|
@@ -6820,22 +6809,7 @@ ${reason}`,
|
|
|
6820
6809
|
}
|
|
6821
6810
|
}
|
|
6822
6811
|
}
|
|
6823
|
-
|
|
6824
|
-
if (effectiveCwd && turnContext.runtime === "codex") {
|
|
6825
|
-
const tmpDir = join13(effectiveCwd, "node_modules", ".cache", "cabane-tmp", turnId);
|
|
6826
|
-
try {
|
|
6827
|
-
mkdirSync9(tmpDir, { recursive: true });
|
|
6828
|
-
turnEnv = { ...hookEnv, TMPDIR: tmpDir };
|
|
6829
|
-
} catch (err) {
|
|
6830
|
-
turnLog.warn(
|
|
6831
|
-
{ err: err instanceof Error ? err.message : String(err), tmpDir },
|
|
6832
|
-
"dispatcher: failed to create per-turn TMPDIR \u2014 proceeding with the inherited temp dir"
|
|
6833
|
-
);
|
|
6834
|
-
}
|
|
6835
|
-
}
|
|
6836
|
-
if (turnContext.runtime === "codex" && process.env.CABANE_GATE_LOCK_DIR) {
|
|
6837
|
-
turnEnv = { ...turnEnv, CABANE_GATE_LOCK_DIR: process.env.CABANE_GATE_LOCK_DIR };
|
|
6838
|
-
}
|
|
6812
|
+
const turnEnv = hookEnv;
|
|
6839
6813
|
const key = runKey(payload.conversationId, payload.agentId);
|
|
6840
6814
|
const abortController = new AbortController();
|
|
6841
6815
|
this.aborts.set(key, abortController);
|
|
@@ -7700,7 +7674,7 @@ async function enumerateOpencodeModels(serverUrl, fetchImpl = fetch) {
|
|
|
7700
7674
|
import {
|
|
7701
7675
|
existsSync as existsSync10,
|
|
7702
7676
|
mkdirSync as mkdirSync10,
|
|
7703
|
-
readdirSync as
|
|
7677
|
+
readdirSync as readdirSync3,
|
|
7704
7678
|
readFileSync as readFileSync8,
|
|
7705
7679
|
renameSync as renameSync3,
|
|
7706
7680
|
rmSync as rmSync6,
|
|
@@ -7757,7 +7731,7 @@ var Outbox = class {
|
|
|
7757
7731
|
if (!existsSync10(dir2)) return [];
|
|
7758
7732
|
let names;
|
|
7759
7733
|
try {
|
|
7760
|
-
names =
|
|
7734
|
+
names = readdirSync3(dir2);
|
|
7761
7735
|
} catch {
|
|
7762
7736
|
return [];
|
|
7763
7737
|
}
|
|
@@ -7792,7 +7766,7 @@ var Outbox = class {
|
|
|
7792
7766
|
const dir2 = this.dir();
|
|
7793
7767
|
if (!existsSync10(dir2)) return 0;
|
|
7794
7768
|
try {
|
|
7795
|
-
return
|
|
7769
|
+
return readdirSync3(dir2).filter((n) => n.endsWith(".json")).length;
|
|
7796
7770
|
} catch {
|
|
7797
7771
|
return 0;
|
|
7798
7772
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cabane/companion",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.32",
|
|
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",
|