@tryarcanist/cli 0.1.42 → 0.1.44
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 +144 -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
|
});
|
|
@@ -1163,6 +1284,9 @@ ${event.answer ? `**Answer:** ${event.answer}
|
|
|
1163
1284
|
`;
|
|
1164
1285
|
case "session_resumed_cold":
|
|
1165
1286
|
return `*[sandbox cold-restarted; in-sandbox state was lost${event.lostSnapshotImageId ? ` (snapshot ${event.lostSnapshotImageId} no longer usable)` : ""}]*
|
|
1287
|
+
`;
|
|
1288
|
+
case "customer_activity":
|
|
1289
|
+
return `**${event.title}:** ${event.summary}
|
|
1166
1290
|
`;
|
|
1167
1291
|
case "raw_opencode":
|
|
1168
1292
|
return "";
|
|
@@ -1570,6 +1694,7 @@ async function createCommand(repoUrl, promptArg, options, command) {
|
|
|
1570
1694
|
if (repoError) {
|
|
1571
1695
|
throw new CliError("user", repoError);
|
|
1572
1696
|
}
|
|
1697
|
+
const uploadedFiles = await resolveUploadedFileOptions(options.uploadedFile);
|
|
1573
1698
|
const idempotencyKey = options.idempotencyKey ?? randomIdempotencyKey();
|
|
1574
1699
|
const sessionIdempotencyKey = `${idempotencyKey}:session`;
|
|
1575
1700
|
const promptIdempotencyKey = `${idempotencyKey}:prompt`;
|
|
@@ -1591,7 +1716,7 @@ async function createCommand(repoUrl, promptArg, options, command) {
|
|
|
1591
1716
|
{
|
|
1592
1717
|
method: "POST",
|
|
1593
1718
|
headers: { "Idempotency-Key": promptIdempotencyKey },
|
|
1594
|
-
body: JSON.stringify({ prompt })
|
|
1719
|
+
body: JSON.stringify({ prompt, ...uploadedFiles?.length ? { uploadedFiles } : {} })
|
|
1595
1720
|
}
|
|
1596
1721
|
);
|
|
1597
1722
|
promptId = promptData.prompt?.promptId ?? promptData.prompt?.id;
|
|
@@ -1678,13 +1803,14 @@ async function messageCommand(sessionId, promptArg, options = {}, command) {
|
|
|
1678
1803
|
const runtime = getRuntimeOptions(command, options);
|
|
1679
1804
|
const config = requireConfig(runtime);
|
|
1680
1805
|
const prompt = await resolvePromptInput(promptArg, options);
|
|
1806
|
+
const uploadedFiles = await resolveUploadedFileOptions(options.uploadedFile);
|
|
1681
1807
|
const response = await apiFetch(
|
|
1682
1808
|
config,
|
|
1683
1809
|
`/api/sessions/${sessionId}/prompts`,
|
|
1684
1810
|
{
|
|
1685
1811
|
method: "POST",
|
|
1686
1812
|
headers: { "Idempotency-Key": options.idempotencyKey ?? randomIdempotencyKey() },
|
|
1687
|
-
body: JSON.stringify({ prompt })
|
|
1813
|
+
body: JSON.stringify({ prompt, ...uploadedFiles?.length ? { uploadedFiles } : {} })
|
|
1688
1814
|
}
|
|
1689
1815
|
);
|
|
1690
1816
|
const promptId = response.prompt?.promptId ?? response.prompt?.id;
|
|
@@ -1930,7 +2056,11 @@ program.hook("preAction", (_thisCommand, actionCommand) => {
|
|
|
1930
2056
|
applyColorEnvironment(getRuntimeOptions(actionCommand));
|
|
1931
2057
|
});
|
|
1932
2058
|
function addCreateOptions(cmd) {
|
|
1933
|
-
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(
|
|
2059
|
+
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(
|
|
2060
|
+
"--uploaded-file <path>",
|
|
2061
|
+
"Attach a local text file to the prompt as uploadedFiles; repeat for multiple files",
|
|
2062
|
+
collectUploadedFileOption
|
|
2063
|
+
).option("--wait", "Wait for the created prompt to finish and exit non-zero if it fails").option(
|
|
1934
2064
|
"--poll-interval <ms>",
|
|
1935
2065
|
"Polling interval in milliseconds while waiting",
|
|
1936
2066
|
String(DEFAULT_WATCH_POLL_INTERVAL_MS)
|
|
@@ -1939,6 +2069,7 @@ function addCreateOptions(cmd) {
|
|
|
1939
2069
|
`
|
|
1940
2070
|
Examples:
|
|
1941
2071
|
arcanist sessions create https://github.com/org/repo "fix bug"
|
|
2072
|
+
arcanist sessions create https://github.com/org/repo "review this trace" --uploaded-file trace.txt
|
|
1942
2073
|
printf "fix bug" | arcanist sessions create https://github.com/org/repo --prompt-stdin --json
|
|
1943
2074
|
printf "fix bug" | arcanist sessions create https://github.com/org/repo --prompt-stdin --wait
|
|
1944
2075
|
arcanist sessions create https://github.com/org/repo - --json | jq -r .sessionId | xargs -I{} arcanist sessions events {} --follow --json
|
|
@@ -1946,11 +2077,16 @@ Examples:
|
|
|
1946
2077
|
);
|
|
1947
2078
|
}
|
|
1948
2079
|
function addSendOptions(cmd) {
|
|
1949
|
-
return cmd.argument("<session-id>", "Session ID").argument("[prompt]", "Prompt to send, or '-' to read stdin").option("--prompt-stdin", "Read prompt from stdin").option(
|
|
2080
|
+
return cmd.argument("<session-id>", "Session ID").argument("[prompt]", "Prompt to send, or '-' to read stdin").option("--prompt-stdin", "Read prompt from stdin").option(
|
|
2081
|
+
"--uploaded-file <path>",
|
|
2082
|
+
"Attach a local text file to the prompt as uploadedFiles; repeat for multiple files",
|
|
2083
|
+
collectUploadedFileOption
|
|
2084
|
+
).option("--idempotency-key <uuid>", "Request idempotency key for safe manual retries").addHelpText(
|
|
1950
2085
|
"after",
|
|
1951
2086
|
`
|
|
1952
2087
|
Examples:
|
|
1953
2088
|
arcanist sessions send <session-id> "also update tests"
|
|
2089
|
+
arcanist sessions send <session-id> "use this log" --uploaded-file failing.log
|
|
1954
2090
|
printf "also update tests" | arcanist sessions send <session-id> --prompt-stdin --json
|
|
1955
2091
|
`
|
|
1956
2092
|
);
|