@cabane/companion 0.6.173 → 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 +74 -60
- package/dist/runtime.js +44 -44
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -2509,7 +2509,7 @@ var init_pump = __esm({
|
|
|
2509
2509
|
});
|
|
2510
2510
|
|
|
2511
2511
|
// packages/agent-runtime/src/turn-control/descriptions.ts
|
|
2512
|
-
var MAX_ASK_QUESTION_BYTES, MAX_ASK_HEADLINE_BYTES, MAX_ASK_ITEMS, MAX_ASK_OPTION_BYTES, ASK_OPTION_NOT_A_STRING, MIN_ASK_OPTIONS, MAX_ASK_OPTIONS, MAX_WAKE_NOTE_BYTES, MAX_WITHDRAW_REASON_BYTES, MAX_SKIP_REASON_BYTES, TURN_CONTROL_TOOLS, TURN_CONTROL_TOOL_DESCRIPTIONS, TURN_CONTROL_FIELD_DESCRIPTIONS;
|
|
2512
|
+
var MAX_ASK_QUESTION_BYTES, MAX_ASK_HEADLINE_BYTES, MAX_ASK_ITEMS, MAX_ASK_OPTION_BYTES, ASK_OPTION_NOT_A_STRING, MIN_ASK_OPTIONS, MAX_ASK_OPTIONS, ASK_TARGET_PATTERN, ASK_TARGET_INVALID, MAX_WAKE_NOTE_BYTES, MAX_WITHDRAW_REASON_BYTES, MAX_SKIP_REASON_BYTES, TURN_CONTROL_TOOLS, TURN_CONTROL_TOOL_DESCRIPTIONS, TURN_CONTROL_FIELD_DESCRIPTIONS;
|
|
2513
2513
|
var init_descriptions = __esm({
|
|
2514
2514
|
"packages/agent-runtime/src/turn-control/descriptions.ts"() {
|
|
2515
2515
|
"use strict";
|
|
@@ -2520,6 +2520,8 @@ var init_descriptions = __esm({
|
|
|
2520
2520
|
ASK_OPTION_NOT_A_STRING = "An ask option is a plain string label.";
|
|
2521
2521
|
MIN_ASK_OPTIONS = 2;
|
|
2522
2522
|
MAX_ASK_OPTIONS = 4;
|
|
2523
|
+
ASK_TARGET_PATTERN = /^(?:[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}|@?[A-Za-z0-9-]{2,32})$/;
|
|
2524
|
+
ASK_TARGET_INVALID = "An ask target is a user id or an @handle.";
|
|
2523
2525
|
MAX_WAKE_NOTE_BYTES = 2e3;
|
|
2524
2526
|
MAX_WITHDRAW_REASON_BYTES = 400;
|
|
2525
2527
|
MAX_SKIP_REASON_BYTES = 500;
|
|
@@ -2531,7 +2533,7 @@ var init_descriptions = __esm({
|
|
|
2531
2533
|
"withdraw_ask"
|
|
2532
2534
|
];
|
|
2533
2535
|
TURN_CONTROL_TOOL_DESCRIPTIONS = {
|
|
2534
|
-
ask: `Ask a person a structured question you need answered to continue, then end your turn; don't wait for the reply. \`targetUserId\` is a person on your roster
|
|
2536
|
+
ask: `Ask a person a structured question you need answered to continue, then end your turn; don't wait for the reply. \`targetUserId\` is a person on your roster, by their \`@handle\`; to bring in an agent, name it with \`@handle\` in your reply.
|
|
2535
2537
|
- Form: one question (\`headline\`, optional \`question\` body, \`options\`) or \`questions\`, 1\u2013${MAX_ASK_ITEMS} \`{ headline, body?, options? }\`; never both. Decisions that arise together go in ONE call, in the order you want them answered. A second \`ask\` is a separate item, never an edit; raise one only for a genuinely new question.
|
|
2536
2538
|
- \`headline\`: one sentence ending in \`?\`. Body (\`question\` or each item's \`body\`): a sentence or two of framing, not a report; inline Markdown only, no lists or headings. \`options\`: ${MIN_ASK_OPTIONS}\u2013${MAX_ASK_OPTIONS} one-click answers (always for a yes/no go-ahead), each a distinct course of action you can carry out without asking back.
|
|
2537
2539
|
- Voice, in headline, body and options: "I" for yourself, never your own name; name everyone else who decides or acts, as on your roster. Never "you", "your", "we", "us", "our" or their contractions, nor "he", "she" or "they" for a named actor. Each option reads correctly alone. A quotation stays accurate: say who said it.
|
|
@@ -2546,7 +2548,7 @@ var init_descriptions = __esm({
|
|
|
2546
2548
|
};
|
|
2547
2549
|
TURN_CONTROL_FIELD_DESCRIPTIONS = {
|
|
2548
2550
|
ask: {
|
|
2549
|
-
targetUserId: "The
|
|
2551
|
+
targetUserId: "The person to ask: their `@handle` from your turn context's roster (preferred), or their user id.",
|
|
2550
2552
|
question: `SINGLE-question form: a short body \u2014 one or two sentences of framing the headline can't hold. NOT a report (up to ${MAX_ASK_QUESTION_BYTES} characters, inline markdown only). Provide EITHER this or \`questions\`, not both. Put the crisp one-sentence question in \`headline\`.`,
|
|
2551
2553
|
headline: `SINGLE-question form: the question itself as ONE clear, capitalized sentence ending in \`?\` ("Should Ledger deploy once Mia approves?"), up to ${MAX_ASK_HEADLINE_BYTES} characters. What the person reads first in For you and on the chip \u2014 one scannable question, no elaboration (that goes in \`question\`). Strongly encouraged.`,
|
|
2552
2554
|
options: `SINGLE-question form: optional ${MIN_ASK_OPTIONS}\u2013${MAX_ASK_OPTIONS} suggested one-click answers, up to ${MAX_ASK_OPTION_BYTES} characters each. Each option must make sense on its own.`,
|
|
@@ -8508,7 +8510,7 @@ function createTurnControlMcpServer(acts, skipState) {
|
|
|
8508
8510
|
ASK_TOOL,
|
|
8509
8511
|
TURN_CONTROL_TOOL_DESCRIPTIONS.ask,
|
|
8510
8512
|
{
|
|
8511
|
-
targetUserId: z17.string().
|
|
8513
|
+
targetUserId: z17.string().regex(ASK_TARGET_PATTERN, { error: ASK_TARGET_INVALID }).describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask.targetUserId),
|
|
8512
8514
|
question: z17.string().min(1).max(MAX_ASK_QUESTION_BYTES).optional().describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask.question),
|
|
8513
8515
|
headline: z17.string().min(1).max(MAX_ASK_HEADLINE_BYTES).optional().describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask.headline),
|
|
8514
8516
|
options: z17.array(z17.string({ error: ASK_OPTION_NOT_A_STRING }).min(1).max(MAX_ASK_OPTION_BYTES)).min(MIN_ASK_OPTIONS).max(MAX_ASK_OPTIONS).optional().describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask.options),
|
|
@@ -12115,11 +12117,60 @@ init_config();
|
|
|
12115
12117
|
init_bundled_binaries();
|
|
12116
12118
|
init_cabane_command();
|
|
12117
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
|
|
12118
12171
|
init_install_kind();
|
|
12119
12172
|
init_install_record();
|
|
12120
12173
|
init_term();
|
|
12121
|
-
import { homedir as homedir6 } from "os";
|
|
12122
|
-
import { join as join7 } from "path";
|
|
12123
12174
|
var HARNESS_LABELS = {
|
|
12124
12175
|
"claude-code": "Claude Code",
|
|
12125
12176
|
codex: "Codex",
|
|
@@ -12174,23 +12225,23 @@ function agentRows(detection, opts = {}) {
|
|
|
12174
12225
|
});
|
|
12175
12226
|
}
|
|
12176
12227
|
function agentRowText(runtime, state, opts = {}) {
|
|
12177
|
-
const
|
|
12228
|
+
const word = HARNESS_STATE_LABEL[harnessEntry(runtime, state).state];
|
|
12178
12229
|
switch (state.state) {
|
|
12179
12230
|
case "usable": {
|
|
12180
12231
|
const version = state.version ? ` (${state.version})` : "";
|
|
12181
|
-
if (state.via === "api_key") return
|
|
12182
|
-
if (state.via === "container") return
|
|
12183
|
-
if (state.via === "serve") return
|
|
12184
|
-
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}`;
|
|
12185
12236
|
}
|
|
12186
12237
|
case "signed_out":
|
|
12187
|
-
return opts.signInHint && runtime !== "opencode" ?
|
|
12238
|
+
return opts.signInHint && runtime !== "opencode" ? `${word} \u2014 run ${loginCommand(runtime)}` : word;
|
|
12188
12239
|
case "not_running":
|
|
12189
|
-
return
|
|
12240
|
+
return `${word} \u2014 not running`;
|
|
12190
12241
|
case "off":
|
|
12191
|
-
return
|
|
12242
|
+
return `${word} \u2014 turned off in config`;
|
|
12192
12243
|
case "problem":
|
|
12193
|
-
return runtime === "opencode" ?
|
|
12244
|
+
return runtime === "opencode" ? `${word} \u2014 not running` : `${word} \u2014 ${unusableReason(runtime, state.check) ?? "could not be checked"}`;
|
|
12194
12245
|
}
|
|
12195
12246
|
}
|
|
12196
12247
|
var AGENT_CHOICES = [
|
|
@@ -12279,7 +12330,7 @@ function nothingUsableLines(detection) {
|
|
|
12279
12330
|
if (reason) lines2.push(reason);
|
|
12280
12331
|
}
|
|
12281
12332
|
}
|
|
12282
|
-
lines2.push(`Cabane can run while you sign in to a
|
|
12333
|
+
lines2.push(`Cabane can run while you sign in to a harness.`);
|
|
12283
12334
|
return lines2;
|
|
12284
12335
|
}
|
|
12285
12336
|
function indent(line) {
|
|
@@ -13458,7 +13509,7 @@ function turnFailureCopy(reason, runtime) {
|
|
|
13458
13509
|
if (reason?.startsWith("runtime_incomplete:"))
|
|
13459
13510
|
return `The agent installation is incomplete. ${repairAction()} Then restart the companion.`;
|
|
13460
13511
|
if (/^(runtime|codex|opencode)_unavailable:/.test(reason ?? ""))
|
|
13461
|
-
return "The
|
|
13512
|
+
return "The harness is off on this device. Enable it here, or choose another harness or device in Cabane.";
|
|
13462
13513
|
if (reason?.startsWith("model_unavailable:"))
|
|
13463
13514
|
return "The selected model is unavailable. Choose a different model in the agent settings in Cabane.";
|
|
13464
13515
|
if (reason?.startsWith("prepare_failed:"))
|
|
@@ -13837,7 +13888,6 @@ init_install_kind();
|
|
|
13837
13888
|
init_src();
|
|
13838
13889
|
var ConnectorHealthStore = class {
|
|
13839
13890
|
byRuntime = /* @__PURE__ */ new Map();
|
|
13840
|
-
sources = /* @__PURE__ */ new Map();
|
|
13841
13891
|
// Record a settled turn's outcome for `runtime`. A success heals to `ok`; a
|
|
13842
13892
|
// classified failure sets `limited`(+reset) / `auth_expired` with the reason as
|
|
13843
13893
|
// `last_error`; anything else (a user Stop, a timeout, an unclassified error)
|
|
@@ -13846,7 +13896,6 @@ var ConnectorHealthStore = class {
|
|
|
13846
13896
|
recordSettle(runtime, outcome) {
|
|
13847
13897
|
if (outcome.ok) {
|
|
13848
13898
|
this.byRuntime.set(runtime, { quotaState: "ok", limitedUntil: null, lastError: null });
|
|
13849
|
-
this.sources.set(runtime, "turn");
|
|
13850
13899
|
return;
|
|
13851
13900
|
}
|
|
13852
13901
|
const failure = decodeFailureReason(outcome.errorReason);
|
|
@@ -13858,26 +13907,6 @@ var ConnectorHealthStore = class {
|
|
|
13858
13907
|
limitedUntil: quota.limitedUntil,
|
|
13859
13908
|
lastError: outcome.errorReason ?? null
|
|
13860
13909
|
});
|
|
13861
|
-
this.sources.set(runtime, "turn");
|
|
13862
|
-
}
|
|
13863
|
-
// Record what the companion's own sign-in check found for a connected
|
|
13864
|
-
// runtime at `start` or `connect`, so the heartbeat carries "sign-in needed"
|
|
13865
|
-
// before any turn fails. Signed out seeds `auth_expired` unless a turn has
|
|
13866
|
-
// already spoken; usable clears only a seed of our own.
|
|
13867
|
-
recordLocalCheck(runtime, usable) {
|
|
13868
|
-
const source = this.sources.get(runtime);
|
|
13869
|
-
if (!usable) {
|
|
13870
|
-
if (source === "turn" && this.byRuntime.get(runtime)?.quotaState !== "ok") return;
|
|
13871
|
-
this.byRuntime.set(runtime, {
|
|
13872
|
-
quotaState: "auth_expired",
|
|
13873
|
-
limitedUntil: null,
|
|
13874
|
-
lastError: encodeFailureReason({ kind: "auth_expired" })
|
|
13875
|
-
});
|
|
13876
|
-
this.sources.set(runtime, "local");
|
|
13877
|
-
return;
|
|
13878
|
-
}
|
|
13879
|
-
if (source !== "local") return;
|
|
13880
|
-
this.byRuntime.set(runtime, { quotaState: "ok", limitedUntil: null, lastError: null });
|
|
13881
13910
|
}
|
|
13882
13911
|
// The store's current opinion for one runtime, or undefined when it has
|
|
13883
13912
|
// no observation. Read by the dispatcher at idle-timeout fire: an idle reap
|
|
@@ -16362,6 +16391,9 @@ var CompanionSupervisor = class {
|
|
|
16362
16391
|
// Omitted until the first observed failure or heal, so the server's
|
|
16363
16392
|
// status-less manifest synthesis still runs for a fresh companion.
|
|
16364
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) } : {},
|
|
16365
16397
|
...opencodeModels !== null ? { models: opencodeModels } : {},
|
|
16366
16398
|
...this.updater ? { update: this.updater.report() } : {}
|
|
16367
16399
|
});
|
|
@@ -17314,7 +17346,6 @@ var CompanionSupervisor = class {
|
|
|
17314
17346
|
...this.probeOpencode ? { probeOpencode: this.probeOpencode } : {}
|
|
17315
17347
|
});
|
|
17316
17348
|
this.detection = detection;
|
|
17317
|
-
this.seedSignInHealth(previous, detection);
|
|
17318
17349
|
this.logBundledTransitions();
|
|
17319
17350
|
this.harnessVersions = {
|
|
17320
17351
|
claudeCode: detectedVersion(detection["claude-code"]),
|
|
@@ -17372,23 +17403,6 @@ var CompanionSupervisor = class {
|
|
|
17372
17403
|
const bare = advertisedRuntimes(this.detection).filter((runtime) => !named.has(runtime)).map((runtime) => ({ runtime }));
|
|
17373
17404
|
return [...health, ...bare];
|
|
17374
17405
|
}
|
|
17375
|
-
// Seed connector health from what detection saw of a sign-in, so Cabane says
|
|
17376
|
-
// "sign-in needed" before a turn fails. Only a LAPSE seeds it — an agent that
|
|
17377
|
-
// was usable at an earlier look and is signed out now — because a device
|
|
17378
|
-
// that never signed in to Codex has nothing to report about Codex. Usable
|
|
17379
|
-
// again clears a seed of our own.
|
|
17380
|
-
seedSignInHealth(previous, detection) {
|
|
17381
|
-
for (const runtime of ["claude-code", "codex"]) {
|
|
17382
|
-
const now = detection[runtime];
|
|
17383
|
-
if (now.state === "usable") {
|
|
17384
|
-
if (now.via !== "container") this.connectorHealth.recordLocalCheck(runtime, true);
|
|
17385
|
-
continue;
|
|
17386
|
-
}
|
|
17387
|
-
if (now.state === "signed_out" && previous?.[runtime].state === "usable") {
|
|
17388
|
-
this.connectorHealth.recordLocalCheck(runtime, false);
|
|
17389
|
-
}
|
|
17390
|
-
}
|
|
17391
|
-
}
|
|
17392
17406
|
// Beat NOW and wait for it to land. `start` calls this before asking the
|
|
17393
17407
|
// person anything, so the browser's connect step already shows what this
|
|
17394
17408
|
// machine has rather than sitting blank while the terminal waits on an answer.
|
|
@@ -18361,7 +18375,7 @@ async function startScript(opts, interactive) {
|
|
|
18361
18375
|
}
|
|
18362
18376
|
if (!isDevicePaired()) {
|
|
18363
18377
|
refuse([
|
|
18364
|
-
`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.`
|
|
18365
18379
|
]);
|
|
18366
18380
|
return;
|
|
18367
18381
|
}
|
|
@@ -18812,7 +18826,7 @@ async function setupScript(opts, deps) {
|
|
|
18812
18826
|
const detection = await detect(cfg, running, deps);
|
|
18813
18827
|
ui.intro("Cabane setup");
|
|
18814
18828
|
const usable = anyUsable(detection);
|
|
18815
|
-
ui.step(["
|
|
18829
|
+
ui.step(["Harnesses on this device", ...agentLines(detection, { signInHint: !usable })]);
|
|
18816
18830
|
if (!usable && hasProblem(detection)) {
|
|
18817
18831
|
ui.cancel(`Fix the above, then run ${cabane("setup")} again.`);
|
|
18818
18832
|
process.exitCode = 1;
|
|
@@ -19212,7 +19226,7 @@ function strayStatusLines(running, find = findStrayCompanions) {
|
|
|
19212
19226
|
}
|
|
19213
19227
|
async function agentLines2(cfg, running) {
|
|
19214
19228
|
const detection = running ?? await detectHarnesses(cfg);
|
|
19215
|
-
return agentRows(detection
|
|
19229
|
+
return agentRows(detection).map((agent) => row2(agent.label, agent.text));
|
|
19216
19230
|
}
|
|
19217
19231
|
function sandboxStatusLines(cfg) {
|
|
19218
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:"))
|
|
@@ -3468,6 +3468,8 @@ var MAX_ASK_OPTION_BYTES = 200;
|
|
|
3468
3468
|
var ASK_OPTION_NOT_A_STRING = "An ask option is a plain string label.";
|
|
3469
3469
|
var MIN_ASK_OPTIONS = 2;
|
|
3470
3470
|
var MAX_ASK_OPTIONS = 4;
|
|
3471
|
+
var ASK_TARGET_PATTERN = /^(?:[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}|@?[A-Za-z0-9-]{2,32})$/;
|
|
3472
|
+
var ASK_TARGET_INVALID = "An ask target is a user id or an @handle.";
|
|
3471
3473
|
var MAX_WAKE_NOTE_BYTES = 2e3;
|
|
3472
3474
|
var MAX_WITHDRAW_REASON_BYTES = 400;
|
|
3473
3475
|
var MAX_SKIP_REASON_BYTES = 500;
|
|
@@ -3479,7 +3481,7 @@ var TURN_CONTROL_TOOLS = [
|
|
|
3479
3481
|
"withdraw_ask"
|
|
3480
3482
|
];
|
|
3481
3483
|
var TURN_CONTROL_TOOL_DESCRIPTIONS = {
|
|
3482
|
-
ask: `Ask a person a structured question you need answered to continue, then end your turn; don't wait for the reply. \`targetUserId\` is a person on your roster
|
|
3484
|
+
ask: `Ask a person a structured question you need answered to continue, then end your turn; don't wait for the reply. \`targetUserId\` is a person on your roster, by their \`@handle\`; to bring in an agent, name it with \`@handle\` in your reply.
|
|
3483
3485
|
- Form: one question (\`headline\`, optional \`question\` body, \`options\`) or \`questions\`, 1\u2013${MAX_ASK_ITEMS} \`{ headline, body?, options? }\`; never both. Decisions that arise together go in ONE call, in the order you want them answered. A second \`ask\` is a separate item, never an edit; raise one only for a genuinely new question.
|
|
3484
3486
|
- \`headline\`: one sentence ending in \`?\`. Body (\`question\` or each item's \`body\`): a sentence or two of framing, not a report; inline Markdown only, no lists or headings. \`options\`: ${MIN_ASK_OPTIONS}\u2013${MAX_ASK_OPTIONS} one-click answers (always for a yes/no go-ahead), each a distinct course of action you can carry out without asking back.
|
|
3485
3487
|
- Voice, in headline, body and options: "I" for yourself, never your own name; name everyone else who decides or acts, as on your roster. Never "you", "your", "we", "us", "our" or their contractions, nor "he", "she" or "they" for a named actor. Each option reads correctly alone. A quotation stays accurate: say who said it.
|
|
@@ -3494,7 +3496,7 @@ var TURN_CONTROL_TOOL_DESCRIPTIONS = {
|
|
|
3494
3496
|
};
|
|
3495
3497
|
var TURN_CONTROL_FIELD_DESCRIPTIONS = {
|
|
3496
3498
|
ask: {
|
|
3497
|
-
targetUserId: "The
|
|
3499
|
+
targetUserId: "The person to ask: their `@handle` from your turn context's roster (preferred), or their user id.",
|
|
3498
3500
|
question: `SINGLE-question form: a short body \u2014 one or two sentences of framing the headline can't hold. NOT a report (up to ${MAX_ASK_QUESTION_BYTES} characters, inline markdown only). Provide EITHER this or \`questions\`, not both. Put the crisp one-sentence question in \`headline\`.`,
|
|
3499
3501
|
headline: `SINGLE-question form: the question itself as ONE clear, capitalized sentence ending in \`?\` ("Should Ledger deploy once Mia approves?"), up to ${MAX_ASK_HEADLINE_BYTES} characters. What the person reads first in For you and on the chip \u2014 one scannable question, no elaboration (that goes in \`question\`). Strongly encouraged.`,
|
|
3500
3502
|
options: `SINGLE-question form: optional ${MIN_ASK_OPTIONS}\u2013${MAX_ASK_OPTIONS} suggested one-click answers, up to ${MAX_ASK_OPTION_BYTES} characters each. Each option must make sense on its own.`,
|
|
@@ -7759,7 +7761,6 @@ import { createSdkMcpServer, tool } from "@anthropic-ai/claude-agent-sdk";
|
|
|
7759
7761
|
// src/connector-health.ts
|
|
7760
7762
|
var ConnectorHealthStore = class {
|
|
7761
7763
|
byRuntime = /* @__PURE__ */ new Map();
|
|
7762
|
-
sources = /* @__PURE__ */ new Map();
|
|
7763
7764
|
// Record a settled turn's outcome for `runtime`. A success heals to `ok`; a
|
|
7764
7765
|
// classified failure sets `limited`(+reset) / `auth_expired` with the reason as
|
|
7765
7766
|
// `last_error`; anything else (a user Stop, a timeout, an unclassified error)
|
|
@@ -7768,7 +7769,6 @@ var ConnectorHealthStore = class {
|
|
|
7768
7769
|
recordSettle(runtime, outcome) {
|
|
7769
7770
|
if (outcome.ok) {
|
|
7770
7771
|
this.byRuntime.set(runtime, { quotaState: "ok", limitedUntil: null, lastError: null });
|
|
7771
|
-
this.sources.set(runtime, "turn");
|
|
7772
7772
|
return;
|
|
7773
7773
|
}
|
|
7774
7774
|
const failure = decodeFailureReason(outcome.errorReason);
|
|
@@ -7780,26 +7780,6 @@ var ConnectorHealthStore = class {
|
|
|
7780
7780
|
limitedUntil: quota.limitedUntil,
|
|
7781
7781
|
lastError: outcome.errorReason ?? null
|
|
7782
7782
|
});
|
|
7783
|
-
this.sources.set(runtime, "turn");
|
|
7784
|
-
}
|
|
7785
|
-
// Record what the companion's own sign-in check found for a connected
|
|
7786
|
-
// runtime at `start` or `connect`, so the heartbeat carries "sign-in needed"
|
|
7787
|
-
// before any turn fails. Signed out seeds `auth_expired` unless a turn has
|
|
7788
|
-
// already spoken; usable clears only a seed of our own.
|
|
7789
|
-
recordLocalCheck(runtime, usable) {
|
|
7790
|
-
const source = this.sources.get(runtime);
|
|
7791
|
-
if (!usable) {
|
|
7792
|
-
if (source === "turn" && this.byRuntime.get(runtime)?.quotaState !== "ok") return;
|
|
7793
|
-
this.byRuntime.set(runtime, {
|
|
7794
|
-
quotaState: "auth_expired",
|
|
7795
|
-
limitedUntil: null,
|
|
7796
|
-
lastError: encodeFailureReason({ kind: "auth_expired" })
|
|
7797
|
-
});
|
|
7798
|
-
this.sources.set(runtime, "local");
|
|
7799
|
-
return;
|
|
7800
|
-
}
|
|
7801
|
-
if (source !== "local") return;
|
|
7802
|
-
this.byRuntime.set(runtime, { quotaState: "ok", limitedUntil: null, lastError: null });
|
|
7803
7783
|
}
|
|
7804
7784
|
// The store's current opinion for one runtime, or undefined when it has
|
|
7805
7785
|
// no observation. Read by the dispatcher at idle-timeout fire: an idle reap
|
|
@@ -9101,7 +9081,7 @@ function createTurnControlMcpServer(acts, skipState) {
|
|
|
9101
9081
|
ASK_TOOL,
|
|
9102
9082
|
TURN_CONTROL_TOOL_DESCRIPTIONS.ask,
|
|
9103
9083
|
{
|
|
9104
|
-
targetUserId: z17.string().
|
|
9084
|
+
targetUserId: z17.string().regex(ASK_TARGET_PATTERN, { error: ASK_TARGET_INVALID }).describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask.targetUserId),
|
|
9105
9085
|
question: z17.string().min(1).max(MAX_ASK_QUESTION_BYTES).optional().describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask.question),
|
|
9106
9086
|
headline: z17.string().min(1).max(MAX_ASK_HEADLINE_BYTES).optional().describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask.headline),
|
|
9107
9087
|
options: z17.array(z17.string({ error: ASK_OPTION_NOT_A_STRING }).min(1).max(MAX_ASK_OPTION_BYTES)).min(MIN_ASK_OPTIONS).max(MAX_ASK_OPTIONS).optional().describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask.options),
|
|
@@ -12765,6 +12745,41 @@ function runtimeIssuesFor(gates) {
|
|
|
12765
12745
|
return issues;
|
|
12766
12746
|
}
|
|
12767
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
|
+
|
|
12768
12783
|
// src/harness-versions.ts
|
|
12769
12784
|
var EMPTY = { claudeCode: null, opencode: null, codex: null };
|
|
12770
12785
|
function parseVersionToken(raw) {
|
|
@@ -14299,6 +14314,9 @@ var CompanionSupervisor = class {
|
|
|
14299
14314
|
// Omitted until the first observed failure or heal, so the server's
|
|
14300
14315
|
// status-less manifest synthesis still runs for a fresh companion.
|
|
14301
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) } : {},
|
|
14302
14320
|
...opencodeModels !== null ? { models: opencodeModels } : {},
|
|
14303
14321
|
...this.updater ? { update: this.updater.report() } : {}
|
|
14304
14322
|
});
|
|
@@ -15251,7 +15269,6 @@ var CompanionSupervisor = class {
|
|
|
15251
15269
|
...this.probeOpencode ? { probeOpencode: this.probeOpencode } : {}
|
|
15252
15270
|
});
|
|
15253
15271
|
this.detection = detection;
|
|
15254
|
-
this.seedSignInHealth(previous, detection);
|
|
15255
15272
|
this.logBundledTransitions();
|
|
15256
15273
|
this.harnessVersions = {
|
|
15257
15274
|
claudeCode: detectedVersion(detection["claude-code"]),
|
|
@@ -15309,23 +15326,6 @@ var CompanionSupervisor = class {
|
|
|
15309
15326
|
const bare = advertisedRuntimes(this.detection).filter((runtime) => !named.has(runtime)).map((runtime) => ({ runtime }));
|
|
15310
15327
|
return [...health, ...bare];
|
|
15311
15328
|
}
|
|
15312
|
-
// Seed connector health from what detection saw of a sign-in, so Cabane says
|
|
15313
|
-
// "sign-in needed" before a turn fails. Only a LAPSE seeds it — an agent that
|
|
15314
|
-
// was usable at an earlier look and is signed out now — because a device
|
|
15315
|
-
// that never signed in to Codex has nothing to report about Codex. Usable
|
|
15316
|
-
// again clears a seed of our own.
|
|
15317
|
-
seedSignInHealth(previous, detection) {
|
|
15318
|
-
for (const runtime of ["claude-code", "codex"]) {
|
|
15319
|
-
const now = detection[runtime];
|
|
15320
|
-
if (now.state === "usable") {
|
|
15321
|
-
if (now.via !== "container") this.connectorHealth.recordLocalCheck(runtime, true);
|
|
15322
|
-
continue;
|
|
15323
|
-
}
|
|
15324
|
-
if (now.state === "signed_out" && previous?.[runtime].state === "usable") {
|
|
15325
|
-
this.connectorHealth.recordLocalCheck(runtime, false);
|
|
15326
|
-
}
|
|
15327
|
-
}
|
|
15328
|
-
}
|
|
15329
15329
|
// Beat NOW and wait for it to land. `start` calls this before asking the
|
|
15330
15330
|
// person anything, so the browser's connect step already shows what this
|
|
15331
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",
|