@lambdacurry/arbor 0.24.1 → 0.24.3

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 +8 -0
  2. package/dist/arbor.js +252 -27
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -101,6 +101,14 @@ and `attachment list` (`attachment_list`, `list_attachments`) recover receipts;
101
101
  pages with `--cursor`. Finalized receipts replay with the same authentic token even after expiry.
102
102
  Duplicate SHA-256 warnings on the same Thread are best-effort; concurrent uploads may not warn.
103
103
 
104
+ Several files work in one command: `arbor attachment upload a.png b.png --thread-id thr_… --description "A" --description "B"`.
105
+ Repeated `--description` flags pair with files by order (none = filename defaults, otherwise exactly one per
106
+ file; write a value that starts with `--` as `--description=<value>`; values are literal, so a leading `[` is text). A blank description or a path listed twice is an error. The whole batch is checked before the
107
+ first request and uploads one file at a time; a mid-batch failure prints what uploaded, what failed and what
108
+ was not attempted, and exits non-zero. `--dry-run` prints the plan and makes no network call. Every receipt
109
+ carries a `nextStep` (unbound until bound to `contribute`/`respond`, max 10 per contribution; descriptions are
110
+ permanent) and may warn when a description is reused for a different file on the same Thread.
111
+
104
112
  MCP exposes **one** tool, `attachment`, with `upload_url` / `attach` / `get` / `list` verbs.
105
113
  Refresh a stale client tool catalog if it still shows the retired individual tools, or use
106
114
  `arbor attachment upload`. `thread get --view full` retains body/Attachment read-back.
package/dist/arbor.js CHANGED
@@ -2,7 +2,7 @@
2
2
  // package.json
3
3
  var package_default = {
4
4
  name: "@lambdacurry/arbor",
5
- version: "0.24.1",
5
+ version: "0.24.3",
6
6
  description: "The Arbor CLI — a shared workspace for people and agents. The human + headless-agent write path over Arbor's guarded operation surface.",
7
7
  keywords: [
8
8
  "agents",
@@ -5162,6 +5162,7 @@ var ARTIFACT_TYPES = [
5162
5162
  ];
5163
5163
  // ../core/src/blob/attachment-upload.ts
5164
5164
  var MAX_ATTACHMENT_BYTES = 1e8;
5165
+ var ATTACHMENT_ATTACH_NEXT_STEP = "Unbound until you pass attachmentIds (max 10 per contribution) to contribute or respond. The description is permanent (it becomes the Artifact summary): to fix it before binding, re-upload and bind only the new id. Paste attachments[].markdown verbatim; don't rebuild it from the description.";
5165
5166
  var AGENT_ATTACHMENT_MIME_TYPES = [
5166
5167
  "image/png",
5167
5168
  "image/jpeg",
@@ -8235,6 +8236,11 @@ var MAX_WAY_LEN = WAYS_TO_HELP_LIMITS.maxItemLength;
8235
8236
 
8236
8237
  // ../core/src/ops/computer-output.ts
8237
8238
  var COMPUTER_OUTPUT_DEFAULT_STALE_MS = 24 * 60 * 60 * 1000;
8239
+ var OPERATION_LOSS_REASONS = new Set([
8240
+ "process_provider_lost",
8241
+ "workspace_expired",
8242
+ "run_runtime_lost"
8243
+ ]);
8238
8244
 
8239
8245
  // ../core/src/ops/computer-run-retention.ts
8240
8246
  var COMPUTER_RUN_RETENTION_WARNING_MS = 24 * 60 * 60 * 1000;
@@ -10328,8 +10334,19 @@ var MCP_OUTPUT_SCHEMAS = {
10328
10334
  uploadedByProfileId: id,
10329
10335
  createdAt: timestamp,
10330
10336
  attachments: array(attachment2),
10337
+ replayed: boolean2().optional(),
10338
+ nextStep: string2().optional(),
10331
10339
  warnings: array(string2()).optional()
10332
10340
  }),
10341
+ attachment_upload: looseObject({
10342
+ attachmentId: id.optional(),
10343
+ attachmentIds: array(id),
10344
+ attachments: array(attachment2),
10345
+ nextStep: string2().optional(),
10346
+ warnings: array(string2()).optional(),
10347
+ failed: array(looseObject({ file: string2(), error: string2() })).optional(),
10348
+ notAttempted: array(string2()).optional()
10349
+ }),
10333
10350
  computer_export: looseObject({
10334
10351
  computerSessionId: id,
10335
10352
  replayed: boolean2(),
@@ -10421,8 +10438,6 @@ var MCP_OUTPUT_SCHEMAS = {
10421
10438
  })
10422
10439
  };
10423
10440
  function outputSchemaFor(actionName) {
10424
- if (actionName === "attachment_upload")
10425
- actionName = "attachment_attach";
10426
10441
  return MCP_OUTPUT_SCHEMAS[actionName];
10427
10442
  }
10428
10443
 
@@ -11287,6 +11302,17 @@ function proseList(description) {
11287
11302
  function isProseList(schema) {
11288
11303
  return PROSE_LISTS.has(schema);
11289
11304
  }
11305
+ var VERBATIM_LISTS = registry();
11306
+ function verbatimList(description) {
11307
+ const schema = array(string2()).describe(description);
11308
+ PROSE_LISTS.add(schema, { literal: true });
11309
+ VERBATIM_LISTS.add(schema, { verbatim: true });
11310
+ return schema;
11311
+ }
11312
+ function isVerbatimList(schema) {
11313
+ return VERBATIM_LISTS.has(schema);
11314
+ }
11315
+ var ATTACHMENT_DESCRIPTION_HINT = "durable summary and default alt text; defaults to the filename when omitted; permanent once bound; 1-500 chars; give each file its own";
11290
11316
  function forward(operation) {
11291
11317
  const run = (ex, input) => ex.call(operation, input);
11292
11318
  run.operation = operation;
@@ -12314,14 +12340,14 @@ var ACTION_DEFINITIONS = [
12314
12340
  {
12315
12341
  name: "attachment",
12316
12342
  title: "Upload and read Thread Attachments",
12317
- description: "Upload a file (100 MB cap): upload_url → signed PUT (5 min) → attach (1 h after PUT expiry); receipts replay after expiry; JSON syntax checks stop at 10 MB, larger JSON gets UTF-8/control checks only, so validate locally. get/list recover receipts; mediaUrl/downloadUrl require an Arbor credential (401 otherwise), with no public read URLs.",
12343
+ description: "Upload (100 MB): upload_url → signed PUT (5 min) → attach (1 h after PUT expiry); receipts replay after expiry; JSON syntax checked ≤10 MB, larger JSON gets UTF-8/control checks only (validate locally); get/list recover receipts; read URLs need Arbor credentials (401 otherwise), no public URLs. Missing tool: refresh the MCP tool catalog; CLI: arbor attachment upload (Node/Bun).",
12318
12344
  inputSchema: {
12319
12345
  verb: _enum(["upload_url", "attach", "get", "list"]),
12320
12346
  threadId: string2().optional().describe("upload_url/list: Thread id, thr_…"),
12321
12347
  filename: string2().min(1).max(1000).optional().describe("upload_url: original filename"),
12322
12348
  mimeType: _enum(AGENT_ATTACHMENT_MIME_TYPES).optional().describe("upload_url: supported MIME matching the bytes"),
12323
12349
  size: number2().optional().describe("upload_url: exact file size, at most 100000000 bytes; compress, split, or link larger files"),
12324
- description: string2().min(1).max(500).optional(),
12350
+ description: string2().min(1).max(500).optional().describe(`upload_url: ${ATTACHMENT_DESCRIPTION_HINT}`),
12325
12351
  sha256: string2().regex(/^[a-f0-9]{64}$/).optional().describe("upload_url: optional SHA-256 hex; also signed into the PUT checksum header"),
12326
12352
  token: string2().optional().describe("attach: token from upload_url; first attach before attachExpiresAt, replay finalized receipts even after expiry"),
12327
12353
  attachmentId: string2().optional().describe("get: attachment id, att_…"),
@@ -12345,7 +12371,7 @@ var ACTION_DEFINITIONS = [
12345
12371
  filename: string2().min(1).max(1000),
12346
12372
  mimeType: _enum(AGENT_ATTACHMENT_MIME_TYPES),
12347
12373
  size: number2(),
12348
- description: string2().min(1).max(500).optional(),
12374
+ description: string2().min(1).max(500).optional().describe(ATTACHMENT_DESCRIPTION_HINT),
12349
12375
  sha256: string2().regex(/^[a-f0-9]{64}$/).optional()
12350
12376
  },
12351
12377
  surfaces: ["cli"],
@@ -12381,12 +12407,25 @@ var ACTION_DEFINITIONS = [
12381
12407
  },
12382
12408
  {
12383
12409
  name: "attachment_upload",
12384
- title: "Upload a local Thread file",
12385
- description: "Upload with Node and SHA-256 via upload_url → presigned PUT → attach (100 MB cap); compress, split, or link larger files; JSON syntax checks stop at 10 MB, larger JSON gets UTF-8/control checks only. PUT expires in 5 min, first attach gets 1 h more, receipts replay after expiry; js-exec cannot upload; mediaUrl/downloadUrl require an Arbor credential (401 otherwise), no public read URLs; for a missing tool refresh the MCP tool catalog or use arbor attachment upload.",
12410
+ title: "Upload local Thread files",
12411
+ description: "Upload local files with Node or Bun (SHA-256 → presigned PUT → attach, 100 MB cap)",
12412
+ cliHelpLead: "Requires Node or Bun.",
12413
+ cliHelpNotes: [
12414
+ "Upload with SHA-256 via upload_url → presigned PUT → attach (100 MB cap); compress, split, or link larger files.",
12415
+ 'Several files: `arbor attachment upload a.png b.png --thread-id thr_… --description "A" --description "B"`. Repeated --description flags pair with the files by ORDER: pass none (each defaults to its filename) or exactly one per file; a value that starts with `--` must be written --description=<value>. The whole batch is checked before the first request, then uploaded one by one; if one fails the output lists what uploaded and what failed (exit non-zero). --dry-run prints the plan and makes no network call.',
12416
+ `Description: ${ATTACHMENT_DESCRIPTION_HINT}. An upload is unbound until you pass its attachmentIds to contribute or respond (max 10 per contribution); to fix a description before binding, re-upload and bind only the new id. Paste attachments[].markdown verbatim; don't rebuild it from the description.`,
12417
+ "PUT links expire after five minutes; first attach is allowed until one hour after PUT expiry. Finalized receipts replay after expiry.",
12418
+ "JSON syntax checks stop at 10 MB; larger JSON gets UTF-8/control checks only, so validate syntax locally.",
12419
+ "js-exec lacks file descriptors, node:crypto, and binary-safe fetch; run this command with Node or Bun.",
12420
+ "With only curl available, use the MCP attachment tool: upload_url → returned curl PUT command → attach with the returned token.",
12421
+ "mediaUrl/downloadUrl require an Arbor credential (401 otherwise); no public read URLs. The R2 PUT uses only the required signed headers, never an Arbor credential."
12422
+ ].join(`
12423
+ `),
12386
12424
  inputSchema: {
12387
12425
  threadId: string2().min(1),
12388
- file: string2().min(1).describe("local file path (or positional file); requires Node"),
12389
- description: string2().min(1).max(500).optional()
12426
+ file: verbatimList("local file path(s), or positional files; uploaded in order; requires Node or Bun"),
12427
+ description: verbatimList(`${ATTACHMENT_DESCRIPTION_HINT}; repeat --description once per file, paired by order (none = filenames)`).optional(),
12428
+ dryRun: boolean2().optional().describe("print the upload plan (file, mime, size, sha256, description) with no network call")
12390
12429
  },
12391
12430
  surfaces: ["cli"],
12392
12431
  toolset: "loop",
@@ -14667,6 +14706,19 @@ class UsageError extends Error {
14667
14706
  }
14668
14707
  }
14669
14708
 
14709
+ class PartialUploadError extends Error {
14710
+ partialResult;
14711
+ code;
14712
+ details;
14713
+ constructor(message, cause, partialResult) {
14714
+ super(message);
14715
+ this.partialResult = partialResult;
14716
+ this.name = "PartialUploadError";
14717
+ this.code = codeForError(cause);
14718
+ this.details = detailsForError(cause);
14719
+ }
14720
+ }
14721
+
14670
14722
  // src/config.ts
14671
14723
  var PROD_API_URL = "https://arborthreads.com";
14672
14724
  var PROFILE_NAME = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/;
@@ -15041,7 +15093,7 @@ var httpExecutor = {
15041
15093
  };
15042
15094
 
15043
15095
  // src/attachment-upload.ts
15044
- import { basename } from "node:path";
15096
+ import { basename, resolve } from "node:path";
15045
15097
  function checkFile(stat) {
15046
15098
  const isFile = typeof stat.isFile === "function" ? stat.isFile() : stat.isFile;
15047
15099
  if (!isFile)
@@ -15051,8 +15103,7 @@ function checkFile(stat) {
15051
15103
  if (!stat.size)
15052
15104
  throw new UsageError("Attachment file is empty (0 bytes); choose a non-empty file");
15053
15105
  }
15054
- async function uploadLocalAttachment(executor, input, fetcher = fetch) {
15055
- const file = String(input.file);
15106
+ async function preflightFile(file) {
15056
15107
  const filename = basename(file);
15057
15108
  const fs = await import("node:fs");
15058
15109
  const initialStat = fs.statSync(file);
@@ -15060,8 +15111,11 @@ async function uploadLocalAttachment(executor, input, fetcher = fetch) {
15060
15111
  const mimeType = localAttachmentMime(filename);
15061
15112
  if (!mimeType)
15062
15113
  throw new UsageError(`Unsupported file extension for ${initialStat.size} bytes (limit ${MAX_ATTACHMENT_BYTES} bytes). Use PNG/JPEG/GIF/WebP/PDF/TXT/JSON/CSV, or link the file.`);
15063
- if (typeof fs.openSync !== "function")
15064
- throw new UsageError("Attachment upload requires Node: this js-exec runtime cannot PUT binary file bytes. Run `arbor attachment upload <file> --thread-id <thread>` with Node.");
15114
+ if ([fs.openSync, fs.fstatSync, fs.readSync, fs.closeSync].some((api) => typeof api !== "function"))
15115
+ throw new UsageError("Attachment upload requires Node or Bun: js-exec lacks file descriptors, node:crypto, and binary-safe fetch. Run `arbor attachment upload <file> --thread-id <thread>` with Node or Bun, or use MCP attachment upload_url → curl PUT → attach.");
15116
+ return { fs, local: { file, filename, mimeType, size: initialStat.size } };
15117
+ }
15118
+ async function putLocalAttachment(executor, fs, { file, filename, mimeType }, input, fetcher) {
15065
15119
  const descriptor = fs.openSync(file, "r");
15066
15120
  let bytes;
15067
15121
  try {
@@ -15107,6 +15161,135 @@ async function uploadLocalAttachment(executor, input, fetcher = fetch) {
15107
15161
  throw new ApiError(`Attachment PUT failed (${response.status}) for ${bytes.length} bytes (limit ${MAX_ATTACHMENT_BYTES} bytes). Request a fresh upload_url if expired, then retry; compress, split, or link files above the limit.`, response.status);
15108
15162
  return executor.call("attachment.attach", { token: link.token });
15109
15163
  }
15164
+ function normalizeDescription(value) {
15165
+ return value.trim().replace(/\s+/g, " ");
15166
+ }
15167
+ function asList(value) {
15168
+ if (value === undefined)
15169
+ return [];
15170
+ return (Array.isArray(value) ? value : [value]).map(String);
15171
+ }
15172
+ async function planBatch(input) {
15173
+ const files = asList(input.file);
15174
+ const descriptions = asList(input.description);
15175
+ if (files.length === 0)
15176
+ throw new UsageError("missing required flag: --file");
15177
+ if (descriptions.length > 0 && descriptions.length !== files.length) {
15178
+ throw new UsageError(files.length > 1 && descriptions.length === 1 ? `One --description for ${files.length} files would give every file the same permanent description. Pass one --description per file in file order, or none to default each to its filename.` : `Got ${descriptions.length} --description flag${descriptions.length === 1 ? "" : "s"} for ${files.length} file${files.length === 1 ? "" : "s"}; descriptions pair with files by order, so pass none or exactly one per file.`, { reason: "validation.invalid_value", field: "description" });
15179
+ }
15180
+ for (const description of descriptions) {
15181
+ const length = normalizeDescription(description).length;
15182
+ if (length < 1)
15183
+ throw new UsageError("--description is blank; give each file a real description, or omit --description to default it to the filename", { reason: "validation.invalid_value", field: "description" });
15184
+ if (length > 500)
15185
+ throw new UsageError("--description must be 1–500 characters", {
15186
+ reason: "validation.invalid_value",
15187
+ field: "description"
15188
+ });
15189
+ }
15190
+ const paths = new Map;
15191
+ for (const file of files) {
15192
+ if (!file.trim())
15193
+ throw new UsageError("--file is blank; pass a path to a local file");
15194
+ const key = resolve(file);
15195
+ const first = paths.get(key);
15196
+ if (first !== undefined)
15197
+ throw new UsageError(`${file} is listed twice${first === file ? "" : ` (as ${first})`}; each file uploads once, so list it once.`, { reason: "validation.invalid_value", field: "file" });
15198
+ paths.set(key, file);
15199
+ }
15200
+ let fs;
15201
+ const plan = [];
15202
+ for (const [index, file] of files.entries()) {
15203
+ const prepared = await preflightFile(file);
15204
+ fs = prepared.fs;
15205
+ plan.push({ local: prepared.local, description: descriptions[index] });
15206
+ }
15207
+ const seen = new Map;
15208
+ for (const { local, description } of plan) {
15209
+ if (description === undefined)
15210
+ continue;
15211
+ const key = normalizeDescription(description);
15212
+ const first = seen.get(key);
15213
+ if (first !== undefined)
15214
+ throw new UsageError(`Duplicate --description "${key}" for ${first} and ${local.file}; descriptions are permanent, so give each file its own.`, { reason: "validation.invalid_value", field: "description" });
15215
+ seen.set(key, local.file);
15216
+ }
15217
+ return { fs, plan };
15218
+ }
15219
+ function sha256File(fs, file, createHash) {
15220
+ const hash = createHash("sha256");
15221
+ const chunk = Buffer.alloc(1048576);
15222
+ const descriptor = fs.openSync(file, "r");
15223
+ try {
15224
+ for (;; ) {
15225
+ const count = fs.readSync(descriptor, chunk, 0, chunk.length, null);
15226
+ if (!count)
15227
+ break;
15228
+ hash.update(chunk.subarray(0, count));
15229
+ }
15230
+ } finally {
15231
+ fs.closeSync(descriptor);
15232
+ }
15233
+ return hash.digest("hex");
15234
+ }
15235
+ async function dryRunPlan(fs, threadId, plan) {
15236
+ const { createHash } = await import("node:crypto");
15237
+ return {
15238
+ dryRun: true,
15239
+ threadId,
15240
+ files: plan.map(({ local, description }) => ({
15241
+ file: local.file,
15242
+ filename: local.filename,
15243
+ mimeType: local.mimeType,
15244
+ size: local.size,
15245
+ sha256: sha256File(fs, local.file, createHash),
15246
+ description: description ?? local.filename,
15247
+ descriptionSource: description === undefined ? "filename default" : "explicit",
15248
+ status: "unbound until attached"
15249
+ }))
15250
+ };
15251
+ }
15252
+ function record3(value) {
15253
+ return value !== null && typeof value === "object" ? value : {};
15254
+ }
15255
+ function aggregateReceipt(uploaded, extra = {}) {
15256
+ const warnings = uploaded.flatMap(({ local, receipt }) => asList(record3(receipt).warnings).map((warning) => `${local.filename}: ${warning}`));
15257
+ return {
15258
+ attachmentIds: uploaded.flatMap(({ receipt }) => {
15259
+ const r = record3(receipt);
15260
+ return Array.isArray(r.attachmentIds) ? r.attachmentIds : typeof r.attachmentId === "string" ? [r.attachmentId] : [];
15261
+ }),
15262
+ attachments: uploaded.flatMap(({ receipt }) => {
15263
+ const attachments = record3(receipt).attachments;
15264
+ return Array.isArray(attachments) ? attachments : [];
15265
+ }),
15266
+ nextStep: ATTACHMENT_ATTACH_NEXT_STEP,
15267
+ ...warnings.length > 0 ? { warnings } : {},
15268
+ ...extra
15269
+ };
15270
+ }
15271
+ async function uploadLocalAttachments(executor, input, fetcher = fetch) {
15272
+ const { fs, plan } = await planBatch(input);
15273
+ if (input.dryRun === true)
15274
+ return dryRunPlan(fs, String(input.threadId), plan);
15275
+ const upload = ({ local, description }) => putLocalAttachment(executor, fs, local, { threadId: input.threadId, ...description !== undefined ? { description } : {} }, fetcher);
15276
+ if (plan.length === 1)
15277
+ return upload(plan[0]);
15278
+ const uploaded = [];
15279
+ for (const [index, item] of plan.entries()) {
15280
+ try {
15281
+ uploaded.push({ local: item.local, receipt: await upload(item) });
15282
+ } catch (cause) {
15283
+ const reason = cause instanceof Error ? cause.message : String(cause);
15284
+ const notAttempted = plan.slice(index + 1).map(({ local }) => local.file);
15285
+ throw new PartialUploadError(`Uploaded ${uploaded.length} of ${plan.length} files; ${item.local.file} failed: ${reason.replace(/\.$/, "")}. Uploaded files stay valid but unbound; re-run only the failed and not-attempted files.`, cause, aggregateReceipt(uploaded, {
15286
+ failed: [{ file: item.local.file, error: reason }],
15287
+ notAttempted
15288
+ }));
15289
+ }
15290
+ }
15291
+ return aggregateReceipt(uploaded);
15292
+ }
15110
15293
 
15111
15294
  // src/output.ts
15112
15295
  function writeOutput(value, stream, fallback) {
@@ -15198,15 +15381,20 @@ function emitData(data, _action, ctx) {
15198
15381
  function emitError(err, _action, ctx) {
15199
15382
  const code = codeForError(err);
15200
15383
  const message = err instanceof Error ? err.message : String(err);
15384
+ const partial = err instanceof PartialUploadError ? err.partialResult : undefined;
15201
15385
  if (ctx.json) {
15202
15386
  const envelope = {
15203
15387
  ok: false,
15204
- error: { code, message, ...detailsForError(err) }
15388
+ error: { code, message, ...detailsForError(err) },
15389
+ ...partial !== undefined ? { data: partial } : {}
15205
15390
  };
15206
15391
  writeStdout(`${JSON.stringify(envelope)}
15207
15392
  `);
15208
15393
  process.exitCode = exitCodeForError(code);
15209
15394
  } else {
15395
+ if (partial !== undefined)
15396
+ writeStdout(`${JSON.stringify(partial, null, 2)}
15397
+ `);
15210
15398
  writeStderr(`✗ ${message}
15211
15399
  `);
15212
15400
  process.exitCode = 1;
@@ -15228,7 +15416,7 @@ var DEFAULT_PROCESS_DEADLINE_MS = 1800000;
15228
15416
  var PROCESS_OUTPUT_PAGE_BYTES = 4096;
15229
15417
  var POLL_MS = 400;
15230
15418
  var processRead = ACTIONS3.find((action) => action.name === "computer_process_read");
15231
- function record3(value) {
15419
+ function record4(value) {
15232
15420
  return value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
15233
15421
  }
15234
15422
  function isRunning(value) {
@@ -15256,7 +15444,7 @@ function sleep(ms) {
15256
15444
  return new Promise((resolve) => setTimeout(resolve, ms));
15257
15445
  }
15258
15446
  function isComputerExecHandle(value) {
15259
- const rec = record3(value);
15447
+ const rec = record4(value);
15260
15448
  return Boolean(rec && typeof rec.processId === "string" && (isRunning(rec) || rec.truncated === true));
15261
15449
  }
15262
15450
  async function waitForComputerExec(executor, input, value, ctx) {
@@ -15271,7 +15459,7 @@ async function waitForComputerExec(executor, input, value, ctx) {
15271
15459
  `, ctx);
15272
15460
  while (isRunning(latest) && Date.now() < deadline) {
15273
15461
  await sleep(POLL_MS);
15274
- const read = record3(await processRead.run(executor, {
15462
+ const read = record4(await processRead.run(executor, {
15275
15463
  processId: latest.processId,
15276
15464
  maxBytes: PROCESS_OUTPUT_PAGE_BYTES,
15277
15465
  ...cursor ? { cursor } : {},
@@ -15345,6 +15533,7 @@ function kindOf(node) {
15345
15533
  return "string";
15346
15534
  }
15347
15535
  }
15536
+ var BARE_VALUE_HINT = (flag) => ` (a value that starts with "--" must be written --${flag}=<value>)`;
15348
15537
  function acceptedFlags(inputSchema) {
15349
15538
  const out = new Set;
15350
15539
  for (const spec of flagsForSchema(inputSchema)) {
@@ -15377,7 +15566,8 @@ function flagsForSchema(inputSchema) {
15377
15566
  required: !optional,
15378
15567
  nullable,
15379
15568
  description: description(inner) ?? description(raw),
15380
- literal: isProseList(inner)
15569
+ literal: isProseList(inner),
15570
+ verbatim: isVerbatimList(inner)
15381
15571
  };
15382
15572
  });
15383
15573
  }
@@ -15418,7 +15608,7 @@ function buildInput(inputSchema, flags, command) {
15418
15608
  continue;
15419
15609
  }
15420
15610
  if (spec.kind !== "boolean" && raw === true) {
15421
- throw new UsageError(`--${spec.flag} expects a value`, {
15611
+ throw new UsageError(`--${spec.flag} expects a value${BARE_VALUE_HINT(spec.flag)}`, {
15422
15612
  reason: "validation.missing_field",
15423
15613
  field: spec.field
15424
15614
  });
@@ -15441,6 +15631,10 @@ function buildInput(inputSchema, flags, command) {
15441
15631
  case "array": {
15442
15632
  const repeated = Array.isArray(raw);
15443
15633
  const tokens = repeated ? raw : [String(raw)];
15634
+ if (spec.verbatim) {
15635
+ input[spec.field] = tokens.map(String);
15636
+ break;
15637
+ }
15444
15638
  const out = [];
15445
15639
  for (const token of tokens) {
15446
15640
  const text = String(token).trim();
@@ -15696,6 +15890,7 @@ var POSITIONAL_FIELDS = {
15696
15890
  code_implementations: ["namePath", "path"],
15697
15891
  code_diagnostics: ["path"]
15698
15892
  };
15893
+ var VARIADIC_LAST_POSITIONAL = new Set(["attachment_upload"]);
15699
15894
  function positionalSpecs(action) {
15700
15895
  const fields = POSITIONAL_FIELDS[action.name];
15701
15896
  if (!fields)
@@ -15783,7 +15978,11 @@ function toolNameAliases(action) {
15783
15978
  function helpLine(action) {
15784
15979
  const positionals = positionalSpecs(action);
15785
15980
  const positionalFlags = new Set(positionals.map((spec) => spec.flag));
15786
- const args = positionals.map((spec) => spec.required ? ` <${spec.field}>` : ` [<${spec.field}>]`).join("");
15981
+ const args = positionals.map((spec, index) => {
15982
+ const variadic = VARIADIC_LAST_POSITIONAL.has(action.name) && index === positionals.length - 1;
15983
+ const name = `${spec.field}${variadic ? ">..." : ">"}`;
15984
+ return spec.required ? ` <${name}` : ` [<${name}]`;
15985
+ }).join("");
15787
15986
  const flags = flagsForSchema(action.inputSchema).filter((spec) => !positionalFlags.has(spec.flag)).map((f) => {
15788
15987
  const token = f.kind === "boolean" ? `--${f.flag}` : `--${f.flag} <>`;
15789
15988
  return f.required ? token : `[${token}]`;
@@ -15809,7 +16008,11 @@ function commandHelp(positionals) {
15809
16008
  const token = f.kind === "boolean" ? `--${f.flag}` : `--${f.flag} <${f.kind}>`;
15810
16009
  return f.required ? token : `[${token}]`;
15811
16010
  }).join(" ");
15812
- const usageArgs = helpPositionals.map((spec) => spec.required ? ` <${spec.field}>` : ` [<${spec.field}>]`).join("");
16011
+ const usageArgs = helpPositionals.map((spec, index) => {
16012
+ const variadic = VARIADIC_LAST_POSITIONAL.has(action.name) && index === helpPositionals.length - 1;
16013
+ const name = `${spec.field}${variadic ? ">..." : ">"}`;
16014
+ return spec.required ? ` <${name}` : ` [<${name}]`;
16015
+ }).join("");
15813
16016
  const usage = `Usage: arbor ${commandWords(action)}${usageArgs}${usageFlags ? ` ${usageFlags}` : ""}`;
15814
16017
  const lines = specs.map((f) => {
15815
16018
  const positional = helpPositionals.find((spec) => spec.flag === f.flag);
@@ -15838,12 +16041,24 @@ Aliases: ${aliases.map((alias) => `\`arbor ${alias}\``).join(", ")} (tool-name f
15838
16041
  const waitLines = action.name === "computer_exec" ? [
15839
16042
  " --no-wait [<boolean>] [optional] — return a running processId immediately instead of polling computer_process_read (implied by --background); mutually exclusive with --wait-for-terminal-ms"
15840
16043
  ] : [];
16044
+ const notes = action.cliHelpNotes ? `
16045
+
16046
+ ${action.cliHelpNotes}` : "";
16047
+ if (action.cliHelpLead) {
16048
+ return `${usage}
16049
+
16050
+ ${action.cliHelpLead}
16051
+
16052
+ ${lines.join(`
16053
+ `)}${primaryNote}${explicitNote}${aliasNote}${notes}
16054
+ `;
16055
+ }
15841
16056
  return `${heading} — ${action.description}
15842
16057
 
15843
16058
  ${usage}${waitLines.length > 0 ? " [--no-wait]" : ""}
15844
16059
 
15845
16060
  ${[...lines, ...waitLines].join(`
15846
- `)}${primaryNote}${explicitNote}${aliasNote}
16061
+ `)}${primaryNote}${explicitNote}${aliasNote}${notes}
15847
16062
  `;
15848
16063
  }
15849
16064
  function commandCatalog() {
@@ -15879,10 +16094,12 @@ function applyPositionals(action, extras, flags) {
15879
16094
  if (positionalTargets.length === 0) {
15880
16095
  throw new UsageError(`unexpected argument: ${extras[0]} (\`${commandWords(action)}\` takes no positional — use flags)`);
15881
16096
  }
15882
- if (extras.length > positionalTargets.length) {
16097
+ const fixed = positionalTargets.length - 1;
16098
+ const values = VARIADIC_LAST_POSITIONAL.has(action.name) && extras.length > fixed ? [...extras.slice(0, fixed), extras.slice(fixed)] : extras;
16099
+ if (values.length > positionalTargets.length) {
15883
16100
  throw new UsageError(`too many arguments: \`${commandWords(action)}\` takes ${positionalTargets.length === 1 ? "one positional" : `${positionalTargets.length} positionals`}; pass the rest as flags`);
15884
16101
  }
15885
- for (const [index, value] of extras.entries()) {
16102
+ for (const [index, value] of values.entries()) {
15886
16103
  const target = positionalTargets[index];
15887
16104
  if (flags[target.flag] !== undefined || flags[`${target.flag}-file`] !== undefined) {
15888
16105
  throw new UsageError(`pass --${target.flag} either as a flag or as the positional argument, not both`);
@@ -15920,8 +16137,16 @@ async function runObjectVerb(positionals, flags, ctx) {
15920
16137
  const unknown = Object.keys(flags).filter((f) => !accepted.has(f) && !RESERVED_FLAGS.has(f));
15921
16138
  if (unknown.length > 0) {
15922
16139
  const hints = unknown.map((f) => listFileHint(action, f)).filter(Boolean);
16140
+ const bare = flagsForSchema(action.inputSchema).find((spec) => spec.kind !== "boolean" && flags[spec.flag] === true);
16141
+ if (bare)
16142
+ hints.push(`--${bare.flag} has no value${BARE_VALUE_HINT(bare.flag)}`);
15923
16143
  throw new UsageError(`unknown flag${unknown.length > 1 ? "s" : ""}: ${unknown.map((f) => `--${f}`).join(", ")}${hints.length > 0 ? ` — ${hints.join("; ")}` : ""}`);
15924
16144
  }
16145
+ if (action.name === "attachment_upload") {
16146
+ const dryRun = lastValue(flags["dry-run"]);
16147
+ if (typeof dryRun === "string" && dryRun !== "true" && dryRun !== "false")
16148
+ throw new UsageError(`--dry-run takes no value but swallowed "${dryRun}"; put --dry-run after the files or write --dry-run=true`, { reason: "validation.invalid_value", field: "dryRun" });
16149
+ }
15925
16150
  const inputFlags = { ...flags };
15926
16151
  if (action.name === "app_request" && lastValue(inputFlags.json) === true) {
15927
16152
  delete inputFlags.json;
@@ -15958,7 +16183,7 @@ async function runObjectVerb(positionals, flags, ctx) {
15958
16183
  advise(`Agent registered. Save its apiKey now — it is shown only once:
15959
16184
  `, ctx);
15960
16185
  }
15961
- let value = action.name === "attachment_upload" ? await uploadLocalAttachment(httpExecutor, input) : await action.run(httpExecutor, input);
16186
+ let value = action.name === "attachment_upload" ? await uploadLocalAttachments(httpExecutor, input) : await action.run(httpExecutor, input);
15962
16187
  if (action.name === "computer_exec" && !truthyFlag(flags["no-wait"]) && input.waitForTerminalMs === undefined && input.background !== true) {
15963
16188
  value = await waitForComputerExec(httpExecutor, input, value, ctx);
15964
16189
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lambdacurry/arbor",
3
- "version": "0.24.1",
3
+ "version": "0.24.3",
4
4
  "description": "The Arbor CLI — a shared workspace for people and agents. The human + headless-agent write path over Arbor's guarded operation surface.",
5
5
  "keywords": [
6
6
  "agents",