@lambdacurry/arbor 0.24.2 → 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 +226 -23
- 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;
|
|
@@ -12321,7 +12347,7 @@ var ACTION_DEFINITIONS = [
|
|
|
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,11 +12407,13 @@ var ACTION_DEFINITIONS = [
|
|
|
12381
12407
|
},
|
|
12382
12408
|
{
|
|
12383
12409
|
name: "attachment_upload",
|
|
12384
|
-
title: "Upload
|
|
12385
|
-
description: "Upload
|
|
12410
|
+
title: "Upload local Thread files",
|
|
12411
|
+
description: "Upload local files with Node or Bun (SHA-256 → presigned PUT → attach, 100 MB cap)",
|
|
12386
12412
|
cliHelpLead: "Requires Node or Bun.",
|
|
12387
12413
|
cliHelpNotes: [
|
|
12388
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.`,
|
|
12389
12417
|
"PUT links expire after five minutes; first attach is allowed until one hour after PUT expiry. Finalized receipts replay after expiry.",
|
|
12390
12418
|
"JSON syntax checks stop at 10 MB; larger JSON gets UTF-8/control checks only, so validate syntax locally.",
|
|
12391
12419
|
"js-exec lacks file descriptors, node:crypto, and binary-safe fetch; run this command with Node or Bun.",
|
|
@@ -12395,8 +12423,9 @@ var ACTION_DEFINITIONS = [
|
|
|
12395
12423
|
`),
|
|
12396
12424
|
inputSchema: {
|
|
12397
12425
|
threadId: string2().min(1),
|
|
12398
|
-
file:
|
|
12399
|
-
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")
|
|
12400
12429
|
},
|
|
12401
12430
|
surfaces: ["cli"],
|
|
12402
12431
|
toolset: "loop",
|
|
@@ -14677,6 +14706,19 @@ class UsageError extends Error {
|
|
|
14677
14706
|
}
|
|
14678
14707
|
}
|
|
14679
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
|
+
|
|
14680
14722
|
// src/config.ts
|
|
14681
14723
|
var PROD_API_URL = "https://arborthreads.com";
|
|
14682
14724
|
var PROFILE_NAME = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/;
|
|
@@ -15051,7 +15093,7 @@ var httpExecutor = {
|
|
|
15051
15093
|
};
|
|
15052
15094
|
|
|
15053
15095
|
// src/attachment-upload.ts
|
|
15054
|
-
import { basename } from "node:path";
|
|
15096
|
+
import { basename, resolve } from "node:path";
|
|
15055
15097
|
function checkFile(stat) {
|
|
15056
15098
|
const isFile = typeof stat.isFile === "function" ? stat.isFile() : stat.isFile;
|
|
15057
15099
|
if (!isFile)
|
|
@@ -15061,8 +15103,7 @@ function checkFile(stat) {
|
|
|
15061
15103
|
if (!stat.size)
|
|
15062
15104
|
throw new UsageError("Attachment file is empty (0 bytes); choose a non-empty file");
|
|
15063
15105
|
}
|
|
15064
|
-
async function
|
|
15065
|
-
const file = String(input.file);
|
|
15106
|
+
async function preflightFile(file) {
|
|
15066
15107
|
const filename = basename(file);
|
|
15067
15108
|
const fs = await import("node:fs");
|
|
15068
15109
|
const initialStat = fs.statSync(file);
|
|
@@ -15072,6 +15113,9 @@ async function uploadLocalAttachment(executor, input, fetcher = fetch) {
|
|
|
15072
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.`);
|
|
15073
15114
|
if ([fs.openSync, fs.fstatSync, fs.readSync, fs.closeSync].some((api) => typeof api !== "function"))
|
|
15074
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) {
|
|
15075
15119
|
const descriptor = fs.openSync(file, "r");
|
|
15076
15120
|
let bytes;
|
|
15077
15121
|
try {
|
|
@@ -15117,6 +15161,135 @@ async function uploadLocalAttachment(executor, input, fetcher = fetch) {
|
|
|
15117
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);
|
|
15118
15162
|
return executor.call("attachment.attach", { token: link.token });
|
|
15119
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
|
+
}
|
|
15120
15293
|
|
|
15121
15294
|
// src/output.ts
|
|
15122
15295
|
function writeOutput(value, stream, fallback) {
|
|
@@ -15208,15 +15381,20 @@ function emitData(data, _action, ctx) {
|
|
|
15208
15381
|
function emitError(err, _action, ctx) {
|
|
15209
15382
|
const code = codeForError(err);
|
|
15210
15383
|
const message = err instanceof Error ? err.message : String(err);
|
|
15384
|
+
const partial = err instanceof PartialUploadError ? err.partialResult : undefined;
|
|
15211
15385
|
if (ctx.json) {
|
|
15212
15386
|
const envelope = {
|
|
15213
15387
|
ok: false,
|
|
15214
|
-
error: { code, message, ...detailsForError(err) }
|
|
15388
|
+
error: { code, message, ...detailsForError(err) },
|
|
15389
|
+
...partial !== undefined ? { data: partial } : {}
|
|
15215
15390
|
};
|
|
15216
15391
|
writeStdout(`${JSON.stringify(envelope)}
|
|
15217
15392
|
`);
|
|
15218
15393
|
process.exitCode = exitCodeForError(code);
|
|
15219
15394
|
} else {
|
|
15395
|
+
if (partial !== undefined)
|
|
15396
|
+
writeStdout(`${JSON.stringify(partial, null, 2)}
|
|
15397
|
+
`);
|
|
15220
15398
|
writeStderr(`✗ ${message}
|
|
15221
15399
|
`);
|
|
15222
15400
|
process.exitCode = 1;
|
|
@@ -15238,7 +15416,7 @@ var DEFAULT_PROCESS_DEADLINE_MS = 1800000;
|
|
|
15238
15416
|
var PROCESS_OUTPUT_PAGE_BYTES = 4096;
|
|
15239
15417
|
var POLL_MS = 400;
|
|
15240
15418
|
var processRead = ACTIONS3.find((action) => action.name === "computer_process_read");
|
|
15241
|
-
function
|
|
15419
|
+
function record4(value) {
|
|
15242
15420
|
return value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
15243
15421
|
}
|
|
15244
15422
|
function isRunning(value) {
|
|
@@ -15266,7 +15444,7 @@ function sleep(ms) {
|
|
|
15266
15444
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
15267
15445
|
}
|
|
15268
15446
|
function isComputerExecHandle(value) {
|
|
15269
|
-
const rec =
|
|
15447
|
+
const rec = record4(value);
|
|
15270
15448
|
return Boolean(rec && typeof rec.processId === "string" && (isRunning(rec) || rec.truncated === true));
|
|
15271
15449
|
}
|
|
15272
15450
|
async function waitForComputerExec(executor, input, value, ctx) {
|
|
@@ -15281,7 +15459,7 @@ async function waitForComputerExec(executor, input, value, ctx) {
|
|
|
15281
15459
|
`, ctx);
|
|
15282
15460
|
while (isRunning(latest) && Date.now() < deadline) {
|
|
15283
15461
|
await sleep(POLL_MS);
|
|
15284
|
-
const read =
|
|
15462
|
+
const read = record4(await processRead.run(executor, {
|
|
15285
15463
|
processId: latest.processId,
|
|
15286
15464
|
maxBytes: PROCESS_OUTPUT_PAGE_BYTES,
|
|
15287
15465
|
...cursor ? { cursor } : {},
|
|
@@ -15355,6 +15533,7 @@ function kindOf(node) {
|
|
|
15355
15533
|
return "string";
|
|
15356
15534
|
}
|
|
15357
15535
|
}
|
|
15536
|
+
var BARE_VALUE_HINT = (flag) => ` (a value that starts with "--" must be written --${flag}=<value>)`;
|
|
15358
15537
|
function acceptedFlags(inputSchema) {
|
|
15359
15538
|
const out = new Set;
|
|
15360
15539
|
for (const spec of flagsForSchema(inputSchema)) {
|
|
@@ -15387,7 +15566,8 @@ function flagsForSchema(inputSchema) {
|
|
|
15387
15566
|
required: !optional,
|
|
15388
15567
|
nullable,
|
|
15389
15568
|
description: description(inner) ?? description(raw),
|
|
15390
|
-
literal: isProseList(inner)
|
|
15569
|
+
literal: isProseList(inner),
|
|
15570
|
+
verbatim: isVerbatimList(inner)
|
|
15391
15571
|
};
|
|
15392
15572
|
});
|
|
15393
15573
|
}
|
|
@@ -15428,7 +15608,7 @@ function buildInput(inputSchema, flags, command) {
|
|
|
15428
15608
|
continue;
|
|
15429
15609
|
}
|
|
15430
15610
|
if (spec.kind !== "boolean" && raw === true) {
|
|
15431
|
-
throw new UsageError(`--${spec.flag} expects a value`, {
|
|
15611
|
+
throw new UsageError(`--${spec.flag} expects a value${BARE_VALUE_HINT(spec.flag)}`, {
|
|
15432
15612
|
reason: "validation.missing_field",
|
|
15433
15613
|
field: spec.field
|
|
15434
15614
|
});
|
|
@@ -15451,6 +15631,10 @@ function buildInput(inputSchema, flags, command) {
|
|
|
15451
15631
|
case "array": {
|
|
15452
15632
|
const repeated = Array.isArray(raw);
|
|
15453
15633
|
const tokens = repeated ? raw : [String(raw)];
|
|
15634
|
+
if (spec.verbatim) {
|
|
15635
|
+
input[spec.field] = tokens.map(String);
|
|
15636
|
+
break;
|
|
15637
|
+
}
|
|
15454
15638
|
const out = [];
|
|
15455
15639
|
for (const token of tokens) {
|
|
15456
15640
|
const text = String(token).trim();
|
|
@@ -15706,6 +15890,7 @@ var POSITIONAL_FIELDS = {
|
|
|
15706
15890
|
code_implementations: ["namePath", "path"],
|
|
15707
15891
|
code_diagnostics: ["path"]
|
|
15708
15892
|
};
|
|
15893
|
+
var VARIADIC_LAST_POSITIONAL = new Set(["attachment_upload"]);
|
|
15709
15894
|
function positionalSpecs(action) {
|
|
15710
15895
|
const fields = POSITIONAL_FIELDS[action.name];
|
|
15711
15896
|
if (!fields)
|
|
@@ -15793,7 +15978,11 @@ function toolNameAliases(action) {
|
|
|
15793
15978
|
function helpLine(action) {
|
|
15794
15979
|
const positionals = positionalSpecs(action);
|
|
15795
15980
|
const positionalFlags = new Set(positionals.map((spec) => spec.flag));
|
|
15796
|
-
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("");
|
|
15797
15986
|
const flags = flagsForSchema(action.inputSchema).filter((spec) => !positionalFlags.has(spec.flag)).map((f) => {
|
|
15798
15987
|
const token = f.kind === "boolean" ? `--${f.flag}` : `--${f.flag} <>`;
|
|
15799
15988
|
return f.required ? token : `[${token}]`;
|
|
@@ -15819,7 +16008,11 @@ function commandHelp(positionals) {
|
|
|
15819
16008
|
const token = f.kind === "boolean" ? `--${f.flag}` : `--${f.flag} <${f.kind}>`;
|
|
15820
16009
|
return f.required ? token : `[${token}]`;
|
|
15821
16010
|
}).join(" ");
|
|
15822
|
-
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("");
|
|
15823
16016
|
const usage = `Usage: arbor ${commandWords(action)}${usageArgs}${usageFlags ? ` ${usageFlags}` : ""}`;
|
|
15824
16017
|
const lines = specs.map((f) => {
|
|
15825
16018
|
const positional = helpPositionals.find((spec) => spec.flag === f.flag);
|
|
@@ -15901,10 +16094,12 @@ function applyPositionals(action, extras, flags) {
|
|
|
15901
16094
|
if (positionalTargets.length === 0) {
|
|
15902
16095
|
throw new UsageError(`unexpected argument: ${extras[0]} (\`${commandWords(action)}\` takes no positional — use flags)`);
|
|
15903
16096
|
}
|
|
15904
|
-
|
|
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) {
|
|
15905
16100
|
throw new UsageError(`too many arguments: \`${commandWords(action)}\` takes ${positionalTargets.length === 1 ? "one positional" : `${positionalTargets.length} positionals`}; pass the rest as flags`);
|
|
15906
16101
|
}
|
|
15907
|
-
for (const [index, value] of
|
|
16102
|
+
for (const [index, value] of values.entries()) {
|
|
15908
16103
|
const target = positionalTargets[index];
|
|
15909
16104
|
if (flags[target.flag] !== undefined || flags[`${target.flag}-file`] !== undefined) {
|
|
15910
16105
|
throw new UsageError(`pass --${target.flag} either as a flag or as the positional argument, not both`);
|
|
@@ -15942,8 +16137,16 @@ async function runObjectVerb(positionals, flags, ctx) {
|
|
|
15942
16137
|
const unknown = Object.keys(flags).filter((f) => !accepted.has(f) && !RESERVED_FLAGS.has(f));
|
|
15943
16138
|
if (unknown.length > 0) {
|
|
15944
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)}`);
|
|
15945
16143
|
throw new UsageError(`unknown flag${unknown.length > 1 ? "s" : ""}: ${unknown.map((f) => `--${f}`).join(", ")}${hints.length > 0 ? ` — ${hints.join("; ")}` : ""}`);
|
|
15946
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
|
+
}
|
|
15947
16150
|
const inputFlags = { ...flags };
|
|
15948
16151
|
if (action.name === "app_request" && lastValue(inputFlags.json) === true) {
|
|
15949
16152
|
delete inputFlags.json;
|
|
@@ -15980,7 +16183,7 @@ async function runObjectVerb(positionals, flags, ctx) {
|
|
|
15980
16183
|
advise(`Agent registered. Save its apiKey now — it is shown only once:
|
|
15981
16184
|
`, ctx);
|
|
15982
16185
|
}
|
|
15983
|
-
let value = action.name === "attachment_upload" ? await
|
|
16186
|
+
let value = action.name === "attachment_upload" ? await uploadLocalAttachments(httpExecutor, input) : await action.run(httpExecutor, input);
|
|
15984
16187
|
if (action.name === "computer_exec" && !truthyFlag(flags["no-wait"]) && input.waitForTerminalMs === undefined && input.background !== true) {
|
|
15985
16188
|
value = await waitForComputerExec(httpExecutor, input, value, ctx);
|
|
15986
16189
|
}
|
package/package.json
CHANGED