@tryarcanist/cli 0.1.228 → 0.1.230

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 (3) hide show
  1. package/README.md +25 -0
  2. package/dist/index.js +142 -20
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -362,6 +362,31 @@ arcanist sessions usage abc123
362
362
  arcanist sessions usage abc123 --json
363
363
  ```
364
364
 
365
+ ### `arcanist sessions artifacts <session-id>`
366
+
367
+ Lists the artifacts a session produced (screenshots, videos) as `artifactId`, `kind`, `label`, `filename`, `bytes`, `createdAt`.
368
+
369
+ ```bash
370
+ arcanist sessions artifacts abc123
371
+ arcanist sessions artifacts abc123 --json
372
+ arcanist sessions artifacts abc123 --all -o ./run-evidence
373
+ ```
374
+
375
+ `--all` downloads every artifact for the session instead of listing them, and `-o, --output <dir>` picks the destination directory (default: the current directory). An existing file is never overwritten: if the target name is already taken - by another artifact in the same run or by a file already on disk - the artifact ID is prefixed instead.
376
+
377
+ JSON mode returns `{artifacts}` for a listing and `{downloaded}` for `--all`, where each entry is `{artifactId, path}`.
378
+
379
+ ### `arcanist sessions artifacts get <session-id> <artifact-id>`
380
+
381
+ Downloads one artifact and prints the path it wrote.
382
+
383
+ ```bash
384
+ arcanist sessions artifacts get abc123 art-1
385
+ arcanist sessions artifacts get abc123 art-1 -o ./run-evidence
386
+ ```
387
+
388
+ `-o, --output <dir>` picks the destination directory (default: the current directory). An artifact ID that is not in the session exits non-zero. JSON mode returns `{artifactId, path}`.
389
+
365
390
  ### `arcanist repos list`
366
391
 
367
392
  ```bash
package/dist/index.js CHANGED
@@ -267,6 +267,9 @@ async function apiFetch(config, path, init) {
267
267
  async function apiFetchText(config, path, init) {
268
268
  return apiRequest(config, path, init, (res) => res.text());
269
269
  }
270
+ async function apiFetchBytes(config, path, init) {
271
+ return apiRequest(config, path, init, async (res) => new Uint8Array(await res.arrayBuffer()));
272
+ }
270
273
  async function resolveBusinessId(config, options) {
271
274
  if (options.business && options.business.trim()) return options.business.trim();
272
275
  const whoami = await apiFetch(config, "/api/auth/whoami");
@@ -534,7 +537,7 @@ async function readHiddenPrompt(prompt) {
534
537
  if (!process.stdin.isTTY || !process.stdout.isTTY) {
535
538
  throw new CliError("user", "No interactive terminal available. Re-run with --token-stdin or set ARCANIST_TOKEN.");
536
539
  }
537
- return new Promise((resolve2, reject) => {
540
+ return new Promise((resolve3, reject) => {
538
541
  const stdin = process.stdin;
539
542
  const inputChars = [];
540
543
  let ansiCarry = "";
@@ -550,7 +553,7 @@ async function readHiddenPrompt(prompt) {
550
553
  settled = true;
551
554
  cleanup();
552
555
  process.stdout.write("\n");
553
- resolve2(inputChars.join(""));
556
+ resolve3(inputChars.join(""));
554
557
  };
555
558
  const fail2 = (error) => {
556
559
  if (settled) return;
@@ -678,7 +681,7 @@ var VendorBinaryMissingError = class extends CliError {
678
681
  }
679
682
  };
680
683
  function runVendorLoginProcess(spec) {
681
- return new Promise((resolve2, reject) => {
684
+ return new Promise((resolve3, reject) => {
682
685
  const child = spawn(spec.binPath, spec.args, {
683
686
  stdio: "inherit",
684
687
  env: { ...process.env, ...spec.env }
@@ -692,7 +695,7 @@ function runVendorLoginProcess(spec) {
692
695
  });
693
696
  child.on("close", (code) => {
694
697
  if (code === 0) {
695
- resolve2();
698
+ resolve3();
696
699
  return;
697
700
  }
698
701
  reject(
@@ -806,7 +809,7 @@ function resolveVendorBin(config, optionPath) {
806
809
  return optionPath?.trim() || process.env[config.binEnvVar]?.trim() || config.defaultBin;
807
810
  }
808
811
  function runVendorInstall(config, command) {
809
- return new Promise((resolve2, reject) => {
812
+ return new Promise((resolve3, reject) => {
810
813
  const child = spawn2("bash", ["-c", command], { stdio: ["inherit", 2, "inherit"] });
811
814
  child.on("error", (err) => {
812
815
  reject(
@@ -817,7 +820,7 @@ function runVendorInstall(config, command) {
817
820
  });
818
821
  child.on("close", (code) => {
819
822
  if (code === 0) {
820
- resolve2();
823
+ resolve3();
821
824
  return;
822
825
  }
823
826
  reject(
@@ -1025,7 +1028,7 @@ async function agentSubscriptionLogoutCommand(config, options, command) {
1025
1028
 
1026
1029
  // ../../shared/utils/timing.ts
1027
1030
  function sleep(ms) {
1028
- return new Promise((resolve2) => setTimeout(resolve2, ms));
1031
+ return new Promise((resolve3) => setTimeout(resolve3, ms));
1029
1032
  }
1030
1033
 
1031
1034
  // src/status-poll.ts
@@ -1142,7 +1145,7 @@ async function anubisCommand(prUrl, options = {}, command) {
1142
1145
  config,
1143
1146
  `/api/anubis-runs/status?prUrl=${encodeURIComponent(prUrl)}`
1144
1147
  );
1145
- if (status.sessionStatus === "active") return null;
1148
+ if (status.sessionStatus === "in_flight") return null;
1146
1149
  assertSameAttempt(trigger.sessionId, status.sessionId, "Anubis");
1147
1150
  return status;
1148
1151
  }
@@ -1153,6 +1156,102 @@ async function anubisCommand(prUrl, options = {}, command) {
1153
1156
  });
1154
1157
  }
1155
1158
 
1159
+ // src/commands/artifacts.ts
1160
+ import { existsSync as existsSync3, mkdirSync as mkdirSync2, writeFileSync as writeFileSync2 } from "fs";
1161
+ import { basename, join as join4, resolve as resolve2 } from "path";
1162
+ async function fetchArtifacts(config, sessionId) {
1163
+ const payload = await apiFetch(
1164
+ config,
1165
+ `/api/sessions/${encodeURIComponent(sessionId)}/artifacts/list`
1166
+ );
1167
+ return payload.artifacts ?? [];
1168
+ }
1169
+ function artifactFilename(artifact) {
1170
+ const recorded = artifact.metadata?.filename;
1171
+ const safe = recorded ? sanitizeTerminalText(basename(recorded)) : "";
1172
+ return safe && safe !== "." && safe !== ".." ? safe : `${sanitizeTerminalText(artifact.type)}-${artifact.artifactId}`;
1173
+ }
1174
+ async function downloadArtifact(config, sessionId, artifact, outputDir, usedNames) {
1175
+ const filename = artifactFilename(artifact);
1176
+ const taken = usedNames.has(filename) || existsSync3(join4(outputDir, filename));
1177
+ const targetName = taken ? `${artifact.artifactId}-${filename}` : filename;
1178
+ usedNames.add(targetName);
1179
+ const bytes = await apiFetchBytes(
1180
+ config,
1181
+ `/api/sessions/${encodeURIComponent(sessionId)}/artifacts/${encodeURIComponent(artifact.artifactId)}/view?filename=${encodeURIComponent(filename)}`
1182
+ );
1183
+ const targetPath = join4(outputDir, targetName);
1184
+ writeFileSync2(targetPath, bytes);
1185
+ return targetPath;
1186
+ }
1187
+ function resolveOutputDir(output) {
1188
+ const dir = resolve2(output ?? process.cwd());
1189
+ mkdirSync2(dir, { recursive: true });
1190
+ return dir;
1191
+ }
1192
+ async function listArtifactsCommand(sessionId, options, command) {
1193
+ const runtime = getRuntimeOptions(command, options);
1194
+ const config = requireConfig(runtime);
1195
+ const artifacts2 = await fetchArtifacts(config, sessionId);
1196
+ if (options.all) {
1197
+ await downloadAllArtifacts(config, sessionId, artifacts2, options, command);
1198
+ return;
1199
+ }
1200
+ emit(command, options, { artifacts: artifacts2 }, (payload) => {
1201
+ if (payload.artifacts.length === 0) {
1202
+ console.log(`No artifacts for session ${sessionId}.`);
1203
+ return;
1204
+ }
1205
+ console.log(["ARTIFACT ID", "KIND", "LABEL", "FILENAME", "BYTES", "CREATED AT"].join(" "));
1206
+ for (const artifact of payload.artifacts) {
1207
+ console.log(
1208
+ [
1209
+ artifact.artifactId,
1210
+ sanitizeTerminalText(artifact.type),
1211
+ sanitizeTerminalText(artifact.metadata?.label ?? "-"),
1212
+ artifactFilename(artifact),
1213
+ artifact.metadata?.bytes ?? "-",
1214
+ new Date(artifact.createdAt).toISOString()
1215
+ ].join(" ")
1216
+ );
1217
+ }
1218
+ });
1219
+ }
1220
+ async function downloadAllArtifacts(config, sessionId, artifacts2, options, command) {
1221
+ if (artifacts2.length === 0) {
1222
+ emit(command, options, { downloaded: [] }, () => {
1223
+ console.log(`No artifacts for session ${sessionId}.`);
1224
+ });
1225
+ return;
1226
+ }
1227
+ const outputDir = resolveOutputDir(options.output);
1228
+ const usedNames = /* @__PURE__ */ new Set();
1229
+ const downloaded = [];
1230
+ for (const artifact of artifacts2) {
1231
+ const path = await downloadArtifact(config, sessionId, artifact, outputDir, usedNames);
1232
+ downloaded.push({ artifactId: artifact.artifactId, path });
1233
+ }
1234
+ emit(command, options, { downloaded }, (payload) => {
1235
+ for (const entry of payload.downloaded) console.log(entry.path);
1236
+ });
1237
+ }
1238
+ async function getArtifactCommand(sessionId, artifactId, options, command) {
1239
+ const runtime = getRuntimeOptions(command, options);
1240
+ const config = requireConfig(runtime);
1241
+ const artifacts2 = await fetchArtifacts(config, sessionId);
1242
+ const artifact = artifacts2.find((candidate) => candidate.artifactId === artifactId);
1243
+ if (!artifact) {
1244
+ throw new CliError("user", `Artifact ${artifactId} not found in session ${sessionId}.`, {
1245
+ hint: `Run \`arcanist sessions artifacts ${sessionId}\` to list the artifact IDs for this session.`
1246
+ });
1247
+ }
1248
+ const outputDir = resolveOutputDir(options.output);
1249
+ const path = await downloadArtifact(config, sessionId, artifact, outputDir, /* @__PURE__ */ new Set());
1250
+ emit(command, options, { artifactId, path }, (payload) => {
1251
+ console.log(payload.path);
1252
+ });
1253
+ }
1254
+
1156
1255
  // src/commands/auth.ts
1157
1256
  async function whoamiCommand(options, command) {
1158
1257
  const { config } = resolveBusinessContext(command, options);
@@ -2147,7 +2246,7 @@ function formatTime(value) {
2147
2246
 
2148
2247
  // src/commands/codex.ts
2149
2248
  import { readFile as readFile2 } from "fs/promises";
2150
- import { join as join4 } from "path";
2249
+ import { join as join5 } from "path";
2151
2250
  var CODEX_SUBSCRIPTION_PATH = "/api/settings/codex-subscription";
2152
2251
  var CODEX_SUBSCRIPTION_AUTH_JSON_PATH = "/api/settings/codex-subscription/auth-json";
2153
2252
  var CODEX_SUBSCRIPTION_ENABLED_PATH = "/api/settings/codex-subscription/enabled";
@@ -2185,7 +2284,7 @@ async function codexLoginCommand(options, command) {
2185
2284
  await runCodexDeviceLogin(codexPath, codexHome);
2186
2285
  let raw;
2187
2286
  try {
2188
- raw = await readFile2(join4(codexHome, "auth.json"), "utf8");
2287
+ raw = await readFile2(join5(codexHome, "auth.json"), "utf8");
2189
2288
  } catch {
2190
2289
  throw new CliError("user", "Codex login completed but no auth.json was written.", {
2191
2290
  hint: "Verify `codex login --device-auth` succeeds on its own, then re-run `arcanist codex login`."
@@ -2259,7 +2358,7 @@ async function codexLogoutCommand(options, command) {
2259
2358
 
2260
2359
  // src/uploads.ts
2261
2360
  import { readFile as readFile3 } from "fs/promises";
2262
- import { basename, extname } from "path";
2361
+ import { basename as basename2, extname } from "path";
2263
2362
 
2264
2363
  // ../../shared/constants/uploads.ts
2265
2364
  var MAX_UPLOADED_FILES = 5;
@@ -2355,11 +2454,11 @@ function validateUploadedFilePayload(files, existingNames = []) {
2355
2454
  async function resolveUploadedFileOptions(files) {
2356
2455
  const paths = normalizeUploadedFileOptions(files);
2357
2456
  if (paths.length === 0) return void 0;
2358
- const names = paths.map((path) => basename(path));
2457
+ const names = paths.map((path) => basename2(path));
2359
2458
  validateUploadedFileNames(names);
2360
2459
  const uploadedFiles = await Promise.all(
2361
2460
  paths.map(async (path) => {
2362
- const name = basename(path);
2461
+ const name = basename2(path);
2363
2462
  try {
2364
2463
  return { name, content: await readFile3(path, "utf8") };
2365
2464
  } catch (err) {
@@ -4764,7 +4863,7 @@ async function reviewCommand(prUrl, options = {}, command) {
4764
4863
  }
4765
4864
 
4766
4865
  // src/commands/sandbox.ts
4767
- import { existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
4866
+ import { existsSync as existsSync4, mkdirSync as mkdirSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync3 } from "fs";
4768
4867
  import { dirname as dirname2 } from "path";
4769
4868
 
4770
4869
  // ../../shared/sandbox-layer/parser.ts
@@ -5311,7 +5410,7 @@ function assertCleanAndPushed() {
5311
5410
  throw new CliError("user", "Current commit is not pushed; push before building the sandbox layer.");
5312
5411
  }
5313
5412
  function assertManifestExists(path) {
5314
- if (!existsSync3(path)) throw new CliError("user", `Missing sandbox manifest: ${path}`);
5413
+ if (!existsSync4(path)) throw new CliError("user", `Missing sandbox manifest: ${path}`);
5315
5414
  }
5316
5415
  function repoPath(repo) {
5317
5416
  return `${repo.owner}/${repo.repo}`;
@@ -5528,11 +5627,11 @@ function sourceBody(sourceRepo, manifestPath) {
5528
5627
  }
5529
5628
  async function sandboxInitCommand(options = {}, command) {
5530
5629
  const runtime = getRuntimeOptions(command, options);
5531
- if (existsSync3(DEFAULT_MANIFEST_PATH)) throw new CliError("conflict", `${DEFAULT_MANIFEST_PATH} already exists`);
5630
+ if (existsSync4(DEFAULT_MANIFEST_PATH)) throw new CliError("conflict", `${DEFAULT_MANIFEST_PATH} already exists`);
5532
5631
  const layerPath = ".arcanist/sandbox.layer.Dockerfile";
5533
- if (existsSync3(layerPath)) throw new CliError("conflict", `${layerPath} already exists`);
5534
- mkdirSync2(dirname2(DEFAULT_MANIFEST_PATH), { recursive: true });
5535
- writeFileSync2(
5632
+ if (existsSync4(layerPath)) throw new CliError("conflict", `${layerPath} already exists`);
5633
+ mkdirSync3(dirname2(DEFAULT_MANIFEST_PATH), { recursive: true });
5634
+ writeFileSync3(
5536
5635
  DEFAULT_MANIFEST_PATH,
5537
5636
  [
5538
5637
  "version: 1",
@@ -5545,7 +5644,7 @@ async function sandboxInitCommand(options = {}, command) {
5545
5644
  ].join("\n"),
5546
5645
  "utf8"
5547
5646
  );
5548
- writeFileSync2(
5647
+ writeFileSync3(
5549
5648
  layerPath,
5550
5649
  [
5551
5650
  "# Only RUN and ENV instructions are supported.",
@@ -6485,6 +6584,29 @@ Examples:
6485
6584
  arcanist sessions usage <session-id> --json
6486
6585
  `
6487
6586
  ).action((sessionId, options, command) => usageCommand(sessionId, options, command));
6587
+ var artifacts = sessions.command("artifacts").description("List or download session artifacts (screenshots, videos)").argument("<session-id>", "Session ID").option("-o, --output <dir>", "Directory to write downloads into; defaults to the current directory").option("--all", "Download every artifact for the session instead of listing them").addHelpText(
6588
+ "after",
6589
+ `
6590
+ Examples:
6591
+ arcanist sessions artifacts <session-id>
6592
+ arcanist sessions artifacts <session-id> --json
6593
+ arcanist sessions artifacts <session-id> --all -o ./run-evidence
6594
+
6595
+ JSON:
6596
+ JSON mode returns {artifacts} for a listing and {downloaded} for --all, where each downloaded entry is {artifactId, path}.
6597
+ `
6598
+ ).action((sessionId, options, command) => listArtifactsCommand(sessionId, options, command));
6599
+ artifacts.command("get").description("Download one session artifact by ID").argument("<session-id>", "Session ID").argument("<artifact-id>", "Artifact ID from `arcanist sessions artifacts`").option("-o, --output <dir>", "Directory to write the download into; defaults to the current directory").addHelpText(
6600
+ "after",
6601
+ `
6602
+ Examples:
6603
+ arcanist sessions artifacts get <session-id> <artifact-id>
6604
+ arcanist sessions artifacts get <session-id> <artifact-id> -o ./run-evidence
6605
+
6606
+ JSON:
6607
+ JSON mode returns {artifactId, path}.
6608
+ `
6609
+ ).action((sessionId, artifactId, options, command) => getArtifactCommand(sessionId, artifactId, options, command));
6488
6610
  var repos = program.command("repos").description("Repository discovery commands");
6489
6611
  repos.command("list").description("List accessible repositories").addHelpText(
6490
6612
  "after",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tryarcanist/cli",
3
- "version": "0.1.228",
3
+ "version": "0.1.230",
4
4
  "description": "CLI for Arcanist — create and manage coding agent sessions",
5
5
  "type": "module",
6
6
  "bin": {