@lambdacurry/arbor 0.23.40 → 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.
- package/README.md +35 -0
- package/dist/arbor.js +244 -6
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -73,6 +73,41 @@ 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 ./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_…
|
|
83
|
+
```
|
|
84
|
+
|
|
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.
|
|
110
|
+
|
|
76
111
|
## Output contract (agent-friendly)
|
|
77
112
|
|
|
78
113
|
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.
|
|
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",
|
|
@@ -5160,6 +5160,33 @@ var ARTIFACT_TYPES = [
|
|
|
5160
5160
|
"eval-rubric",
|
|
5161
5161
|
"external-reference"
|
|
5162
5162
|
];
|
|
5163
|
+
// ../core/src/blob/attachment-upload.ts
|
|
5164
|
+
var MAX_ATTACHMENT_BYTES = 1e8;
|
|
5165
|
+
var AGENT_ATTACHMENT_MIME_TYPES = [
|
|
5166
|
+
"image/png",
|
|
5167
|
+
"image/jpeg",
|
|
5168
|
+
"image/gif",
|
|
5169
|
+
"image/webp",
|
|
5170
|
+
"application/pdf",
|
|
5171
|
+
"text/plain",
|
|
5172
|
+
"application/json",
|
|
5173
|
+
"text/csv"
|
|
5174
|
+
];
|
|
5175
|
+
function localAttachmentMime(filename) {
|
|
5176
|
+
const extension = filename.toLowerCase().split(".").at(-1) ?? "";
|
|
5177
|
+
const types = {
|
|
5178
|
+
png: "image/png",
|
|
5179
|
+
jpg: "image/jpeg",
|
|
5180
|
+
jpeg: "image/jpeg",
|
|
5181
|
+
gif: "image/gif",
|
|
5182
|
+
webp: "image/webp",
|
|
5183
|
+
pdf: "application/pdf",
|
|
5184
|
+
txt: "text/plain",
|
|
5185
|
+
json: "application/json",
|
|
5186
|
+
csv: "text/csv"
|
|
5187
|
+
};
|
|
5188
|
+
return types[extension];
|
|
5189
|
+
}
|
|
5163
5190
|
// ../core/src/feedback/constants.ts
|
|
5164
5191
|
var ARBOR_FEEDBACK_TYPES = [
|
|
5165
5192
|
"comment",
|
|
@@ -8576,6 +8603,7 @@ var attachment2 = looseObject({
|
|
|
8576
8603
|
size: number2(),
|
|
8577
8604
|
sha256: string2(),
|
|
8578
8605
|
downloadUrl: string2(),
|
|
8606
|
+
mediaUrl: string2().optional(),
|
|
8579
8607
|
markdown: string2()
|
|
8580
8608
|
});
|
|
8581
8609
|
var checkpoint = looseObject({
|
|
@@ -10249,6 +10277,59 @@ var MCP_OUTPUT_SCHEMAS = {
|
|
|
10249
10277
|
sha256: string2()
|
|
10250
10278
|
})
|
|
10251
10279
|
}),
|
|
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({
|
|
10325
|
+
attachmentId: id,
|
|
10326
|
+
attachmentIds: array(id),
|
|
10327
|
+
downloadUrl: string2(),
|
|
10328
|
+
uploadedByProfileId: id,
|
|
10329
|
+
createdAt: timestamp,
|
|
10330
|
+
attachments: array(attachment2),
|
|
10331
|
+
warnings: array(string2()).optional()
|
|
10332
|
+
}),
|
|
10252
10333
|
computer_export: looseObject({
|
|
10253
10334
|
computerSessionId: id,
|
|
10254
10335
|
replayed: boolean2(),
|
|
@@ -10340,6 +10421,8 @@ var MCP_OUTPUT_SCHEMAS = {
|
|
|
10340
10421
|
})
|
|
10341
10422
|
};
|
|
10342
10423
|
function outputSchemaFor(actionName) {
|
|
10424
|
+
if (actionName === "attachment_upload")
|
|
10425
|
+
actionName = "attachment_attach";
|
|
10343
10426
|
return MCP_OUTPUT_SCHEMAS[actionName];
|
|
10344
10427
|
}
|
|
10345
10428
|
|
|
@@ -10706,6 +10789,7 @@ var MCP_TOOL_ANNOTATIONS = {
|
|
|
10706
10789
|
admin_feedback_edit: destructive,
|
|
10707
10790
|
admin_feedback_curate: destructive,
|
|
10708
10791
|
contribute: additive,
|
|
10792
|
+
attachment: additive,
|
|
10709
10793
|
edit: additive,
|
|
10710
10794
|
stamp: additive,
|
|
10711
10795
|
react: idempotent,
|
|
@@ -11635,7 +11719,7 @@ var ACTION_DEFINITIONS = [
|
|
|
11635
11719
|
{
|
|
11636
11720
|
name: "contribute",
|
|
11637
11721
|
title: "Contribute to a thread",
|
|
11638
|
-
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.",
|
|
11639
11723
|
inputSchema: {
|
|
11640
11724
|
threadId: string2().describe("the thread id, e.g. thr_…"),
|
|
11641
11725
|
type: _enum(CONTRIBUTION_TYPES).describe("the contribution type (AD-066 taxonomy)"),
|
|
@@ -11644,7 +11728,7 @@ var ACTION_DEFINITIONS = [
|
|
|
11644
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`),
|
|
11645
11729
|
confidence: number2().int().min(0).max(100).optional().describe("optional 0–100 confidence"),
|
|
11646
11730
|
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
|
|
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."),
|
|
11648
11732
|
mentions: array(object({
|
|
11649
11733
|
kind: _enum(["person", "contribution", "artifact"]),
|
|
11650
11734
|
id: string2(),
|
|
@@ -12227,6 +12311,87 @@ var ACTION_DEFINITIONS = [
|
|
|
12227
12311
|
toolset: "loop",
|
|
12228
12312
|
run: forward("computer_code.diagnostics")
|
|
12229
12313
|
},
|
|
12314
|
+
{
|
|
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.",
|
|
12318
|
+
inputSchema: {
|
|
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
|
|
12329
|
+
},
|
|
12330
|
+
surfaces: ["mcp"],
|
|
12331
|
+
toolset: "loop",
|
|
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")
|
|
12381
|
+
},
|
|
12382
|
+
{
|
|
12383
|
+
name: "attachment_upload",
|
|
12384
|
+
title: "Upload a local Thread file",
|
|
12385
|
+
description: "Upload with Node and SHA-256 via upload_url → presigned PUT → attach (100 MB cap); compress, split, or link larger files; JSON syntax checks stop at 10 MB, larger JSON gets UTF-8/control checks only. PUT expires in 5 min, first attach gets 1 h more, receipts replay after expiry; js-exec cannot upload; mediaUrl/downloadUrl require an Arbor credential (401 otherwise), no public read URLs; for a missing tool refresh the MCP tool catalog or use arbor attachment upload.",
|
|
12386
|
+
inputSchema: {
|
|
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()
|
|
12390
|
+
},
|
|
12391
|
+
surfaces: ["cli"],
|
|
12392
|
+
toolset: "loop",
|
|
12393
|
+
run: forward("attachment.upload_url")
|
|
12394
|
+
},
|
|
12230
12395
|
{
|
|
12231
12396
|
name: "computer_export",
|
|
12232
12397
|
title: "Export a Thread file",
|
|
@@ -12586,7 +12751,7 @@ var ACTION_DEFINITIONS = [
|
|
|
12586
12751
|
{
|
|
12587
12752
|
name: "respond",
|
|
12588
12753
|
title: "Respond to a request",
|
|
12589
|
-
description: "Fulfill a contribution request
|
|
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.",
|
|
12590
12755
|
inputSchema: {
|
|
12591
12756
|
requestId: string2().describe("the request id, req_…"),
|
|
12592
12757
|
type: _enum(CONTRIBUTION_TYPES).describe("the contribution type"),
|
|
@@ -12714,7 +12879,7 @@ var ACTION_DEFINITIONS = [
|
|
|
12714
12879
|
{
|
|
12715
12880
|
name: "thread_get",
|
|
12716
12881
|
title: "Read a thread",
|
|
12717
|
-
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.",
|
|
12718
12883
|
inputSchema: {
|
|
12719
12884
|
threadId: string2().describe("the thread id, e.g. thr_…"),
|
|
12720
12885
|
view: _enum(["full", "compact"]).optional().describe("MCP/CLI/API default compact = bounded decision-ready context; full = transcript page"),
|
|
@@ -14875,6 +15040,74 @@ var httpExecutor = {
|
|
|
14875
15040
|
}
|
|
14876
15041
|
};
|
|
14877
15042
|
|
|
15043
|
+
// src/attachment-upload.ts
|
|
15044
|
+
import { basename } from "node:path";
|
|
15045
|
+
function checkFile(stat) {
|
|
15046
|
+
const isFile = typeof stat.isFile === "function" ? stat.isFile() : stat.isFile;
|
|
15047
|
+
if (!isFile)
|
|
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");
|
|
15053
|
+
}
|
|
15054
|
+
async function uploadLocalAttachment(executor, input, fetcher = fetch) {
|
|
15055
|
+
const file = String(input.file);
|
|
15056
|
+
const filename = basename(file);
|
|
15057
|
+
const fs = await import("node:fs");
|
|
15058
|
+
const initialStat = fs.statSync(file);
|
|
15059
|
+
checkFile(initialStat);
|
|
15060
|
+
const mimeType = localAttachmentMime(filename);
|
|
15061
|
+
if (!mimeType)
|
|
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");
|
|
15066
|
+
let bytes;
|
|
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;
|
|
15077
|
+
}
|
|
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);
|
|
15085
|
+
}
|
|
15086
|
+
const { createHash } = await import("node:crypto");
|
|
15087
|
+
const link = await executor.call("attachment.upload_url", {
|
|
15088
|
+
threadId: input.threadId,
|
|
15089
|
+
filename,
|
|
15090
|
+
mimeType,
|
|
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 });
|
|
15109
|
+
}
|
|
15110
|
+
|
|
14878
15111
|
// src/output.ts
|
|
14879
15112
|
function writeOutput(value, stream, fallback) {
|
|
14880
15113
|
if (stream && typeof stream.write === "function") {
|
|
@@ -15408,6 +15641,8 @@ function artifactEditIdempotencyKey(input) {
|
|
|
15408
15641
|
return `cli-artifact-edit:${artifactId}:${version}:${digest}`;
|
|
15409
15642
|
}
|
|
15410
15643
|
var COMMAND_WORD_OVERRIDES = {
|
|
15644
|
+
get_attachment: "attachment get",
|
|
15645
|
+
list_attachments: "attachment list",
|
|
15411
15646
|
contribute_arbor_feedback: "feedback",
|
|
15412
15647
|
feedback_recall: "feedback find",
|
|
15413
15648
|
feedback_thread_get: "feedback thread",
|
|
@@ -15431,6 +15666,8 @@ function commandWords(action) {
|
|
|
15431
15666
|
var CLI_ACTIONS = ACTIONS3.filter((a) => a.surfaces.includes("cli") || a.surfaces.includes("computer-cli"));
|
|
15432
15667
|
var BY_ACTION_NAME = new Map(CLI_ACTIONS.map((action) => [action.name, action]));
|
|
15433
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);
|
|
15434
15671
|
for (const [alias, actionName] of Object.entries(MCP_COMMAND_ALIASES)) {
|
|
15435
15672
|
const action = BY_ACTION_NAME.get(actionName);
|
|
15436
15673
|
if (action)
|
|
@@ -15438,6 +15675,7 @@ for (const [alias, actionName] of Object.entries(MCP_COMMAND_ALIASES)) {
|
|
|
15438
15675
|
}
|
|
15439
15676
|
var MAX_WORDS = Math.max(1, ...CLI_ACTIONS.map((a) => commandWords(a).split(" ").length));
|
|
15440
15677
|
var POSITIONAL_FIELDS = {
|
|
15678
|
+
attachment_upload: ["file"],
|
|
15441
15679
|
contribute_arbor_feedback: ["body"],
|
|
15442
15680
|
feedback_recall: ["query"],
|
|
15443
15681
|
feedback_thread_get: ["threadId"],
|
|
@@ -15720,7 +15958,7 @@ async function runObjectVerb(positionals, flags, ctx) {
|
|
|
15720
15958
|
advise(`Agent registered. Save its apiKey now — it is shown only once:
|
|
15721
15959
|
`, ctx);
|
|
15722
15960
|
}
|
|
15723
|
-
let value = await action.run(httpExecutor, input);
|
|
15961
|
+
let value = action.name === "attachment_upload" ? await uploadLocalAttachment(httpExecutor, input) : await action.run(httpExecutor, input);
|
|
15724
15962
|
if (action.name === "computer_exec" && !truthyFlag(flags["no-wait"]) && input.waitForTerminalMs === undefined && input.background !== true) {
|
|
15725
15963
|
value = await waitForComputerExec(httpExecutor, input, value, ctx);
|
|
15726
15964
|
}
|
package/package.json
CHANGED