@tryarcanist/cli 0.1.22 → 0.1.24

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 +228 -164
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -272,158 +272,16 @@ async function whoamiCommand(options, command) {
272
272
  if (payload.tokenScope) console.log(`Token scope: ${String(payload.tokenScope)}`);
273
273
  }
274
274
 
275
- // src/commands/create.ts
276
- var REPO_URL_PATTERNS = [
277
- /^[a-zA-Z0-9_.-]+\/[a-zA-Z0-9_.-]+$/,
278
- /^git@[^:]+:[^/]+\/[^/]+?(?:\.git)?$/,
279
- /^https?:\/\/github\.com\/[^/]+\/[^/]+?(?:\.git)?\/?$/
280
- ];
281
- function validateRepoUrl(url) {
282
- if (REPO_URL_PATTERNS.some((pattern) => pattern.test(url))) return null;
283
- return `Invalid repo URL: "${url}". Expected a GitHub URL (https://github.com/owner/repo) or owner/repo shorthand.`;
284
- }
285
- async function createCommand(repoUrl, promptArg, options, command) {
286
- const runtime = getRuntimeOptions(command, options);
287
- const config = requireConfig(runtime);
288
- const prompt = await resolvePromptInput(promptArg, options);
289
- const repoError = validateRepoUrl(repoUrl);
290
- if (repoError) {
291
- throw new CliError("user", repoError);
292
- }
293
- const idempotencyKey = options.idempotencyKey ?? randomIdempotencyKey();
294
- const sessionIdempotencyKey = `${idempotencyKey}:session`;
295
- const promptIdempotencyKey = `${idempotencyKey}:prompt`;
296
- const sessionData = await apiFetch(config, "/api/sessions", {
297
- method: "POST",
298
- headers: { "Idempotency-Key": sessionIdempotencyKey },
299
- body: JSON.stringify({
300
- context: { repoUrl },
301
- ...options.model ? { model: options.model } : {}
302
- })
303
- });
304
- const sessionId = sessionData.sessionId;
305
- try {
306
- const promptData = await apiFetch(config, `/api/sessions/${sessionId}/prompts`, {
307
- method: "POST",
308
- headers: { "Idempotency-Key": promptIdempotencyKey },
309
- body: JSON.stringify({ prompt })
310
- });
311
- const promptId = promptData.prompt?.promptId ?? promptData.prompt?.id;
312
- if (isJson(command, options)) {
313
- writeJson({
314
- sessionId,
315
- ...sessionData.sessionUrl ? { sessionUrl: sessionData.sessionUrl } : {},
316
- repoUrl,
317
- ...options.model ? { model: options.model } : {},
318
- ...promptId ? { promptId } : {}
319
- });
320
- return;
321
- }
322
- } catch (err) {
323
- throw new CliError(err instanceof CliError ? err.code : "server", `Session created (${sessionId}) but prompt failed: ${err instanceof Error ? err.message : String(err)}`, {
324
- exitCode: err instanceof CliError ? err.exitCode : void 0,
325
- hint: `Retry with: arcanist sessions send ${sessionId} --prompt-stdin`,
326
- requestId: err instanceof CliError ? err.requestId : void 0
327
- });
328
- }
329
- console.log(`Session: ${sessionId}`);
330
- if (sessionData.sessionUrl) console.log(`URL: ${sessionData.sessionUrl}`);
331
- }
332
-
333
- // src/commands/login.ts
334
- import { createInterface as createInterface2 } from "readline";
335
- async function loginCommand(options, command) {
336
- const runtime = getRuntimeOptions(command, options);
337
- let token;
338
- if (options.tokenStdin) {
339
- token = await readStdinTrimmed();
340
- } else if (runtime.token) {
341
- token = runtime.token;
342
- } else {
343
- token = await promptHidden("Enter your CLI token: ");
344
- }
345
- if (!token) {
346
- throw new CliError("user", "No token provided.");
347
- }
348
- if (!token.startsWith("arc_")) {
349
- throw new CliError("user", "Invalid token format. Token must start with 'arc_'.");
350
- }
351
- if (runtime.apiUrl) {
352
- const urlError = validateApiUrl(runtime.apiUrl);
353
- if (urlError) {
354
- throw new CliError("user", urlError);
355
- }
356
- }
357
- const apiUrl = normalizeBaseUrl(resolveLoginApiUrl(runtime.apiUrl));
358
- saveConfig({ apiUrl, token });
359
- if (runtime.json) {
360
- writeJson({ ok: true, apiUrl });
361
- } else if (!runtime.quiet) {
362
- console.log(`Logged in. API: ${apiUrl}`);
363
- }
364
- try {
365
- const res = await fetch(`${apiUrl}/api/cli-tokens`, {
366
- headers: { Authorization: `Bearer ${token}`, "User-Agent": CLI_USER_AGENT }
367
- });
368
- if (res.ok && !runtime.json && !runtime.quiet) {
369
- console.log("Token verified.");
370
- } else if (res.status === 401 && !runtime.json && !runtime.quiet) {
371
- console.warn("Warning: Token could not be verified (401). It may be invalid or expired.");
372
- }
373
- } catch {
374
- if (!runtime.json && !runtime.quiet) console.warn("Warning: Could not reach API to verify token.");
375
- }
376
- }
377
- function promptHidden(prompt) {
378
- if (!process.stdin.isTTY || !process.stdout.isTTY) {
379
- return Promise.reject(new CliError("user", "No interactive terminal available. Re-run with --token-stdin or set ARCANIST_TOKEN."));
380
- }
381
- return new Promise((resolve, reject) => {
382
- const rl = createInterface2({ input: process.stdin, output: process.stdout });
383
- process.stdout.write(prompt);
384
- const stdin = process.stdin;
385
- if (stdin.isTTY) stdin.setRawMode(true);
386
- let input = "";
387
- const onData = (char) => {
388
- const c = char.toString();
389
- if (c === "\n" || c === "\r") {
390
- if (stdin.isTTY) stdin.setRawMode(false);
391
- stdin.removeListener("data", onData);
392
- process.stdout.write("\n");
393
- rl.close();
394
- resolve(input);
395
- } else if (c === "") {
396
- if (stdin.isTTY) stdin.setRawMode(false);
397
- stdin.removeListener("data", onData);
398
- rl.close();
399
- reject(new CliError("user", "Interrupted.", { exitCode: 130 }));
400
- } else if (c === "\x7F" || c === "\b") {
401
- input = input.slice(0, -1);
402
- } else {
403
- input += c;
404
- }
405
- };
406
- stdin.on("data", onData);
407
- });
275
+ // ../../shared/utils/timing.ts
276
+ function sleep(ms) {
277
+ return new Promise((resolve) => setTimeout(resolve, ms));
408
278
  }
409
279
 
410
- // src/commands/message.ts
411
- async function messageCommand(sessionId, promptArg, options = {}, command) {
412
- const runtime = getRuntimeOptions(command, options);
413
- const config = requireConfig(runtime);
414
- const prompt = await resolvePromptInput(promptArg, options);
415
- const response = await apiFetch(config, `/api/sessions/${sessionId}/prompts`, {
416
- method: "POST",
417
- headers: { "Idempotency-Key": options.idempotencyKey ?? randomIdempotencyKey() },
418
- body: JSON.stringify({ prompt })
419
- });
420
- const promptId = response.prompt?.promptId ?? response.prompt?.id;
421
- if (isJson(command, options)) {
422
- writeJson({ sessionId, ...promptId ? { promptId } : {} });
423
- return;
424
- }
425
- console.log(`Message sent to session ${sessionId}.`);
426
- }
280
+ // src/constants/watch.ts
281
+ var MIN_WATCH_POLL_INTERVAL_MS = 1;
282
+ var DEFAULT_WATCH_POLL_INTERVAL_MS = 1e3;
283
+ var WATCH_REPLAY_PAGE_SIZE = 200;
284
+ var WATCH_TERMINAL_STATUSES = /* @__PURE__ */ new Set(["idle", "stopped", "archived"]);
427
285
 
428
286
  // ../../shared/utils/type-guards.ts
429
287
  function isRecord(value) {
@@ -1178,16 +1036,6 @@ function renderWatchEvent(event, state) {
1178
1036
  }
1179
1037
  }
1180
1038
 
1181
- // ../../shared/utils/timing.ts
1182
- function sleep(ms) {
1183
- return new Promise((resolve) => setTimeout(resolve, ms));
1184
- }
1185
-
1186
- // src/constants/watch.ts
1187
- var DEFAULT_WATCH_POLL_INTERVAL_MS = 1e3;
1188
- var WATCH_REPLAY_PAGE_SIZE = 200;
1189
- var WATCH_TERMINAL_STATUSES = /* @__PURE__ */ new Set(["idle", "archived"]);
1190
-
1191
1039
  // src/commands/watch.ts
1192
1040
  function parsePollInterval(raw) {
1193
1041
  if (!raw) return DEFAULT_WATCH_POLL_INTERVAL_MS;
@@ -1213,10 +1061,26 @@ async function fetchPromptLabels(config, sessionId) {
1213
1061
  const data = await apiFetch(config, `/api/sessions/${sessionId}/prompts`);
1214
1062
  return buildPromptLabelMap(data.prompts);
1215
1063
  }
1064
+ function extractSessionStatus(payload) {
1065
+ const session = payload.session;
1066
+ if (session && typeof session === "object" && typeof session.status === "string") {
1067
+ return String(session.status);
1068
+ }
1069
+ return typeof payload.status === "string" ? payload.status : "unknown";
1070
+ }
1071
+ async function waitForSessionToBecomeIdle(config, sessionId, pollIntervalMs) {
1072
+ const effectivePollIntervalMs = Math.max(pollIntervalMs, MIN_WATCH_POLL_INTERVAL_MS);
1073
+ while (true) {
1074
+ const payload = await apiFetch(config, `/api/sessions/${sessionId}`);
1075
+ if (WATCH_TERMINAL_STATUSES.has(extractSessionStatus(payload))) return;
1076
+ await sleep(effectivePollIntervalMs);
1077
+ }
1078
+ }
1216
1079
  async function watchCommand(sessionId, options, command) {
1217
1080
  const runtime = getRuntimeOptions(command, options);
1218
1081
  const config = requireConfig(runtime);
1219
1082
  const pollIntervalMs = parsePollInterval(options.pollInterval);
1083
+ const effectivePollIntervalMs = Math.max(pollIntervalMs, MIN_WATCH_POLL_INTERVAL_MS);
1220
1084
  const initialAfterSequence = parseNonNegativeInteger(options.afterSequence, "--after-sequence", 0);
1221
1085
  const pageSize = parseNonNegativeInteger(options.limit, "--limit", WATCH_REPLAY_PAGE_SIZE);
1222
1086
  if (pageSize === 0) {
@@ -1285,15 +1149,214 @@ async function watchCommand(sessionId, options, command) {
1285
1149
  }
1286
1150
  if (receivedFullPage) continue;
1287
1151
  if (parsed.status?.status && WATCH_TERMINAL_STATUSES.has(parsed.status.status)) break;
1288
- if (pollIntervalMs > 0) {
1289
- await sleep(pollIntervalMs);
1290
- }
1152
+ await sleep(effectivePollIntervalMs);
1291
1153
  }
1292
1154
  } finally {
1293
1155
  if (textOpen) process.stdout.write("\n");
1294
1156
  }
1295
1157
  }
1296
1158
 
1159
+ // src/commands/create.ts
1160
+ var REPO_URL_PATTERNS = [
1161
+ /^[a-zA-Z0-9_.-]+\/[a-zA-Z0-9_.-]+$/,
1162
+ /^git@[^:]+:[^/]+\/[^/]+?(?:\.git)?$/,
1163
+ /^https?:\/\/github\.com\/[^/]+\/[^/]+?(?:\.git)?\/?$/
1164
+ ];
1165
+ function validateRepoUrl(url) {
1166
+ if (REPO_URL_PATTERNS.some((pattern) => pattern.test(url))) return null;
1167
+ return `Invalid repo URL: "${url}". Expected a GitHub URL (https://github.com/owner/repo) or owner/repo shorthand.`;
1168
+ }
1169
+ function selectCreatedPrompt(prompts, promptId) {
1170
+ if (prompts.length === 0) return null;
1171
+ if (promptId) {
1172
+ return prompts.find((prompt) => prompt.promptId === promptId) ?? null;
1173
+ }
1174
+ return prompts[prompts.length - 1] ?? null;
1175
+ }
1176
+ function buildPromptFailureError(sessionId, prompt, sessionUrl) {
1177
+ const baseMessage = prompt.error?.trim() || "Prompt execution failed.";
1178
+ const location = sessionUrl ? ` ${sessionUrl}` : "";
1179
+ return new CliError("user", `Prompt failed in session ${sessionId}: ${baseMessage}${location}`, {
1180
+ hint: `Inspect with: arcanist sessions transcript ${sessionId}`
1181
+ });
1182
+ }
1183
+ async function waitForCreatedPrompt(sessionId, promptId, sessionUrl, pollIntervalMs, runtime, command) {
1184
+ const config = requireConfig(runtime);
1185
+ const jsonMode = isJson(command, runtime);
1186
+ if (jsonMode) {
1187
+ await waitForSessionToBecomeIdle(config, sessionId, pollIntervalMs);
1188
+ } else {
1189
+ await watchCommand(sessionId, { ...runtime, pollInterval: String(pollIntervalMs) }, command);
1190
+ }
1191
+ const promptList = await apiFetch(config, `/api/sessions/${sessionId}/prompts`);
1192
+ const createdPrompt = selectCreatedPrompt(promptList.prompts, promptId);
1193
+ if (!createdPrompt) {
1194
+ throw new CliError("server", `Prompt status was unavailable after session ${sessionId} became idle.`, {
1195
+ hint: `Inspect with: arcanist sessions transcript ${sessionId}`
1196
+ });
1197
+ }
1198
+ if (createdPrompt.status === "failed") {
1199
+ throw buildPromptFailureError(sessionId, createdPrompt, sessionUrl);
1200
+ }
1201
+ if (createdPrompt.status !== "completed") {
1202
+ throw new CliError("server", `Prompt ${createdPrompt.promptId} did not reach a terminal success state before session ${sessionId} became idle (status: ${createdPrompt.status}).`, {
1203
+ hint: `Inspect with: arcanist sessions transcript ${sessionId}`
1204
+ });
1205
+ }
1206
+ }
1207
+ async function createCommand(repoUrl, promptArg, options, command) {
1208
+ const runtime = getRuntimeOptions(command, options);
1209
+ const config = requireConfig(runtime);
1210
+ const prompt = await resolvePromptInput(promptArg, options);
1211
+ const waitPollIntervalMs = options.wait ? parsePollInterval(options.pollInterval) : null;
1212
+ const repoError = validateRepoUrl(repoUrl);
1213
+ if (repoError) {
1214
+ throw new CliError("user", repoError);
1215
+ }
1216
+ const idempotencyKey = options.idempotencyKey ?? randomIdempotencyKey();
1217
+ const sessionIdempotencyKey = `${idempotencyKey}:session`;
1218
+ const promptIdempotencyKey = `${idempotencyKey}:prompt`;
1219
+ const sessionData = await apiFetch(config, "/api/sessions", {
1220
+ method: "POST",
1221
+ headers: { "Idempotency-Key": sessionIdempotencyKey },
1222
+ body: JSON.stringify({
1223
+ context: { repoUrl },
1224
+ ...options.model ? { model: options.model } : {}
1225
+ })
1226
+ });
1227
+ const sessionId = sessionData.sessionId;
1228
+ let promptId;
1229
+ try {
1230
+ const promptData = await apiFetch(config, `/api/sessions/${sessionId}/prompts`, {
1231
+ method: "POST",
1232
+ headers: { "Idempotency-Key": promptIdempotencyKey },
1233
+ body: JSON.stringify({ prompt })
1234
+ });
1235
+ promptId = promptData.prompt?.promptId ?? promptData.prompt?.id;
1236
+ } catch (err) {
1237
+ throw new CliError(err instanceof CliError ? err.code : "server", `Session created (${sessionId}) but prompt failed: ${err instanceof Error ? err.message : String(err)}`, {
1238
+ exitCode: err instanceof CliError ? err.exitCode : void 0,
1239
+ hint: `Retry with: arcanist sessions send ${sessionId} --prompt-stdin`,
1240
+ requestId: err instanceof CliError ? err.requestId : void 0
1241
+ });
1242
+ }
1243
+ if (options.wait) {
1244
+ if (!isJson(command, options)) {
1245
+ console.log(`Session: ${sessionId}`);
1246
+ if (sessionData.sessionUrl) console.log(`URL: ${sessionData.sessionUrl}`);
1247
+ }
1248
+ await waitForCreatedPrompt(sessionId, promptId, sessionData.sessionUrl, waitPollIntervalMs, runtime, command);
1249
+ }
1250
+ if (isJson(command, options)) {
1251
+ writeJson({
1252
+ sessionId,
1253
+ ...sessionData.sessionUrl ? { sessionUrl: sessionData.sessionUrl } : {},
1254
+ repoUrl,
1255
+ ...options.model ? { model: options.model } : {},
1256
+ ...promptId ? { promptId } : {}
1257
+ });
1258
+ return;
1259
+ }
1260
+ if (options.wait) return;
1261
+ console.log(`Session: ${sessionId}`);
1262
+ if (sessionData.sessionUrl) console.log(`URL: ${sessionData.sessionUrl}`);
1263
+ }
1264
+
1265
+ // src/commands/login.ts
1266
+ import { createInterface as createInterface2 } from "readline";
1267
+ async function loginCommand(options, command) {
1268
+ const runtime = getRuntimeOptions(command, options);
1269
+ let token;
1270
+ if (options.tokenStdin) {
1271
+ token = await readStdinTrimmed();
1272
+ } else if (runtime.token) {
1273
+ token = runtime.token;
1274
+ } else {
1275
+ token = await promptHidden("Enter your CLI token: ");
1276
+ }
1277
+ if (!token) {
1278
+ throw new CliError("user", "No token provided.");
1279
+ }
1280
+ if (!token.startsWith("arc_")) {
1281
+ throw new CliError("user", "Invalid token format. Token must start with 'arc_'.");
1282
+ }
1283
+ if (runtime.apiUrl) {
1284
+ const urlError = validateApiUrl(runtime.apiUrl);
1285
+ if (urlError) {
1286
+ throw new CliError("user", urlError);
1287
+ }
1288
+ }
1289
+ const apiUrl = normalizeBaseUrl(resolveLoginApiUrl(runtime.apiUrl));
1290
+ saveConfig({ apiUrl, token });
1291
+ if (runtime.json) {
1292
+ writeJson({ ok: true, apiUrl });
1293
+ } else if (!runtime.quiet) {
1294
+ console.log(`Logged in. API: ${apiUrl}`);
1295
+ }
1296
+ try {
1297
+ const res = await fetch(`${apiUrl}/api/cli-tokens`, {
1298
+ headers: { Authorization: `Bearer ${token}`, "User-Agent": CLI_USER_AGENT }
1299
+ });
1300
+ if (res.ok && !runtime.json && !runtime.quiet) {
1301
+ console.log("Token verified.");
1302
+ } else if (res.status === 401 && !runtime.json && !runtime.quiet) {
1303
+ console.warn("Warning: Token could not be verified (401). It may be invalid or expired.");
1304
+ }
1305
+ } catch {
1306
+ if (!runtime.json && !runtime.quiet) console.warn("Warning: Could not reach API to verify token.");
1307
+ }
1308
+ }
1309
+ function promptHidden(prompt) {
1310
+ if (!process.stdin.isTTY || !process.stdout.isTTY) {
1311
+ return Promise.reject(new CliError("user", "No interactive terminal available. Re-run with --token-stdin or set ARCANIST_TOKEN."));
1312
+ }
1313
+ return new Promise((resolve, reject) => {
1314
+ const rl = createInterface2({ input: process.stdin, output: process.stdout });
1315
+ process.stdout.write(prompt);
1316
+ const stdin = process.stdin;
1317
+ if (stdin.isTTY) stdin.setRawMode(true);
1318
+ let input = "";
1319
+ const onData = (char) => {
1320
+ const c = char.toString();
1321
+ if (c === "\n" || c === "\r") {
1322
+ if (stdin.isTTY) stdin.setRawMode(false);
1323
+ stdin.removeListener("data", onData);
1324
+ process.stdout.write("\n");
1325
+ rl.close();
1326
+ resolve(input);
1327
+ } else if (c === "") {
1328
+ if (stdin.isTTY) stdin.setRawMode(false);
1329
+ stdin.removeListener("data", onData);
1330
+ rl.close();
1331
+ reject(new CliError("user", "Interrupted.", { exitCode: 130 }));
1332
+ } else if (c === "\x7F" || c === "\b") {
1333
+ input = input.slice(0, -1);
1334
+ } else {
1335
+ input += c;
1336
+ }
1337
+ };
1338
+ stdin.on("data", onData);
1339
+ });
1340
+ }
1341
+
1342
+ // src/commands/message.ts
1343
+ async function messageCommand(sessionId, promptArg, options = {}, command) {
1344
+ const runtime = getRuntimeOptions(command, options);
1345
+ const config = requireConfig(runtime);
1346
+ const prompt = await resolvePromptInput(promptArg, options);
1347
+ const response = await apiFetch(config, `/api/sessions/${sessionId}/prompts`, {
1348
+ method: "POST",
1349
+ headers: { "Idempotency-Key": options.idempotencyKey ?? randomIdempotencyKey() },
1350
+ body: JSON.stringify({ prompt })
1351
+ });
1352
+ const promptId = response.prompt?.promptId ?? response.prompt?.id;
1353
+ if (isJson(command, options)) {
1354
+ writeJson({ sessionId, ...promptId ? { promptId } : {} });
1355
+ return;
1356
+ }
1357
+ console.log(`Message sent to session ${sessionId}.`);
1358
+ }
1359
+
1297
1360
  // src/commands/sessions.ts
1298
1361
  async function listSessionsCommand(options, command) {
1299
1362
  const runtime = getRuntimeOptions(command, options);
@@ -1501,10 +1564,11 @@ program.hook("preAction", (_thisCommand, actionCommand) => {
1501
1564
  applyColorEnvironment(getRuntimeOptions(actionCommand));
1502
1565
  });
1503
1566
  function addCreateOptions(cmd) {
1504
- return cmd.argument("<repo-url>", "Repository URL").argument("[prompt]", "Prompt to send, or '-' to read stdin").option("--model <model>", "Model to use").option("--prompt-stdin", "Read prompt from stdin").option("--idempotency-key <uuid>", "Request idempotency key for safe manual retries").addHelpText("after", `
1567
+ return cmd.argument("<repo-url>", "Repository URL").argument("[prompt]", "Prompt to send, or '-' to read stdin").option("--model <model>", "Model to use").option("--prompt-stdin", "Read prompt from stdin").option("--wait", "Wait for the created prompt to finish and exit non-zero if it fails").option("--poll-interval <ms>", "Polling interval in milliseconds while waiting", String(DEFAULT_WATCH_POLL_INTERVAL_MS)).option("--idempotency-key <uuid>", "Request idempotency key for safe manual retries").addHelpText("after", `
1505
1568
  Examples:
1506
1569
  arcanist sessions create https://github.com/org/repo "fix bug"
1507
1570
  printf "fix bug" | arcanist sessions create https://github.com/org/repo --prompt-stdin --json
1571
+ printf "fix bug" | arcanist sessions create https://github.com/org/repo --prompt-stdin --wait
1508
1572
  arcanist sessions create https://github.com/org/repo - --json | jq -r .sessionId | xargs -I{} arcanist sessions events {} --follow --json
1509
1573
  `);
1510
1574
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tryarcanist/cli",
3
- "version": "0.1.22",
3
+ "version": "0.1.24",
4
4
  "description": "CLI for Arcanist — create and manage coding agent sessions",
5
5
  "type": "module",
6
6
  "bin": {