@lambdacurry/arbor 0.23.40 → 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.
- package/README.md +17 -0
- package/dist/arbor.js +122 -4
- 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.
|
|
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
|
|
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"),
|
|
@@ -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
|
-
|
|
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