@lambdacurry/arbor 0.24.1 → 0.24.3
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 +8 -0
- package/dist/arbor.js +252 -27
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -101,6 +101,14 @@ and `attachment list` (`attachment_list`, `list_attachments`) recover receipts;
|
|
|
101
101
|
pages with `--cursor`. Finalized receipts replay with the same authentic token even after expiry.
|
|
102
102
|
Duplicate SHA-256 warnings on the same Thread are best-effort; concurrent uploads may not warn.
|
|
103
103
|
|
|
104
|
+
Several files work in one command: `arbor attachment upload a.png b.png --thread-id thr_… --description "A" --description "B"`.
|
|
105
|
+
Repeated `--description` flags pair with files by order (none = filename defaults, otherwise exactly one per
|
|
106
|
+
file; write a value that starts with `--` as `--description=<value>`; values are literal, so a leading `[` is text). A blank description or a path listed twice is an error. The whole batch is checked before the
|
|
107
|
+
first request and uploads one file at a time; a mid-batch failure prints what uploaded, what failed and what
|
|
108
|
+
was not attempted, and exits non-zero. `--dry-run` prints the plan and makes no network call. Every receipt
|
|
109
|
+
carries a `nextStep` (unbound until bound to `contribute`/`respond`, max 10 per contribution; descriptions are
|
|
110
|
+
permanent) and may warn when a description is reused for a different file on the same Thread.
|
|
111
|
+
|
|
104
112
|
MCP exposes **one** tool, `attachment`, with `upload_url` / `attach` / `get` / `list` verbs.
|
|
105
113
|
Refresh a stale client tool catalog if it still shows the retired individual tools, or use
|
|
106
114
|
`arbor attachment upload`. `thread get --view full` retains body/Attachment read-back.
|
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.24.
|
|
5
|
+
version: "0.24.3",
|
|
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",
|
|
@@ -5162,6 +5162,7 @@ var ARTIFACT_TYPES = [
|
|
|
5162
5162
|
];
|
|
5163
5163
|
// ../core/src/blob/attachment-upload.ts
|
|
5164
5164
|
var MAX_ATTACHMENT_BYTES = 1e8;
|
|
5165
|
+
var ATTACHMENT_ATTACH_NEXT_STEP = "Unbound until you pass attachmentIds (max 10 per contribution) to contribute or respond. The description is permanent (it becomes the Artifact summary): to fix it before binding, re-upload and bind only the new id. Paste attachments[].markdown verbatim; don't rebuild it from the description.";
|
|
5165
5166
|
var AGENT_ATTACHMENT_MIME_TYPES = [
|
|
5166
5167
|
"image/png",
|
|
5167
5168
|
"image/jpeg",
|
|
@@ -8235,6 +8236,11 @@ var MAX_WAY_LEN = WAYS_TO_HELP_LIMITS.maxItemLength;
|
|
|
8235
8236
|
|
|
8236
8237
|
// ../core/src/ops/computer-output.ts
|
|
8237
8238
|
var COMPUTER_OUTPUT_DEFAULT_STALE_MS = 24 * 60 * 60 * 1000;
|
|
8239
|
+
var OPERATION_LOSS_REASONS = new Set([
|
|
8240
|
+
"process_provider_lost",
|
|
8241
|
+
"workspace_expired",
|
|
8242
|
+
"run_runtime_lost"
|
|
8243
|
+
]);
|
|
8238
8244
|
|
|
8239
8245
|
// ../core/src/ops/computer-run-retention.ts
|
|
8240
8246
|
var COMPUTER_RUN_RETENTION_WARNING_MS = 24 * 60 * 60 * 1000;
|
|
@@ -10328,8 +10334,19 @@ var MCP_OUTPUT_SCHEMAS = {
|
|
|
10328
10334
|
uploadedByProfileId: id,
|
|
10329
10335
|
createdAt: timestamp,
|
|
10330
10336
|
attachments: array(attachment2),
|
|
10337
|
+
replayed: boolean2().optional(),
|
|
10338
|
+
nextStep: string2().optional(),
|
|
10331
10339
|
warnings: array(string2()).optional()
|
|
10332
10340
|
}),
|
|
10341
|
+
attachment_upload: looseObject({
|
|
10342
|
+
attachmentId: id.optional(),
|
|
10343
|
+
attachmentIds: array(id),
|
|
10344
|
+
attachments: array(attachment2),
|
|
10345
|
+
nextStep: string2().optional(),
|
|
10346
|
+
warnings: array(string2()).optional(),
|
|
10347
|
+
failed: array(looseObject({ file: string2(), error: string2() })).optional(),
|
|
10348
|
+
notAttempted: array(string2()).optional()
|
|
10349
|
+
}),
|
|
10333
10350
|
computer_export: looseObject({
|
|
10334
10351
|
computerSessionId: id,
|
|
10335
10352
|
replayed: boolean2(),
|
|
@@ -10421,8 +10438,6 @@ var MCP_OUTPUT_SCHEMAS = {
|
|
|
10421
10438
|
})
|
|
10422
10439
|
};
|
|
10423
10440
|
function outputSchemaFor(actionName) {
|
|
10424
|
-
if (actionName === "attachment_upload")
|
|
10425
|
-
actionName = "attachment_attach";
|
|
10426
10441
|
return MCP_OUTPUT_SCHEMAS[actionName];
|
|
10427
10442
|
}
|
|
10428
10443
|
|
|
@@ -11287,6 +11302,17 @@ function proseList(description) {
|
|
|
11287
11302
|
function isProseList(schema) {
|
|
11288
11303
|
return PROSE_LISTS.has(schema);
|
|
11289
11304
|
}
|
|
11305
|
+
var VERBATIM_LISTS = registry();
|
|
11306
|
+
function verbatimList(description) {
|
|
11307
|
+
const schema = array(string2()).describe(description);
|
|
11308
|
+
PROSE_LISTS.add(schema, { literal: true });
|
|
11309
|
+
VERBATIM_LISTS.add(schema, { verbatim: true });
|
|
11310
|
+
return schema;
|
|
11311
|
+
}
|
|
11312
|
+
function isVerbatimList(schema) {
|
|
11313
|
+
return VERBATIM_LISTS.has(schema);
|
|
11314
|
+
}
|
|
11315
|
+
var ATTACHMENT_DESCRIPTION_HINT = "durable summary and default alt text; defaults to the filename when omitted; permanent once bound; 1-500 chars; give each file its own";
|
|
11290
11316
|
function forward(operation) {
|
|
11291
11317
|
const run = (ex, input) => ex.call(operation, input);
|
|
11292
11318
|
run.operation = operation;
|
|
@@ -12314,14 +12340,14 @@ var ACTION_DEFINITIONS = [
|
|
|
12314
12340
|
{
|
|
12315
12341
|
name: "attachment",
|
|
12316
12342
|
title: "Upload and read Thread Attachments",
|
|
12317
|
-
description: "Upload
|
|
12343
|
+
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).",
|
|
12318
12344
|
inputSchema: {
|
|
12319
12345
|
verb: _enum(["upload_url", "attach", "get", "list"]),
|
|
12320
12346
|
threadId: string2().optional().describe("upload_url/list: Thread id, thr_…"),
|
|
12321
12347
|
filename: string2().min(1).max(1000).optional().describe("upload_url: original filename"),
|
|
12322
12348
|
mimeType: _enum(AGENT_ATTACHMENT_MIME_TYPES).optional().describe("upload_url: supported MIME matching the bytes"),
|
|
12323
12349
|
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(),
|
|
12350
|
+
description: string2().min(1).max(500).optional().describe(`upload_url: ${ATTACHMENT_DESCRIPTION_HINT}`),
|
|
12325
12351
|
sha256: string2().regex(/^[a-f0-9]{64}$/).optional().describe("upload_url: optional SHA-256 hex; also signed into the PUT checksum header"),
|
|
12326
12352
|
token: string2().optional().describe("attach: token from upload_url; first attach before attachExpiresAt, replay finalized receipts even after expiry"),
|
|
12327
12353
|
attachmentId: string2().optional().describe("get: attachment id, att_…"),
|
|
@@ -12345,7 +12371,7 @@ var ACTION_DEFINITIONS = [
|
|
|
12345
12371
|
filename: string2().min(1).max(1000),
|
|
12346
12372
|
mimeType: _enum(AGENT_ATTACHMENT_MIME_TYPES),
|
|
12347
12373
|
size: number2(),
|
|
12348
|
-
description: string2().min(1).max(500).optional(),
|
|
12374
|
+
description: string2().min(1).max(500).optional().describe(ATTACHMENT_DESCRIPTION_HINT),
|
|
12349
12375
|
sha256: string2().regex(/^[a-f0-9]{64}$/).optional()
|
|
12350
12376
|
},
|
|
12351
12377
|
surfaces: ["cli"],
|
|
@@ -12381,12 +12407,25 @@ var ACTION_DEFINITIONS = [
|
|
|
12381
12407
|
},
|
|
12382
12408
|
{
|
|
12383
12409
|
name: "attachment_upload",
|
|
12384
|
-
title: "Upload
|
|
12385
|
-
description: "Upload with Node
|
|
12410
|
+
title: "Upload local Thread files",
|
|
12411
|
+
description: "Upload local files with Node or Bun (SHA-256 → presigned PUT → attach, 100 MB cap)",
|
|
12412
|
+
cliHelpLead: "Requires Node or Bun.",
|
|
12413
|
+
cliHelpNotes: [
|
|
12414
|
+
"Upload with SHA-256 via upload_url → presigned PUT → attach (100 MB cap); compress, split, or link larger files.",
|
|
12415
|
+
'Several files: `arbor attachment upload a.png b.png --thread-id thr_… --description "A" --description "B"`. Repeated --description flags pair with the files by ORDER: pass none (each defaults to its filename) or exactly one per file; a value that starts with `--` must be written --description=<value>. The whole batch is checked before the first request, then uploaded one by one; if one fails the output lists what uploaded and what failed (exit non-zero). --dry-run prints the plan and makes no network call.',
|
|
12416
|
+
`Description: ${ATTACHMENT_DESCRIPTION_HINT}. An upload is unbound until you pass its attachmentIds to contribute or respond (max 10 per contribution); to fix a description before binding, re-upload and bind only the new id. Paste attachments[].markdown verbatim; don't rebuild it from the description.`,
|
|
12417
|
+
"PUT links expire after five minutes; first attach is allowed until one hour after PUT expiry. Finalized receipts replay after expiry.",
|
|
12418
|
+
"JSON syntax checks stop at 10 MB; larger JSON gets UTF-8/control checks only, so validate syntax locally.",
|
|
12419
|
+
"js-exec lacks file descriptors, node:crypto, and binary-safe fetch; run this command with Node or Bun.",
|
|
12420
|
+
"With only curl available, use the MCP attachment tool: upload_url → returned curl PUT command → attach with the returned token.",
|
|
12421
|
+
"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."
|
|
12422
|
+
].join(`
|
|
12423
|
+
`),
|
|
12386
12424
|
inputSchema: {
|
|
12387
12425
|
threadId: string2().min(1),
|
|
12388
|
-
file:
|
|
12389
|
-
description:
|
|
12426
|
+
file: verbatimList("local file path(s), or positional files; uploaded in order; requires Node or Bun"),
|
|
12427
|
+
description: verbatimList(`${ATTACHMENT_DESCRIPTION_HINT}; repeat --description once per file, paired by order (none = filenames)`).optional(),
|
|
12428
|
+
dryRun: boolean2().optional().describe("print the upload plan (file, mime, size, sha256, description) with no network call")
|
|
12390
12429
|
},
|
|
12391
12430
|
surfaces: ["cli"],
|
|
12392
12431
|
toolset: "loop",
|
|
@@ -14667,6 +14706,19 @@ class UsageError extends Error {
|
|
|
14667
14706
|
}
|
|
14668
14707
|
}
|
|
14669
14708
|
|
|
14709
|
+
class PartialUploadError extends Error {
|
|
14710
|
+
partialResult;
|
|
14711
|
+
code;
|
|
14712
|
+
details;
|
|
14713
|
+
constructor(message, cause, partialResult) {
|
|
14714
|
+
super(message);
|
|
14715
|
+
this.partialResult = partialResult;
|
|
14716
|
+
this.name = "PartialUploadError";
|
|
14717
|
+
this.code = codeForError(cause);
|
|
14718
|
+
this.details = detailsForError(cause);
|
|
14719
|
+
}
|
|
14720
|
+
}
|
|
14721
|
+
|
|
14670
14722
|
// src/config.ts
|
|
14671
14723
|
var PROD_API_URL = "https://arborthreads.com";
|
|
14672
14724
|
var PROFILE_NAME = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/;
|
|
@@ -15041,7 +15093,7 @@ var httpExecutor = {
|
|
|
15041
15093
|
};
|
|
15042
15094
|
|
|
15043
15095
|
// src/attachment-upload.ts
|
|
15044
|
-
import { basename } from "node:path";
|
|
15096
|
+
import { basename, resolve } from "node:path";
|
|
15045
15097
|
function checkFile(stat) {
|
|
15046
15098
|
const isFile = typeof stat.isFile === "function" ? stat.isFile() : stat.isFile;
|
|
15047
15099
|
if (!isFile)
|
|
@@ -15051,8 +15103,7 @@ function checkFile(stat) {
|
|
|
15051
15103
|
if (!stat.size)
|
|
15052
15104
|
throw new UsageError("Attachment file is empty (0 bytes); choose a non-empty file");
|
|
15053
15105
|
}
|
|
15054
|
-
async function
|
|
15055
|
-
const file = String(input.file);
|
|
15106
|
+
async function preflightFile(file) {
|
|
15056
15107
|
const filename = basename(file);
|
|
15057
15108
|
const fs = await import("node:fs");
|
|
15058
15109
|
const initialStat = fs.statSync(file);
|
|
@@ -15060,8 +15111,11 @@ async function uploadLocalAttachment(executor, input, fetcher = fetch) {
|
|
|
15060
15111
|
const mimeType = localAttachmentMime(filename);
|
|
15061
15112
|
if (!mimeType)
|
|
15062
15113
|
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 (
|
|
15064
|
-
throw new UsageError("Attachment upload requires Node:
|
|
15114
|
+
if ([fs.openSync, fs.fstatSync, fs.readSync, fs.closeSync].some((api) => typeof api !== "function"))
|
|
15115
|
+
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.");
|
|
15116
|
+
return { fs, local: { file, filename, mimeType, size: initialStat.size } };
|
|
15117
|
+
}
|
|
15118
|
+
async function putLocalAttachment(executor, fs, { file, filename, mimeType }, input, fetcher) {
|
|
15065
15119
|
const descriptor = fs.openSync(file, "r");
|
|
15066
15120
|
let bytes;
|
|
15067
15121
|
try {
|
|
@@ -15107,6 +15161,135 @@ async function uploadLocalAttachment(executor, input, fetcher = fetch) {
|
|
|
15107
15161
|
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
15162
|
return executor.call("attachment.attach", { token: link.token });
|
|
15109
15163
|
}
|
|
15164
|
+
function normalizeDescription(value) {
|
|
15165
|
+
return value.trim().replace(/\s+/g, " ");
|
|
15166
|
+
}
|
|
15167
|
+
function asList(value) {
|
|
15168
|
+
if (value === undefined)
|
|
15169
|
+
return [];
|
|
15170
|
+
return (Array.isArray(value) ? value : [value]).map(String);
|
|
15171
|
+
}
|
|
15172
|
+
async function planBatch(input) {
|
|
15173
|
+
const files = asList(input.file);
|
|
15174
|
+
const descriptions = asList(input.description);
|
|
15175
|
+
if (files.length === 0)
|
|
15176
|
+
throw new UsageError("missing required flag: --file");
|
|
15177
|
+
if (descriptions.length > 0 && descriptions.length !== files.length) {
|
|
15178
|
+
throw new UsageError(files.length > 1 && descriptions.length === 1 ? `One --description for ${files.length} files would give every file the same permanent description. Pass one --description per file in file order, or none to default each to its filename.` : `Got ${descriptions.length} --description flag${descriptions.length === 1 ? "" : "s"} for ${files.length} file${files.length === 1 ? "" : "s"}; descriptions pair with files by order, so pass none or exactly one per file.`, { reason: "validation.invalid_value", field: "description" });
|
|
15179
|
+
}
|
|
15180
|
+
for (const description of descriptions) {
|
|
15181
|
+
const length = normalizeDescription(description).length;
|
|
15182
|
+
if (length < 1)
|
|
15183
|
+
throw new UsageError("--description is blank; give each file a real description, or omit --description to default it to the filename", { reason: "validation.invalid_value", field: "description" });
|
|
15184
|
+
if (length > 500)
|
|
15185
|
+
throw new UsageError("--description must be 1–500 characters", {
|
|
15186
|
+
reason: "validation.invalid_value",
|
|
15187
|
+
field: "description"
|
|
15188
|
+
});
|
|
15189
|
+
}
|
|
15190
|
+
const paths = new Map;
|
|
15191
|
+
for (const file of files) {
|
|
15192
|
+
if (!file.trim())
|
|
15193
|
+
throw new UsageError("--file is blank; pass a path to a local file");
|
|
15194
|
+
const key = resolve(file);
|
|
15195
|
+
const first = paths.get(key);
|
|
15196
|
+
if (first !== undefined)
|
|
15197
|
+
throw new UsageError(`${file} is listed twice${first === file ? "" : ` (as ${first})`}; each file uploads once, so list it once.`, { reason: "validation.invalid_value", field: "file" });
|
|
15198
|
+
paths.set(key, file);
|
|
15199
|
+
}
|
|
15200
|
+
let fs;
|
|
15201
|
+
const plan = [];
|
|
15202
|
+
for (const [index, file] of files.entries()) {
|
|
15203
|
+
const prepared = await preflightFile(file);
|
|
15204
|
+
fs = prepared.fs;
|
|
15205
|
+
plan.push({ local: prepared.local, description: descriptions[index] });
|
|
15206
|
+
}
|
|
15207
|
+
const seen = new Map;
|
|
15208
|
+
for (const { local, description } of plan) {
|
|
15209
|
+
if (description === undefined)
|
|
15210
|
+
continue;
|
|
15211
|
+
const key = normalizeDescription(description);
|
|
15212
|
+
const first = seen.get(key);
|
|
15213
|
+
if (first !== undefined)
|
|
15214
|
+
throw new UsageError(`Duplicate --description "${key}" for ${first} and ${local.file}; descriptions are permanent, so give each file its own.`, { reason: "validation.invalid_value", field: "description" });
|
|
15215
|
+
seen.set(key, local.file);
|
|
15216
|
+
}
|
|
15217
|
+
return { fs, plan };
|
|
15218
|
+
}
|
|
15219
|
+
function sha256File(fs, file, createHash) {
|
|
15220
|
+
const hash = createHash("sha256");
|
|
15221
|
+
const chunk = Buffer.alloc(1048576);
|
|
15222
|
+
const descriptor = fs.openSync(file, "r");
|
|
15223
|
+
try {
|
|
15224
|
+
for (;; ) {
|
|
15225
|
+
const count = fs.readSync(descriptor, chunk, 0, chunk.length, null);
|
|
15226
|
+
if (!count)
|
|
15227
|
+
break;
|
|
15228
|
+
hash.update(chunk.subarray(0, count));
|
|
15229
|
+
}
|
|
15230
|
+
} finally {
|
|
15231
|
+
fs.closeSync(descriptor);
|
|
15232
|
+
}
|
|
15233
|
+
return hash.digest("hex");
|
|
15234
|
+
}
|
|
15235
|
+
async function dryRunPlan(fs, threadId, plan) {
|
|
15236
|
+
const { createHash } = await import("node:crypto");
|
|
15237
|
+
return {
|
|
15238
|
+
dryRun: true,
|
|
15239
|
+
threadId,
|
|
15240
|
+
files: plan.map(({ local, description }) => ({
|
|
15241
|
+
file: local.file,
|
|
15242
|
+
filename: local.filename,
|
|
15243
|
+
mimeType: local.mimeType,
|
|
15244
|
+
size: local.size,
|
|
15245
|
+
sha256: sha256File(fs, local.file, createHash),
|
|
15246
|
+
description: description ?? local.filename,
|
|
15247
|
+
descriptionSource: description === undefined ? "filename default" : "explicit",
|
|
15248
|
+
status: "unbound until attached"
|
|
15249
|
+
}))
|
|
15250
|
+
};
|
|
15251
|
+
}
|
|
15252
|
+
function record3(value) {
|
|
15253
|
+
return value !== null && typeof value === "object" ? value : {};
|
|
15254
|
+
}
|
|
15255
|
+
function aggregateReceipt(uploaded, extra = {}) {
|
|
15256
|
+
const warnings = uploaded.flatMap(({ local, receipt }) => asList(record3(receipt).warnings).map((warning) => `${local.filename}: ${warning}`));
|
|
15257
|
+
return {
|
|
15258
|
+
attachmentIds: uploaded.flatMap(({ receipt }) => {
|
|
15259
|
+
const r = record3(receipt);
|
|
15260
|
+
return Array.isArray(r.attachmentIds) ? r.attachmentIds : typeof r.attachmentId === "string" ? [r.attachmentId] : [];
|
|
15261
|
+
}),
|
|
15262
|
+
attachments: uploaded.flatMap(({ receipt }) => {
|
|
15263
|
+
const attachments = record3(receipt).attachments;
|
|
15264
|
+
return Array.isArray(attachments) ? attachments : [];
|
|
15265
|
+
}),
|
|
15266
|
+
nextStep: ATTACHMENT_ATTACH_NEXT_STEP,
|
|
15267
|
+
...warnings.length > 0 ? { warnings } : {},
|
|
15268
|
+
...extra
|
|
15269
|
+
};
|
|
15270
|
+
}
|
|
15271
|
+
async function uploadLocalAttachments(executor, input, fetcher = fetch) {
|
|
15272
|
+
const { fs, plan } = await planBatch(input);
|
|
15273
|
+
if (input.dryRun === true)
|
|
15274
|
+
return dryRunPlan(fs, String(input.threadId), plan);
|
|
15275
|
+
const upload = ({ local, description }) => putLocalAttachment(executor, fs, local, { threadId: input.threadId, ...description !== undefined ? { description } : {} }, fetcher);
|
|
15276
|
+
if (plan.length === 1)
|
|
15277
|
+
return upload(plan[0]);
|
|
15278
|
+
const uploaded = [];
|
|
15279
|
+
for (const [index, item] of plan.entries()) {
|
|
15280
|
+
try {
|
|
15281
|
+
uploaded.push({ local: item.local, receipt: await upload(item) });
|
|
15282
|
+
} catch (cause) {
|
|
15283
|
+
const reason = cause instanceof Error ? cause.message : String(cause);
|
|
15284
|
+
const notAttempted = plan.slice(index + 1).map(({ local }) => local.file);
|
|
15285
|
+
throw new PartialUploadError(`Uploaded ${uploaded.length} of ${plan.length} files; ${item.local.file} failed: ${reason.replace(/\.$/, "")}. Uploaded files stay valid but unbound; re-run only the failed and not-attempted files.`, cause, aggregateReceipt(uploaded, {
|
|
15286
|
+
failed: [{ file: item.local.file, error: reason }],
|
|
15287
|
+
notAttempted
|
|
15288
|
+
}));
|
|
15289
|
+
}
|
|
15290
|
+
}
|
|
15291
|
+
return aggregateReceipt(uploaded);
|
|
15292
|
+
}
|
|
15110
15293
|
|
|
15111
15294
|
// src/output.ts
|
|
15112
15295
|
function writeOutput(value, stream, fallback) {
|
|
@@ -15198,15 +15381,20 @@ function emitData(data, _action, ctx) {
|
|
|
15198
15381
|
function emitError(err, _action, ctx) {
|
|
15199
15382
|
const code = codeForError(err);
|
|
15200
15383
|
const message = err instanceof Error ? err.message : String(err);
|
|
15384
|
+
const partial = err instanceof PartialUploadError ? err.partialResult : undefined;
|
|
15201
15385
|
if (ctx.json) {
|
|
15202
15386
|
const envelope = {
|
|
15203
15387
|
ok: false,
|
|
15204
|
-
error: { code, message, ...detailsForError(err) }
|
|
15388
|
+
error: { code, message, ...detailsForError(err) },
|
|
15389
|
+
...partial !== undefined ? { data: partial } : {}
|
|
15205
15390
|
};
|
|
15206
15391
|
writeStdout(`${JSON.stringify(envelope)}
|
|
15207
15392
|
`);
|
|
15208
15393
|
process.exitCode = exitCodeForError(code);
|
|
15209
15394
|
} else {
|
|
15395
|
+
if (partial !== undefined)
|
|
15396
|
+
writeStdout(`${JSON.stringify(partial, null, 2)}
|
|
15397
|
+
`);
|
|
15210
15398
|
writeStderr(`✗ ${message}
|
|
15211
15399
|
`);
|
|
15212
15400
|
process.exitCode = 1;
|
|
@@ -15228,7 +15416,7 @@ var DEFAULT_PROCESS_DEADLINE_MS = 1800000;
|
|
|
15228
15416
|
var PROCESS_OUTPUT_PAGE_BYTES = 4096;
|
|
15229
15417
|
var POLL_MS = 400;
|
|
15230
15418
|
var processRead = ACTIONS3.find((action) => action.name === "computer_process_read");
|
|
15231
|
-
function
|
|
15419
|
+
function record4(value) {
|
|
15232
15420
|
return value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
15233
15421
|
}
|
|
15234
15422
|
function isRunning(value) {
|
|
@@ -15256,7 +15444,7 @@ function sleep(ms) {
|
|
|
15256
15444
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
15257
15445
|
}
|
|
15258
15446
|
function isComputerExecHandle(value) {
|
|
15259
|
-
const rec =
|
|
15447
|
+
const rec = record4(value);
|
|
15260
15448
|
return Boolean(rec && typeof rec.processId === "string" && (isRunning(rec) || rec.truncated === true));
|
|
15261
15449
|
}
|
|
15262
15450
|
async function waitForComputerExec(executor, input, value, ctx) {
|
|
@@ -15271,7 +15459,7 @@ async function waitForComputerExec(executor, input, value, ctx) {
|
|
|
15271
15459
|
`, ctx);
|
|
15272
15460
|
while (isRunning(latest) && Date.now() < deadline) {
|
|
15273
15461
|
await sleep(POLL_MS);
|
|
15274
|
-
const read =
|
|
15462
|
+
const read = record4(await processRead.run(executor, {
|
|
15275
15463
|
processId: latest.processId,
|
|
15276
15464
|
maxBytes: PROCESS_OUTPUT_PAGE_BYTES,
|
|
15277
15465
|
...cursor ? { cursor } : {},
|
|
@@ -15345,6 +15533,7 @@ function kindOf(node) {
|
|
|
15345
15533
|
return "string";
|
|
15346
15534
|
}
|
|
15347
15535
|
}
|
|
15536
|
+
var BARE_VALUE_HINT = (flag) => ` (a value that starts with "--" must be written --${flag}=<value>)`;
|
|
15348
15537
|
function acceptedFlags(inputSchema) {
|
|
15349
15538
|
const out = new Set;
|
|
15350
15539
|
for (const spec of flagsForSchema(inputSchema)) {
|
|
@@ -15377,7 +15566,8 @@ function flagsForSchema(inputSchema) {
|
|
|
15377
15566
|
required: !optional,
|
|
15378
15567
|
nullable,
|
|
15379
15568
|
description: description(inner) ?? description(raw),
|
|
15380
|
-
literal: isProseList(inner)
|
|
15569
|
+
literal: isProseList(inner),
|
|
15570
|
+
verbatim: isVerbatimList(inner)
|
|
15381
15571
|
};
|
|
15382
15572
|
});
|
|
15383
15573
|
}
|
|
@@ -15418,7 +15608,7 @@ function buildInput(inputSchema, flags, command) {
|
|
|
15418
15608
|
continue;
|
|
15419
15609
|
}
|
|
15420
15610
|
if (spec.kind !== "boolean" && raw === true) {
|
|
15421
|
-
throw new UsageError(`--${spec.flag} expects a value`, {
|
|
15611
|
+
throw new UsageError(`--${spec.flag} expects a value${BARE_VALUE_HINT(spec.flag)}`, {
|
|
15422
15612
|
reason: "validation.missing_field",
|
|
15423
15613
|
field: spec.field
|
|
15424
15614
|
});
|
|
@@ -15441,6 +15631,10 @@ function buildInput(inputSchema, flags, command) {
|
|
|
15441
15631
|
case "array": {
|
|
15442
15632
|
const repeated = Array.isArray(raw);
|
|
15443
15633
|
const tokens = repeated ? raw : [String(raw)];
|
|
15634
|
+
if (spec.verbatim) {
|
|
15635
|
+
input[spec.field] = tokens.map(String);
|
|
15636
|
+
break;
|
|
15637
|
+
}
|
|
15444
15638
|
const out = [];
|
|
15445
15639
|
for (const token of tokens) {
|
|
15446
15640
|
const text = String(token).trim();
|
|
@@ -15696,6 +15890,7 @@ var POSITIONAL_FIELDS = {
|
|
|
15696
15890
|
code_implementations: ["namePath", "path"],
|
|
15697
15891
|
code_diagnostics: ["path"]
|
|
15698
15892
|
};
|
|
15893
|
+
var VARIADIC_LAST_POSITIONAL = new Set(["attachment_upload"]);
|
|
15699
15894
|
function positionalSpecs(action) {
|
|
15700
15895
|
const fields = POSITIONAL_FIELDS[action.name];
|
|
15701
15896
|
if (!fields)
|
|
@@ -15783,7 +15978,11 @@ function toolNameAliases(action) {
|
|
|
15783
15978
|
function helpLine(action) {
|
|
15784
15979
|
const positionals = positionalSpecs(action);
|
|
15785
15980
|
const positionalFlags = new Set(positionals.map((spec) => spec.flag));
|
|
15786
|
-
const args = positionals.map((spec) =>
|
|
15981
|
+
const args = positionals.map((spec, index) => {
|
|
15982
|
+
const variadic = VARIADIC_LAST_POSITIONAL.has(action.name) && index === positionals.length - 1;
|
|
15983
|
+
const name = `${spec.field}${variadic ? ">..." : ">"}`;
|
|
15984
|
+
return spec.required ? ` <${name}` : ` [<${name}]`;
|
|
15985
|
+
}).join("");
|
|
15787
15986
|
const flags = flagsForSchema(action.inputSchema).filter((spec) => !positionalFlags.has(spec.flag)).map((f) => {
|
|
15788
15987
|
const token = f.kind === "boolean" ? `--${f.flag}` : `--${f.flag} <>`;
|
|
15789
15988
|
return f.required ? token : `[${token}]`;
|
|
@@ -15809,7 +16008,11 @@ function commandHelp(positionals) {
|
|
|
15809
16008
|
const token = f.kind === "boolean" ? `--${f.flag}` : `--${f.flag} <${f.kind}>`;
|
|
15810
16009
|
return f.required ? token : `[${token}]`;
|
|
15811
16010
|
}).join(" ");
|
|
15812
|
-
const usageArgs = helpPositionals.map((spec) =>
|
|
16011
|
+
const usageArgs = helpPositionals.map((spec, index) => {
|
|
16012
|
+
const variadic = VARIADIC_LAST_POSITIONAL.has(action.name) && index === helpPositionals.length - 1;
|
|
16013
|
+
const name = `${spec.field}${variadic ? ">..." : ">"}`;
|
|
16014
|
+
return spec.required ? ` <${name}` : ` [<${name}]`;
|
|
16015
|
+
}).join("");
|
|
15813
16016
|
const usage = `Usage: arbor ${commandWords(action)}${usageArgs}${usageFlags ? ` ${usageFlags}` : ""}`;
|
|
15814
16017
|
const lines = specs.map((f) => {
|
|
15815
16018
|
const positional = helpPositionals.find((spec) => spec.flag === f.flag);
|
|
@@ -15838,12 +16041,24 @@ Aliases: ${aliases.map((alias) => `\`arbor ${alias}\``).join(", ")} (tool-name f
|
|
|
15838
16041
|
const waitLines = action.name === "computer_exec" ? [
|
|
15839
16042
|
" --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"
|
|
15840
16043
|
] : [];
|
|
16044
|
+
const notes = action.cliHelpNotes ? `
|
|
16045
|
+
|
|
16046
|
+
${action.cliHelpNotes}` : "";
|
|
16047
|
+
if (action.cliHelpLead) {
|
|
16048
|
+
return `${usage}
|
|
16049
|
+
|
|
16050
|
+
${action.cliHelpLead}
|
|
16051
|
+
|
|
16052
|
+
${lines.join(`
|
|
16053
|
+
`)}${primaryNote}${explicitNote}${aliasNote}${notes}
|
|
16054
|
+
`;
|
|
16055
|
+
}
|
|
15841
16056
|
return `${heading} — ${action.description}
|
|
15842
16057
|
|
|
15843
16058
|
${usage}${waitLines.length > 0 ? " [--no-wait]" : ""}
|
|
15844
16059
|
|
|
15845
16060
|
${[...lines, ...waitLines].join(`
|
|
15846
|
-
`)}${primaryNote}${explicitNote}${aliasNote}
|
|
16061
|
+
`)}${primaryNote}${explicitNote}${aliasNote}${notes}
|
|
15847
16062
|
`;
|
|
15848
16063
|
}
|
|
15849
16064
|
function commandCatalog() {
|
|
@@ -15879,10 +16094,12 @@ function applyPositionals(action, extras, flags) {
|
|
|
15879
16094
|
if (positionalTargets.length === 0) {
|
|
15880
16095
|
throw new UsageError(`unexpected argument: ${extras[0]} (\`${commandWords(action)}\` takes no positional — use flags)`);
|
|
15881
16096
|
}
|
|
15882
|
-
|
|
16097
|
+
const fixed = positionalTargets.length - 1;
|
|
16098
|
+
const values = VARIADIC_LAST_POSITIONAL.has(action.name) && extras.length > fixed ? [...extras.slice(0, fixed), extras.slice(fixed)] : extras;
|
|
16099
|
+
if (values.length > positionalTargets.length) {
|
|
15883
16100
|
throw new UsageError(`too many arguments: \`${commandWords(action)}\` takes ${positionalTargets.length === 1 ? "one positional" : `${positionalTargets.length} positionals`}; pass the rest as flags`);
|
|
15884
16101
|
}
|
|
15885
|
-
for (const [index, value] of
|
|
16102
|
+
for (const [index, value] of values.entries()) {
|
|
15886
16103
|
const target = positionalTargets[index];
|
|
15887
16104
|
if (flags[target.flag] !== undefined || flags[`${target.flag}-file`] !== undefined) {
|
|
15888
16105
|
throw new UsageError(`pass --${target.flag} either as a flag or as the positional argument, not both`);
|
|
@@ -15920,8 +16137,16 @@ async function runObjectVerb(positionals, flags, ctx) {
|
|
|
15920
16137
|
const unknown = Object.keys(flags).filter((f) => !accepted.has(f) && !RESERVED_FLAGS.has(f));
|
|
15921
16138
|
if (unknown.length > 0) {
|
|
15922
16139
|
const hints = unknown.map((f) => listFileHint(action, f)).filter(Boolean);
|
|
16140
|
+
const bare = flagsForSchema(action.inputSchema).find((spec) => spec.kind !== "boolean" && flags[spec.flag] === true);
|
|
16141
|
+
if (bare)
|
|
16142
|
+
hints.push(`--${bare.flag} has no value${BARE_VALUE_HINT(bare.flag)}`);
|
|
15923
16143
|
throw new UsageError(`unknown flag${unknown.length > 1 ? "s" : ""}: ${unknown.map((f) => `--${f}`).join(", ")}${hints.length > 0 ? ` — ${hints.join("; ")}` : ""}`);
|
|
15924
16144
|
}
|
|
16145
|
+
if (action.name === "attachment_upload") {
|
|
16146
|
+
const dryRun = lastValue(flags["dry-run"]);
|
|
16147
|
+
if (typeof dryRun === "string" && dryRun !== "true" && dryRun !== "false")
|
|
16148
|
+
throw new UsageError(`--dry-run takes no value but swallowed "${dryRun}"; put --dry-run after the files or write --dry-run=true`, { reason: "validation.invalid_value", field: "dryRun" });
|
|
16149
|
+
}
|
|
15925
16150
|
const inputFlags = { ...flags };
|
|
15926
16151
|
if (action.name === "app_request" && lastValue(inputFlags.json) === true) {
|
|
15927
16152
|
delete inputFlags.json;
|
|
@@ -15958,7 +16183,7 @@ async function runObjectVerb(positionals, flags, ctx) {
|
|
|
15958
16183
|
advise(`Agent registered. Save its apiKey now — it is shown only once:
|
|
15959
16184
|
`, ctx);
|
|
15960
16185
|
}
|
|
15961
|
-
let value = action.name === "attachment_upload" ? await
|
|
16186
|
+
let value = action.name === "attachment_upload" ? await uploadLocalAttachments(httpExecutor, input) : await action.run(httpExecutor, input);
|
|
15962
16187
|
if (action.name === "computer_exec" && !truthyFlag(flags["no-wait"]) && input.waitForTerminalMs === undefined && input.background !== true) {
|
|
15963
16188
|
value = await waitForComputerExec(httpExecutor, input, value, ctx);
|
|
15964
16189
|
}
|
package/package.json
CHANGED