@cabane/companion 0.6.111 → 0.6.112
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 +75 -11
- package/dist/runtime.js +75 -11
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -2901,9 +2901,27 @@ function describeErrorBody(body) {
|
|
|
2901
2901
|
if (text.length === 0) return void 0;
|
|
2902
2902
|
return text.length > ERROR_BODY_LOG_CAP ? `${text.slice(0, ERROR_BODY_LOG_CAP)}\u2026` : text;
|
|
2903
2903
|
}
|
|
2904
|
+
var CAUSE_TEXT_CAP = 200;
|
|
2905
|
+
function describeError(err) {
|
|
2906
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
2907
|
+
const cause = describeCause(err);
|
|
2908
|
+
return cause === void 0 ? message : `${message}: ${cause}`;
|
|
2909
|
+
}
|
|
2910
|
+
function describeCause(err) {
|
|
2911
|
+
if (!(err instanceof Error)) return void 0;
|
|
2912
|
+
const cause = err.cause;
|
|
2913
|
+
if (cause === void 0 || cause === null) return void 0;
|
|
2914
|
+
if (typeof cause === "object") {
|
|
2915
|
+
const code = cause.code;
|
|
2916
|
+
if (typeof code === "string" && code.length > 0) return code;
|
|
2917
|
+
}
|
|
2918
|
+
const text = cause instanceof Error ? cause.message : typeof cause === "string" ? cause : void 0;
|
|
2919
|
+
if (text === void 0 || text.length === 0) return void 0;
|
|
2920
|
+
return text.length > CAUSE_TEXT_CAP ? `${text.slice(0, CAUSE_TEXT_CAP)}\u2026` : text;
|
|
2921
|
+
}
|
|
2904
2922
|
function apiErrorLogFields(err) {
|
|
2905
2923
|
const fields = {
|
|
2906
|
-
err:
|
|
2924
|
+
err: describeError(err)
|
|
2907
2925
|
};
|
|
2908
2926
|
if (err instanceof ApiError) {
|
|
2909
2927
|
fields.status = err.status;
|
|
@@ -2922,6 +2940,7 @@ import { randomUUID as randomUUID4 } from "crypto";
|
|
|
2922
2940
|
|
|
2923
2941
|
// src/api.ts
|
|
2924
2942
|
var RETRY_BACKOFF_MS = [250, 750];
|
|
2943
|
+
var TURN_CONTEXT_BACKOFF_MS = [250, 750, 2e3, 5e3];
|
|
2925
2944
|
var ACTIVE_RUN_OUTBOX_SEQ = 0;
|
|
2926
2945
|
var LEASE_CHECK_TIMEOUT_MS = 1e4;
|
|
2927
2946
|
function activeRunOutboxKey(conversationId, agentId) {
|
|
@@ -2972,14 +2991,26 @@ var CabaneApi = class {
|
|
|
2972
2991
|
return parsed;
|
|
2973
2992
|
}
|
|
2974
2993
|
async request(method, path, body, opts = {}) {
|
|
2975
|
-
const { signal, retry =
|
|
2976
|
-
const maxAttempts = retry ?
|
|
2994
|
+
const { signal, retry = method === "GET", backoffMs = RETRY_BACKOFF_MS } = opts;
|
|
2995
|
+
const maxAttempts = retry ? backoffMs.length + 1 : 1;
|
|
2977
2996
|
for (let attempt = 1; ; attempt++) {
|
|
2978
2997
|
try {
|
|
2979
2998
|
return await this.attempt(method, path, body, signal);
|
|
2980
2999
|
} catch (err) {
|
|
2981
3000
|
if (attempt >= maxAttempts || signal?.aborted || !isRetryable(err)) throw err;
|
|
2982
|
-
|
|
3001
|
+
const delayMs = backoffMs[attempt - 1];
|
|
3002
|
+
this.opts.log?.info(
|
|
3003
|
+
{
|
|
3004
|
+
method,
|
|
3005
|
+
path: pathForLog(path),
|
|
3006
|
+
attempt,
|
|
3007
|
+
maxAttempts,
|
|
3008
|
+
delayMs,
|
|
3009
|
+
err: describeError(err)
|
|
3010
|
+
},
|
|
3011
|
+
"companion: API call failed transiently; retrying"
|
|
3012
|
+
);
|
|
3013
|
+
await sleep2(delayMs, signal);
|
|
2983
3014
|
if (signal?.aborted) throw err;
|
|
2984
3015
|
}
|
|
2985
3016
|
}
|
|
@@ -3117,9 +3148,16 @@ var CabaneApi = class {
|
|
|
3117
3148
|
// CT1543: `episode` is the dispatch event's own, echoed back so the server can
|
|
3118
3149
|
// tell this frame from a later wake on the same message. Omitted when the
|
|
3119
3150
|
// event didn't carry one, which is the only thing an older event can mean.
|
|
3120
|
-
|
|
3151
|
+
// CT1657: `signal` is the turn's own abort. It matters here more than on any
|
|
3152
|
+
// other read, because this call now waits up to ~8s across five attempts: a
|
|
3153
|
+
// Stop landing in that window used to leave the whole schedule running, and
|
|
3154
|
+
// the person watched a turn they had already stopped keep trying.
|
|
3155
|
+
getTurnContext(conversationId, messageId2, turnId, resumed, fresh, episode, signal) {
|
|
3121
3156
|
const q = `conversationId=${encodeURIComponent(conversationId)}&messageId=${encodeURIComponent(messageId2)}` + (turnId ? `&turnId=${encodeURIComponent(turnId)}` : "") + (resumed ? "&resumed=1" : "") + (fresh ? "&fresh=1" : "") + (episode !== void 0 ? `&episode=${encodeURIComponent(String(episode))}` : "");
|
|
3122
|
-
return this.request("GET", `/api/agent/turn-context?${q}
|
|
3157
|
+
return this.request("GET", `/api/agent/turn-context?${q}`, void 0, {
|
|
3158
|
+
backoffMs: this.opts.turnContextBackoffMs ?? TURN_CONTEXT_BACKOFF_MS,
|
|
3159
|
+
...signal ? { signal } : {}
|
|
3160
|
+
});
|
|
3123
3161
|
}
|
|
3124
3162
|
// CT1380: the local half of a resumed turn's seq floor (see
|
|
3125
3163
|
// `Outbox.maxSeqForTurn`). 0 when no outbox is configured — a one-shot CLI or
|
|
@@ -3204,6 +3242,12 @@ var CabaneApi = class {
|
|
|
3204
3242
|
`/api/workspaces/${workspaceId}/conversations/${conversationId}/participants/agents/${agentId}/turn-lease?${q}`,
|
|
3205
3243
|
void 0,
|
|
3206
3244
|
{
|
|
3245
|
+
// CT1657: the one GET that opts OUT of the default retry. The reasoning
|
|
3246
|
+
// is right above — a failed probe has no answer worth replaying and the
|
|
3247
|
+
// next tick asks again in seconds, so retrying inside this one would
|
|
3248
|
+
// stretch a tick over its successor's slot, which is what the deadline
|
|
3249
|
+
// exists to prevent.
|
|
3250
|
+
retry: false,
|
|
3207
3251
|
signal: AbortSignal.timeout(this.opts.leaseCheckTimeoutMs ?? LEASE_CHECK_TIMEOUT_MS)
|
|
3208
3252
|
}
|
|
3209
3253
|
);
|
|
@@ -3395,6 +3439,10 @@ function isRetryable(err) {
|
|
|
3395
3439
|
function isAbortError(err) {
|
|
3396
3440
|
return err instanceof Error && err.name === "AbortError";
|
|
3397
3441
|
}
|
|
3442
|
+
function pathForLog(path) {
|
|
3443
|
+
const q = path.indexOf("?");
|
|
3444
|
+
return q === -1 ? path : path.slice(0, q);
|
|
3445
|
+
}
|
|
3398
3446
|
function sleep2(ms, signal) {
|
|
3399
3447
|
return new Promise((resolve2) => {
|
|
3400
3448
|
if (signal?.aborted) return resolve2();
|
|
@@ -9569,6 +9617,7 @@ var TurnExecution = class {
|
|
|
9569
9617
|
// identity-checked at the supervisor — then clear the active-run flag);
|
|
9570
9618
|
// everything past the lease settles in `execute()`'s one `finally`.
|
|
9571
9619
|
async run() {
|
|
9620
|
+
this.supervisor.registerAbort(this.turnId, this.abortController);
|
|
9572
9621
|
try {
|
|
9573
9622
|
await this.fetchContext();
|
|
9574
9623
|
this.gateTrigger();
|
|
@@ -9590,6 +9639,8 @@ var TurnExecution = class {
|
|
|
9590
9639
|
this.supervisor.releaseAbort(this.turnId, this.abortController);
|
|
9591
9640
|
const msg = err instanceof Error ? err.message : String(err);
|
|
9592
9641
|
await this.concludeAdmittedRun("setup_failed", `setup_failed: ${msg}`);
|
|
9642
|
+
} else {
|
|
9643
|
+
this.supervisor.releaseAbort(this.turnId, this.abortController);
|
|
9593
9644
|
}
|
|
9594
9645
|
throw err;
|
|
9595
9646
|
}
|
|
@@ -9671,9 +9722,14 @@ var TurnExecution = class {
|
|
|
9671
9722
|
turnId,
|
|
9672
9723
|
this.resumed,
|
|
9673
9724
|
void 0,
|
|
9674
|
-
payload.episode
|
|
9725
|
+
payload.episode,
|
|
9726
|
+
this.abortController.signal
|
|
9675
9727
|
);
|
|
9676
9728
|
} catch (err) {
|
|
9729
|
+
if (this.abortController.signal.aborted) {
|
|
9730
|
+
turnLog.debug("dispatcher: stopped while loading the turn context");
|
|
9731
|
+
throw this.concluded("cancelled");
|
|
9732
|
+
}
|
|
9677
9733
|
const status2 = err instanceof ApiError ? err.status : 0;
|
|
9678
9734
|
if (status2 === 404) {
|
|
9679
9735
|
turnLog.debug(
|
|
@@ -9688,9 +9744,8 @@ var TurnExecution = class {
|
|
|
9688
9744
|
);
|
|
9689
9745
|
throw this.concluded("turn_context_not_admitted");
|
|
9690
9746
|
}
|
|
9691
|
-
|
|
9692
|
-
|
|
9693
|
-
const fetchReason = `fetch_failed: ${reason}`;
|
|
9747
|
+
turnLog.warn(apiErrorLogFields(err), "dispatcher: failed to fetch turn context");
|
|
9748
|
+
const fetchReason = `fetch_failed: ${describeError(err)}`;
|
|
9694
9749
|
throw this.concluded(fetchReason, fetchReason);
|
|
9695
9750
|
}
|
|
9696
9751
|
this.turnContext = turnContext;
|
|
@@ -10053,7 +10108,12 @@ var TurnExecution = class {
|
|
|
10053
10108
|
payload.messageId,
|
|
10054
10109
|
turnId,
|
|
10055
10110
|
false,
|
|
10056
|
-
true
|
|
10111
|
+
true,
|
|
10112
|
+
void 0,
|
|
10113
|
+
// CT1657: the turn's abort. This recompose happens mid-run with the
|
|
10114
|
+
// adapter waiting on it, so a Stop here must end the wait rather than
|
|
10115
|
+
// hold the turn open for the rest of the retry schedule.
|
|
10116
|
+
this.abortController.signal
|
|
10057
10117
|
);
|
|
10058
10118
|
turnLog.debug(
|
|
10059
10119
|
{ reason },
|
|
@@ -10065,6 +10125,10 @@ var TurnExecution = class {
|
|
|
10065
10125
|
content: context.content
|
|
10066
10126
|
};
|
|
10067
10127
|
} catch (err) {
|
|
10128
|
+
if (this.abortController.signal.aborted) {
|
|
10129
|
+
turnLog.debug({ reason }, "dispatcher: stopped while recomposing the turn context");
|
|
10130
|
+
return null;
|
|
10131
|
+
}
|
|
10068
10132
|
turnLog.error(
|
|
10069
10133
|
{ reason, ...apiErrorLogFields(err) },
|
|
10070
10134
|
"Couldn't load the full conversation; continuing with the recent messages."
|
package/dist/runtime.js
CHANGED
|
@@ -2235,9 +2235,27 @@ function describeErrorBody(body) {
|
|
|
2235
2235
|
if (text.length === 0) return void 0;
|
|
2236
2236
|
return text.length > ERROR_BODY_LOG_CAP ? `${text.slice(0, ERROR_BODY_LOG_CAP)}\u2026` : text;
|
|
2237
2237
|
}
|
|
2238
|
+
var CAUSE_TEXT_CAP = 200;
|
|
2239
|
+
function describeError(err) {
|
|
2240
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
2241
|
+
const cause = describeCause(err);
|
|
2242
|
+
return cause === void 0 ? message : `${message}: ${cause}`;
|
|
2243
|
+
}
|
|
2244
|
+
function describeCause(err) {
|
|
2245
|
+
if (!(err instanceof Error)) return void 0;
|
|
2246
|
+
const cause = err.cause;
|
|
2247
|
+
if (cause === void 0 || cause === null) return void 0;
|
|
2248
|
+
if (typeof cause === "object") {
|
|
2249
|
+
const code = cause.code;
|
|
2250
|
+
if (typeof code === "string" && code.length > 0) return code;
|
|
2251
|
+
}
|
|
2252
|
+
const text = cause instanceof Error ? cause.message : typeof cause === "string" ? cause : void 0;
|
|
2253
|
+
if (text === void 0 || text.length === 0) return void 0;
|
|
2254
|
+
return text.length > CAUSE_TEXT_CAP ? `${text.slice(0, CAUSE_TEXT_CAP)}\u2026` : text;
|
|
2255
|
+
}
|
|
2238
2256
|
function apiErrorLogFields(err) {
|
|
2239
2257
|
const fields = {
|
|
2240
|
-
err:
|
|
2258
|
+
err: describeError(err)
|
|
2241
2259
|
};
|
|
2242
2260
|
if (err instanceof ApiError) {
|
|
2243
2261
|
fields.status = err.status;
|
|
@@ -2256,6 +2274,7 @@ import { randomUUID as randomUUID4 } from "crypto";
|
|
|
2256
2274
|
|
|
2257
2275
|
// src/api.ts
|
|
2258
2276
|
var RETRY_BACKOFF_MS = [250, 750];
|
|
2277
|
+
var TURN_CONTEXT_BACKOFF_MS = [250, 750, 2e3, 5e3];
|
|
2259
2278
|
var ACTIVE_RUN_OUTBOX_SEQ = 0;
|
|
2260
2279
|
var LEASE_CHECK_TIMEOUT_MS = 1e4;
|
|
2261
2280
|
function activeRunOutboxKey(conversationId, agentId) {
|
|
@@ -2306,14 +2325,26 @@ var CabaneApi = class {
|
|
|
2306
2325
|
return parsed;
|
|
2307
2326
|
}
|
|
2308
2327
|
async request(method, path, body, opts = {}) {
|
|
2309
|
-
const { signal, retry =
|
|
2310
|
-
const maxAttempts = retry ?
|
|
2328
|
+
const { signal, retry = method === "GET", backoffMs = RETRY_BACKOFF_MS } = opts;
|
|
2329
|
+
const maxAttempts = retry ? backoffMs.length + 1 : 1;
|
|
2311
2330
|
for (let attempt = 1; ; attempt++) {
|
|
2312
2331
|
try {
|
|
2313
2332
|
return await this.attempt(method, path, body, signal);
|
|
2314
2333
|
} catch (err) {
|
|
2315
2334
|
if (attempt >= maxAttempts || signal?.aborted || !isRetryable(err)) throw err;
|
|
2316
|
-
|
|
2335
|
+
const delayMs = backoffMs[attempt - 1];
|
|
2336
|
+
this.opts.log?.info(
|
|
2337
|
+
{
|
|
2338
|
+
method,
|
|
2339
|
+
path: pathForLog(path),
|
|
2340
|
+
attempt,
|
|
2341
|
+
maxAttempts,
|
|
2342
|
+
delayMs,
|
|
2343
|
+
err: describeError(err)
|
|
2344
|
+
},
|
|
2345
|
+
"companion: API call failed transiently; retrying"
|
|
2346
|
+
);
|
|
2347
|
+
await sleep(delayMs, signal);
|
|
2317
2348
|
if (signal?.aborted) throw err;
|
|
2318
2349
|
}
|
|
2319
2350
|
}
|
|
@@ -2451,9 +2482,16 @@ var CabaneApi = class {
|
|
|
2451
2482
|
// CT1543: `episode` is the dispatch event's own, echoed back so the server can
|
|
2452
2483
|
// tell this frame from a later wake on the same message. Omitted when the
|
|
2453
2484
|
// event didn't carry one, which is the only thing an older event can mean.
|
|
2454
|
-
|
|
2485
|
+
// CT1657: `signal` is the turn's own abort. It matters here more than on any
|
|
2486
|
+
// other read, because this call now waits up to ~8s across five attempts: a
|
|
2487
|
+
// Stop landing in that window used to leave the whole schedule running, and
|
|
2488
|
+
// the person watched a turn they had already stopped keep trying.
|
|
2489
|
+
getTurnContext(conversationId, messageId2, turnId, resumed, fresh, episode, signal) {
|
|
2455
2490
|
const q = `conversationId=${encodeURIComponent(conversationId)}&messageId=${encodeURIComponent(messageId2)}` + (turnId ? `&turnId=${encodeURIComponent(turnId)}` : "") + (resumed ? "&resumed=1" : "") + (fresh ? "&fresh=1" : "") + (episode !== void 0 ? `&episode=${encodeURIComponent(String(episode))}` : "");
|
|
2456
|
-
return this.request("GET", `/api/agent/turn-context?${q}
|
|
2491
|
+
return this.request("GET", `/api/agent/turn-context?${q}`, void 0, {
|
|
2492
|
+
backoffMs: this.opts.turnContextBackoffMs ?? TURN_CONTEXT_BACKOFF_MS,
|
|
2493
|
+
...signal ? { signal } : {}
|
|
2494
|
+
});
|
|
2457
2495
|
}
|
|
2458
2496
|
// CT1380: the local half of a resumed turn's seq floor (see
|
|
2459
2497
|
// `Outbox.maxSeqForTurn`). 0 when no outbox is configured — a one-shot CLI or
|
|
@@ -2538,6 +2576,12 @@ var CabaneApi = class {
|
|
|
2538
2576
|
`/api/workspaces/${workspaceId}/conversations/${conversationId}/participants/agents/${agentId}/turn-lease?${q}`,
|
|
2539
2577
|
void 0,
|
|
2540
2578
|
{
|
|
2579
|
+
// CT1657: the one GET that opts OUT of the default retry. The reasoning
|
|
2580
|
+
// is right above — a failed probe has no answer worth replaying and the
|
|
2581
|
+
// next tick asks again in seconds, so retrying inside this one would
|
|
2582
|
+
// stretch a tick over its successor's slot, which is what the deadline
|
|
2583
|
+
// exists to prevent.
|
|
2584
|
+
retry: false,
|
|
2541
2585
|
signal: AbortSignal.timeout(this.opts.leaseCheckTimeoutMs ?? LEASE_CHECK_TIMEOUT_MS)
|
|
2542
2586
|
}
|
|
2543
2587
|
);
|
|
@@ -2729,6 +2773,10 @@ function isRetryable(err) {
|
|
|
2729
2773
|
function isAbortError(err) {
|
|
2730
2774
|
return err instanceof Error && err.name === "AbortError";
|
|
2731
2775
|
}
|
|
2776
|
+
function pathForLog(path) {
|
|
2777
|
+
const q = path.indexOf("?");
|
|
2778
|
+
return q === -1 ? path : path.slice(0, q);
|
|
2779
|
+
}
|
|
2732
2780
|
function sleep(ms, signal) {
|
|
2733
2781
|
return new Promise((resolve2) => {
|
|
2734
2782
|
if (signal?.aborted) return resolve2();
|
|
@@ -8992,6 +9040,7 @@ var TurnExecution = class {
|
|
|
8992
9040
|
// identity-checked at the supervisor — then clear the active-run flag);
|
|
8993
9041
|
// everything past the lease settles in `execute()`'s one `finally`.
|
|
8994
9042
|
async run() {
|
|
9043
|
+
this.supervisor.registerAbort(this.turnId, this.abortController);
|
|
8995
9044
|
try {
|
|
8996
9045
|
await this.fetchContext();
|
|
8997
9046
|
this.gateTrigger();
|
|
@@ -9013,6 +9062,8 @@ var TurnExecution = class {
|
|
|
9013
9062
|
this.supervisor.releaseAbort(this.turnId, this.abortController);
|
|
9014
9063
|
const msg = err instanceof Error ? err.message : String(err);
|
|
9015
9064
|
await this.concludeAdmittedRun("setup_failed", `setup_failed: ${msg}`);
|
|
9065
|
+
} else {
|
|
9066
|
+
this.supervisor.releaseAbort(this.turnId, this.abortController);
|
|
9016
9067
|
}
|
|
9017
9068
|
throw err;
|
|
9018
9069
|
}
|
|
@@ -9094,9 +9145,14 @@ var TurnExecution = class {
|
|
|
9094
9145
|
turnId,
|
|
9095
9146
|
this.resumed,
|
|
9096
9147
|
void 0,
|
|
9097
|
-
payload.episode
|
|
9148
|
+
payload.episode,
|
|
9149
|
+
this.abortController.signal
|
|
9098
9150
|
);
|
|
9099
9151
|
} catch (err) {
|
|
9152
|
+
if (this.abortController.signal.aborted) {
|
|
9153
|
+
turnLog.debug("dispatcher: stopped while loading the turn context");
|
|
9154
|
+
throw this.concluded("cancelled");
|
|
9155
|
+
}
|
|
9100
9156
|
const status = err instanceof ApiError ? err.status : 0;
|
|
9101
9157
|
if (status === 404) {
|
|
9102
9158
|
turnLog.debug(
|
|
@@ -9111,9 +9167,8 @@ var TurnExecution = class {
|
|
|
9111
9167
|
);
|
|
9112
9168
|
throw this.concluded("turn_context_not_admitted");
|
|
9113
9169
|
}
|
|
9114
|
-
|
|
9115
|
-
|
|
9116
|
-
const fetchReason = `fetch_failed: ${reason}`;
|
|
9170
|
+
turnLog.warn(apiErrorLogFields(err), "dispatcher: failed to fetch turn context");
|
|
9171
|
+
const fetchReason = `fetch_failed: ${describeError(err)}`;
|
|
9117
9172
|
throw this.concluded(fetchReason, fetchReason);
|
|
9118
9173
|
}
|
|
9119
9174
|
this.turnContext = turnContext;
|
|
@@ -9476,7 +9531,12 @@ var TurnExecution = class {
|
|
|
9476
9531
|
payload.messageId,
|
|
9477
9532
|
turnId,
|
|
9478
9533
|
false,
|
|
9479
|
-
true
|
|
9534
|
+
true,
|
|
9535
|
+
void 0,
|
|
9536
|
+
// CT1657: the turn's abort. This recompose happens mid-run with the
|
|
9537
|
+
// adapter waiting on it, so a Stop here must end the wait rather than
|
|
9538
|
+
// hold the turn open for the rest of the retry schedule.
|
|
9539
|
+
this.abortController.signal
|
|
9480
9540
|
);
|
|
9481
9541
|
turnLog.debug(
|
|
9482
9542
|
{ reason },
|
|
@@ -9488,6 +9548,10 @@ var TurnExecution = class {
|
|
|
9488
9548
|
content: context.content
|
|
9489
9549
|
};
|
|
9490
9550
|
} catch (err) {
|
|
9551
|
+
if (this.abortController.signal.aborted) {
|
|
9552
|
+
turnLog.debug({ reason }, "dispatcher: stopped while recomposing the turn context");
|
|
9553
|
+
return null;
|
|
9554
|
+
}
|
|
9491
9555
|
turnLog.error(
|
|
9492
9556
|
{ reason, ...apiErrorLogFields(err) },
|
|
9493
9557
|
"Couldn't load the full conversation; continuing with the recent messages."
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cabane/companion",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.112",
|
|
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",
|