@botbuddy/cli 1.57.0 → 1.58.0
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/package.json +1 -1
- package/src/deep-link.mjs +24 -0
- package/src/stack.mjs +19 -3
- package/src/wait.mjs +19 -2
package/package.json
CHANGED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
// BOT-2468 — the dashboard deeplink for what this command just created (a stack
|
|
2
|
+
// lease or a wait). The server answers `web_url` (built from its BOTBUDDY_WEB_URL
|
|
3
|
+
// origin, null when that is unset); the CLI prints it on ONE stderr line telling the
|
|
4
|
+
// agent to show it to the user, and carries it on the JSON receipt. A missing or
|
|
5
|
+
// unsafe value prints nothing: there is no client-side fallback host.
|
|
6
|
+
|
|
7
|
+
/** The server's `web_url` when it is a plain http(s) URL safe to print, else null.
|
|
8
|
+
* Whitespace and control characters are refused so a hostile value cannot inject
|
|
9
|
+
* terminal escapes or split the line. */
|
|
10
|
+
export function safeWebUrl(value) {
|
|
11
|
+
if (typeof value !== "string" || value.length === 0 || value.length > 2048) return null;
|
|
12
|
+
if (/[\s\u0000-\u001f\u007f]/.test(value)) return null;
|
|
13
|
+
let parsed;
|
|
14
|
+
try { parsed = new URL(value); } catch { return null; }
|
|
15
|
+
if (parsed.protocol !== "https:" && parsed.protocol !== "http:") return null;
|
|
16
|
+
return value;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** `<prefix>: 🔗 <url> — show this link …`, or null when there is no safe URL. */
|
|
20
|
+
export function deepLinkNotice(prefix, webUrl) {
|
|
21
|
+
const url = safeWebUrl(webUrl);
|
|
22
|
+
if (!url) return null;
|
|
23
|
+
return `${prefix}: 🔗 ${url} — show this link to the user as a clickable Markdown link in your chat reply`;
|
|
24
|
+
}
|
package/src/stack.mjs
CHANGED
|
@@ -36,6 +36,7 @@ import { basename, dirname, join, relative, isAbsolute } from "path";
|
|
|
36
36
|
import { randomUUID } from "crypto";
|
|
37
37
|
import { fileURLToPath } from "url";
|
|
38
38
|
import { callToolJson, CALL_AUTH_ERROR_CODES } from "./api.mjs";
|
|
39
|
+
import { deepLinkNotice, safeWebUrl } from "./deep-link.mjs";
|
|
39
40
|
import { SERVER_URL, getConfig } from "./config.mjs";
|
|
40
41
|
import { resolveOwnerToken, resolveAgentKey } from "./cli-credentials.mjs";
|
|
41
42
|
import { AGENT_KEY_RE, readAgentSessionTokenEnv } from "./agent-key.mjs";
|
|
@@ -2298,6 +2299,11 @@ export async function cmdUp(opts, {
|
|
|
2298
2299
|
}
|
|
2299
2300
|
let leaseId = d.lease_id;
|
|
2300
2301
|
let state = d.state;
|
|
2302
|
+
// BOT-2468: the lease's /docker?lease= deeplink, printed once as soon as the id is known
|
|
2303
|
+
// and carried on every receipt that names this lease (null from a server without one).
|
|
2304
|
+
const webUrl = safeWebUrl(d.web_url);
|
|
2305
|
+
const linkNotice = deepLinkNotice("bb stack", webUrl);
|
|
2306
|
+
if (linkNotice) process.stderr.write(`${linkNotice}\n`);
|
|
2301
2307
|
// BOT-2099: every lease wait in `stack up` (queue park, Helper provision, Helper takeover)
|
|
2302
2308
|
// survives auth/availability blips (parkOnLease) and adopts the re-minted credential.
|
|
2303
2309
|
const resilientWait = async (isDone, isFailed) => {
|
|
@@ -2330,7 +2336,10 @@ export async function cmdUp(opts, {
|
|
|
2330
2336
|
error: `${r.error}; its server state is unknown to this command — inspect it with \`bb stack status ${leaseId}\` after \`bb doctor --fix\``,
|
|
2331
2337
|
}), opts, EXIT.AUTH);
|
|
2332
2338
|
const emitPlain = emitResult;
|
|
2333
|
-
emitResult = (receipt, o, code) =>
|
|
2339
|
+
emitResult = (receipt, o, code) => {
|
|
2340
|
+
const withIncident = upIncident && !receipt.platform_incident ? { ...receipt, platform_incident: upIncident } : receipt;
|
|
2341
|
+
return emitPlain(withIncident.lease_id === leaseId && !("web_url" in withIncident) ? { ...withIncident, web_url: webUrl } : withIncident, o, code);
|
|
2342
|
+
};
|
|
2334
2343
|
// BOT-1711 (Codex R13): request_stack_lease returned an EXISTING active lease (reused),
|
|
2335
2344
|
// rather than a freshly minted queued/provisioning one. A reused lease was not provisioned
|
|
2336
2345
|
// in this invocation, so its recorded metadata cannot be trusted for a local-exec target.
|
|
@@ -3041,7 +3050,7 @@ export async function runStackLifecycle(opts, childArgv, adapters = {}) {
|
|
|
3041
3050
|
// A receipt always says what happened to a port-block claim. Before allocation
|
|
3042
3051
|
// no claim exists, which is a safe, explicit disposition rather than omission.
|
|
3043
3052
|
// BOT-1803: whether this run re-adopted / re-provisioned an idle lease, and its stack digest.
|
|
3044
|
-
const incidentBox = { incident: null, execution: null, idleLease: null, portBlock: { released: false, reason: "no port-block claim was created before stack allocation" } };
|
|
3053
|
+
const incidentBox = { incident: null, execution: null, idleLease: null, webUrl: null, portBlock: { released: false, reason: "no port-block claim was created before stack allocation" } };
|
|
3045
3054
|
let result;
|
|
3046
3055
|
try {
|
|
3047
3056
|
result = await runStackLifecycleOnce(opts, childArgv, adapters, incidentBox);
|
|
@@ -3051,7 +3060,8 @@ export async function runStackLifecycle(opts, childArgv, adapters = {}) {
|
|
|
3051
3060
|
const withIncident = incidentBox.incident && !result?.incident ? { ...result, incident: incidentBox.incident } : result;
|
|
3052
3061
|
const withExecution = incidentBox.execution && !withIncident?.execution ? { ...withIncident, execution: incidentBox.execution } : withIncident;
|
|
3053
3062
|
const withIdle = incidentBox.idleLease && !withExecution?.idleLease ? { ...withExecution, idleLease: incidentBox.idleLease } : withExecution;
|
|
3054
|
-
|
|
3063
|
+
const withPortBlock = incidentBox.portBlock && !withIdle?.portBlock ? { ...withIdle, portBlock: incidentBox.portBlock } : withIdle;
|
|
3064
|
+
return incidentBox.webUrl && withPortBlock && !withPortBlock.webUrl ? { ...withPortBlock, webUrl: incidentBox.webUrl } : withPortBlock;
|
|
3055
3065
|
}
|
|
3056
3066
|
|
|
3057
3067
|
async function runStackLifecycleOnce(opts, childArgv, adapters, incidentBox) {
|
|
@@ -3514,6 +3524,10 @@ async function runStackLifecycleOnce(opts, childArgv, adapters, incidentBox) {
|
|
|
3514
3524
|
};
|
|
3515
3525
|
}
|
|
3516
3526
|
leaseId = request.data.lease_id;
|
|
3527
|
+
// BOT-2468: the lease's /docker?lease= deeplink, printed once as soon as the id is known.
|
|
3528
|
+
incidentBox.webUrl = safeWebUrl(request.data.web_url);
|
|
3529
|
+
const linkNotice = deepLinkNotice("bb stack", incidentBox.webUrl);
|
|
3530
|
+
if (linkNotice) stderrLine(linkNotice);
|
|
3517
3531
|
// BOT-1803: `reused_idle` — the slot's IDLE lease was re-adopted: it is active with its
|
|
3518
3532
|
// stored connection and nothing is provisioned, so the run goes straight on to the
|
|
3519
3533
|
// post-activation flow. Any other reuse is somebody's LIVE batch, refused as before.
|
|
@@ -3918,6 +3932,8 @@ export function runReceiptFields(result) {
|
|
|
3918
3932
|
// BOT-1797: carry the server's typed refusal code (e.g. stack_path_root_refused)
|
|
3919
3933
|
// so automation can branch on it instead of matching the message text.
|
|
3920
3934
|
command: "run", outcome: result.outcome, code: result.code ?? null, lease_id: result.leaseId ?? null,
|
|
3935
|
+
// BOT-2468: the lease's /docker?lease= deeplink (null when the server sent none).
|
|
3936
|
+
web_url: result.webUrl ?? null,
|
|
3921
3937
|
// BOT-2024: a stale-capacity refusal carries its phase + armable wait on the receipt.
|
|
3922
3938
|
...capacityStaleReceiptFields(result),
|
|
3923
3939
|
...(result.wait_exit_code != null ? { wait_exit_code: result.wait_exit_code, wait_receipt: result.wait_receipt ?? null } : {}),
|
package/src/wait.mjs
CHANGED
|
@@ -25,6 +25,7 @@ import { touchAgentStateExpiry } from "./agent-state.mjs";
|
|
|
25
25
|
import { SETUP_BLOCK } from "./setup-block.mjs";
|
|
26
26
|
import { createWaitCheckpointStore, WaitCheckpointError } from "./wait-checkpoint.mjs";
|
|
27
27
|
import { realpath } from "node:fs/promises";
|
|
28
|
+
import { deepLinkNotice, safeWebUrl } from "./deep-link.mjs";
|
|
28
29
|
|
|
29
30
|
// A protocol is deliberately distinct from package semver: compatible pinned
|
|
30
31
|
// clients keep working until the server raises this minimum, while a stale
|
|
@@ -537,6 +538,9 @@ async function registerWait(opts, conditions, deadlineIso) {
|
|
|
537
538
|
// unmatchable alias registration.
|
|
538
539
|
recoveryError: typeof body.recovery_error === "string" ? body.recovery_error : null,
|
|
539
540
|
terminalReceipt: body.terminal_receipt && typeof body.terminal_receipt === "object" ? body.terminal_receipt : null,
|
|
541
|
+
// BOT-2468: the wait's /waits?wait= deeplink (null from a relay without BOTBUDDY_WEB_URL;
|
|
542
|
+
// absent from a pre-BOT-2468 relay — both print no link line).
|
|
543
|
+
webUrl: safeWebUrl(body.web_url),
|
|
540
544
|
};
|
|
541
545
|
}
|
|
542
546
|
|
|
@@ -843,8 +847,11 @@ function publicWaitReceipt(receipt, {
|
|
|
843
847
|
sessionAgentId = null,
|
|
844
848
|
sessionId = null,
|
|
845
849
|
maxBytes = 10240,
|
|
850
|
+
// BOT-2468: the registered wait's deeplink; undefined (no relay registration) omits the field.
|
|
851
|
+
webUrl = undefined,
|
|
846
852
|
} = {}) {
|
|
847
|
-
|
|
853
|
+
const linked = webUrl === undefined || (receipt && "web_url" in receipt) ? receipt : { ...receipt, web_url: webUrl };
|
|
854
|
+
return truncateReceipt(withClientIdentity(withPrincipalReceipt(linked, null, {
|
|
848
855
|
sessionTenant,
|
|
849
856
|
agentId,
|
|
850
857
|
sessionAgentId,
|
|
@@ -1760,6 +1767,9 @@ export async function runWait(argv, { recoveryLocalWaitId = null } = {}) {
|
|
|
1760
1767
|
// register and reused as the timeout receipt's error. Null when there is
|
|
1761
1768
|
// nothing to report (no ci scope=next+branch condition, or no run yet).
|
|
1762
1769
|
let ciArmNotice = null;
|
|
1770
|
+
// BOT-2468: the wait's /waits?wait= deeplink from register; stays undefined when the
|
|
1771
|
+
// wait never registered (live-only/offline), so its receipt carries no web_url key.
|
|
1772
|
+
let registeredWebUrl;
|
|
1763
1773
|
let replayMatchedClaim = false;
|
|
1764
1774
|
let replayExpiredRecovery = false;
|
|
1765
1775
|
if (needsRelay) {
|
|
@@ -1809,6 +1819,7 @@ export async function runWait(argv, { recoveryLocalWaitId = null } = {}) {
|
|
|
1809
1819
|
registeredAgentId = reg.agentId;
|
|
1810
1820
|
registeredSessionAgentId = reg.sessionAgentId;
|
|
1811
1821
|
registeredSessionId = reg.sessionId;
|
|
1822
|
+
registeredWebUrl = reg.webUrl ?? null;
|
|
1812
1823
|
// A freshly minted session token may represent the same immutable agent,
|
|
1813
1824
|
// but never a different tenant/agent or worktree. Check before exposing a
|
|
1814
1825
|
// result or opening the SSE stream.
|
|
@@ -1841,7 +1852,7 @@ export async function runWait(argv, { recoveryLocalWaitId = null } = {}) {
|
|
|
1841
1852
|
if (reg.terminalReceipt) {
|
|
1842
1853
|
const recovered = preserveCheckpointCursor(publicWaitReceipt(reg.terminalReceipt, {
|
|
1843
1854
|
sessionTenant, agentId: registeredAgentId, sessionAgentId: registeredSessionAgentId,
|
|
1844
|
-
sessionId: registeredSessionId, maxBytes: opts.receiptMaxBytes,
|
|
1855
|
+
sessionId: registeredSessionId, maxBytes: opts.receiptMaxBytes, webUrl: registeredWebUrl,
|
|
1845
1856
|
}), checkpoint.safe_cursor, checkpoint.created_at);
|
|
1846
1857
|
const recoveredCancellation = recovered.outcome === "cancelled";
|
|
1847
1858
|
await store.update(checkpoint.local_wait_id, {
|
|
@@ -1858,6 +1869,11 @@ export async function runWait(argv, { recoveryLocalWaitId = null } = {}) {
|
|
|
1858
1869
|
emitReceipt(recovered, { versioned: true });
|
|
1859
1870
|
process.exit(exitForRecoveredOutcome(recovered));
|
|
1860
1871
|
}
|
|
1872
|
+
// BOT-2468: the wait's dashboard deeplink, once, at arm time (and again on
|
|
1873
|
+
// `bb wait resume`, which re-registers). An agent that armed this in the
|
|
1874
|
+
// background reads this first 🔗 line once and posts the link in its chat.
|
|
1875
|
+
const linkNotice = deepLinkNotice("bb-wait", registeredWebUrl);
|
|
1876
|
+
if (linkNotice) process.stderr.write(`${linkNotice}\n`);
|
|
1861
1877
|
// BOT-1539: if any pr-review/pr-state target already has an outstanding
|
|
1862
1878
|
// review, tell the operator now (stderr, one line per PR). Purely
|
|
1863
1879
|
// informational — it does not change arming or exit semantics.
|
|
@@ -2104,6 +2120,7 @@ export async function runWait(argv, { recoveryLocalWaitId = null } = {}) {
|
|
|
2104
2120
|
sessionAgentId: registeredSessionAgentId,
|
|
2105
2121
|
sessionId: registeredSessionId,
|
|
2106
2122
|
maxBytes: opts.receiptMaxBytes,
|
|
2123
|
+
webUrl: registeredWebUrl,
|
|
2107
2124
|
});
|
|
2108
2125
|
let terminalPersistedFromFrame = false;
|
|
2109
2126
|
|