@lambdacurry/arbor 0.23.41 → 0.24.1

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 +29 -11
  2. package/dist/arbor.js +179 -59
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -76,19 +76,37 @@ This prints a URL + short code to relay to the human, who approves it in their l
76
76
  ## Attach a local file
77
77
 
78
78
  ```bash
79
- arbor attachment upload --thread-id thr_… --file ./screenshot.png --description "Settings dialog after save"
80
- # Alias: arbor attachment_upload --thread-id thr_… --file ./screenshot.png
79
+ arbor attachment upload ./screenshot.png --thread-id thr_… --description "Settings dialog after save"
80
+ # Alias: arbor attachment_upload --file ./screenshot.png --thread-id thr_…
81
+ arbor attachment get --attachment-id att_…
82
+ arbor attachment list --thread-id thr_…
81
83
  ```
82
84
 
83
- No Arbor Computer Run is needed. Files are capped at **5 MiB**; supported extensions are
84
- PNG, JPG/JPEG, GIF, WebP, PDF, TXT, JSON, and CSV. MIME is inferred from the extension and
85
- validated against bytes on the server. Description defaults to the sanitized filename.
86
-
87
- The result contains `attachmentId`, `attachmentIds`, `downloadUrl`, uploader/time, and
88
- `attachments[].markdown`. Put that Markdown in `contribute` or `respond` body and pass
89
- `--attachment-ids att_…` to bind the file. Downloads require your own Arbor credential
90
- and current access to the destination Thread's Space. See the
91
- [attachment upload runbook](../../docs/runbooks/attachment-upload.md) for MCP and RPC input.
85
+ Requires **Node**: js-exec coerces binary fetch bodies to strings, so upload fails early
86
+ there with a Node requirement; metadata/get/list/upload-url/attach commands remain portable.
87
+ The CLI computes SHA-256, requests a five-minute `upload_url`, and PUTs the file directly
88
+ to R2 with the exact signed headers and no Arbor Authorization header, then calls `attach` with its token.
89
+ Files must be non-empty and at most **100 MB (100,000,000 bytes)**; compress, split, or
90
+ link larger files. PNG/JPEG/GIF/WebP/PDF/TXT/JSON/CSV are supported. There is no inline
91
+ base64 upload. First attach is allowed until `attachExpiresAt` (one hour after PUT expiry);
92
+ finalized receipts remain replayable after expiry. If a PUT or first attach expires, request
93
+ a fresh upload URL and upload again. JSON syntax is checked only up to 10 MB (10,000,000 bytes);
94
+ larger JSON receives UTF-8/control-byte checks only, so validate its syntax locally.
95
+
96
+ The receipt contains uploader/time, SHA-256, `attachmentId`, `attachmentIds`, and
97
+ `attachments[].markdown`. Bind the ids and place the Markdown in `contribute`/`respond`.
98
+ `mediaUrl` and `downloadUrl` require an Arbor credential (401 otherwise); read URLs are
99
+ private, never signed/public. `attachment get` (aliases `attachment_get`, `get_attachment`)
100
+ and `attachment list` (`attachment_list`, `list_attachments`) recover receipts; the latter
101
+ pages with `--cursor`. Finalized receipts replay with the same authentic token even after expiry.
102
+ Duplicate SHA-256 warnings on the same Thread are best-effort; concurrent uploads may not warn.
103
+
104
+ MCP exposes **one** tool, `attachment`, with `upload_url` / `attach` / `get` / `list` verbs.
105
+ Refresh a stale client tool catalog if it still shows the retired individual tools, or use
106
+ `arbor attachment upload`. `thread get --view full` retains body/Attachment read-back.
107
+ Prefer `contribute --idempotency-key` for retries, then `mark_as_response` to bind an
108
+ existing contribution as a response; direct `respond` has no idempotency-key input yet.
109
+ See the [operator runbook](../../docs/runbooks/attachment-upload.md) for required configuration.
92
110
 
93
111
  ## Output contract (agent-friendly)
94
112
 
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.23.41",
5
+ version: "0.24.1",
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",
@@ -5161,8 +5161,7 @@ var ARTIFACT_TYPES = [
5161
5161
  "external-reference"
5162
5162
  ];
5163
5163
  // ../core/src/blob/attachment-upload.ts
5164
- var MAX_AGENT_ATTACHMENT_BYTES = 5 * 1024 * 1024;
5165
- var MAX_AGENT_ATTACHMENT_BASE64 = 4 * Math.ceil(MAX_AGENT_ATTACHMENT_BYTES / 3);
5164
+ var MAX_ATTACHMENT_BYTES = 1e8;
5166
5165
  var AGENT_ATTACHMENT_MIME_TYPES = [
5167
5166
  "image/png",
5168
5167
  "image/jpeg",
@@ -8604,6 +8603,7 @@ var attachment2 = looseObject({
8604
8603
  size: number2(),
8605
8604
  sha256: string2(),
8606
8605
  downloadUrl: string2(),
8606
+ mediaUrl: string2().optional(),
8607
8607
  markdown: string2()
8608
8608
  });
8609
8609
  var checkpoint = looseObject({
@@ -10277,13 +10277,58 @@ var MCP_OUTPUT_SCHEMAS = {
10277
10277
  sha256: string2()
10278
10278
  })
10279
10279
  }),
10280
- upload_attachment: looseObject({
10280
+ get_attachment: looseObject({
10281
+ ...attachment2.shape,
10282
+ sha256: nullableString,
10283
+ uploadedByProfileId: id,
10284
+ createdAt: timestamp
10285
+ }),
10286
+ list_attachments: looseObject({
10287
+ items: array(looseObject({
10288
+ ...attachment2.shape,
10289
+ sha256: nullableString,
10290
+ uploadedByProfileId: id,
10291
+ createdAt: timestamp
10292
+ })),
10293
+ nextCursor: nullableString,
10294
+ total: number2()
10295
+ }),
10296
+ attachment: looseObject({
10297
+ url: string2().optional(),
10298
+ method: literal("PUT").optional(),
10299
+ headers: record(string2(), string2()).optional(),
10300
+ token: string2().optional(),
10301
+ expiresAt: timestamp.optional(),
10302
+ attachExpiresAt: timestamp.optional(),
10303
+ maxBytes: number2().optional(),
10304
+ curl: string2().optional(),
10305
+ nextStep: string2().optional(),
10306
+ attachmentId: id.optional(),
10307
+ attachmentIds: array(id).optional(),
10308
+ attachments: array(attachment2).optional(),
10309
+ filename: string2().optional(),
10310
+ mimeType: string2().optional(),
10311
+ size: number2().optional(),
10312
+ sha256: nullableString.optional(),
10313
+ markdown: string2().optional(),
10314
+ downloadUrl: string2().optional(),
10315
+ mediaUrl: string2().optional(),
10316
+ uploadedByProfileId: id.optional(),
10317
+ createdAt: timestamp.optional(),
10318
+ items: array(jsonObject).optional(),
10319
+ nextCursor: nullableString.optional(),
10320
+ total: number2().optional(),
10321
+ warnings: array(string2()).optional(),
10322
+ replayed: boolean2().optional()
10323
+ }),
10324
+ attachment_attach: looseObject({
10281
10325
  attachmentId: id,
10282
10326
  attachmentIds: array(id),
10283
10327
  downloadUrl: string2(),
10284
10328
  uploadedByProfileId: id,
10285
10329
  createdAt: timestamp,
10286
- attachments: array(attachment2)
10330
+ attachments: array(attachment2),
10331
+ warnings: array(string2()).optional()
10287
10332
  }),
10288
10333
  computer_export: looseObject({
10289
10334
  computerSessionId: id,
@@ -10377,7 +10422,7 @@ var MCP_OUTPUT_SCHEMAS = {
10377
10422
  };
10378
10423
  function outputSchemaFor(actionName) {
10379
10424
  if (actionName === "attachment_upload")
10380
- actionName = "upload_attachment";
10425
+ actionName = "attachment_attach";
10381
10426
  return MCP_OUTPUT_SCHEMAS[actionName];
10382
10427
  }
10383
10428
 
@@ -10744,7 +10789,7 @@ var MCP_TOOL_ANNOTATIONS = {
10744
10789
  admin_feedback_edit: destructive,
10745
10790
  admin_feedback_curate: destructive,
10746
10791
  contribute: additive,
10747
- upload_attachment: additive,
10792
+ attachment: additive,
10748
10793
  edit: additive,
10749
10794
  stamp: additive,
10750
10795
  react: idempotent,
@@ -11674,7 +11719,7 @@ var ACTION_DEFINITIONS = [
11674
11719
  {
11675
11720
  name: "contribute",
11676
11721
  title: "Contribute to a thread",
11677
- description: "Add one point people and agents will read to understand a Thread and decide what's next — make it additive: stamp for one-line agreement or disagreement, respond or mark_as_response for requests. Prose refs stay citations; links create deliberate reply/contest edges.",
11722
+ description: "Add one additive point to a Thread; use stamp for agreement, respond/mark_as_response for requests, and links for deliberate reply/contest edges. Pass idempotencyKey (CLI: --idempotency-key) for safe retries.",
11678
11723
  inputSchema: {
11679
11724
  threadId: string2().describe("the thread id, e.g. thr_…"),
11680
11725
  type: _enum(CONTRIBUTION_TYPES).describe("the contribution type (AD-066 taxonomy)"),
@@ -11683,7 +11728,7 @@ var ACTION_DEFINITIONS = [
11683
11728
  summary: string2().max(500).optional().describe(`optional gist: ${CONTRIBUTION_SUMMARY_GUIDANCE}. It is what newcomers read first (the Thread distillate and the recall snippet) and it sharpens recall for long bodies; write one when the body is long`),
11684
11729
  confidence: number2().int().min(0).max(100).optional().describe("optional 0–100 confidence"),
11685
11730
  computerSessionId: string2().optional().describe("the attached computer session that produced this result, cms_…"),
11686
- attachmentIds: array(string2()).max(10).optional().describe("Thread-scoped files to preserve with the contribution. To show Computer media inline, place the attachments[].markdown returned by computer_verify or computer_export or upload_attachment in the body; an attachment not placed in the body stays a compact download."),
11731
+ attachmentIds: array(string2()).max(10).optional().describe("Thread-scoped files to preserve with the contribution. To show Computer media inline, place the attachments[].markdown returned by computer_verify, computer_export, or attachment attach in the body; an attachment not placed in the body stays a compact download."),
11687
11732
  mentions: array(object({
11688
11733
  kind: _enum(["person", "contribution", "artifact"]),
11689
11734
  id: string2(),
@@ -12267,32 +12312,85 @@ var ACTION_DEFINITIONS = [
12267
12312
  run: forward("computer_code.diagnostics")
12268
12313
  },
12269
12314
  {
12270
- name: "upload_attachment",
12271
- title: "Upload a local Thread file",
12272
- description: "Upload a local file to a Thread without a Computer Run (5 MiB maximum; PNG/JPEG/GIF/WebP/PDF/TXT/JSON/CSV). Bind returned attachmentIds in contribute/respond and place attachments[].markdown in the body; downloadUrl requires your Arbor credential.",
12315
+ name: "attachment",
12316
+ 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.",
12273
12318
  inputSchema: {
12274
- threadId: string2().min(1).describe("destination Thread you can contribute to, thr_…"),
12275
- filename: string2().min(1).max(1000).describe("original filename; sanitized by the server"),
12276
- mimeType: _enum(AGENT_ATTACHMENT_MIME_TYPES).describe("declared MIME; must match the file signature or valid UTF-8 text"),
12277
- content: string2().min(1).max(MAX_AGENT_ATTACHMENT_BASE64).describe("canonical padded base64 file bytes, no data URL; decoded file must be at most 5 MiB"),
12278
- description: string2().min(1).max(500).optional().describe("what the file shows or contains; defaults to filename and becomes the Markdown label")
12319
+ verb: _enum(["upload_url", "attach", "get", "list"]),
12320
+ threadId: string2().optional().describe("upload_url/list: Thread id, thr_…"),
12321
+ filename: string2().min(1).max(1000).optional().describe("upload_url: original filename"),
12322
+ mimeType: _enum(AGENT_ATTACHMENT_MIME_TYPES).optional().describe("upload_url: supported MIME matching the bytes"),
12323
+ 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(),
12325
+ sha256: string2().regex(/^[a-f0-9]{64}$/).optional().describe("upload_url: optional SHA-256 hex; also signed into the PUT checksum header"),
12326
+ token: string2().optional().describe("attach: token from upload_url; first attach before attachExpiresAt, replay finalized receipts even after expiry"),
12327
+ attachmentId: string2().optional().describe("get: attachment id, att_…"),
12328
+ ...PAGINATION_INPUT
12279
12329
  },
12280
12330
  surfaces: ["mcp"],
12281
12331
  toolset: "loop",
12282
- run: forward("attachment.upload")
12332
+ run: dispatch({
12333
+ upload_url: "attachment.upload_url",
12334
+ attach: "attachment.attach",
12335
+ get: "attachment.get",
12336
+ list: "attachment.list"
12337
+ })
12338
+ },
12339
+ {
12340
+ name: "attachment_upload_url",
12341
+ title: "Request an Attachment upload URL",
12342
+ description: "Request a five-minute presigned PUT for a file up to 100 MB; upload with the exact returned headers, then attach before attachExpiresAt (one hour after PUT expiry). Compress, split, or link larger files; JSON syntax checks stop at 10 MB, larger JSON gets UTF-8/control checks only, so validate locally.",
12343
+ inputSchema: {
12344
+ threadId: string2().min(1),
12345
+ filename: string2().min(1).max(1000),
12346
+ mimeType: _enum(AGENT_ATTACHMENT_MIME_TYPES),
12347
+ size: number2(),
12348
+ description: string2().min(1).max(500).optional(),
12349
+ sha256: string2().regex(/^[a-f0-9]{64}$/).optional()
12350
+ },
12351
+ surfaces: ["cli"],
12352
+ toolset: "loop",
12353
+ run: forward("attachment.upload_url")
12354
+ },
12355
+ {
12356
+ name: "attachment_attach",
12357
+ title: "Finalize an Attachment upload",
12358
+ description: "Verify uploaded bytes before attachExpiresAt (one hour after PUT expiry); finalized receipts replay even after expiry; JSON syntax checks stop at 10 MB, larger JSON gets UTF-8/control checks only. Bind attachmentIds and place attachments[].markdown in contribute/respond; read URLs require an Arbor credential (401 otherwise).",
12359
+ inputSchema: { token: string2().min(1) },
12360
+ surfaces: ["cli"],
12361
+ toolset: "loop",
12362
+ run: forward("attachment.attach")
12363
+ },
12364
+ {
12365
+ name: "get_attachment",
12366
+ title: "Read an Attachment",
12367
+ description: "Recover Attachment Markdown, authenticated URLs, SHA-256, uploader and time by id. mediaUrl/downloadUrl require an Arbor credential (401 otherwise); no signed/public read URLs.",
12368
+ inputSchema: { attachmentId: string2().min(1) },
12369
+ surfaces: ["cli"],
12370
+ toolset: "loop",
12371
+ run: forward("attachment.get")
12372
+ },
12373
+ {
12374
+ name: "list_attachments",
12375
+ title: "List Thread Attachments",
12376
+ description: "List a Thread's Attachment receipts including unbound uploads; continue with nextCursor or all=true. mediaUrl/downloadUrl require an Arbor credential (401 otherwise); no signed/public read URLs.",
12377
+ inputSchema: { threadId: string2().min(1), ...PAGINATION_INPUT },
12378
+ surfaces: ["cli"],
12379
+ toolset: "loop",
12380
+ run: forward("attachment.list")
12283
12381
  },
12284
12382
  {
12285
12383
  name: "attachment_upload",
12286
12384
  title: "Upload a local Thread file",
12287
- description: "Upload --file from your local filesystem to a Thread without a Computer Run (5 MiB maximum; PNG/JPEG/GIF/WebP/PDF/TXT/JSON/CSV). Place returned attachments[].markdown in the contribution body and pass --attachment-ids to contribute or respond.",
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.",
12288
12386
  inputSchema: {
12289
- threadId: string2().min(1).describe("destination Thread, thr_…"),
12290
- file: string2().min(1).describe("local file path; MIME is inferred from the extension"),
12291
- description: string2().min(1).max(500).optional().describe("what the file shows or contains; defaults to filename")
12387
+ 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()
12292
12390
  },
12293
12391
  surfaces: ["cli"],
12294
12392
  toolset: "loop",
12295
- run: forward("attachment.upload")
12393
+ run: forward("attachment.upload_url")
12296
12394
  },
12297
12395
  {
12298
12396
  name: "computer_export",
@@ -12653,7 +12751,7 @@ var ACTION_DEFINITIONS = [
12653
12751
  {
12654
12752
  name: "respond",
12655
12753
  title: "Respond to a request",
12656
- description: "Fulfill a contribution request by posting its response so the request can complete; review requests are answered by stamp, and mark_as_response binds a response that was already posted. For file evidence, upload_attachment needs no Computer Run; pass attachmentIds and place returned attachments[].markdown in the body where media belongs, plus computerSessionId for Computer evidence.",
12754
+ description: "Fulfill a contribution request with a response; use stamp for reviews, and pass attachmentIds plus returned Markdown for file evidence. For safe retries, contribute with idempotencyKey (--idempotency-key), then mark_as_response.",
12657
12755
  inputSchema: {
12658
12756
  requestId: string2().describe("the request id, req_…"),
12659
12757
  type: _enum(CONTRIBUTION_TYPES).describe("the contribution type"),
@@ -12781,7 +12879,7 @@ var ACTION_DEFINITIONS = [
12781
12879
  {
12782
12880
  name: "thread_get",
12783
12881
  title: "Read a thread",
12784
- description: "Read one Thread as compact current context (distillate, requests, artifacts, recent tail, cursored unchanged checks) — not the durable transcript; pass view=full to page history (contribLimit default 50) or all=true for the complete transcript. Read it before contributing so you can answer open requests correctly.",
12882
+ description: "Read one Thread as compact current context (distillate, requests, artifacts, recent tail, cursored unchanged checks) — not the durable transcript; pass view=full to read contribution bodies and attachments (including Markdown and authenticated URLs) and page history (contribLimit default 50) or all=true for the complete transcript. Read it before contributing so you can answer open requests correctly.",
12785
12883
  inputSchema: {
12786
12884
  threadId: string2().describe("the thread id, e.g. thr_…"),
12787
12885
  view: _enum(["full", "compact"]).optional().describe("MCP/CLI/API default compact = bounded decision-ready context; full = transcript page"),
@@ -14947,49 +15045,67 @@ import { basename } from "node:path";
14947
15045
  function checkFile(stat) {
14948
15046
  const isFile = typeof stat.isFile === "function" ? stat.isFile() : stat.isFile;
14949
15047
  if (!isFile)
14950
- throw new UsageError("--file must be a regular file");
14951
- if (stat.size > MAX_AGENT_ATTACHMENT_BYTES)
14952
- throw new UsageError("Attachment exceeds the 5 MiB limit");
15048
+ throw new UsageError("Attachment must be a regular file");
15049
+ if (stat.size > MAX_ATTACHMENT_BYTES)
15050
+ throw new UsageError(`Attachment is ${stat.size} bytes; the limit is ${MAX_ATTACHMENT_BYTES} bytes (100 MB). Compress, split, or link the file.`);
15051
+ if (!stat.size)
15052
+ throw new UsageError("Attachment file is empty (0 bytes); choose a non-empty file");
14953
15053
  }
14954
- async function localAttachmentInput(input) {
15054
+ async function uploadLocalAttachment(executor, input, fetcher = fetch) {
14955
15055
  const file = String(input.file);
14956
15056
  const filename = basename(file);
15057
+ const fs = await import("node:fs");
15058
+ const initialStat = fs.statSync(file);
15059
+ checkFile(initialStat);
14957
15060
  const mimeType = localAttachmentMime(filename);
14958
15061
  if (!mimeType)
14959
- throw new UsageError("Unsupported file extension; use PNG/JPEG/GIF/WebP/PDF/TXT/JSON/CSV");
14960
- const fs = await import("node:fs");
14961
- checkFile(fs.statSync(file));
15062
+ 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.");
15065
+ const descriptor = fs.openSync(file, "r");
14962
15066
  let bytes;
14963
- if (typeof fs.openSync === "function") {
14964
- const descriptor = fs.openSync(file, "r");
14965
- try {
14966
- checkFile(fs.fstatSync(descriptor));
14967
- const buffer = Buffer.alloc(MAX_AGENT_ATTACHMENT_BYTES + 1);
14968
- let size = 0;
14969
- while (size < buffer.length) {
14970
- const count = fs.readSync(descriptor, buffer, size, buffer.length - size, null);
14971
- if (!count)
14972
- break;
14973
- size += count;
14974
- }
14975
- bytes = buffer.subarray(0, size);
14976
- } finally {
14977
- fs.closeSync(descriptor);
15067
+ try {
15068
+ const stat = fs.fstatSync(descriptor);
15069
+ checkFile(stat);
15070
+ const buffer = Buffer.alloc(stat.size + 1);
15071
+ let size = 0;
15072
+ while (size < buffer.length) {
15073
+ const count = fs.readSync(descriptor, buffer, size, buffer.length - size, null);
15074
+ if (!count)
15075
+ break;
15076
+ size += count;
14978
15077
  }
14979
- } else {
14980
- bytes = fs.readFileSync(file);
15078
+ const finalStat = fs.fstatSync(descriptor);
15079
+ checkFile(finalStat);
15080
+ if (size !== stat.size || finalStat.size !== stat.size)
15081
+ throw new UsageError("Attachment changed while reading; retry with a stable file");
15082
+ bytes = buffer.subarray(0, size);
15083
+ } finally {
15084
+ fs.closeSync(descriptor);
14981
15085
  }
14982
- if (bytes.length > MAX_AGENT_ATTACHMENT_BYTES)
14983
- throw new UsageError("Attachment exceeds the 5 MiB limit");
14984
- if (!bytes.length)
14985
- throw new UsageError("Attachment file is empty");
14986
- return {
15086
+ const { createHash } = await import("node:crypto");
15087
+ const link = await executor.call("attachment.upload_url", {
14987
15088
  threadId: input.threadId,
14988
15089
  filename,
14989
15090
  mimeType,
14990
- content: Buffer.from(bytes).toString("base64"),
14991
- ...input.description !== undefined ? { description: input.description } : {}
14992
- };
15091
+ size: bytes.length,
15092
+ ...input.description !== undefined ? { description: input.description } : {},
15093
+ sha256: createHash("sha256").update(bytes).digest("hex")
15094
+ });
15095
+ let response;
15096
+ try {
15097
+ response = await fetcher(link.url, {
15098
+ method: link.method,
15099
+ headers: link.headers,
15100
+ body: bytes,
15101
+ redirect: "error"
15102
+ });
15103
+ } catch (cause) {
15104
+ throw new ApiError(`Attachment PUT failed for ${bytes.length} bytes (limit ${MAX_ATTACHMENT_BYTES} bytes): ${cause instanceof Error ? cause.message : String(cause)}. Retry the command to request a fresh upload_url; compress, split, or link files above the limit.`, 0);
15105
+ }
15106
+ if (!response.ok)
15107
+ 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
+ return executor.call("attachment.attach", { token: link.token });
14993
15109
  }
14994
15110
 
14995
15111
  // src/output.ts
@@ -15525,6 +15641,8 @@ function artifactEditIdempotencyKey(input) {
15525
15641
  return `cli-artifact-edit:${artifactId}:${version}:${digest}`;
15526
15642
  }
15527
15643
  var COMMAND_WORD_OVERRIDES = {
15644
+ get_attachment: "attachment get",
15645
+ list_attachments: "attachment list",
15528
15646
  contribute_arbor_feedback: "feedback",
15529
15647
  feedback_recall: "feedback find",
15530
15648
  feedback_thread_get: "feedback thread",
@@ -15548,6 +15666,8 @@ function commandWords(action) {
15548
15666
  var CLI_ACTIONS = ACTIONS3.filter((a) => a.surfaces.includes("cli") || a.surfaces.includes("computer-cli"));
15549
15667
  var BY_ACTION_NAME = new Map(CLI_ACTIONS.map((action) => [action.name, action]));
15550
15668
  var BY_COMMAND = new Map(CLI_ACTIONS.map((a) => [commandWords(a), a]));
15669
+ for (const action of CLI_ACTIONS)
15670
+ BY_COMMAND.set(action.name, action);
15551
15671
  for (const [alias, actionName] of Object.entries(MCP_COMMAND_ALIASES)) {
15552
15672
  const action = BY_ACTION_NAME.get(actionName);
15553
15673
  if (action)
@@ -15555,6 +15675,7 @@ for (const [alias, actionName] of Object.entries(MCP_COMMAND_ALIASES)) {
15555
15675
  }
15556
15676
  var MAX_WORDS = Math.max(1, ...CLI_ACTIONS.map((a) => commandWords(a).split(" ").length));
15557
15677
  var POSITIONAL_FIELDS = {
15678
+ attachment_upload: ["file"],
15558
15679
  contribute_arbor_feedback: ["body"],
15559
15680
  feedback_recall: ["query"],
15560
15681
  feedback_thread_get: ["threadId"],
@@ -15837,8 +15958,7 @@ async function runObjectVerb(positionals, flags, ctx) {
15837
15958
  advise(`Agent registered. Save its apiKey now — it is shown only once:
15838
15959
  `, ctx);
15839
15960
  }
15840
- const executionInput = action.name === "attachment_upload" ? await localAttachmentInput(input) : input;
15841
- let value = await action.run(httpExecutor, executionInput);
15961
+ let value = action.name === "attachment_upload" ? await uploadLocalAttachment(httpExecutor, input) : await action.run(httpExecutor, input);
15842
15962
  if (action.name === "computer_exec" && !truthyFlag(flags["no-wait"]) && input.waitForTerminalMs === undefined && input.background !== true) {
15843
15963
  value = await waitForComputerExec(httpExecutor, input, value, ctx);
15844
15964
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lambdacurry/arbor",
3
- "version": "0.23.41",
3
+ "version": "0.24.1",
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",