@tryarcanist/cli 0.1.220 → 0.1.222

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 +179 -190
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -69,6 +69,15 @@ function parseApiErrorBody(body) {
69
69
  return null;
70
70
  }
71
71
  }
72
+ function parseConflictBody(err) {
73
+ if (!(err instanceof ApiError) || err.status !== 409) return null;
74
+ try {
75
+ const parsed = JSON.parse(err.body);
76
+ return parsed && typeof parsed === "object" ? parsed : null;
77
+ } catch {
78
+ return null;
79
+ }
80
+ }
72
81
 
73
82
  // src/errors.ts
74
83
  var EXIT_CODE_INTERRUPTED = 130;
@@ -975,7 +984,13 @@ async function reviewCommand(prUrl, options = {}, command) {
975
984
  const { config } = resolveBusinessContext(command, options);
976
985
  const trigger = await apiFetch(config, "/api/pr-reviews", {
977
986
  method: "POST",
978
- body: JSON.stringify({ prUrl, ...options.focus ? { focus: options.focus } : {} })
987
+ body: JSON.stringify({
988
+ prUrl,
989
+ ...options.focus ? { focus: options.focus } : {},
990
+ // Send an explicitly passed model even when empty (e.g. --model "$VAR" with an
991
+ // unset VAR) so the server rejects it as invalid instead of silently routing.
992
+ ...options.model !== void 0 ? { model: options.model } : {}
993
+ })
979
994
  });
980
995
  if (!options.wait || trigger.cached) {
981
996
  emit(command, options, trigger, (payload) => {
@@ -1836,6 +1851,26 @@ var SESSION_START_MODEL_PROVIDER_GROUPS_BY_SUBSCRIPTIONS = {
1836
1851
  "true:true": buildSubscriptionAwareGroups({ codexSubscription: true, grokSubscription: true })
1837
1852
  };
1838
1853
 
1854
+ // src/utils/pagination.ts
1855
+ var MAX_ALL_PAGES = 1e3;
1856
+ async function fetchPages(label, all, initialCursor, fetchPage) {
1857
+ const items = [];
1858
+ let cursor2 = initialCursor;
1859
+ let nextCursor = null;
1860
+ let pageCount = 0;
1861
+ do {
1862
+ pageCount += 1;
1863
+ if (all && pageCount > MAX_ALL_PAGES) {
1864
+ throw new CliError("user", `${label} exceeded ${MAX_ALL_PAGES} pages without reaching the end.`);
1865
+ }
1866
+ const page = await fetchPage(cursor2);
1867
+ items.push(...page.items);
1868
+ nextCursor = page.nextCursor;
1869
+ cursor2 = nextCursor ?? void 0;
1870
+ } while (all && nextCursor);
1871
+ return { items, nextCursor: all ? null : nextCursor };
1872
+ }
1873
+
1839
1874
  // ../../shared/github/repo-url.ts
1840
1875
  function parseGithubRepoFullName(value) {
1841
1876
  const shorthand = value.match(/^([a-zA-Z0-9_.-]+)\/([a-zA-Z0-9_.-]+)$/);
@@ -1847,6 +1882,32 @@ function parseGithubRepoFullName(value) {
1847
1882
  return null;
1848
1883
  }
1849
1884
 
1885
+ // src/git.ts
1886
+ import { execFileSync as execFileSync2 } from "child_process";
1887
+ function git(args) {
1888
+ try {
1889
+ return execFileSync2("git", args, { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }).trim();
1890
+ } catch (err) {
1891
+ throw new CliError("user", `git ${args.join(" ")} failed: ${stringifyError(err)}`);
1892
+ }
1893
+ }
1894
+ function currentRepo() {
1895
+ const parsed = parseGithubRepoFullName(git(["remote", "get-url", "origin"]));
1896
+ if (!parsed) throw new CliError("user", "origin remote must point at a GitHub repository");
1897
+ return { owner: parsed.owner, repo: parsed.repo };
1898
+ }
1899
+
1900
+ // src/utils/repo-arg.ts
1901
+ function parseRepoArg(value, argName = "repo") {
1902
+ const parsed = parseGithubRepoFullName(value);
1903
+ if (!parsed) throw new CliError("user", `${argName} must be owner/name or a GitHub URL`);
1904
+ return { owner: parsed.owner, repo: parsed.repo.replace(/\.git$/, "") };
1905
+ }
1906
+ function parseRepoArgOrCurrent(value, argName = "repo") {
1907
+ if (value === void 0) return currentRepo();
1908
+ return parseRepoArg(value, argName);
1909
+ }
1910
+
1850
1911
  // src/commands/automations.ts
1851
1912
  var AUTOMATION_ERROR_HINTS = {
1852
1913
  invalid_cron: "Use a standard five-field cron expression, for example: */15 * * * *",
@@ -1863,9 +1924,8 @@ var AUTOMATION_ERROR_HINTS = {
1863
1924
  repo_skills_unavailable: "Arcanist could not verify the repo's skills right now. Retry once GitHub skill discovery is healthy.",
1864
1925
  unknown_skill: "That leading slash skill does not exist in the selected repository."
1865
1926
  };
1866
- var MAX_ALL_PAGES = 1e3;
1867
1927
  async function createAutomationCommand(repoUrl, promptArg, options, command) {
1868
- const repo = parseAutomationRepo(repoUrl);
1928
+ const repo = parseRepoArg(repoUrl, "repo-url");
1869
1929
  const prompt = await resolvePromptInput(promptArg, options);
1870
1930
  const slackTeamId = options.slackTeamId?.trim();
1871
1931
  const slackChannelId = options.slackChannelId?.trim();
@@ -1912,27 +1972,22 @@ async function createAutomationCommand(repoUrl, promptArg, options, command) {
1912
1972
  async function listAutomationsCommand(options, command) {
1913
1973
  const runtime = getRuntimeOptions(command, options);
1914
1974
  const config = requireConfig(runtime);
1915
- const items = [];
1916
- let cursor2 = options.cursor;
1917
- let nextCursor = null;
1918
- let pageCount = 0;
1919
- do {
1920
- pageCount += 1;
1921
- if (options.all === true && pageCount > MAX_ALL_PAGES) {
1922
- throw new CliError("user", `automations list --all exceeded ${MAX_ALL_PAGES} pages without reaching the end.`);
1975
+ const { items, nextCursor } = await fetchPages(
1976
+ "automations list --all",
1977
+ options.all === true,
1978
+ options.cursor,
1979
+ async (cursor2) => {
1980
+ const query = new URLSearchParams();
1981
+ if (options.limit) query.set("limit", options.limit);
1982
+ if (cursor2) query.set("cursor", cursor2);
1983
+ const payload = await automationApiFetch(
1984
+ config,
1985
+ `/api/automation/schedules${query.size ? `?${query.toString()}` : ""}`
1986
+ );
1987
+ return { items: payload.data.items, nextCursor: payload.data.nextCursor };
1923
1988
  }
1924
- const query = new URLSearchParams();
1925
- if (options.limit) query.set("limit", options.limit);
1926
- if (cursor2) query.set("cursor", cursor2);
1927
- const payload = await automationApiFetch(
1928
- config,
1929
- `/api/automation/schedules${query.size ? `?${query.toString()}` : ""}`
1930
- );
1931
- items.push(...payload.data.items);
1932
- nextCursor = payload.data.nextCursor;
1933
- cursor2 = nextCursor ?? void 0;
1934
- } while (options.all === true && nextCursor);
1935
- const output = { items, nextCursor: options.all === true ? null : nextCursor };
1989
+ );
1990
+ const output = { items, nextCursor };
1936
1991
  if (isJson(command, options)) {
1937
1992
  writeJson(output);
1938
1993
  return;
@@ -1964,14 +2019,6 @@ async function deleteAutomationCommand(id, options, command) {
1964
2019
  }
1965
2020
  console.log(`Deleted automation ${id}.`);
1966
2021
  }
1967
- function parseAutomationRepo(value) {
1968
- if (/^git@/i.test(value) && !/^git@github\.com:/i.test(value)) {
1969
- throw new CliError("user", "repo-url must be owner/name or a GitHub URL.");
1970
- }
1971
- const parsed = parseGithubRepoFullName(value);
1972
- if (!parsed) throw new CliError("user", "repo-url must be owner/name or a GitHub URL.");
1973
- return { owner: parsed.owner, repo: parsed.repo };
1974
- }
1975
2022
  function resolveAutomationModelBackend(rawModel, normalizedModel) {
1976
2023
  if (normalizedModel) {
1977
2024
  const backend = getAgentRuntimeBackendForModel(normalizedModel);
@@ -2380,6 +2427,21 @@ function normalizeUploadedFileOptions(files) {
2380
2427
  function clampPollInterval(ms, minimum) {
2381
2428
  return Math.max(ms, minimum);
2382
2429
  }
2430
+ function parsePollInterval(raw, opts = {}) {
2431
+ const defaultMs = opts.defaultMs ?? DEFAULT_WATCH_POLL_INTERVAL_MS;
2432
+ const minMs = opts.minMs ?? MIN_WATCH_POLL_INTERVAL_MS;
2433
+ const maxMs = opts.maxMs === void 0 ? MAX_WATCH_POLL_INTERVAL_MS : opts.maxMs;
2434
+ if (!raw) return defaultMs;
2435
+ if (!/^\d+$/.test(raw)) {
2436
+ throw new CliError("user", "Polling interval must be a non-negative integer.");
2437
+ }
2438
+ const value = Number(raw);
2439
+ if (value < minMs || maxMs !== null && value > maxMs) {
2440
+ const range = maxMs === null ? `at least ${minMs}` : `between ${minMs} and ${maxMs}`;
2441
+ throw new CliError("user", `Polling interval must be ${range} milliseconds.`);
2442
+ }
2443
+ return value;
2444
+ }
2383
2445
 
2384
2446
  // ../../shared/session/no-change-outcome.ts
2385
2447
  function isNoChangesPromptResult(result) {
@@ -3883,20 +3945,6 @@ function applyDisconnectMask(view, mask, now) {
3883
3945
  }
3884
3946
 
3885
3947
  // src/commands/watch.ts
3886
- function parsePollInterval(raw) {
3887
- if (!raw) return DEFAULT_WATCH_POLL_INTERVAL_MS;
3888
- if (!/^\d+$/.test(raw)) {
3889
- throw new CliError("user", "Polling interval must be a non-negative integer.");
3890
- }
3891
- const value = Number(raw);
3892
- if (value < MIN_WATCH_POLL_INTERVAL_MS || value > MAX_WATCH_POLL_INTERVAL_MS) {
3893
- throw new CliError(
3894
- "user",
3895
- `Polling interval must be between ${MIN_WATCH_POLL_INTERVAL_MS} and ${MAX_WATCH_POLL_INTERVAL_MS} milliseconds.`
3896
- );
3897
- }
3898
- return value;
3899
- }
3900
3948
  function parseNonNegativeInteger(raw, name, defaultValue) {
3901
3949
  if (!raw) return defaultValue;
3902
3950
  if (!/^\d+$/.test(raw)) {
@@ -4321,16 +4369,6 @@ function parseEgressAllowlistSourceFile(content, key = EGRESS_ALLOWLIST_SOURCE_P
4321
4369
  }
4322
4370
 
4323
4371
  // src/commands/egress.ts
4324
- function parseRepoArg(value) {
4325
- const parsed = parseGithubRepoFullName(value);
4326
- if (!parsed) throw new CliError("user", "repo must be owner/name or a GitHub URL");
4327
- return {
4328
- ...parsed,
4329
- // Preserve the legacy shorthand contract while URLs and SSH remotes are
4330
- // normalized by the shared parser itself.
4331
- repo: /^[^/:]+\/[^/]+\.git$/.test(value.trim()) ? parsed.repo.slice(0, -4) : parsed.repo
4332
- };
4333
- }
4334
4372
  async function egressSourceSetCommand(sourceRepoArg, options = {}, command) {
4335
4373
  const { config } = resolveBusinessContext(command, options);
4336
4374
  const businessId = await resolveBusinessId(config, options);
@@ -4563,13 +4601,13 @@ function parseCreateConflict(error) {
4563
4601
  let message = error.message;
4564
4602
  let sessionId;
4565
4603
  let sessionUrl;
4566
- try {
4567
- const parsed = JSON.parse(error.body);
4568
- const parsedMessage = typeof parsed.error === "string" ? parsed.error : typeof parsed.error?.message === "string" ? parsed.error.message : void 0;
4604
+ const parsed = parseConflictBody(error);
4605
+ if (parsed) {
4606
+ const rawError = parsed.error;
4607
+ const parsedMessage = typeof rawError === "string" ? rawError : rawError && typeof rawError === "object" && typeof rawError.message === "string" ? rawError.message : void 0;
4569
4608
  if (parsedMessage) message = parsedMessage;
4570
4609
  if (typeof parsed.sessionId === "string") sessionId = parsed.sessionId;
4571
4610
  if (typeof parsed.sessionUrl === "string") sessionUrl = parsed.sessionUrl;
4572
- } catch {
4573
4611
  }
4574
4612
  const location = sessionUrl ? ` (${sessionUrl})` : "";
4575
4613
  const existing = sessionId ? ` Existing verifier session: ${sessionId}${location}.` : "";
@@ -4661,28 +4699,7 @@ async function qaCommand(prUrl, options, command) {
4661
4699
  console.log(`Follow with: arcanist sessions events ${sessionId} --follow --json`);
4662
4700
  }
4663
4701
 
4664
- // src/git.ts
4665
- import { execFileSync as execFileSync2 } from "child_process";
4666
- function git(args) {
4667
- try {
4668
- return execFileSync2("git", args, { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }).trim();
4669
- } catch (err) {
4670
- throw new CliError("user", `git ${args.join(" ")} failed: ${stringifyError(err)}`);
4671
- }
4672
- }
4673
- function currentRepo() {
4674
- const parsed = parseGithubRepoFullName(git(["remote", "get-url", "origin"]));
4675
- if (!parsed) throw new CliError("user", "origin remote must point at a GitHub repository");
4676
- return { owner: parsed.owner, repo: parsed.repo };
4677
- }
4678
-
4679
4702
  // src/commands/repos.ts
4680
- function parseRepoArg2(value) {
4681
- if (value === void 0) return currentRepo();
4682
- const parsed = parseGithubRepoFullName(value);
4683
- if (!parsed) throw new CliError("user", "repo must be owner/name or a GitHub URL");
4684
- return { owner: parsed.owner, repo: parsed.repo };
4685
- }
4686
4703
  async function reposListCommand(options = {}, command) {
4687
4704
  const { config } = resolveBusinessContext(command, options);
4688
4705
  const payload = await apiFetch(
@@ -4698,8 +4715,7 @@ async function reposListCommand(options = {}, command) {
4698
4715
  }
4699
4716
  async function repoBranchesCommand(repoArg, options = {}, command) {
4700
4717
  const { config } = resolveBusinessContext(command, options);
4701
- const repo = parseRepoArg2(repoArg);
4702
- await apiFetch(config, "/api/repos");
4718
+ const repo = parseRepoArgOrCurrent(repoArg);
4703
4719
  const payload = await apiFetch(
4704
4720
  config,
4705
4721
  `/api/repos/${encodeURIComponent(repo.owner)}/${encodeURIComponent(repo.repo)}/branches`
@@ -4710,7 +4726,7 @@ async function repoBranchesCommand(repoArg, options = {}, command) {
4710
4726
  }
4711
4727
  async function repoSkillsCommand(repoArg, options = {}, command) {
4712
4728
  const { config } = resolveBusinessContext(command, options);
4713
- const repo = parseRepoArg2(repoArg);
4729
+ const repo = parseRepoArgOrCurrent(repoArg);
4714
4730
  const payload = await apiFetch(
4715
4731
  config,
4716
4732
  `/api/repos/${encodeURIComponent(repo.owner)}/${encodeURIComponent(repo.repo)}/skills`
@@ -4758,17 +4774,13 @@ async function respondCommand(sessionId, answerArg, options = {}, command) {
4758
4774
  }
4759
4775
  }
4760
4776
  function parseRespondConflict(err) {
4761
- if (!(err instanceof ApiError) || err.status !== 409) return null;
4762
- try {
4763
- const body = JSON.parse(err.body);
4764
- if (body.error !== "session_not_respondable") return null;
4765
- return {
4766
- message: body.reason ? `Session cannot be answered from phase=${body.reason}.` : "Session does not have a pending question.",
4767
- reason: body.reason ?? "session_not_respondable"
4768
- };
4769
- } catch {
4770
- return null;
4771
- }
4777
+ const body = parseConflictBody(err);
4778
+ if (!body || body.error !== "session_not_respondable") return null;
4779
+ const reason = typeof body.reason === "string" ? body.reason : void 0;
4780
+ return {
4781
+ message: reason ? `Session cannot be answered from phase=${reason}.` : "Session does not have a pending question.",
4782
+ reason: reason ?? "session_not_respondable"
4783
+ };
4772
4784
  }
4773
4785
 
4774
4786
  // src/commands/sandbox.ts
@@ -5321,23 +5333,9 @@ function assertCleanAndPushed() {
5321
5333
  function assertManifestExists(path) {
5322
5334
  if (!existsSync3(path)) throw new CliError("user", `Missing sandbox manifest: ${path}`);
5323
5335
  }
5324
- function parseRepoArg3(value, fallback) {
5325
- if (!value) return fallback();
5326
- const parsed = parseGithubRepoFullName(value);
5327
- if (!parsed) throw new CliError("user", "repo must be owner/name or a GitHub URL");
5328
- return { owner: parsed.owner, repo: parsed.repo };
5329
- }
5330
5336
  function repoPath(repo) {
5331
5337
  return `${repo.owner}/${repo.repo}`;
5332
5338
  }
5333
- function parsePollInterval2(value) {
5334
- if (!value) return DEFAULT_POLL_INTERVAL_MS;
5335
- const parsed = Number(value);
5336
- if (!Number.isInteger(parsed) || parsed < MIN_POLL_INTERVAL_MS) {
5337
- throw new CliError("user", `--poll-interval must be an integer >= ${MIN_POLL_INTERVAL_MS}.`);
5338
- }
5339
- return parsed;
5340
- }
5341
5339
  function printBuildRequest(buildRequest) {
5342
5340
  console.log(
5343
5341
  [
@@ -5609,8 +5607,8 @@ async function sandboxBuildCommand(sourceRepoArg, options = {}, command) {
5609
5607
  const manifestPath = options.manifest ?? DEFAULT_MANIFEST_PATH;
5610
5608
  assertManifestExists(manifestPath);
5611
5609
  if (!options.ref) assertCleanAndPushed();
5612
- const sourceRepo = parseRepoArg3(sourceRepoArg, currentRepo);
5613
- const targetRepo = options.targetRepo ? parseRepoArg3(options.targetRepo, currentRepo) : null;
5610
+ const sourceRepo = parseRepoArgOrCurrent(sourceRepoArg);
5611
+ const targetRepo = options.targetRepo ? parseRepoArgOrCurrent(options.targetRepo) : null;
5614
5612
  const businessId = await resolveBusinessId(config, options);
5615
5613
  const ref = options.ref ?? currentRef();
5616
5614
  let payload;
@@ -5648,7 +5646,11 @@ async function sandboxBuildCommand(sourceRepoArg, options = {}, command) {
5648
5646
  if (!options.wait && !options.follow) return;
5649
5647
  const finalBuild = await waitForBuild(config, businessId, payload.buildRequest.id, {
5650
5648
  follow: options.follow,
5651
- pollIntervalMs: parsePollInterval2(options.pollInterval),
5649
+ pollIntervalMs: parsePollInterval(options.pollInterval, {
5650
+ defaultMs: DEFAULT_POLL_INTERVAL_MS,
5651
+ minMs: MIN_POLL_INTERVAL_MS,
5652
+ maxMs: null
5653
+ }),
5652
5654
  json
5653
5655
  });
5654
5656
  if (json)
@@ -5661,7 +5663,7 @@ async function sandboxBuildCommand(sourceRepoArg, options = {}, command) {
5661
5663
  async function sandboxStatusCommand(repoArg, options = {}, command) {
5662
5664
  const { config } = resolveBusinessContext(command, options);
5663
5665
  const businessId = await resolveBusinessId(config, options);
5664
- const repo = parseRepoArg3(repoArg, currentRepo);
5666
+ const repo = parseRepoArgOrCurrent(repoArg);
5665
5667
  const payload = await apiFetch(
5666
5668
  config,
5667
5669
  `/api/businesses/${encodeURIComponent(businessId)}/repos/${encodeURIComponent(repo.owner)}/${encodeURIComponent(
@@ -5673,9 +5675,9 @@ async function sandboxStatusCommand(repoArg, options = {}, command) {
5673
5675
  async function sandboxHistoryCommand(sourceRepoArg, options = {}, command) {
5674
5676
  const { config } = resolveBusinessContext(command, options);
5675
5677
  const businessId = await resolveBusinessId(config, options);
5676
- const sourceRepo = parseRepoArg3(sourceRepoArg, currentRepo);
5678
+ const sourceRepo = parseRepoArgOrCurrent(sourceRepoArg);
5677
5679
  const params = new URLSearchParams({ sourceRepo: repoPath(sourceRepo) });
5678
- if (options.targetRepo) params.set("targetRepo", repoPath(parseRepoArg3(options.targetRepo, currentRepo)));
5680
+ if (options.targetRepo) params.set("targetRepo", repoPath(parseRepoArgOrCurrent(options.targetRepo)));
5679
5681
  if (options.status) params.set("status", options.status);
5680
5682
  if (options.limit) {
5681
5683
  const limit = Number(options.limit);
@@ -5770,13 +5772,19 @@ async function sandboxLogsCommand(buildId, options = {}, command) {
5770
5772
  }
5771
5773
  if (!options.follow) return;
5772
5774
  if (payload.logs.length > 0) afterSequence = payload.logs[payload.logs.length - 1].sequence;
5773
- await sleep(parsePollInterval2(options.pollInterval));
5775
+ await sleep(
5776
+ parsePollInterval(options.pollInterval, {
5777
+ defaultMs: DEFAULT_POLL_INTERVAL_MS,
5778
+ minMs: MIN_POLL_INTERVAL_MS,
5779
+ maxMs: null
5780
+ })
5781
+ );
5774
5782
  }
5775
5783
  }
5776
5784
  async function sandboxAssignDefaultCommand(sourceRepoArg, options = {}, command) {
5777
5785
  const { config } = resolveBusinessContext(command, options);
5778
5786
  const businessId = await resolveBusinessId(config, options);
5779
- const sourceRepo = parseRepoArg3(sourceRepoArg, currentRepo);
5787
+ const sourceRepo = parseRepoArgOrCurrent(sourceRepoArg);
5780
5788
  const manifestPath = options.manifest ?? DEFAULT_MANIFEST_PATH;
5781
5789
  const payload = await apiFetch(
5782
5790
  config,
@@ -5793,8 +5801,8 @@ async function sandboxAssignDefaultCommand(sourceRepoArg, options = {}, command)
5793
5801
  async function sandboxAssignRepoCommand(targetRepoArg, sourceRepoArg, options = {}, command) {
5794
5802
  const { config } = resolveBusinessContext(command, options);
5795
5803
  const businessId = await resolveBusinessId(config, options);
5796
- const targetRepo = parseRepoArg3(targetRepoArg, currentRepo);
5797
- const sourceRepo = parseRepoArg3(sourceRepoArg, currentRepo);
5804
+ const targetRepo = parseRepoArgOrCurrent(targetRepoArg);
5805
+ const sourceRepo = parseRepoArgOrCurrent(sourceRepoArg);
5798
5806
  const manifestPath = options.manifest ?? DEFAULT_MANIFEST_PATH;
5799
5807
  const payload = await apiFetch(
5800
5808
  config,
@@ -5832,7 +5840,7 @@ async function sandboxUnassignRepoCommand(targetRepoArg, options = {}, command)
5832
5840
  }
5833
5841
  const { config } = resolveBusinessContext(command, options);
5834
5842
  const businessId = await resolveBusinessId(config, options);
5835
- const targetRepo = parseRepoArg3(targetRepoArg, currentRepo);
5843
+ const targetRepo = parseRepoArgOrCurrent(targetRepoArg);
5836
5844
  if (!options.yes) await confirmOrThrow(`Clear sandbox layer assignment for ${repoPath(targetRepo)}?`);
5837
5845
  const payload = await apiFetch(
5838
5846
  config,
@@ -5845,35 +5853,29 @@ async function sandboxUnassignRepoCommand(targetRepoArg, options = {}, command)
5845
5853
  }
5846
5854
 
5847
5855
  // src/commands/sessions.ts
5848
- var MAX_ALL_PAGES2 = 1e3;
5849
5856
  async function listSessionsCommand(options, command) {
5850
5857
  const runtime = getRuntimeOptions(command, options);
5851
5858
  const config = requireConfig(runtime);
5852
- const sessions2 = [];
5853
- let cursor2 = options.cursor;
5854
- let nextCursor = null;
5855
- let pageCount = 0;
5856
- do {
5857
- pageCount += 1;
5858
- if (options.all === true && pageCount > MAX_ALL_PAGES2) {
5859
- throw new CliError("user", `sessions list --all exceeded ${MAX_ALL_PAGES2} pages without reaching the end.`);
5860
- }
5861
- const query = new URLSearchParams();
5862
- if (options.status) query.set("status", options.status);
5863
- if (options.scope) query.set("scope", options.scope);
5864
- if (options.search) query.set("q", options.search);
5865
- if (options.repo) query.set("repo", options.repo);
5866
- if (options.limit) query.set("limit", options.limit);
5867
- if (cursor2) query.set("cursor", cursor2);
5868
- const payload2 = await apiFetch(
5869
- config,
5870
- `/api/sessions${query.size ? `?${query.toString()}` : ""}`
5871
- );
5872
- sessions2.push(...payload2.sessions);
5873
- nextCursor = payload2.nextCursor;
5874
- cursor2 = nextCursor ?? void 0;
5875
- } while (options.all === true && nextCursor);
5876
- const payload = { sessions: sessions2, nextCursor: options.all === true ? null : nextCursor };
5859
+ const { items: sessions2, nextCursor } = await fetchPages(
5860
+ "sessions list --all",
5861
+ options.all === true,
5862
+ options.cursor,
5863
+ async (cursor2) => {
5864
+ const query = new URLSearchParams();
5865
+ if (options.status) query.set("status", options.status);
5866
+ if (options.scope) query.set("scope", options.scope);
5867
+ if (options.search) query.set("q", options.search);
5868
+ if (options.repo) query.set("repo", options.repo);
5869
+ if (options.limit) query.set("limit", options.limit);
5870
+ if (cursor2) query.set("cursor", cursor2);
5871
+ const payload2 = await apiFetch(
5872
+ config,
5873
+ `/api/sessions${query.size ? `?${query.toString()}` : ""}`
5874
+ );
5875
+ return { items: payload2.sessions, nextCursor: payload2.nextCursor };
5876
+ }
5877
+ );
5878
+ const payload = { sessions: sessions2, nextCursor };
5877
5879
  if (isJson(command, options)) {
5878
5880
  writeJson(payload);
5879
5881
  return;
@@ -6015,25 +6017,15 @@ async function stopCommand(sessionId, options = {}, command) {
6015
6017
  });
6016
6018
  }
6017
6019
  function parseStopBlocked(err) {
6018
- if (!(err instanceof ApiError) || err.status !== 409) return null;
6019
- try {
6020
- const body = JSON.parse(err.body);
6021
- if (body.error !== "session_not_stoppable") return null;
6022
- return { reason: body.reason ?? "not_stoppable" };
6023
- } catch {
6024
- return null;
6025
- }
6020
+ const body = parseConflictBody(err);
6021
+ if (!body || body.error !== "session_not_stoppable") return null;
6022
+ return { reason: typeof body.reason === "string" ? body.reason : "not_stoppable" };
6026
6023
  }
6027
6024
 
6028
6025
  // src/commands/test-creds.ts
6029
6026
  import { readFileSync as readFileSync4 } from "fs";
6030
- function parseRepoArg4(value) {
6031
- const parsed = parseGithubRepoFullName(value);
6032
- if (!parsed) throw new CliError("user", "repo must be owner/name or a GitHub URL");
6033
- return { owner: parsed.owner, name: parsed.repo };
6034
- }
6035
6027
  function basePath(businessId, repo) {
6036
- return `/api/businesses/${encodeURIComponent(businessId)}/repos/${encodeURIComponent(repo.owner)}/${encodeURIComponent(repo.name)}/test-credentials`;
6028
+ return `/api/businesses/${encodeURIComponent(businessId)}/repos/${encodeURIComponent(repo.owner)}/${encodeURIComponent(repo.repo)}/test-credentials`;
6037
6029
  }
6038
6030
  function readValue(options) {
6039
6031
  const sources = [options.value, options.valueFile, options.valueStdin].filter((v) => v !== void 0 && v !== false);
@@ -6046,11 +6038,11 @@ function readValue(options) {
6046
6038
  }
6047
6039
  async function listTestCredentialsCommand(repo, options, command) {
6048
6040
  const { config } = resolveBusinessContext(command, options);
6049
- const repoArg = parseRepoArg4(repo);
6041
+ const repoArg = parseRepoArg(repo);
6050
6042
  const payload = await apiFetch(config, basePath(options.business, repoArg));
6051
6043
  emit(command, options, payload, (listPayload) => {
6052
6044
  if (listPayload.credentials.length === 0) {
6053
- console.log(`No test credentials configured for ${repoArg.owner}/${repoArg.name}.`);
6045
+ console.log(`No test credentials configured for ${repoArg.owner}/${repoArg.repo}.`);
6054
6046
  return;
6055
6047
  }
6056
6048
  for (const cred of listPayload.credentials) {
@@ -6061,7 +6053,7 @@ async function listTestCredentialsCommand(repo, options, command) {
6061
6053
  }
6062
6054
  async function setTestCredentialCommand(repo, name, options, command) {
6063
6055
  const { config } = resolveBusinessContext(command, options);
6064
- const repoArg = parseRepoArg4(repo);
6056
+ const repoArg = parseRepoArg(repo);
6065
6057
  const value = readValue(options);
6066
6058
  const payload = await apiFetch(
6067
6059
  config,
@@ -6072,7 +6064,7 @@ async function setTestCredentialCommand(repo, name, options, command) {
6072
6064
  command,
6073
6065
  options,
6074
6066
  payload,
6075
- () => console.log(`Set test credential '${name}' for ${repoArg.owner}/${repoArg.name}.`)
6067
+ () => console.log(`Set test credential '${name}' for ${repoArg.owner}/${repoArg.repo}.`)
6076
6068
  );
6077
6069
  }
6078
6070
  async function deleteTestCredentialCommand(repo, name, options, command) {
@@ -6080,11 +6072,11 @@ async function deleteTestCredentialCommand(repo, name, options, command) {
6080
6072
  throw new CliError("user", "`test-creds delete --json` requires --yes.");
6081
6073
  }
6082
6074
  if (options.yes !== true) {
6083
- const repoArg2 = parseRepoArg4(repo);
6084
- await confirmOrThrow(`Delete test credential '${name}' for ${repoArg2.owner}/${repoArg2.name}?`);
6075
+ const repoArg2 = parseRepoArg(repo);
6076
+ await confirmOrThrow(`Delete test credential '${name}' for ${repoArg2.owner}/${repoArg2.repo}?`);
6085
6077
  }
6086
6078
  const { config } = resolveBusinessContext(command, options);
6087
- const repoArg = parseRepoArg4(repo);
6079
+ const repoArg = parseRepoArg(repo);
6088
6080
  const payload = await apiFetch(
6089
6081
  config,
6090
6082
  `${basePath(options.business, repoArg)}/${encodeURIComponent(name)}`,
@@ -6094,35 +6086,29 @@ async function deleteTestCredentialCommand(repo, name, options, command) {
6094
6086
  command,
6095
6087
  options,
6096
6088
  payload,
6097
- () => console.log(`Deleted test credential '${name}' for ${repoArg.owner}/${repoArg.name}.`)
6089
+ () => console.log(`Deleted test credential '${name}' for ${repoArg.owner}/${repoArg.repo}.`)
6098
6090
  );
6099
6091
  }
6100
6092
 
6101
6093
  // src/commands/tokens.ts
6102
- var MAX_ALL_PAGES3 = 1e3;
6103
6094
  async function listTokensCommand(options, command) {
6104
6095
  const { config } = resolveBusinessContext(command, options);
6105
- const data = [];
6106
- let cursor2 = options.cursor;
6107
- let nextCursor = null;
6108
- let pageCount = 0;
6109
- do {
6110
- pageCount += 1;
6111
- if (options.all === true && pageCount > MAX_ALL_PAGES3) {
6112
- throw new CliError("user", `tokens list --all exceeded ${MAX_ALL_PAGES3} pages without reaching the end.`);
6096
+ const { items: data, nextCursor } = await fetchPages(
6097
+ "tokens list --all",
6098
+ options.all === true,
6099
+ options.cursor,
6100
+ async (cursor2) => {
6101
+ const query = new URLSearchParams();
6102
+ if (options.limit) query.set("limit", options.limit);
6103
+ if (cursor2) query.set("cursor", cursor2);
6104
+ const payload2 = await apiFetch(
6105
+ config,
6106
+ `/api/cli-tokens${query.size ? `?${query.toString()}` : ""}`
6107
+ );
6108
+ return { items: payload2.data, nextCursor: payload2.nextCursor };
6113
6109
  }
6114
- const query = new URLSearchParams();
6115
- if (options.limit) query.set("limit", options.limit);
6116
- if (cursor2) query.set("cursor", cursor2);
6117
- const payload2 = await apiFetch(
6118
- config,
6119
- `/api/cli-tokens${query.size ? `?${query.toString()}` : ""}`
6120
- );
6121
- data.push(...payload2.data);
6122
- nextCursor = payload2.nextCursor;
6123
- cursor2 = nextCursor ?? void 0;
6124
- } while (options.all === true && nextCursor);
6125
- const payload = { data, nextCursor: options.all === true ? null : nextCursor };
6110
+ );
6111
+ const payload = { data, nextCursor };
6126
6112
  emit(command, options, payload, (listPayload) => {
6127
6113
  if (listPayload.data.length === 0) {
6128
6114
  console.log("No CLI tokens found.");
@@ -6405,14 +6391,17 @@ cursor.command("use <state>").description("Turn using your saved Cursor subscrip
6405
6391
  cursor.command("status").description("Show whether your workspace is eligible and whether a Cursor subscription auth is saved").action((options, command) => agentSubscriptionStatusCommand(CURSOR_SUBSCRIPTION_CLI_CONFIG, options, command));
6406
6392
  cursor.command("logout").description("Deactivate the selector and remove the stored Cursor subscription auth for your user").action((options, command) => agentSubscriptionLogoutCommand(CURSOR_SUBSCRIPTION_CLI_CONFIG, options, command));
6407
6393
  var sessions = program.command("sessions").description("Session commands");
6408
- program.command("review <pr-url>").description("Run a Zeus review for a GitHub pull request").option("--focus <text>", "Focus the review on a specific concern").option("--wait", "Wait for the review and print its verdict and findings").addHelpText(
6394
+ program.command("review <pr-url>").description("Run a Zeus review for a GitHub pull request").option("--focus <text>", "Focus the review on a specific concern").option("--model <model>", "Pin the reviewer model (Arcanist members only); defaults to automatic routing").option("--wait", "Wait for the review and print its verdict and findings").addHelpText(
6409
6395
  "after",
6410
6396
  `
6411
6397
  Examples:
6412
6398
  arcanist review https://github.com/org/repo/pull/123 --wait --json
6413
6399
  arcanist review https://github.com/org/repo/pull/123 --focus "Check auth" --wait
6400
+ arcanist review https://github.com/org/repo/pull/123 --model gpt-5.5 --wait
6414
6401
 
6415
- Zeus always runs the platform-managed Codex reviewer; --model and --backend do not apply to reviews.
6402
+ When the PR came from an Arcanist session, Zeus defaults to the runtime backend opposite
6403
+ the author's as a cross-check; otherwise it uses the default Codex reviewer. --model pins
6404
+ a specific reviewer model instead (members only).
6416
6405
  Zeus review is currently available to Arcanist members only.
6417
6406
  `
6418
6407
  ).action((prUrl, options, command) => reviewCommand(prUrl, options, command));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tryarcanist/cli",
3
- "version": "0.1.220",
3
+ "version": "0.1.222",
4
4
  "description": "CLI for Arcanist — create and manage coding agent sessions",
5
5
  "type": "module",
6
6
  "bin": {