@cabane/companion 0.6.172 → 0.6.174
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 +6181 -6408
- package/dist/pairing-config.js +38 -100
- package/dist/runtime.js +6 -3
- package/package.json +1 -1
package/dist/pairing-config.js
CHANGED
|
@@ -432,114 +432,53 @@ function saveConfig(cfg) {
|
|
|
432
432
|
}
|
|
433
433
|
|
|
434
434
|
// src/enrollment.ts
|
|
435
|
-
|
|
436
|
-
constructor() {
|
|
437
|
-
super("this pairing code expired before it was confirmed.");
|
|
438
|
-
this.name = "EnrollmentExpiredError";
|
|
439
|
-
}
|
|
440
|
-
};
|
|
441
|
-
var EnrollmentCancelledError = class extends CompanionError {
|
|
442
|
-
constructor() {
|
|
443
|
-
super("pairing was cancelled.");
|
|
444
|
-
this.name = "EnrollmentCancelledError";
|
|
445
|
-
}
|
|
446
|
-
};
|
|
447
|
-
function trimBase(baseUrl) {
|
|
448
|
-
return baseUrl.endsWith("/") ? baseUrl.slice(0, -1) : baseUrl;
|
|
449
|
-
}
|
|
450
|
-
async function postJson(baseUrl, path, body) {
|
|
435
|
+
async function claimSetupCode(baseUrl, input) {
|
|
451
436
|
let res;
|
|
452
437
|
try {
|
|
453
|
-
res = await fetch(`${
|
|
438
|
+
res = await fetch(`${baseUrl.replace(/\/$/, "")}/api/device-enrollment/claim`, {
|
|
454
439
|
method: "POST",
|
|
455
|
-
headers: {
|
|
456
|
-
|
|
440
|
+
headers: {
|
|
441
|
+
"content-type": "application/json",
|
|
442
|
+
...input.deviceToken ? { authorization: `Bearer ${input.deviceToken}` } : {}
|
|
443
|
+
},
|
|
444
|
+
body: JSON.stringify({
|
|
445
|
+
userCode: input.userCode,
|
|
446
|
+
label: input.label,
|
|
447
|
+
platform: process.platform,
|
|
448
|
+
arch: process.arch,
|
|
449
|
+
nodeMajor: Number.parseInt(process.versions.node.split(".")[0], 10)
|
|
450
|
+
})
|
|
457
451
|
});
|
|
458
|
-
} catch
|
|
452
|
+
} catch {
|
|
459
453
|
throw new CompanionError(
|
|
460
|
-
`
|
|
454
|
+
`Couldn't reach Cabane at ${baseUrl}. Check the server URL and your connection.`
|
|
461
455
|
);
|
|
462
456
|
}
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
return parsed;
|
|
482
|
-
}
|
|
483
|
-
function requestEnrollmentCode(baseUrl, opts = {}) {
|
|
484
|
-
return postJson(baseUrl, "/api/device-enrollment/code", {
|
|
485
|
-
...opts.label ? { label: opts.label } : {},
|
|
486
|
-
...hostFacts()
|
|
487
|
-
});
|
|
488
|
-
}
|
|
489
|
-
function hostFacts() {
|
|
490
|
-
const nodeMajor = Number.parseInt(process.versions.node.split(".")[0] ?? "", 10);
|
|
457
|
+
if (res.status === 429)
|
|
458
|
+
throw new CompanionError(
|
|
459
|
+
"Too many attempts from this network. Wait a few minutes and paste the setup line again."
|
|
460
|
+
);
|
|
461
|
+
if (res.status === 404 && input.deviceToken)
|
|
462
|
+
throw new CompanionError(
|
|
463
|
+
"The setup code or saved device credential is unavailable. If this device was removed, run cabane logout, then take a fresh setup line from your workspace\u2019s Settings \u2192 Devices."
|
|
464
|
+
);
|
|
465
|
+
if (res.status === 404 || res.status === 410)
|
|
466
|
+
throw new CompanionError(
|
|
467
|
+
"That setup code is unavailable. Take a fresh setup line from your workspace\u2019s Settings \u2192 Devices."
|
|
468
|
+
);
|
|
469
|
+
if (!res.ok) throw new ApiError(res.status, `Setup failed (${res.status}).`);
|
|
470
|
+
const body = await res.json();
|
|
471
|
+
if (!body || typeof body !== "object" || !("baseUrl" in body) || typeof body.baseUrl !== "string" || !("deviceId" in body) || typeof body.deviceId !== "string")
|
|
472
|
+
throw new CompanionError("Cabane returned an incomplete setup response.");
|
|
473
|
+
const token = "deviceToken" in body && typeof body.deviceToken === "string" ? body.deviceToken : input.deviceToken;
|
|
474
|
+
if (!token) throw new CompanionError("Cabane returned no device credential.");
|
|
491
475
|
return {
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
476
|
+
baseUrl: body.baseUrl,
|
|
477
|
+
deviceToken: token,
|
|
478
|
+
deviceId: body.deviceId,
|
|
479
|
+
..."deviceLabel" in body && typeof body.deviceLabel === "string" ? { deviceLabel: body.deviceLabel } : {}
|
|
495
480
|
};
|
|
496
481
|
}
|
|
497
|
-
function pollOnce(baseUrl, deviceCode) {
|
|
498
|
-
return postJson(baseUrl, "/api/device-enrollment/poll", { deviceCode });
|
|
499
|
-
}
|
|
500
|
-
var sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
501
|
-
async function runDeviceFlow(baseUrl, print, opts = {}) {
|
|
502
|
-
if (opts.signal?.aborted) throw new EnrollmentCancelledError();
|
|
503
|
-
const code = await requestEnrollmentCode(baseUrl, {
|
|
504
|
-
...opts.label ? { label: opts.label } : {}
|
|
505
|
-
});
|
|
506
|
-
if (opts.onCode) await opts.onCode(code);
|
|
507
|
-
print("");
|
|
508
|
-
print(` Enter this code at ${code.verificationUri}`);
|
|
509
|
-
print("");
|
|
510
|
-
print(` ${code.userCode}`);
|
|
511
|
-
print("");
|
|
512
|
-
print(` or open: ${code.verificationUriComplete}`);
|
|
513
|
-
print("");
|
|
514
|
-
print("Waiting for you to confirm it in Cabane\u2026");
|
|
515
|
-
return awaitEnrollment(baseUrl, code, opts);
|
|
516
|
-
}
|
|
517
|
-
async function awaitEnrollment(baseUrl, code, opts = {}) {
|
|
518
|
-
const now = opts.now ?? (() => Date.now());
|
|
519
|
-
const wait = opts.sleepMs ?? sleep;
|
|
520
|
-
const deadline = now() + code.expiresIn * 1e3;
|
|
521
|
-
let intervalMs = Math.max(1, code.interval) * 1e3;
|
|
522
|
-
while (now() < deadline) {
|
|
523
|
-
await wait(intervalMs);
|
|
524
|
-
if (opts.signal?.aborted) throw new EnrollmentCancelledError();
|
|
525
|
-
const res = await pollOnce(baseUrl, code.deviceCode);
|
|
526
|
-
if (res.status === "complete") {
|
|
527
|
-
if (!res.deviceToken || !res.baseUrl) {
|
|
528
|
-
throw new CompanionError("the server reported the pairing complete but returned no token.");
|
|
529
|
-
}
|
|
530
|
-
return {
|
|
531
|
-
baseUrl: res.baseUrl,
|
|
532
|
-
deviceToken: res.deviceToken,
|
|
533
|
-
...res.deviceId ? { deviceId: res.deviceId } : {},
|
|
534
|
-
...res.deviceLabel ? { deviceLabel: res.deviceLabel } : {},
|
|
535
|
-
...res.ownerName ? { ownerName: res.ownerName } : {}
|
|
536
|
-
};
|
|
537
|
-
}
|
|
538
|
-
if (res.status === "expired") throw new EnrollmentExpiredError();
|
|
539
|
-
if (res.status === "slow_down") intervalMs += 1e3;
|
|
540
|
-
}
|
|
541
|
-
throw new EnrollmentExpiredError();
|
|
542
|
-
}
|
|
543
482
|
|
|
544
483
|
// src/pairing-config.ts
|
|
545
484
|
var DEFAULT_BASE_URL = "https://app.cabane.ai";
|
|
@@ -574,9 +513,8 @@ function isDevicePaired() {
|
|
|
574
513
|
export {
|
|
575
514
|
ApiError,
|
|
576
515
|
CompanionError,
|
|
516
|
+
claimSetupCode,
|
|
577
517
|
isDevicePaired,
|
|
578
|
-
requestEnrollmentCode,
|
|
579
518
|
resolvePairBaseUrl,
|
|
580
|
-
runDeviceFlow,
|
|
581
519
|
writePairedConfig
|
|
582
520
|
};
|
package/dist/runtime.js
CHANGED
|
@@ -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.`,
|
|
@@ -9101,7 +9103,7 @@ function createTurnControlMcpServer(acts, skipState) {
|
|
|
9101
9103
|
ASK_TOOL,
|
|
9102
9104
|
TURN_CONTROL_TOOL_DESCRIPTIONS.ask,
|
|
9103
9105
|
{
|
|
9104
|
-
targetUserId: z17.string().
|
|
9106
|
+
targetUserId: z17.string().regex(ASK_TARGET_PATTERN, { error: ASK_TARGET_INVALID }).describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask.targetUserId),
|
|
9105
9107
|
question: z17.string().min(1).max(MAX_ASK_QUESTION_BYTES).optional().describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask.question),
|
|
9106
9108
|
headline: z17.string().min(1).max(MAX_ASK_HEADLINE_BYTES).optional().describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask.headline),
|
|
9107
9109
|
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),
|
|
@@ -14270,6 +14272,7 @@ var CompanionSupervisor = class {
|
|
|
14270
14272
|
async sendHeartbeat() {
|
|
14271
14273
|
if (!this.deviceApi) return;
|
|
14272
14274
|
try {
|
|
14275
|
+
if (!this.detection || advertisedRuntimes(this.detection).length === 0) await this.detect();
|
|
14273
14276
|
const store = loadSecretStoreTolerant(
|
|
14274
14277
|
(m) => this.log.warn({ err: m }, "Could not read the secrets file. Check ~/.cabane/secrets.json.")
|
|
14275
14278
|
);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cabane/companion",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.174",
|
|
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",
|