@tryarcanist/cli 0.1.41 → 0.1.43
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/dist/index.js +154 -8
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -273,7 +273,7 @@ async function readHiddenPrompt(prompt) {
|
|
|
273
273
|
process.stdout.write("\n");
|
|
274
274
|
resolve(inputChars.join(""));
|
|
275
275
|
};
|
|
276
|
-
const
|
|
276
|
+
const fail2 = (error) => {
|
|
277
277
|
if (settled) return;
|
|
278
278
|
settled = true;
|
|
279
279
|
cleanup();
|
|
@@ -290,7 +290,7 @@ async function readHiddenPrompt(prompt) {
|
|
|
290
290
|
return;
|
|
291
291
|
}
|
|
292
292
|
if (char === "") {
|
|
293
|
-
|
|
293
|
+
fail2(new CliError("user", "Interrupted.", { exitCode: 130 }));
|
|
294
294
|
return;
|
|
295
295
|
}
|
|
296
296
|
if (char === "\x7F" || char === "\b") {
|
|
@@ -306,7 +306,7 @@ async function readHiddenPrompt(prompt) {
|
|
|
306
306
|
}
|
|
307
307
|
};
|
|
308
308
|
const onError = (error) => {
|
|
309
|
-
|
|
309
|
+
fail2(new CliError("user", `Failed to read input: ${error.message}`));
|
|
310
310
|
};
|
|
311
311
|
process.stdout.write(prompt);
|
|
312
312
|
stdin.resume();
|
|
@@ -383,6 +383,128 @@ async function whoamiCommand(options, command) {
|
|
|
383
383
|
if (payload.tokenScope) console.log(`Token scope: ${String(payload.tokenScope)}`);
|
|
384
384
|
}
|
|
385
385
|
|
|
386
|
+
// src/uploads.ts
|
|
387
|
+
import { readFile } from "fs/promises";
|
|
388
|
+
import { basename, extname } from "path";
|
|
389
|
+
|
|
390
|
+
// ../../shared/constants/uploads.ts
|
|
391
|
+
var MAX_UPLOADED_FILES = 5;
|
|
392
|
+
var MAX_UPLOADED_FILE_SIZE_BYTES = 102400;
|
|
393
|
+
var UPLOADED_FILE_EXTENSIONS = [".md", ".txt", ".csv", ".json", ".yaml", ".yml", ".xml", ".html"];
|
|
394
|
+
var MAX_UPLOADED_IMAGE_SIZE_BYTES = 5 * 1024 * 1024;
|
|
395
|
+
var ALLOWED_IMAGE_MEDIA_TYPES = ["image/png", "image/jpeg", "image/gif", "image/webp"];
|
|
396
|
+
|
|
397
|
+
// ../../shared/utils/uploads.ts
|
|
398
|
+
var ALLOWED_IMAGE_MEDIA_TYPE_SET = new Set(ALLOWED_IMAGE_MEDIA_TYPES);
|
|
399
|
+
function ok(value) {
|
|
400
|
+
return { ok: true, value };
|
|
401
|
+
}
|
|
402
|
+
function fail(error) {
|
|
403
|
+
return { ok: false, error };
|
|
404
|
+
}
|
|
405
|
+
function validateUploadedName(name, kind) {
|
|
406
|
+
const trimmed = name.trim();
|
|
407
|
+
if (!trimmed) {
|
|
408
|
+
return `Uploaded ${kind} name must be a non-empty string`;
|
|
409
|
+
}
|
|
410
|
+
if (trimmed.includes("/") || trimmed.includes("\\")) {
|
|
411
|
+
return `Invalid uploaded ${kind} name: ${trimmed}`;
|
|
412
|
+
}
|
|
413
|
+
return null;
|
|
414
|
+
}
|
|
415
|
+
function trimNamedItems(items) {
|
|
416
|
+
return items.map((item) => ({ ...item, name: item.name.trim() }));
|
|
417
|
+
}
|
|
418
|
+
function hasNullBytes(content) {
|
|
419
|
+
return content.slice(0, 8192).includes("\0");
|
|
420
|
+
}
|
|
421
|
+
function checkUploadCapacity(currentCount, incomingCount, max, label) {
|
|
422
|
+
const remaining = max - currentCount;
|
|
423
|
+
if (incomingCount > remaining) {
|
|
424
|
+
return `Maximum ${max} ${label} (${remaining} remaining)`;
|
|
425
|
+
}
|
|
426
|
+
return null;
|
|
427
|
+
}
|
|
428
|
+
function deduplicateByName(items, existingNames = []) {
|
|
429
|
+
const seen = new Set(existingNames);
|
|
430
|
+
const deduped = [];
|
|
431
|
+
for (const item of items) {
|
|
432
|
+
if (seen.has(item.name)) continue;
|
|
433
|
+
seen.add(item.name);
|
|
434
|
+
deduped.push(item);
|
|
435
|
+
}
|
|
436
|
+
return deduped;
|
|
437
|
+
}
|
|
438
|
+
function validateUploadedFilePayload(files, existingNames = []) {
|
|
439
|
+
const trimmed = trimNamedItems(files);
|
|
440
|
+
const deduped = deduplicateByName(trimmed, existingNames);
|
|
441
|
+
const capacityError = checkUploadCapacity(existingNames.length, deduped.length, MAX_UPLOADED_FILES, "uploaded files");
|
|
442
|
+
if (capacityError) return fail(capacityError);
|
|
443
|
+
for (const file of deduped) {
|
|
444
|
+
const nameError = validateUploadedName(file.name, "file");
|
|
445
|
+
if (nameError) return fail(nameError);
|
|
446
|
+
const byteLength = new TextEncoder().encode(file.content).byteLength;
|
|
447
|
+
if (byteLength > MAX_UPLOADED_FILE_SIZE_BYTES) {
|
|
448
|
+
return fail(`Uploaded file too large: ${file.name} (${byteLength} bytes, max ${MAX_UPLOADED_FILE_SIZE_BYTES})`);
|
|
449
|
+
}
|
|
450
|
+
if (hasNullBytes(file.content)) {
|
|
451
|
+
return fail(`Binary files are not supported: ${file.name}`);
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
return ok(deduped);
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
// src/uploads.ts
|
|
458
|
+
async function resolveUploadedFileOptions(files) {
|
|
459
|
+
const paths = normalizeUploadedFileOptions(files);
|
|
460
|
+
if (paths.length === 0) return void 0;
|
|
461
|
+
const names = paths.map((path) => basename(path));
|
|
462
|
+
validateUploadedFileNames(names);
|
|
463
|
+
const uploadedFiles = await Promise.all(
|
|
464
|
+
paths.map(async (path) => {
|
|
465
|
+
const name = basename(path);
|
|
466
|
+
try {
|
|
467
|
+
return { name, content: await readFile(path, "utf8") };
|
|
468
|
+
} catch (err) {
|
|
469
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
470
|
+
throw new CliError("user", `Failed to read uploaded file ${path}: ${message}`);
|
|
471
|
+
}
|
|
472
|
+
})
|
|
473
|
+
);
|
|
474
|
+
const validation = validateUploadedFilePayload(uploadedFiles);
|
|
475
|
+
if (!validation.ok) {
|
|
476
|
+
throw new CliError("user", validation.error);
|
|
477
|
+
}
|
|
478
|
+
return validation.value;
|
|
479
|
+
}
|
|
480
|
+
function collectUploadedFileOption(path, previous = []) {
|
|
481
|
+
return [...previous, path];
|
|
482
|
+
}
|
|
483
|
+
function validateUploadedFileNames(names) {
|
|
484
|
+
const seen = /* @__PURE__ */ new Set();
|
|
485
|
+
const duplicates = /* @__PURE__ */ new Set();
|
|
486
|
+
for (const name of names) {
|
|
487
|
+
if (seen.has(name)) duplicates.add(name);
|
|
488
|
+
seen.add(name);
|
|
489
|
+
}
|
|
490
|
+
if (duplicates.size > 0) {
|
|
491
|
+
throw new CliError(
|
|
492
|
+
"user",
|
|
493
|
+
`Duplicate uploaded-file basenames: ${[...duplicates].join(", ")}. Rename or move them so each basename is unique.`
|
|
494
|
+
);
|
|
495
|
+
}
|
|
496
|
+
for (const name of names) {
|
|
497
|
+
const ext = extname(name).toLowerCase();
|
|
498
|
+
if (!UPLOADED_FILE_EXTENSIONS.includes(ext)) {
|
|
499
|
+
throw new CliError("user", `Unsupported file type: ${name}. Supported: ${UPLOADED_FILE_EXTENSIONS.join(", ")}`);
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
function normalizeUploadedFileOptions(files) {
|
|
504
|
+
if (!files) return [];
|
|
505
|
+
return Array.isArray(files) ? files : [files];
|
|
506
|
+
}
|
|
507
|
+
|
|
386
508
|
// ../../shared/utils/timing.ts
|
|
387
509
|
function sleep(ms) {
|
|
388
510
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
@@ -577,7 +699,6 @@ function getRawSessionEventData(event) {
|
|
|
577
699
|
case "idle":
|
|
578
700
|
return withPromptId(event, {
|
|
579
701
|
...base,
|
|
580
|
-
...typeof payload.taskType === "string" ? { taskType: payload.taskType } : {},
|
|
581
702
|
...typeof payload.sessionEditCount === "number" ? { sessionEditCount: payload.sessionEditCount } : {},
|
|
582
703
|
...typeof payload.sessionPromptCount === "number" ? { sessionPromptCount: payload.sessionPromptCount } : {}
|
|
583
704
|
});
|
|
@@ -730,6 +851,16 @@ function flattenSessionEvents(raw) {
|
|
|
730
851
|
});
|
|
731
852
|
continue;
|
|
732
853
|
}
|
|
854
|
+
if (type === "session_resumed_cold") {
|
|
855
|
+
merged.push({
|
|
856
|
+
type: "session_resumed_cold",
|
|
857
|
+
id: resolveEventId(data, "rescold", merged.length),
|
|
858
|
+
...typeof data?.reason === "string" ? { reason: data.reason } : {},
|
|
859
|
+
...typeof data?.lostSnapshotImageId === "string" ? { lostSnapshotImageId: data.lostSnapshotImageId } : data?.lostSnapshotImageId === null ? { lostSnapshotImageId: null } : {},
|
|
860
|
+
...resolvePromptId(data) ? { promptId: resolvePromptId(data) } : {}
|
|
861
|
+
});
|
|
862
|
+
continue;
|
|
863
|
+
}
|
|
733
864
|
if (type === "raw_opencode") {
|
|
734
865
|
const partType = typeof data?.partType === "string" ? data.partType : void 0;
|
|
735
866
|
const eventType = typeof data?.eventType === "string" ? data.eventType : void 0;
|
|
@@ -1150,6 +1281,9 @@ ${event.answer ? `**Answer:** ${event.answer}
|
|
|
1150
1281
|
`;
|
|
1151
1282
|
case "session_error":
|
|
1152
1283
|
return `**Error:** ${formatSessionErrorMessage(event.error, event.code)}
|
|
1284
|
+
`;
|
|
1285
|
+
case "session_resumed_cold":
|
|
1286
|
+
return `*[sandbox cold-restarted; in-sandbox state was lost${event.lostSnapshotImageId ? ` (snapshot ${event.lostSnapshotImageId} no longer usable)` : ""}]*
|
|
1153
1287
|
`;
|
|
1154
1288
|
case "raw_opencode":
|
|
1155
1289
|
return "";
|
|
@@ -1557,6 +1691,7 @@ async function createCommand(repoUrl, promptArg, options, command) {
|
|
|
1557
1691
|
if (repoError) {
|
|
1558
1692
|
throw new CliError("user", repoError);
|
|
1559
1693
|
}
|
|
1694
|
+
const uploadedFiles = await resolveUploadedFileOptions(options.uploadedFile);
|
|
1560
1695
|
const idempotencyKey = options.idempotencyKey ?? randomIdempotencyKey();
|
|
1561
1696
|
const sessionIdempotencyKey = `${idempotencyKey}:session`;
|
|
1562
1697
|
const promptIdempotencyKey = `${idempotencyKey}:prompt`;
|
|
@@ -1578,7 +1713,7 @@ async function createCommand(repoUrl, promptArg, options, command) {
|
|
|
1578
1713
|
{
|
|
1579
1714
|
method: "POST",
|
|
1580
1715
|
headers: { "Idempotency-Key": promptIdempotencyKey },
|
|
1581
|
-
body: JSON.stringify({ prompt })
|
|
1716
|
+
body: JSON.stringify({ prompt, ...uploadedFiles?.length ? { uploadedFiles } : {} })
|
|
1582
1717
|
}
|
|
1583
1718
|
);
|
|
1584
1719
|
promptId = promptData.prompt?.promptId ?? promptData.prompt?.id;
|
|
@@ -1665,13 +1800,14 @@ async function messageCommand(sessionId, promptArg, options = {}, command) {
|
|
|
1665
1800
|
const runtime = getRuntimeOptions(command, options);
|
|
1666
1801
|
const config = requireConfig(runtime);
|
|
1667
1802
|
const prompt = await resolvePromptInput(promptArg, options);
|
|
1803
|
+
const uploadedFiles = await resolveUploadedFileOptions(options.uploadedFile);
|
|
1668
1804
|
const response = await apiFetch(
|
|
1669
1805
|
config,
|
|
1670
1806
|
`/api/sessions/${sessionId}/prompts`,
|
|
1671
1807
|
{
|
|
1672
1808
|
method: "POST",
|
|
1673
1809
|
headers: { "Idempotency-Key": options.idempotencyKey ?? randomIdempotencyKey() },
|
|
1674
|
-
body: JSON.stringify({ prompt })
|
|
1810
|
+
body: JSON.stringify({ prompt, ...uploadedFiles?.length ? { uploadedFiles } : {} })
|
|
1675
1811
|
}
|
|
1676
1812
|
);
|
|
1677
1813
|
const promptId = response.prompt?.promptId ?? response.prompt?.id;
|
|
@@ -1917,7 +2053,11 @@ program.hook("preAction", (_thisCommand, actionCommand) => {
|
|
|
1917
2053
|
applyColorEnvironment(getRuntimeOptions(actionCommand));
|
|
1918
2054
|
});
|
|
1919
2055
|
function addCreateOptions(cmd) {
|
|
1920
|
-
return cmd.argument("<repo-url>", "Repository URL").argument("[prompt]", "Prompt to send, or '-' to read stdin").option("--model <model>", "Model to use").option("--reasoning-effort <effort>", "Reasoning effort to use for models that support it").option("--prompt-stdin", "Read prompt from stdin").option(
|
|
2056
|
+
return cmd.argument("<repo-url>", "Repository URL").argument("[prompt]", "Prompt to send, or '-' to read stdin").option("--model <model>", "Model to use").option("--reasoning-effort <effort>", "Reasoning effort to use for models that support it").option("--prompt-stdin", "Read prompt from stdin").option(
|
|
2057
|
+
"--uploaded-file <path>",
|
|
2058
|
+
"Attach a local text file to the prompt as uploadedFiles; repeat for multiple files",
|
|
2059
|
+
collectUploadedFileOption
|
|
2060
|
+
).option("--wait", "Wait for the created prompt to finish and exit non-zero if it fails").option(
|
|
1921
2061
|
"--poll-interval <ms>",
|
|
1922
2062
|
"Polling interval in milliseconds while waiting",
|
|
1923
2063
|
String(DEFAULT_WATCH_POLL_INTERVAL_MS)
|
|
@@ -1926,6 +2066,7 @@ function addCreateOptions(cmd) {
|
|
|
1926
2066
|
`
|
|
1927
2067
|
Examples:
|
|
1928
2068
|
arcanist sessions create https://github.com/org/repo "fix bug"
|
|
2069
|
+
arcanist sessions create https://github.com/org/repo "review this trace" --uploaded-file trace.txt
|
|
1929
2070
|
printf "fix bug" | arcanist sessions create https://github.com/org/repo --prompt-stdin --json
|
|
1930
2071
|
printf "fix bug" | arcanist sessions create https://github.com/org/repo --prompt-stdin --wait
|
|
1931
2072
|
arcanist sessions create https://github.com/org/repo - --json | jq -r .sessionId | xargs -I{} arcanist sessions events {} --follow --json
|
|
@@ -1933,11 +2074,16 @@ Examples:
|
|
|
1933
2074
|
);
|
|
1934
2075
|
}
|
|
1935
2076
|
function addSendOptions(cmd) {
|
|
1936
|
-
return cmd.argument("<session-id>", "Session ID").argument("[prompt]", "Prompt to send, or '-' to read stdin").option("--prompt-stdin", "Read prompt from stdin").option(
|
|
2077
|
+
return cmd.argument("<session-id>", "Session ID").argument("[prompt]", "Prompt to send, or '-' to read stdin").option("--prompt-stdin", "Read prompt from stdin").option(
|
|
2078
|
+
"--uploaded-file <path>",
|
|
2079
|
+
"Attach a local text file to the prompt as uploadedFiles; repeat for multiple files",
|
|
2080
|
+
collectUploadedFileOption
|
|
2081
|
+
).option("--idempotency-key <uuid>", "Request idempotency key for safe manual retries").addHelpText(
|
|
1937
2082
|
"after",
|
|
1938
2083
|
`
|
|
1939
2084
|
Examples:
|
|
1940
2085
|
arcanist sessions send <session-id> "also update tests"
|
|
2086
|
+
arcanist sessions send <session-id> "use this log" --uploaded-file failing.log
|
|
1941
2087
|
printf "also update tests" | arcanist sessions send <session-id> --prompt-stdin --json
|
|
1942
2088
|
`
|
|
1943
2089
|
);
|