@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.
Files changed (2) hide show
  1. package/dist/index.js +154 -8
  2. 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 fail = (error) => {
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
- fail(new CliError("user", "Interrupted.", { exitCode: 130 }));
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
- fail(new CliError("user", `Failed to read input: ${error.message}`));
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("--wait", "Wait for the created prompt to finish and exit non-zero if it fails").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("--idempotency-key <uuid>", "Request idempotency key for safe manual retries").addHelpText(
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
  );
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tryarcanist/cli",
3
- "version": "0.1.41",
3
+ "version": "0.1.43",
4
4
  "description": "CLI for Arcanist — create and manage coding agent sessions",
5
5
  "type": "module",
6
6
  "bin": {