@tryarcanist/cli 0.1.42 → 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 +141 -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
|
});
|
|
@@ -1570,6 +1691,7 @@ async function createCommand(repoUrl, promptArg, options, command) {
|
|
|
1570
1691
|
if (repoError) {
|
|
1571
1692
|
throw new CliError("user", repoError);
|
|
1572
1693
|
}
|
|
1694
|
+
const uploadedFiles = await resolveUploadedFileOptions(options.uploadedFile);
|
|
1573
1695
|
const idempotencyKey = options.idempotencyKey ?? randomIdempotencyKey();
|
|
1574
1696
|
const sessionIdempotencyKey = `${idempotencyKey}:session`;
|
|
1575
1697
|
const promptIdempotencyKey = `${idempotencyKey}:prompt`;
|
|
@@ -1591,7 +1713,7 @@ async function createCommand(repoUrl, promptArg, options, command) {
|
|
|
1591
1713
|
{
|
|
1592
1714
|
method: "POST",
|
|
1593
1715
|
headers: { "Idempotency-Key": promptIdempotencyKey },
|
|
1594
|
-
body: JSON.stringify({ prompt })
|
|
1716
|
+
body: JSON.stringify({ prompt, ...uploadedFiles?.length ? { uploadedFiles } : {} })
|
|
1595
1717
|
}
|
|
1596
1718
|
);
|
|
1597
1719
|
promptId = promptData.prompt?.promptId ?? promptData.prompt?.id;
|
|
@@ -1678,13 +1800,14 @@ async function messageCommand(sessionId, promptArg, options = {}, command) {
|
|
|
1678
1800
|
const runtime = getRuntimeOptions(command, options);
|
|
1679
1801
|
const config = requireConfig(runtime);
|
|
1680
1802
|
const prompt = await resolvePromptInput(promptArg, options);
|
|
1803
|
+
const uploadedFiles = await resolveUploadedFileOptions(options.uploadedFile);
|
|
1681
1804
|
const response = await apiFetch(
|
|
1682
1805
|
config,
|
|
1683
1806
|
`/api/sessions/${sessionId}/prompts`,
|
|
1684
1807
|
{
|
|
1685
1808
|
method: "POST",
|
|
1686
1809
|
headers: { "Idempotency-Key": options.idempotencyKey ?? randomIdempotencyKey() },
|
|
1687
|
-
body: JSON.stringify({ prompt })
|
|
1810
|
+
body: JSON.stringify({ prompt, ...uploadedFiles?.length ? { uploadedFiles } : {} })
|
|
1688
1811
|
}
|
|
1689
1812
|
);
|
|
1690
1813
|
const promptId = response.prompt?.promptId ?? response.prompt?.id;
|
|
@@ -1930,7 +2053,11 @@ program.hook("preAction", (_thisCommand, actionCommand) => {
|
|
|
1930
2053
|
applyColorEnvironment(getRuntimeOptions(actionCommand));
|
|
1931
2054
|
});
|
|
1932
2055
|
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(
|
|
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(
|
|
1934
2061
|
"--poll-interval <ms>",
|
|
1935
2062
|
"Polling interval in milliseconds while waiting",
|
|
1936
2063
|
String(DEFAULT_WATCH_POLL_INTERVAL_MS)
|
|
@@ -1939,6 +2066,7 @@ function addCreateOptions(cmd) {
|
|
|
1939
2066
|
`
|
|
1940
2067
|
Examples:
|
|
1941
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
|
|
1942
2070
|
printf "fix bug" | arcanist sessions create https://github.com/org/repo --prompt-stdin --json
|
|
1943
2071
|
printf "fix bug" | arcanist sessions create https://github.com/org/repo --prompt-stdin --wait
|
|
1944
2072
|
arcanist sessions create https://github.com/org/repo - --json | jq -r .sessionId | xargs -I{} arcanist sessions events {} --follow --json
|
|
@@ -1946,11 +2074,16 @@ Examples:
|
|
|
1946
2074
|
);
|
|
1947
2075
|
}
|
|
1948
2076
|
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(
|
|
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(
|
|
1950
2082
|
"after",
|
|
1951
2083
|
`
|
|
1952
2084
|
Examples:
|
|
1953
2085
|
arcanist sessions send <session-id> "also update tests"
|
|
2086
|
+
arcanist sessions send <session-id> "use this log" --uploaded-file failing.log
|
|
1954
2087
|
printf "also update tests" | arcanist sessions send <session-id> --prompt-stdin --json
|
|
1955
2088
|
`
|
|
1956
2089
|
);
|