@brainbase-labs/cli 0.34.0 → 0.36.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.
Files changed (3) hide show
  1. package/README.md +54 -2
  2. package/dist/index.js +290 -11
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -27,6 +27,58 @@ brainbase benchmark list # list benchmarks for the linked agent
27
27
 
28
28
  Run `brainbase help` to see every command.
29
29
 
30
+ ## Task follow-ups
31
+
32
+ Send an instruction to an existing task, including while its agent is running:
33
+
34
+ ```sh
35
+ brainbase task send <task-id> "Also update the Linear ticket"
36
+ brainbase task send <task-id> "Use the revised approach instead" --json
37
+ brainbase task stop <task-id>
38
+ ```
39
+
40
+ `send` prints the input ID and generation to stderr before submitting and reports delivery
41
+ status in stdout. Initial acceptance can say **delivery pending** while the
42
+ server determines whether native steering is available. Native steering follows
43
+ the harness's own timing. A confirmed fallback says **queued for the next turn**.
44
+ `submitted` means the harness has received the input; it does not mean the agent
45
+ has acted on it. Multiple follow-ups can join one execution and receive one
46
+ combined answer.
47
+
48
+ By default, both commands read the current input generation before sending their request.
49
+ The credential must have receipt-read access as well as permission to send or stop.
50
+ Resource-scoped keys can lack that read access when task-read enforcement is enabled.
51
+ If the read fails, no mutation is sent. Use `--generation <n>` to bypass the read only
52
+ with a trusted current cutoff for a new operation, or the original cutoff for a retry.
53
+ A concurrent Stop prevents a send from joining newer work with an older generation.
54
+ If the HTTP response is lost or you press Ctrl-C, keep the input ID and generation,
55
+ and repeat the exact message with `--input-id <uuid> --generation <n>` to recover that request safely. Reusing an ID
56
+ does not replay an existing input. A **delivery unconfirmed** receipt means
57
+ the agent may already have received the instruction; deliberately resend
58
+ without `--input-id` only if you want to retry it as a new input.
59
+
60
+ Known pre-admission HTTP 503 refusals include `task_inputs_disabled`,
61
+ `task_input_recovery_unavailable`, and recognized sandbox provisioning or access
62
+ codes. These mean no input was admitted by that request. Retry the same ID, message
63
+ and generation after the service recovers. Unrecognized server or network failures
64
+ can leave delivery unconfirmed. A recovered receipt reports `idempotency_replayed: true`
65
+ in JSON when the server supplies its replay header.
66
+
67
+ `stop` stops execution and cancels pending follow-ups while retaining their
68
+ text. A new instruction will not replay them. Before submitting, it prints a retry
69
+ command with `--generation <n>`. Keep that command even if the response is lost:
70
+ retrying the original generation cannot interrupt newer work. An error can mean
71
+ queued inputs were already cancelled while the original execution is still running.
72
+ Follow the error's retry or operator-recovery guidance. Omit `--generation` only
73
+ when deliberately issuing a new Stop. JSON includes the original `expected_generation`
74
+ separately from the server's resulting `generation`.
75
+
76
+ Ctrl-C in `task create --wait`
77
+ or `task logs --follow` still only detaches; it does not stop remote work.
78
+ Both commands support `--json`. `send` exits 1 for unconfirmed, cancelled or
79
+ failed inputs, and for request errors. Other acknowledged states exit 0;
80
+ this confirms admission or delivery, not task success.
81
+
30
82
  ## Benchmark management
31
83
 
32
84
  `benchmark init`, `benchmark validate`, and command help are local and need no
@@ -237,7 +289,7 @@ Playbook files carry a small YAML frontmatter (`title`, `description`) — the C
237
289
 
238
290
  | Variable | Purpose |
239
291
  |-|-|
240
- | `BRAINBASE_CONTROL_PLANE_URL` | Override the MAS host. Agent/orchestration requests use `/v2/cli`; `task create` uses `/v2/tasks`. The default is `https://api.brainbaselabs.com`. |
292
+ | `BRAINBASE_CONTROL_PLANE_URL` | Override the MAS host. Agent/orchestration requests use `/v2/cli`; task commands use `/v2/tasks`. The default is `https://api.brainbaselabs.com`. |
241
293
  | `BRAINBASE_API_URL` | Legacy KLS host override. Control requests use `/api/cli`; it is also the fallback for model-proxy and registry traffic. |
242
294
  | `BRAINBASE_PROXY_URL` | Override the model-proxy host used when enabling harness tracking. |
243
295
  | `BRAINBASE_REGISTRY_URL` | Override the registry API host. |
@@ -256,7 +308,7 @@ it talks to:
256
308
  |-|-|
257
309
  | `agent`, `orchestration`, `link`, `unlink`, `sync`, `status`, `team` | `BRAINBASE_TOKEN`, then the `auth.json` session |
258
310
  | `template`, `skill`, `token` | `BRAINBASE_TOKEN`, then the session, then `token.json` |
259
- | `task create` | `BRAINBASE_TOKEN`, then the session, then `token.json` — the last only when no session is configured |
311
+ | `task` commands | `BRAINBASE_TOKEN`, then the session, then `token.json` — the last only when no session is configured |
260
312
  | `benchmark` control-plane commands | `BRAINBASE_TOKEN`, then the session |
261
313
 
262
314
  `brainbase whoami` (or `--json`) reports which one is active.
package/dist/index.js CHANGED
@@ -36018,7 +36018,7 @@ function padStart(s, n) {
36018
36018
  // package.json
36019
36019
  var package_default = {
36020
36020
  name: "@brainbase-labs/cli",
36021
- version: "0.34.0",
36021
+ version: "0.36.0",
36022
36022
  description: "Pack, share, and install agent templates across harnesses (Claude Code, Codex, ...).",
36023
36023
  type: "module",
36024
36024
  bin: {
@@ -54605,7 +54605,7 @@ async function masReadRequest(pathname, callerSignal) {
54605
54605
  } catch {}
54606
54606
  if (!res.ok) {
54607
54607
  const message = masApiErrorMessage(body, res.status);
54608
- throw new ApiError(res.status === 401 ? withRejectedSessionHint(message, credential.source, "managed-task") : message, res.status, body);
54608
+ throw new ApiError(res.status === 401 ? withRejectedSessionHint(message, credential.source, "managed-task") : res.status === 404 && isUnroutedPath(body) ? `This control plane does not serve ${pathname} yet. Update the server, or use the web app.` : message, res.status, body);
54609
54609
  }
54610
54610
  return body;
54611
54611
  }
@@ -54623,7 +54623,7 @@ function withRequestDeadline(callerSignal, timeoutMs) {
54623
54623
  });
54624
54624
  return combined.signal;
54625
54625
  }
54626
- async function masResourceRequest(pathname, init = {}) {
54626
+ async function masResourceRequest(pathname, init = {}, onHeaders) {
54627
54627
  const credential = await resolveMasCredential();
54628
54628
  let currentSession = credential.session;
54629
54629
  const res = await sendWithAuthRetry(credential.source === "session" ? currentSession : null, async (refreshed) => {
@@ -54648,6 +54648,7 @@ async function masResourceRequest(pathname, init = {}) {
54648
54648
  const message = masApiErrorMessage(body, res.status);
54649
54649
  throw new ApiError(res.status === 401 ? withRejectedSessionHint(message, credential.source, "managed-task") : res.status === 404 && isUnroutedPath(body) ? `This control plane does not serve ${pathname} yet. Update the server, or use the web app.` : message, res.status, body);
54650
54650
  }
54651
+ onHeaders?.(res.headers);
54651
54652
  return body;
54652
54653
  }
54653
54654
 
@@ -54687,6 +54688,12 @@ function masItems(body, what) {
54687
54688
  }
54688
54689
  throw new ApiError(`MAS returned an unreadable ${what} response.`, undefined, body);
54689
54690
  }
54691
+ function isTaskInputReceipt(body) {
54692
+ if (!body || typeof body !== "object")
54693
+ return false;
54694
+ const receipt = body;
54695
+ return typeof receipt.id === "string" && typeof receipt.task_id === "string" && Array.isArray(receipt.message_ids) && receipt.message_ids.every((id) => typeof id === "string") && typeof receipt.status === "string" && ["queued", "dispatching", "submitted", "unconfirmed", "cancelled", "completed", "failed"].includes(receipt.status) && (receipt.turn_id === null || typeof receipt.turn_id === "string") && Number.isInteger(receipt.generation) && Number.isInteger(receipt.sequence) && (receipt.reason === undefined || receipt.reason === null || typeof receipt.reason === "string") && typeof receipt.created_at === "string" && typeof receipt.updated_at === "string";
54696
+ }
54690
54697
  var masApi = {
54691
54698
  async createTask(input, options) {
54692
54699
  const body = await masRequest("/tasks", {
@@ -54696,6 +54703,30 @@ var masApi = {
54696
54703
  });
54697
54704
  return parseMasTaskCreateResponse(body);
54698
54705
  },
54706
+ async getTaskInputGeneration(taskId) {
54707
+ const body = await masReadRequest(`/tasks/${encodeURIComponent(taskId)}/inputs?limit=1&include_messages=false`);
54708
+ if (!body || !Array.isArray(body.inputs) || typeof body.generation !== "number" || !Number.isSafeInteger(body.generation) || body.generation < 0) {
54709
+ throw new ApiError("MAS returned an unreadable task input generation.", undefined, body);
54710
+ }
54711
+ return body.generation;
54712
+ },
54713
+ async sendTaskInput(taskId, input) {
54714
+ let replayed = false;
54715
+ const body = await masResourceRequest(`/tasks/${encodeURIComponent(taskId)}/inputs`, { method: "POST", body: JSON.stringify(input), keepalive: false }, (headers) => {
54716
+ replayed = headers.get("Idempotency-Replayed") === "true";
54717
+ });
54718
+ if (!body || !isTaskInputReceipt(body.input) || body.input.id.toLowerCase() !== input.input_id.toLowerCase() || body.input.task_id.toLowerCase() !== taskId.toLowerCase() || !Array.isArray(body.messages) || typeof body.run_started !== "boolean" || !(body.accepted_turn_id === null || typeof body.accepted_turn_id === "string")) {
54719
+ throw new ApiError("MAS returned an unreadable follow-up receipt.", undefined, body);
54720
+ }
54721
+ return replayed ? { ...body, idempotency_replayed: true } : body;
54722
+ },
54723
+ async stopTask(taskId, expectedGeneration) {
54724
+ const body = await masResourceRequest(`/tasks/${encodeURIComponent(taskId)}/interrupt`, { method: "POST", body: JSON.stringify({ expected_generation: expectedGeneration }), keepalive: false });
54725
+ if (!body || typeof body.id !== "string" || body.id.toLowerCase() !== taskId.toLowerCase() || typeof body.status !== "string" || !Number.isInteger(body.generation) || !Array.isArray(body.cancelled_inputs) || !body.cancelled_inputs.every((input) => isTaskInputReceipt(input) && input.task_id.toLowerCase() === taskId.toLowerCase() && input.status === "cancelled")) {
54726
+ throw new ApiError("MAS did not confirm Stop and pending follow-up cancellation.", undefined, body);
54727
+ }
54728
+ return body;
54729
+ },
54699
54730
  async listTasks(options = {}) {
54700
54731
  const params = new URLSearchParams;
54701
54732
  if (options.agentId)
@@ -81143,6 +81174,177 @@ function oneLine2(value) {
81143
81174
  return flattened.length > 300 ? `${flattened.slice(0, 299)}…` : flattened;
81144
81175
  }
81145
81176
 
81177
+ // src/cli/task-input.ts
81178
+ import { randomUUID as randomUUID3 } from "node:crypto";
81179
+ function errorCode(error2) {
81180
+ if (!(error2 instanceof ApiError) || !error2.body || typeof error2.body !== "object")
81181
+ return;
81182
+ const detail = error2.body.detail;
81183
+ if (!detail || typeof detail !== "object")
81184
+ return;
81185
+ const code = detail.code;
81186
+ return typeof code === "string" ? code : undefined;
81187
+ }
81188
+ function stopRetryCommand(taskId, generation) {
81189
+ const argument = /^[A-Za-z0-9_-]+$/.test(taskId) ? taskId : `'${sanitizeLabel(taskId).replaceAll("'", "'\\''")}'`;
81190
+ return `brainbase task stop ${argument} --generation ${generation}`;
81191
+ }
81192
+ var NO_ADMISSION_CODES = new Set([
81193
+ "task_inputs_disabled",
81194
+ "task_input_recovery_unavailable",
81195
+ "aws_microvm_unavailable",
81196
+ "modal_sandboxes_unavailable",
81197
+ "modal_sandbox_authorization_unavailable",
81198
+ "bbsandbox_unavailable",
81199
+ "bbsandbox_authorization_unavailable",
81200
+ "cloudflare_unavailable",
81201
+ "cloudflare_authorization_unavailable"
81202
+ ]);
81203
+ var STOP_RECOVERY_CODES = new Set([
81204
+ "turn_interrupt_unsupported",
81205
+ "turn_interrupt_recovery_required",
81206
+ "task_recovery_required"
81207
+ ]);
81208
+ var INPUT_STATUS_TEXT = {
81209
+ queued: "Accepted; delivery pending.",
81210
+ dispatching: "Delivery in progress; the harness has not acknowledged it yet.",
81211
+ submitted: "Received by the harness; this does not confirm the instruction has been acted on.",
81212
+ unconfirmed: "Delivery unconfirmed. The agent may have received this message. Send it again only if you intend to retry; omit --input-id to create a new follow-up.",
81213
+ cancelled: "Cancelled. The text is retained and will not be replayed by a new instruction.",
81214
+ completed: "The execution containing this follow-up has completed.",
81215
+ failed: "Follow-up failed. Its text is retained."
81216
+ };
81217
+ var NEXT_TURN_REASONS = new Set([
81218
+ "unsupported",
81219
+ "closing",
81220
+ "stale",
81221
+ "native_rejected",
81222
+ "native_fallback"
81223
+ ]);
81224
+ async function runTaskSend(taskId, message, options = {}) {
81225
+ if (!message.trim())
81226
+ throw new Error("The follow-up message must not be blank.");
81227
+ const inputId = options.inputId?.trim().toLowerCase() ?? randomUUID3();
81228
+ if (!/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(inputId)) {
81229
+ throw new Error("--input-id must be a UUID.");
81230
+ }
81231
+ console.error(`Input ID: ${inputId}`);
81232
+ let result2;
81233
+ let expectedGeneration = options.generation;
81234
+ let attempted = false;
81235
+ try {
81236
+ expectedGeneration ??= await masApi.getTaskInputGeneration(taskId);
81237
+ console.error([
81238
+ `Input generation: ${expectedGeneration}`,
81239
+ `If interrupted, rerun with --input-id ${inputId} and the identical message, plus --generation ${expectedGeneration}, to recover this request.`
81240
+ ].join(`
81241
+ `));
81242
+ attempted = true;
81243
+ result2 = await masApi.sendTaskInput(taskId, {
81244
+ input_id: inputId,
81245
+ messages: [{ role: "user", content: message }],
81246
+ expected_generation: expectedGeneration
81247
+ });
81248
+ } catch (error2) {
81249
+ const status = error2 instanceof ApiError ? error2.status : undefined;
81250
+ const code = errorCode(error2);
81251
+ const rejected = status === 503 && NO_ADMISSION_CODES.has(code ?? "");
81252
+ const uncertain = attempted && !rejected && (status === undefined || status >= 500);
81253
+ const detail = error2 instanceof Error ? error2.message : String(error2);
81254
+ const retry = `rerun with --input-id ${inputId} and the identical message${expectedGeneration === undefined ? "" : `, plus --generation ${expectedGeneration}`}`;
81255
+ const recovery = uncertain ? ` Delivery is unconfirmed. To recover the same request safely, ${retry}.` : rejected ? ` No input was admitted by this request. After service recovery, ${retry}.` : !attempted ? ` No input was sent: the task generation could not be read. After resolving the error, ${retry}. Use --generation <n> only with a trusted current cutoff, or the original cutoff for a retry.` : "";
81256
+ if (options.json) {
81257
+ console.log(JSON.stringify({
81258
+ task_id: taskId,
81259
+ input_id: inputId,
81260
+ expected_generation: expectedGeneration,
81261
+ error: { message: `${detail}${recovery}`, status, code },
81262
+ ...uncertain ? { delivery: "unconfirmed" } : {}
81263
+ }));
81264
+ process.exitCode = 1;
81265
+ return;
81266
+ }
81267
+ throw new ApiError(`Follow-up ${inputId}: ${detail}${recovery}`, status, error2 instanceof ApiError ? error2.body : undefined);
81268
+ }
81269
+ if (options.json) {
81270
+ console.log(JSON.stringify({ ...result2, expected_generation: expectedGeneration }, null, 2));
81271
+ } else {
81272
+ const receipt = result2.input;
81273
+ const lines = [
81274
+ `Task ID: ${sanitizeLabel(taskId)}`,
81275
+ `Input ID: ${sanitizeLabel(receipt.id)}`,
81276
+ `Status: ${receipt.status}`,
81277
+ receipt.status === "queued" && NEXT_TURN_REASONS.has(receipt.reason ?? "") ? "Queued for the next turn." : INPUT_STATUS_TEXT[receipt.status]
81278
+ ];
81279
+ if (result2.idempotency_replayed)
81280
+ lines.push("Recovered an existing receipt; no new delivery was requested.");
81281
+ if (receipt.turn_id)
81282
+ lines.push(`Turn ID: ${sanitizeLabel(receipt.turn_id)}`);
81283
+ if (receipt.reason)
81284
+ lines.push(`Reason: ${sanitizeLabel(receipt.reason)}`);
81285
+ console.log(lines.join(`
81286
+ `));
81287
+ }
81288
+ if (["unconfirmed", "cancelled", "failed"].includes(result2.input.status)) {
81289
+ process.exitCode = 1;
81290
+ }
81291
+ }
81292
+ async function runTaskStop(taskId, options = {}) {
81293
+ let task;
81294
+ let expectedGeneration = options.generation;
81295
+ let attempted = false;
81296
+ try {
81297
+ expectedGeneration ??= await masApi.getTaskInputGeneration(taskId);
81298
+ console.error([
81299
+ `Stop generation: ${expectedGeneration}`,
81300
+ "If interrupted, retry the original Stop with:",
81301
+ stopRetryCommand(taskId, expectedGeneration)
81302
+ ].join(`
81303
+ `));
81304
+ attempted = true;
81305
+ task = await masApi.stopTask(taskId, expectedGeneration);
81306
+ } catch (error2) {
81307
+ const status = error2 instanceof ApiError ? error2.status : undefined;
81308
+ const code = errorCode(error2);
81309
+ const detail = error2 instanceof Error ? error2.message : String(error2);
81310
+ let guidance = "";
81311
+ if (!attempted) {
81312
+ guidance = " Stop was not sent: the task generation could not be read. Use --generation <n> only with a trusted current cutoff, or the original cutoff for a retry.";
81313
+ } else if (status === undefined || status >= 500 || code === "turn_interrupt_pending" || STOP_RECOVERY_CODES.has(code ?? "")) {
81314
+ guidance = " Stop is not confirmed. Pending follow-ups may already be cancelled; their text is retained. The original execution may still be running.";
81315
+ const retry = stopRetryCommand(taskId, expectedGeneration);
81316
+ guidance += code === "turn_interrupt_unsupported" ? " If the original execution finishes, try the original Stop again. Contact support if it still cannot be confirmed. Retry with:" : STOP_RECOVERY_CODES.has(code ?? "") ? " Wait for operator recovery. Retain this command for any later retry:" : ` ${code === "turn_interrupt_pending" || code === "turn_interrupt_unconfirmed" ? "Wait at least one second, then retry" : "Check the task, then retry"} the original Stop with:`;
81317
+ guidance += `
81318
+ ${retry}
81319
+ Omit --generation only for a deliberately new Stop.`;
81320
+ }
81321
+ const message = `${detail}${guidance}`;
81322
+ if (options.json) {
81323
+ console.log(JSON.stringify({
81324
+ task_id: taskId,
81325
+ expected_generation: expectedGeneration,
81326
+ error: { message, status, code }
81327
+ }));
81328
+ process.exitCode = 1;
81329
+ return;
81330
+ }
81331
+ throw new ApiError(message, status, error2 instanceof ApiError ? error2.body : undefined);
81332
+ }
81333
+ if (options.json) {
81334
+ console.log(JSON.stringify({ ...task, expected_generation: expectedGeneration }, null, 2));
81335
+ return;
81336
+ }
81337
+ console.log([
81338
+ `Task ID: ${sanitizeLabel(task.id)}`,
81339
+ `Status: ${sanitizeLabel(task.status)}`,
81340
+ `Stop generation: ${expectedGeneration}`,
81341
+ `Cancelled follow-ups: ${task.cancelled_inputs.length}`,
81342
+ ...task.cancelled_inputs.map((input) => `Cancelled input: ${sanitizeLabel(input.id)}`),
81343
+ "Follow-ups covered by this Stop are cancelled. Their text is retained; a new instruction will not replay them."
81344
+ ].join(`
81345
+ `));
81346
+ }
81347
+
81146
81348
  // src/core/argv.ts
81147
81349
  class ArgvParseError extends Error {
81148
81350
  code = "invalid_arguments";
@@ -81399,6 +81601,15 @@ function requiredTaskId(positionals, usage) {
81399
81601
  }
81400
81602
  return positionals[0].trim();
81401
81603
  }
81604
+ function inputGeneration(value) {
81605
+ if (value === undefined)
81606
+ return;
81607
+ const parsed = Number(value.trim());
81608
+ if (!/^\d+$/.test(value.trim()) || !Number.isSafeInteger(parsed)) {
81609
+ throw new Error("--generation must be a non-negative safe whole number.");
81610
+ }
81611
+ return parsed;
81612
+ }
81402
81613
  async function runTask(cwd2, sub, args) {
81403
81614
  switch (sub) {
81404
81615
  case "create": {
@@ -81430,6 +81641,44 @@ async function runTask(cwd2, sub, args) {
81430
81641
  });
81431
81642
  return;
81432
81643
  }
81644
+ case "send": {
81645
+ const parsed = parseArgv(args, [
81646
+ { key: "inputId", names: ["--input-id"], kind: "value" },
81647
+ { key: "generation", names: ["--generation"], kind: "value" },
81648
+ { key: "json", names: ["--json"], kind: "boolean" },
81649
+ { key: "help", names: ["--help", "-h"], kind: "boolean" }
81650
+ ]);
81651
+ if (booleanOption(parsed, "help")) {
81652
+ printHelp4();
81653
+ return;
81654
+ }
81655
+ if (parsed.positionals.length !== 2 || !parsed.positionals[0]?.trim()) {
81656
+ throw new Error("Usage: brainbase task send <task-id> <message> [--input-id <uuid>] [--generation <n>] [--json]");
81657
+ }
81658
+ await runTaskSend(parsed.positionals[0].trim(), parsed.positionals[1], {
81659
+ inputId: stringOption(parsed, "inputId"),
81660
+ generation: inputGeneration(stringOption(parsed, "generation")),
81661
+ json: booleanOption(parsed, "json")
81662
+ });
81663
+ return;
81664
+ }
81665
+ case "stop": {
81666
+ const parsed = parseArgv(args, [
81667
+ { key: "generation", names: ["--generation"], kind: "value" },
81668
+ { key: "json", names: ["--json"], kind: "boolean" },
81669
+ { key: "help", names: ["--help", "-h"], kind: "boolean" }
81670
+ ]);
81671
+ if (booleanOption(parsed, "help")) {
81672
+ printHelp4();
81673
+ return;
81674
+ }
81675
+ const taskId = requiredTaskId(parsed.positionals, "brainbase task stop <task-id> [--generation <n>] [--json]");
81676
+ await runTaskStop(taskId, {
81677
+ generation: inputGeneration(stringOption(parsed, "generation")),
81678
+ json: booleanOption(parsed, "json")
81679
+ });
81680
+ return;
81681
+ }
81433
81682
  case "get": {
81434
81683
  if (args.some((arg) => HELP_FLAGS.has(arg))) {
81435
81684
  printHelp4();
@@ -81482,6 +81731,8 @@ function printHelp4() {
81482
81731
  out.push(` ${import_picocolors56.default.cyan("list")} ${import_picocolors56.default.dim("recent tasks you can reach (--json for scripts)")}`);
81483
81732
  out.push(` ${import_picocolors56.default.cyan("get")} ${import_picocolors56.default.dim("<task-id>")} ${import_picocolors56.default.dim("one task: status, agent, machine, eval verdicts")}`);
81484
81733
  out.push(` ${import_picocolors56.default.cyan("logs")} ${import_picocolors56.default.dim("<task-id> [--follow]")} ${import_picocolors56.default.dim("one line per event in the task's transcript, or tail it live")}`);
81734
+ out.push(` ${import_picocolors56.default.cyan("send")} ${import_picocolors56.default.dim("<task-id> <message>")} ${import_picocolors56.default.dim("send a follow-up, including while the agent is running")}`);
81735
+ out.push(` ${import_picocolors56.default.cyan("stop")} ${import_picocolors56.default.dim("<task-id>")} ${import_picocolors56.default.dim("stop execution and cancel pending follow-ups, retaining their text")}`);
81485
81736
  out.push("");
81486
81737
  out.push(` ${import_picocolors56.default.bold("create flags")}`);
81487
81738
  out.push(` ${import_picocolors56.default.dim("--message <text>")} required first user message`);
@@ -81499,12 +81750,24 @@ function printHelp4() {
81499
81750
  out.push(` ${import_picocolors56.default.dim("--limit <n>")} stop after <n> events instead of the whole transcript; with --follow, the first page size`);
81500
81751
  out.push(` ${import_picocolors56.default.dim("--follow, -f")} stay attached and print events as they land, until the task finishes`);
81501
81752
  out.push("");
81753
+ out.push(` ${import_picocolors56.default.bold("send flags")}`);
81754
+ out.push(` ${import_picocolors56.default.dim("--input-id <uuid>")} recover the same request using its original ID and identical message`);
81755
+ out.push(` ${import_picocolors56.default.dim(" omit it to send a new follow-up; an unconfirmed receipt is never automatically replayed")}`);
81756
+ out.push(` ${import_picocolors56.default.dim(" admission disabled/recovery unavailable means no input was admitted; retry the same ID after recovery")}`);
81757
+ out.push("");
81758
+ out.push(` ${import_picocolors56.default.bold("send and stop flags")}`);
81759
+ out.push(` ${import_picocolors56.default.dim("--generation <n>")} retain the printed generation when retrying; defaults to a fresh snapshot for a new operation`);
81760
+ out.push(` ${import_picocolors56.default.dim(" Stop errors may follow queued-input cancellation; retry the printed command to avoid stopping newer work")}`);
81761
+ out.push("");
81502
81762
  out.push(` ${import_picocolors56.default.bold("every subcommand")}`);
81503
- out.push(` ${import_picocolors56.default.dim("--json")} machine-readable output — create prints task_id, agent_id and status; list, get and logs print the raw records`);
81763
+ out.push(` ${import_picocolors56.default.dim("--json")} machine-readable output — create prints task_id, agent_id and status; list, get, logs, send and stop print the raw records`);
81764
+ out.push(` ${import_picocolors56.default.dim(" send and stop include expected_generation for safe retries; send also reports idempotency_replayed when present")}`);
81504
81765
  out.push(` ${import_picocolors56.default.dim(" logs --follow --json prints one record per line, since a live stream has no end to close on")}`);
81505
81766
  out.push("");
81506
81767
  out.push(` ${import_picocolors56.default.dim("Exit codes with --wait:")} ${import_picocolors56.default.dim("0 succeeded · 1 failed, needs input, or timed out · 130 interrupted")}`);
81507
81768
  out.push(` ${import_picocolors56.default.dim("Exit codes with --follow:")} ${import_picocolors56.default.dim("0 task reached a terminal state · 1 stream refused, or unreachable for 60s · 130 interrupted (Ctrl-C) · 143 terminated")}`);
81769
+ out.push(` ${import_picocolors56.default.dim("Exit codes with send:")} ${import_picocolors56.default.dim("0 queued, dispatching, submitted or completed · 1 unconfirmed, cancelled, failed or request error")}`);
81770
+ out.push(` ${import_picocolors56.default.dim("Ctrl-C while waiting or following logs detaches; use task stop to cancel remote work.")}`);
81508
81771
  out.push("");
81509
81772
  out.push(` ${import_picocolors56.default.dim("Flag-like values:")} use ${import_picocolors56.default.cyan("--flag=value")} or ${import_picocolors56.default.cyan("--flag -- <value>")}`);
81510
81773
  out.push("");
@@ -85418,9 +85681,12 @@ class BenchmarkApiClient {
85418
85681
  query: { baseline_target_id: baselineTargetId }
85419
85682
  });
85420
85683
  }
85421
- exportRunResults(runId) {
85684
+ exportRunResults(runId, query = {}) {
85422
85685
  return this.request(`/runs/${encodeURIComponent(runId)}/results/export`, {
85423
- mode: "binary"
85686
+ mode: "binary",
85687
+ query: {
85688
+ include_billing_receipts: query.includeBillingReceipts || undefined
85689
+ }
85424
85690
  });
85425
85691
  }
85426
85692
  listTrials(runId, query = {}) {
@@ -87134,6 +87400,11 @@ var DEFINITIONS = [
87134
87400
  { key: "yes", names: ["--yes", "-y"], kind: "boolean" },
87135
87401
  { key: "all", names: ["--all"], kind: "boolean" },
87136
87402
  { key: "force", names: ["--force"], kind: "boolean" },
87403
+ {
87404
+ key: "includeBillingReceipts",
87405
+ names: ["--include-billing-receipts"],
87406
+ kind: "boolean"
87407
+ },
87137
87408
  { key: "archived", names: ["--archived"], kind: "boolean" },
87138
87409
  { key: "preview", names: ["--preview"], kind: "boolean" },
87139
87410
  { key: "watch", names: ["--watch"], kind: "boolean" },
@@ -88696,11 +88967,17 @@ async function runArtifacts(cwd2, argv, deps) {
88696
88967
  }, options);
88697
88968
  }
88698
88969
  async function runExportResults(cwd2, argv, deps) {
88699
- const options = parseControlOptions(argv, ["output", "force"]);
88700
- expectPositionals(options.positionals, 1, 1, "brainbase benchmark export-results <run-id> [--output <path>]");
88970
+ const options = parseControlOptions(argv, [
88971
+ "output",
88972
+ "force",
88973
+ "includeBillingReceipts"
88974
+ ]);
88975
+ expectPositionals(options.positionals, 1, 1, "brainbase benchmark export-results <run-id> [--output <path>] [--include-billing-receipts]");
88701
88976
  const runId = options.positionals[0];
88702
88977
  const { client, output } = context(cwd2, "benchmark export-results", options, deps);
88703
- const response = await client.exportRunResults(runId);
88978
+ const response = await client.exportRunResults(runId, {
88979
+ includeBillingReceipts: bool(options.parsed, "includeBillingReceipts")
88980
+ });
88704
88981
  const fallback = safeExportFilename(response.filename, `benchmark-run-${runId}-results.zip`);
88705
88982
  const written = writeBinaryOutput(cwd2, optionalNonblank(options.parsed, "output"), response.bytes, fallback, bool(options.parsed, "force"));
88706
88983
  output.result({
@@ -88913,7 +89190,7 @@ function commandLevelHelp(sub, argv) {
88913
89190
  case "artifacts":
88914
89191
  return usage("brainbase benchmark artifacts <run-id> <attempt-id> [--limit <n>] [--cursor <cursor>] [--all]");
88915
89192
  case "export-results":
88916
- return usage("brainbase benchmark export-results <run-id> [--output <path>] [--force]");
89193
+ return usage("brainbase benchmark export-results <run-id> [--output <path>] [--force] [--include-billing-receipts]");
88917
89194
  case "history":
88918
89195
  return usage("brainbase benchmark history <benchmark-id> [--limit <n>]");
88919
89196
  case "baseline":
@@ -89463,6 +89740,8 @@ function help() {
89463
89740
  out.push(` ${import_picocolors61.default.cyan("task list")} ${import_picocolors61.default.dim("recent tasks you can reach")}`);
89464
89741
  out.push(` ${import_picocolors61.default.cyan("task get")} ${import_picocolors61.default.dim("<task-id>")} ${import_picocolors61.default.dim("one task: status, agent, machine, eval verdicts")}`);
89465
89742
  out.push(` ${import_picocolors61.default.cyan("task logs")} ${import_picocolors61.default.dim("<task-id> [--follow]")} ${import_picocolors61.default.dim("one line per event in the task's transcript, or tail it live")}`);
89743
+ out.push(` ${import_picocolors61.default.cyan("task send")} ${import_picocolors61.default.dim("<task-id> <message>")} ${import_picocolors61.default.dim("send a follow-up during execution, or queue it for the next turn")}`);
89744
+ out.push(` ${import_picocolors61.default.cyan("task stop")} ${import_picocolors61.default.dim("<task-id>")} ${import_picocolors61.default.dim("stop execution and cancel pending follow-ups, retaining their text")}`);
89466
89745
  out.push("");
89467
89746
  out.push(divider("SANDBOXES"));
89468
89747
  out.push("");
@@ -89556,7 +89835,7 @@ function help() {
89556
89835
  out.push(` ${import_picocolors61.default.dim("BRAINBASE_HOME")} override the local config dir (default ~/.brainbase)`);
89557
89836
  out.push(` ${import_picocolors61.default.dim("BRAINBASE_DEBUG")} print full stack traces on error (any value; unset to disable)`);
89558
89837
  out.push(` ${import_picocolors61.default.dim("BRAINBASE_WEB_URL")} override the web app URL used by login`);
89559
- out.push(` ${import_picocolors61.default.dim("BRAINBASE_CONTROL_PLANE_URL")} override the MAS host (/v2/cli; task create uses /v2/tasks)`);
89838
+ out.push(` ${import_picocolors61.default.dim("BRAINBASE_CONTROL_PLANE_URL")} override the MAS host (/v2/cli; task commands use /v2/tasks)`);
89560
89839
  out.push(` ${import_picocolors61.default.dim("BRAINBASE_API_URL")} legacy KLS host override (uses /api/cli; proxy/registry fallback)`);
89561
89840
  out.push(` ${import_picocolors61.default.dim("BRAINBASE_PROXY_URL")} override the model-proxy URL used by harness traffic (default https://api.v1.brainbaselabs.com)`);
89562
89841
  out.push(` ${import_picocolors61.default.dim("BRAINBASE_REGISTRY_URL")} override the registry API URL (default https://api.v1.brainbaselabs.com)`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@brainbase-labs/cli",
3
- "version": "0.34.0",
3
+ "version": "0.36.0",
4
4
  "description": "Pack, share, and install agent templates across harnesses (Claude Code, Codex, ...).",
5
5
  "type": "module",
6
6
  "bin": {