@lambdacurry/arbor 0.23.39 → 0.23.41

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 +17 -0
  2. package/dist/arbor.js +125 -7
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -73,6 +73,23 @@ npx @lambdacurry/arbor connect <pairing-key>
73
73
 
74
74
  This prints a URL + short code to relay to the human, who approves it in their logged-in session. On approval the agent receives its own key (saved to `~/.arbor/config.json`) and acts as a managed, collaborate-only member of the org thereafter.
75
75
 
76
+ ## Attach a local file
77
+
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
81
+ ```
82
+
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.
92
+
76
93
  ## Output contract (agent-friendly)
77
94
 
78
95
  Every command takes `--json` to emit a stable `{ ok, data | error, meta }` envelope on stdout (with structured error codes and agent-mode exit codes); advisory/human text goes to stderr. A failure envelope adds the server's granular `reason` — `validation.missing_field`, `limit.exceeded`, `conflict.state`, … — beside the coarse `code`, optionally with `field`, `limit`, `retryable`, `phase`, and bounded sanitized `provider` lifecycle diagnostics (AD-237/261). `--quiet` (auto under `--json` or a pipe) silences the advisory stream. `--fields a,b,list:N` projects a result; `--limit`/`--cursor` paginate list commands.
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.39",
5
+ version: "0.23.41",
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",
@@ -5160,6 +5160,34 @@ var ARTIFACT_TYPES = [
5160
5160
  "eval-rubric",
5161
5161
  "external-reference"
5162
5162
  ];
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);
5166
+ var AGENT_ATTACHMENT_MIME_TYPES = [
5167
+ "image/png",
5168
+ "image/jpeg",
5169
+ "image/gif",
5170
+ "image/webp",
5171
+ "application/pdf",
5172
+ "text/plain",
5173
+ "application/json",
5174
+ "text/csv"
5175
+ ];
5176
+ function localAttachmentMime(filename) {
5177
+ const extension = filename.toLowerCase().split(".").at(-1) ?? "";
5178
+ const types = {
5179
+ png: "image/png",
5180
+ jpg: "image/jpeg",
5181
+ jpeg: "image/jpeg",
5182
+ gif: "image/gif",
5183
+ webp: "image/webp",
5184
+ pdf: "application/pdf",
5185
+ txt: "text/plain",
5186
+ json: "application/json",
5187
+ csv: "text/csv"
5188
+ };
5189
+ return types[extension];
5190
+ }
5163
5191
  // ../core/src/feedback/constants.ts
5164
5192
  var ARBOR_FEEDBACK_TYPES = [
5165
5193
  "comment",
@@ -10249,6 +10277,14 @@ var MCP_OUTPUT_SCHEMAS = {
10249
10277
  sha256: string2()
10250
10278
  })
10251
10279
  }),
10280
+ upload_attachment: looseObject({
10281
+ attachmentId: id,
10282
+ attachmentIds: array(id),
10283
+ downloadUrl: string2(),
10284
+ uploadedByProfileId: id,
10285
+ createdAt: timestamp,
10286
+ attachments: array(attachment2)
10287
+ }),
10252
10288
  computer_export: looseObject({
10253
10289
  computerSessionId: id,
10254
10290
  replayed: boolean2(),
@@ -10340,6 +10376,8 @@ var MCP_OUTPUT_SCHEMAS = {
10340
10376
  })
10341
10377
  };
10342
10378
  function outputSchemaFor(actionName) {
10379
+ if (actionName === "attachment_upload")
10380
+ actionName = "upload_attachment";
10343
10381
  return MCP_OUTPUT_SCHEMAS[actionName];
10344
10382
  }
10345
10383
 
@@ -10706,6 +10744,7 @@ var MCP_TOOL_ANNOTATIONS = {
10706
10744
  admin_feedback_edit: destructive,
10707
10745
  admin_feedback_curate: destructive,
10708
10746
  contribute: additive,
10747
+ upload_attachment: additive,
10709
10748
  edit: additive,
10710
10749
  stamp: additive,
10711
10750
  react: idempotent,
@@ -11644,7 +11683,7 @@ var ACTION_DEFINITIONS = [
11644
11683
  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`),
11645
11684
  confidence: number2().int().min(0).max(100).optional().describe("optional 0–100 confidence"),
11646
11685
  computerSessionId: string2().optional().describe("the attached computer session that produced this result, cms_…"),
11647
- 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 in the body; an attachment not placed in the body stays a compact download."),
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."),
11648
11687
  mentions: array(object({
11649
11688
  kind: _enum(["person", "contribution", "artifact"]),
11650
11689
  id: string2(),
@@ -12227,6 +12266,34 @@ var ACTION_DEFINITIONS = [
12227
12266
  toolset: "loop",
12228
12267
  run: forward("computer_code.diagnostics")
12229
12268
  },
12269
+ {
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.",
12273
+ 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")
12279
+ },
12280
+ surfaces: ["mcp"],
12281
+ toolset: "loop",
12282
+ run: forward("attachment.upload")
12283
+ },
12284
+ {
12285
+ name: "attachment_upload",
12286
+ 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.",
12288
+ 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")
12292
+ },
12293
+ surfaces: ["cli"],
12294
+ toolset: "loop",
12295
+ run: forward("attachment.upload")
12296
+ },
12230
12297
  {
12231
12298
  name: "computer_export",
12232
12299
  title: "Export a Thread file",
@@ -12586,7 +12653,7 @@ var ACTION_DEFINITIONS = [
12586
12653
  {
12587
12654
  name: "respond",
12588
12655
  title: "Respond to a request",
12589
- 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 Computer evidence, pass computerSessionId/attachmentIds and place returned attachments[].markdown in the body where media belongs.",
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.",
12590
12657
  inputSchema: {
12591
12658
  requestId: string2().describe("the request id, req_…"),
12592
12659
  type: _enum(CONTRIBUTION_TYPES).describe("the contribution type"),
@@ -13427,7 +13494,7 @@ var ACTION_DEFINITIONS = [
13427
13494
  {
13428
13495
  name: "artifact",
13429
13496
  title: "Create or edit an Artifact",
13430
- description: "Create/edit an Artifact, bind a Thread Live Preview, set a Computer snapshot, or promote a Thread attachment to a file Artifact. Git writes: stable key; same-key retry only after unknown outcome. To reference an exported report within an existing baseline Artifact, use the Attachment's durable downloadUrl instead of promoting it into that artifactId. For a computer:// .tldr snapshot mint: computer_export the exact locator path and same-Thread PNG, then use set_snapshot. Board save and TLP do not mint snapshots. set_snapshot does not wake Computer or change the locator; read artifact_get before changing it, while lifecycle/removal stay in artifact_transition and artifact_delete.",
13497
+ description: "Create/edit an Artifact, bind a Live Preview, set a Computer snapshot, or promote a Thread attachment. Git edits derive retry keys (omitted edits derive one); keyless creates mint per-invocation keys; stable idempotencyKey supports caller retries. Retry only after unknown outcomes; never blindly repeat ambiguous creates. Exported reports use the Attachment downloadUrl; tldraw snapshot and lifecycle actions use their dedicated operations.",
13431
13498
  inputSchema: {
13432
13499
  verb: _enum(["create", "edit", "set_live_preview", "set_snapshot", "promote_attachment"]).describe("which artifact move"),
13433
13500
  title: string2().optional().describe("create: the artifact's title"),
@@ -13445,7 +13512,7 @@ var ACTION_DEFINITIONS = [
13445
13512
  baseVersion: number2().optional().describe("edit: the version you READ (from get/create) — stale base is rejected with the current source"),
13446
13513
  patches: array(object({ old: string2(), new: string2() })).optional().describe("edit: exact-match hunks applied in order, atomically; each old must match exactly once"),
13447
13514
  note: string2().optional().describe("edit: one-line summary of what changed"),
13448
- idempotencyKey: string2().min(1).max(200).optional().describe("create/edit for Git-backed text: stable key for one logical write; reuse unchanged only to recover an unknown outcome, never to start a new write"),
13515
+ idempotencyKey: string2().min(1).max(200).optional().describe("create: optional caller-stable key; omitted creates mint one per invocation; edit: optional stable key (omitted edits derive one from actor/artifact/base/payload); reuse unchanged only to recover an unknown outcome, never to start a new write"),
13449
13516
  sourceAttachmentId: string2().optional().describe("set_snapshot: computer_export of the exact computer:// locator path; MIME application/vnd.tldraw+json, computerPath stamped and matching defaultCwd + locator"),
13450
13517
  previewAttachmentId: string2().optional().describe("set_snapshot: same-Thread computer_export image/png Attachment, used as the asleep face preview"),
13451
13518
  attachmentId: string2().optional().describe("promote_attachment: the upload, att_…"),
@@ -14211,7 +14278,7 @@ function retentionAction(action) {
14211
14278
  if (action.name === "artifact") {
14212
14279
  return {
14213
14280
  ...action,
14214
- description: "Create/edit a durable Artifact with full|checkpoint retention or manage Live Preview/snapshot/attachments. Git text writes require a stable idempotencyKey; retry the same key only after an unknown outcome; promote_attachment creates a separate file Artifact from a source Contribution; exported reports use the Attachment's durable downloadUrl; content edits return artifactVersionId; identical edits reuse the current version; policy/checkpoint changes do not mint content versions.",
14281
+ description: "Create/edit a durable Artifact with full|checkpoint retention or manage Live Preview, snapshots, and attachments. Git edits derive retry keys (omitted edits derive one); keyless creates mint per-invocation keys; stable idempotencyKey supports caller retries; never blindly repeat ambiguous creates. promote_attachment creates a file Artifact from a Contribution; exported reports use the Attachment downloadUrl; policy changes do not mint content versions.",
14215
14282
  inputSchema: {
14216
14283
  ...action.inputSchema,
14217
14284
  historyPolicy: historyPolicySchema,
@@ -14875,6 +14942,56 @@ var httpExecutor = {
14875
14942
  }
14876
14943
  };
14877
14944
 
14945
+ // src/attachment-upload.ts
14946
+ import { basename } from "node:path";
14947
+ function checkFile(stat) {
14948
+ const isFile = typeof stat.isFile === "function" ? stat.isFile() : stat.isFile;
14949
+ 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");
14953
+ }
14954
+ async function localAttachmentInput(input) {
14955
+ const file = String(input.file);
14956
+ const filename = basename(file);
14957
+ const mimeType = localAttachmentMime(filename);
14958
+ 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));
14962
+ 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);
14978
+ }
14979
+ } else {
14980
+ bytes = fs.readFileSync(file);
14981
+ }
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 {
14987
+ threadId: input.threadId,
14988
+ filename,
14989
+ mimeType,
14990
+ content: Buffer.from(bytes).toString("base64"),
14991
+ ...input.description !== undefined ? { description: input.description } : {}
14992
+ };
14993
+ }
14994
+
14878
14995
  // src/output.ts
14879
14996
  function writeOutput(value, stream, fallback) {
14880
14997
  if (stream && typeof stream.write === "function") {
@@ -15720,7 +15837,8 @@ async function runObjectVerb(positionals, flags, ctx) {
15720
15837
  advise(`Agent registered. Save its apiKey now — it is shown only once:
15721
15838
  `, ctx);
15722
15839
  }
15723
- let value = await action.run(httpExecutor, input);
15840
+ const executionInput = action.name === "attachment_upload" ? await localAttachmentInput(input) : input;
15841
+ let value = await action.run(httpExecutor, executionInput);
15724
15842
  if (action.name === "computer_exec" && !truthyFlag(flags["no-wait"]) && input.waitForTerminalMs === undefined && input.background !== true) {
15725
15843
  value = await waitForComputerExec(httpExecutor, input, value, ctx);
15726
15844
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lambdacurry/arbor",
3
- "version": "0.23.39",
3
+ "version": "0.23.41",
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",