@lambdacurry/arbor 0.23.41 → 0.24.2
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.
- package/README.md +29 -11
- package/dist/arbor.js +202 -60
- 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_… --
|
|
80
|
-
# Alias: arbor attachment_upload --thread-id thr_…
|
|
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
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
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.
|
|
5
|
+
version: "0.24.2",
|
|
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
|
|
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
|
-
|
|
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 = "
|
|
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
|
-
|
|
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
|
|
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
|
|
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,95 @@ var ACTION_DEFINITIONS = [
|
|
|
12267
12312
|
run: forward("computer_code.diagnostics")
|
|
12268
12313
|
},
|
|
12269
12314
|
{
|
|
12270
|
-
name: "
|
|
12271
|
-
title: "Upload
|
|
12272
|
-
description: "Upload
|
|
12315
|
+
name: "attachment",
|
|
12316
|
+
title: "Upload and read Thread Attachments",
|
|
12317
|
+
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).",
|
|
12273
12318
|
inputSchema: {
|
|
12274
|
-
|
|
12275
|
-
|
|
12276
|
-
|
|
12277
|
-
|
|
12278
|
-
|
|
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:
|
|
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
|
|
12385
|
+
description: "Upload a local file with Node or Bun (SHA-256 → presigned PUT → attach, 100 MB cap)",
|
|
12386
|
+
cliHelpLead: "Requires Node or Bun.",
|
|
12387
|
+
cliHelpNotes: [
|
|
12388
|
+
"Upload with SHA-256 via upload_url → presigned PUT → attach (100 MB cap); compress, split, or link larger files.",
|
|
12389
|
+
"PUT links expire after five minutes; first attach is allowed until one hour after PUT expiry. Finalized receipts replay after expiry.",
|
|
12390
|
+
"JSON syntax checks stop at 10 MB; larger JSON gets UTF-8/control checks only, so validate syntax locally.",
|
|
12391
|
+
"js-exec lacks file descriptors, node:crypto, and binary-safe fetch; run this command with Node or Bun.",
|
|
12392
|
+
"With only curl available, use the MCP attachment tool: upload_url → returned curl PUT command → attach with the returned token.",
|
|
12393
|
+
"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."
|
|
12394
|
+
].join(`
|
|
12395
|
+
`),
|
|
12288
12396
|
inputSchema: {
|
|
12289
|
-
threadId: string2().min(1)
|
|
12290
|
-
file: string2().min(1).describe("local file path
|
|
12291
|
-
description: string2().min(1).max(500).optional()
|
|
12397
|
+
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()
|
|
12292
12400
|
},
|
|
12293
12401
|
surfaces: ["cli"],
|
|
12294
12402
|
toolset: "loop",
|
|
12295
|
-
run: forward("attachment.
|
|
12403
|
+
run: forward("attachment.upload_url")
|
|
12296
12404
|
},
|
|
12297
12405
|
{
|
|
12298
12406
|
name: "computer_export",
|
|
@@ -12653,7 +12761,7 @@ var ACTION_DEFINITIONS = [
|
|
|
12653
12761
|
{
|
|
12654
12762
|
name: "respond",
|
|
12655
12763
|
title: "Respond to a request",
|
|
12656
|
-
description: "Fulfill a contribution request
|
|
12764
|
+
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
12765
|
inputSchema: {
|
|
12658
12766
|
requestId: string2().describe("the request id, req_…"),
|
|
12659
12767
|
type: _enum(CONTRIBUTION_TYPES).describe("the contribution type"),
|
|
@@ -12781,7 +12889,7 @@ var ACTION_DEFINITIONS = [
|
|
|
12781
12889
|
{
|
|
12782
12890
|
name: "thread_get",
|
|
12783
12891
|
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.",
|
|
12892
|
+
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
12893
|
inputSchema: {
|
|
12786
12894
|
threadId: string2().describe("the thread id, e.g. thr_…"),
|
|
12787
12895
|
view: _enum(["full", "compact"]).optional().describe("MCP/CLI/API default compact = bounded decision-ready context; full = transcript page"),
|
|
@@ -14947,49 +15055,67 @@ import { basename } from "node:path";
|
|
|
14947
15055
|
function checkFile(stat) {
|
|
14948
15056
|
const isFile = typeof stat.isFile === "function" ? stat.isFile() : stat.isFile;
|
|
14949
15057
|
if (!isFile)
|
|
14950
|
-
throw new UsageError("
|
|
14951
|
-
if (stat.size >
|
|
14952
|
-
throw new UsageError(
|
|
15058
|
+
throw new UsageError("Attachment must be a regular file");
|
|
15059
|
+
if (stat.size > MAX_ATTACHMENT_BYTES)
|
|
15060
|
+
throw new UsageError(`Attachment is ${stat.size} bytes; the limit is ${MAX_ATTACHMENT_BYTES} bytes (100 MB). Compress, split, or link the file.`);
|
|
15061
|
+
if (!stat.size)
|
|
15062
|
+
throw new UsageError("Attachment file is empty (0 bytes); choose a non-empty file");
|
|
14953
15063
|
}
|
|
14954
|
-
async function
|
|
15064
|
+
async function uploadLocalAttachment(executor, input, fetcher = fetch) {
|
|
14955
15065
|
const file = String(input.file);
|
|
14956
15066
|
const filename = basename(file);
|
|
15067
|
+
const fs = await import("node:fs");
|
|
15068
|
+
const initialStat = fs.statSync(file);
|
|
15069
|
+
checkFile(initialStat);
|
|
14957
15070
|
const mimeType = localAttachmentMime(filename);
|
|
14958
15071
|
if (!mimeType)
|
|
14959
|
-
throw new UsageError(
|
|
14960
|
-
|
|
14961
|
-
|
|
15072
|
+
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
|
+
if ([fs.openSync, fs.fstatSync, fs.readSync, fs.closeSync].some((api) => typeof api !== "function"))
|
|
15074
|
+
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.");
|
|
15075
|
+
const descriptor = fs.openSync(file, "r");
|
|
14962
15076
|
let bytes;
|
|
14963
|
-
|
|
14964
|
-
const
|
|
14965
|
-
|
|
14966
|
-
|
|
14967
|
-
|
|
14968
|
-
|
|
14969
|
-
|
|
14970
|
-
|
|
14971
|
-
|
|
14972
|
-
|
|
14973
|
-
size += count;
|
|
14974
|
-
}
|
|
14975
|
-
bytes = buffer.subarray(0, size);
|
|
14976
|
-
} finally {
|
|
14977
|
-
fs.closeSync(descriptor);
|
|
15077
|
+
try {
|
|
15078
|
+
const stat = fs.fstatSync(descriptor);
|
|
15079
|
+
checkFile(stat);
|
|
15080
|
+
const buffer = Buffer.alloc(stat.size + 1);
|
|
15081
|
+
let size = 0;
|
|
15082
|
+
while (size < buffer.length) {
|
|
15083
|
+
const count = fs.readSync(descriptor, buffer, size, buffer.length - size, null);
|
|
15084
|
+
if (!count)
|
|
15085
|
+
break;
|
|
15086
|
+
size += count;
|
|
14978
15087
|
}
|
|
14979
|
-
|
|
14980
|
-
|
|
15088
|
+
const finalStat = fs.fstatSync(descriptor);
|
|
15089
|
+
checkFile(finalStat);
|
|
15090
|
+
if (size !== stat.size || finalStat.size !== stat.size)
|
|
15091
|
+
throw new UsageError("Attachment changed while reading; retry with a stable file");
|
|
15092
|
+
bytes = buffer.subarray(0, size);
|
|
15093
|
+
} finally {
|
|
15094
|
+
fs.closeSync(descriptor);
|
|
14981
15095
|
}
|
|
14982
|
-
|
|
14983
|
-
|
|
14984
|
-
if (!bytes.length)
|
|
14985
|
-
throw new UsageError("Attachment file is empty");
|
|
14986
|
-
return {
|
|
15096
|
+
const { createHash } = await import("node:crypto");
|
|
15097
|
+
const link = await executor.call("attachment.upload_url", {
|
|
14987
15098
|
threadId: input.threadId,
|
|
14988
15099
|
filename,
|
|
14989
15100
|
mimeType,
|
|
14990
|
-
|
|
14991
|
-
...input.description !== undefined ? { description: input.description } : {}
|
|
14992
|
-
|
|
15101
|
+
size: bytes.length,
|
|
15102
|
+
...input.description !== undefined ? { description: input.description } : {},
|
|
15103
|
+
sha256: createHash("sha256").update(bytes).digest("hex")
|
|
15104
|
+
});
|
|
15105
|
+
let response;
|
|
15106
|
+
try {
|
|
15107
|
+
response = await fetcher(link.url, {
|
|
15108
|
+
method: link.method,
|
|
15109
|
+
headers: link.headers,
|
|
15110
|
+
body: bytes,
|
|
15111
|
+
redirect: "error"
|
|
15112
|
+
});
|
|
15113
|
+
} catch (cause) {
|
|
15114
|
+
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);
|
|
15115
|
+
}
|
|
15116
|
+
if (!response.ok)
|
|
15117
|
+
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
|
+
return executor.call("attachment.attach", { token: link.token });
|
|
14993
15119
|
}
|
|
14994
15120
|
|
|
14995
15121
|
// src/output.ts
|
|
@@ -15525,6 +15651,8 @@ function artifactEditIdempotencyKey(input) {
|
|
|
15525
15651
|
return `cli-artifact-edit:${artifactId}:${version}:${digest}`;
|
|
15526
15652
|
}
|
|
15527
15653
|
var COMMAND_WORD_OVERRIDES = {
|
|
15654
|
+
get_attachment: "attachment get",
|
|
15655
|
+
list_attachments: "attachment list",
|
|
15528
15656
|
contribute_arbor_feedback: "feedback",
|
|
15529
15657
|
feedback_recall: "feedback find",
|
|
15530
15658
|
feedback_thread_get: "feedback thread",
|
|
@@ -15548,6 +15676,8 @@ function commandWords(action) {
|
|
|
15548
15676
|
var CLI_ACTIONS = ACTIONS3.filter((a) => a.surfaces.includes("cli") || a.surfaces.includes("computer-cli"));
|
|
15549
15677
|
var BY_ACTION_NAME = new Map(CLI_ACTIONS.map((action) => [action.name, action]));
|
|
15550
15678
|
var BY_COMMAND = new Map(CLI_ACTIONS.map((a) => [commandWords(a), a]));
|
|
15679
|
+
for (const action of CLI_ACTIONS)
|
|
15680
|
+
BY_COMMAND.set(action.name, action);
|
|
15551
15681
|
for (const [alias, actionName] of Object.entries(MCP_COMMAND_ALIASES)) {
|
|
15552
15682
|
const action = BY_ACTION_NAME.get(actionName);
|
|
15553
15683
|
if (action)
|
|
@@ -15555,6 +15685,7 @@ for (const [alias, actionName] of Object.entries(MCP_COMMAND_ALIASES)) {
|
|
|
15555
15685
|
}
|
|
15556
15686
|
var MAX_WORDS = Math.max(1, ...CLI_ACTIONS.map((a) => commandWords(a).split(" ").length));
|
|
15557
15687
|
var POSITIONAL_FIELDS = {
|
|
15688
|
+
attachment_upload: ["file"],
|
|
15558
15689
|
contribute_arbor_feedback: ["body"],
|
|
15559
15690
|
feedback_recall: ["query"],
|
|
15560
15691
|
feedback_thread_get: ["threadId"],
|
|
@@ -15717,12 +15848,24 @@ Aliases: ${aliases.map((alias) => `\`arbor ${alias}\``).join(", ")} (tool-name f
|
|
|
15717
15848
|
const waitLines = action.name === "computer_exec" ? [
|
|
15718
15849
|
" --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"
|
|
15719
15850
|
] : [];
|
|
15851
|
+
const notes = action.cliHelpNotes ? `
|
|
15852
|
+
|
|
15853
|
+
${action.cliHelpNotes}` : "";
|
|
15854
|
+
if (action.cliHelpLead) {
|
|
15855
|
+
return `${usage}
|
|
15856
|
+
|
|
15857
|
+
${action.cliHelpLead}
|
|
15858
|
+
|
|
15859
|
+
${lines.join(`
|
|
15860
|
+
`)}${primaryNote}${explicitNote}${aliasNote}${notes}
|
|
15861
|
+
`;
|
|
15862
|
+
}
|
|
15720
15863
|
return `${heading} — ${action.description}
|
|
15721
15864
|
|
|
15722
15865
|
${usage}${waitLines.length > 0 ? " [--no-wait]" : ""}
|
|
15723
15866
|
|
|
15724
15867
|
${[...lines, ...waitLines].join(`
|
|
15725
|
-
`)}${primaryNote}${explicitNote}${aliasNote}
|
|
15868
|
+
`)}${primaryNote}${explicitNote}${aliasNote}${notes}
|
|
15726
15869
|
`;
|
|
15727
15870
|
}
|
|
15728
15871
|
function commandCatalog() {
|
|
@@ -15837,8 +15980,7 @@ async function runObjectVerb(positionals, flags, ctx) {
|
|
|
15837
15980
|
advise(`Agent registered. Save its apiKey now — it is shown only once:
|
|
15838
15981
|
`, ctx);
|
|
15839
15982
|
}
|
|
15840
|
-
|
|
15841
|
-
let value = await action.run(httpExecutor, executionInput);
|
|
15983
|
+
let value = action.name === "attachment_upload" ? await uploadLocalAttachment(httpExecutor, input) : await action.run(httpExecutor, input);
|
|
15842
15984
|
if (action.name === "computer_exec" && !truthyFlag(flags["no-wait"]) && input.waitForTerminalMs === undefined && input.background !== true) {
|
|
15843
15985
|
value = await waitForComputerExec(httpExecutor, input, value, ctx);
|
|
15844
15986
|
}
|
package/package.json
CHANGED