@lambdacurry/arbor 0.24.2 → 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 +226 -23
  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.2",
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;
@@ -12321,7 +12347,7 @@ var ACTION_DEFINITIONS = [
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,11 +12407,13 @@ var ACTION_DEFINITIONS = [
12381
12407
  },
12382
12408
  {
12383
12409
  name: "attachment_upload",
12384
- title: "Upload a local Thread file",
12385
- description: "Upload a local file with Node or Bun (SHA-256 → presigned PUT → attach, 100 MB cap)",
12410
+ title: "Upload local Thread files",
12411
+ description: "Upload local files with Node or Bun (SHA-256 → presigned PUT → attach, 100 MB cap)",
12386
12412
  cliHelpLead: "Requires Node or Bun.",
12387
12413
  cliHelpNotes: [
12388
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.`,
12389
12417
  "PUT links expire after five minutes; first attach is allowed until one hour after PUT expiry. Finalized receipts replay after expiry.",
12390
12418
  "JSON syntax checks stop at 10 MB; larger JSON gets UTF-8/control checks only, so validate syntax locally.",
12391
12419
  "js-exec lacks file descriptors, node:crypto, and binary-safe fetch; run this command with Node or Bun.",
@@ -12395,8 +12423,9 @@ var ACTION_DEFINITIONS = [
12395
12423
  `),
12396
12424
  inputSchema: {
12397
12425
  threadId: string2().min(1),
12398
- file: string2().min(1).describe("local file path (or positional file); requires Node or Bun"),
12399
- 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")
12400
12429
  },
12401
12430
  surfaces: ["cli"],
12402
12431
  toolset: "loop",
@@ -14677,6 +14706,19 @@ class UsageError extends Error {
14677
14706
  }
14678
14707
  }
14679
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
+
14680
14722
  // src/config.ts
14681
14723
  var PROD_API_URL = "https://arborthreads.com";
14682
14724
  var PROFILE_NAME = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/;
@@ -15051,7 +15093,7 @@ var httpExecutor = {
15051
15093
  };
15052
15094
 
15053
15095
  // src/attachment-upload.ts
15054
- import { basename } from "node:path";
15096
+ import { basename, resolve } from "node:path";
15055
15097
  function checkFile(stat) {
15056
15098
  const isFile = typeof stat.isFile === "function" ? stat.isFile() : stat.isFile;
15057
15099
  if (!isFile)
@@ -15061,8 +15103,7 @@ function checkFile(stat) {
15061
15103
  if (!stat.size)
15062
15104
  throw new UsageError("Attachment file is empty (0 bytes); choose a non-empty file");
15063
15105
  }
15064
- async function uploadLocalAttachment(executor, input, fetcher = fetch) {
15065
- const file = String(input.file);
15106
+ async function preflightFile(file) {
15066
15107
  const filename = basename(file);
15067
15108
  const fs = await import("node:fs");
15068
15109
  const initialStat = fs.statSync(file);
@@ -15072,6 +15113,9 @@ async function uploadLocalAttachment(executor, input, fetcher = fetch) {
15072
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.`);
15073
15114
  if ([fs.openSync, fs.fstatSync, fs.readSync, fs.closeSync].some((api) => typeof api !== "function"))
15074
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) {
15075
15119
  const descriptor = fs.openSync(file, "r");
15076
15120
  let bytes;
15077
15121
  try {
@@ -15117,6 +15161,135 @@ async function uploadLocalAttachment(executor, input, fetcher = fetch) {
15117
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);
15118
15162
  return executor.call("attachment.attach", { token: link.token });
15119
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
+ }
15120
15293
 
15121
15294
  // src/output.ts
15122
15295
  function writeOutput(value, stream, fallback) {
@@ -15208,15 +15381,20 @@ function emitData(data, _action, ctx) {
15208
15381
  function emitError(err, _action, ctx) {
15209
15382
  const code = codeForError(err);
15210
15383
  const message = err instanceof Error ? err.message : String(err);
15384
+ const partial = err instanceof PartialUploadError ? err.partialResult : undefined;
15211
15385
  if (ctx.json) {
15212
15386
  const envelope = {
15213
15387
  ok: false,
15214
- error: { code, message, ...detailsForError(err) }
15388
+ error: { code, message, ...detailsForError(err) },
15389
+ ...partial !== undefined ? { data: partial } : {}
15215
15390
  };
15216
15391
  writeStdout(`${JSON.stringify(envelope)}
15217
15392
  `);
15218
15393
  process.exitCode = exitCodeForError(code);
15219
15394
  } else {
15395
+ if (partial !== undefined)
15396
+ writeStdout(`${JSON.stringify(partial, null, 2)}
15397
+ `);
15220
15398
  writeStderr(`✗ ${message}
15221
15399
  `);
15222
15400
  process.exitCode = 1;
@@ -15238,7 +15416,7 @@ var DEFAULT_PROCESS_DEADLINE_MS = 1800000;
15238
15416
  var PROCESS_OUTPUT_PAGE_BYTES = 4096;
15239
15417
  var POLL_MS = 400;
15240
15418
  var processRead = ACTIONS3.find((action) => action.name === "computer_process_read");
15241
- function record3(value) {
15419
+ function record4(value) {
15242
15420
  return value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
15243
15421
  }
15244
15422
  function isRunning(value) {
@@ -15266,7 +15444,7 @@ function sleep(ms) {
15266
15444
  return new Promise((resolve) => setTimeout(resolve, ms));
15267
15445
  }
15268
15446
  function isComputerExecHandle(value) {
15269
- const rec = record3(value);
15447
+ const rec = record4(value);
15270
15448
  return Boolean(rec && typeof rec.processId === "string" && (isRunning(rec) || rec.truncated === true));
15271
15449
  }
15272
15450
  async function waitForComputerExec(executor, input, value, ctx) {
@@ -15281,7 +15459,7 @@ async function waitForComputerExec(executor, input, value, ctx) {
15281
15459
  `, ctx);
15282
15460
  while (isRunning(latest) && Date.now() < deadline) {
15283
15461
  await sleep(POLL_MS);
15284
- const read = record3(await processRead.run(executor, {
15462
+ const read = record4(await processRead.run(executor, {
15285
15463
  processId: latest.processId,
15286
15464
  maxBytes: PROCESS_OUTPUT_PAGE_BYTES,
15287
15465
  ...cursor ? { cursor } : {},
@@ -15355,6 +15533,7 @@ function kindOf(node) {
15355
15533
  return "string";
15356
15534
  }
15357
15535
  }
15536
+ var BARE_VALUE_HINT = (flag) => ` (a value that starts with "--" must be written --${flag}=<value>)`;
15358
15537
  function acceptedFlags(inputSchema) {
15359
15538
  const out = new Set;
15360
15539
  for (const spec of flagsForSchema(inputSchema)) {
@@ -15387,7 +15566,8 @@ function flagsForSchema(inputSchema) {
15387
15566
  required: !optional,
15388
15567
  nullable,
15389
15568
  description: description(inner) ?? description(raw),
15390
- literal: isProseList(inner)
15569
+ literal: isProseList(inner),
15570
+ verbatim: isVerbatimList(inner)
15391
15571
  };
15392
15572
  });
15393
15573
  }
@@ -15428,7 +15608,7 @@ function buildInput(inputSchema, flags, command) {
15428
15608
  continue;
15429
15609
  }
15430
15610
  if (spec.kind !== "boolean" && raw === true) {
15431
- throw new UsageError(`--${spec.flag} expects a value`, {
15611
+ throw new UsageError(`--${spec.flag} expects a value${BARE_VALUE_HINT(spec.flag)}`, {
15432
15612
  reason: "validation.missing_field",
15433
15613
  field: spec.field
15434
15614
  });
@@ -15451,6 +15631,10 @@ function buildInput(inputSchema, flags, command) {
15451
15631
  case "array": {
15452
15632
  const repeated = Array.isArray(raw);
15453
15633
  const tokens = repeated ? raw : [String(raw)];
15634
+ if (spec.verbatim) {
15635
+ input[spec.field] = tokens.map(String);
15636
+ break;
15637
+ }
15454
15638
  const out = [];
15455
15639
  for (const token of tokens) {
15456
15640
  const text = String(token).trim();
@@ -15706,6 +15890,7 @@ var POSITIONAL_FIELDS = {
15706
15890
  code_implementations: ["namePath", "path"],
15707
15891
  code_diagnostics: ["path"]
15708
15892
  };
15893
+ var VARIADIC_LAST_POSITIONAL = new Set(["attachment_upload"]);
15709
15894
  function positionalSpecs(action) {
15710
15895
  const fields = POSITIONAL_FIELDS[action.name];
15711
15896
  if (!fields)
@@ -15793,7 +15978,11 @@ function toolNameAliases(action) {
15793
15978
  function helpLine(action) {
15794
15979
  const positionals = positionalSpecs(action);
15795
15980
  const positionalFlags = new Set(positionals.map((spec) => spec.flag));
15796
- 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("");
15797
15986
  const flags = flagsForSchema(action.inputSchema).filter((spec) => !positionalFlags.has(spec.flag)).map((f) => {
15798
15987
  const token = f.kind === "boolean" ? `--${f.flag}` : `--${f.flag} <>`;
15799
15988
  return f.required ? token : `[${token}]`;
@@ -15819,7 +16008,11 @@ function commandHelp(positionals) {
15819
16008
  const token = f.kind === "boolean" ? `--${f.flag}` : `--${f.flag} <${f.kind}>`;
15820
16009
  return f.required ? token : `[${token}]`;
15821
16010
  }).join(" ");
15822
- 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("");
15823
16016
  const usage = `Usage: arbor ${commandWords(action)}${usageArgs}${usageFlags ? ` ${usageFlags}` : ""}`;
15824
16017
  const lines = specs.map((f) => {
15825
16018
  const positional = helpPositionals.find((spec) => spec.flag === f.flag);
@@ -15901,10 +16094,12 @@ function applyPositionals(action, extras, flags) {
15901
16094
  if (positionalTargets.length === 0) {
15902
16095
  throw new UsageError(`unexpected argument: ${extras[0]} (\`${commandWords(action)}\` takes no positional — use flags)`);
15903
16096
  }
15904
- 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) {
15905
16100
  throw new UsageError(`too many arguments: \`${commandWords(action)}\` takes ${positionalTargets.length === 1 ? "one positional" : `${positionalTargets.length} positionals`}; pass the rest as flags`);
15906
16101
  }
15907
- for (const [index, value] of extras.entries()) {
16102
+ for (const [index, value] of values.entries()) {
15908
16103
  const target = positionalTargets[index];
15909
16104
  if (flags[target.flag] !== undefined || flags[`${target.flag}-file`] !== undefined) {
15910
16105
  throw new UsageError(`pass --${target.flag} either as a flag or as the positional argument, not both`);
@@ -15942,8 +16137,16 @@ async function runObjectVerb(positionals, flags, ctx) {
15942
16137
  const unknown = Object.keys(flags).filter((f) => !accepted.has(f) && !RESERVED_FLAGS.has(f));
15943
16138
  if (unknown.length > 0) {
15944
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)}`);
15945
16143
  throw new UsageError(`unknown flag${unknown.length > 1 ? "s" : ""}: ${unknown.map((f) => `--${f}`).join(", ")}${hints.length > 0 ? ` — ${hints.join("; ")}` : ""}`);
15946
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
+ }
15947
16150
  const inputFlags = { ...flags };
15948
16151
  if (action.name === "app_request" && lastValue(inputFlags.json) === true) {
15949
16152
  delete inputFlags.json;
@@ -15980,7 +16183,7 @@ async function runObjectVerb(positionals, flags, ctx) {
15980
16183
  advise(`Agent registered. Save its apiKey now — it is shown only once:
15981
16184
  `, ctx);
15982
16185
  }
15983
- 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);
15984
16187
  if (action.name === "computer_exec" && !truthyFlag(flags["no-wait"]) && input.waitForTerminalMs === undefined && input.background !== true) {
15985
16188
  value = await waitForComputerExec(httpExecutor, input, value, ctx);
15986
16189
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lambdacurry/arbor",
3
- "version": "0.24.2",
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",