@tryarcanist/cli 0.1.221 → 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 +167 -187
  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;
@@ -1842,6 +1851,26 @@ var SESSION_START_MODEL_PROVIDER_GROUPS_BY_SUBSCRIPTIONS = {
1842
1851
  "true:true": buildSubscriptionAwareGroups({ codexSubscription: true, grokSubscription: true })
1843
1852
  };
1844
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
+
1845
1874
  // ../../shared/github/repo-url.ts
1846
1875
  function parseGithubRepoFullName(value) {
1847
1876
  const shorthand = value.match(/^([a-zA-Z0-9_.-]+)\/([a-zA-Z0-9_.-]+)$/);
@@ -1853,6 +1882,32 @@ function parseGithubRepoFullName(value) {
1853
1882
  return null;
1854
1883
  }
1855
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
+
1856
1911
  // src/commands/automations.ts
1857
1912
  var AUTOMATION_ERROR_HINTS = {
1858
1913
  invalid_cron: "Use a standard five-field cron expression, for example: */15 * * * *",
@@ -1869,9 +1924,8 @@ var AUTOMATION_ERROR_HINTS = {
1869
1924
  repo_skills_unavailable: "Arcanist could not verify the repo's skills right now. Retry once GitHub skill discovery is healthy.",
1870
1925
  unknown_skill: "That leading slash skill does not exist in the selected repository."
1871
1926
  };
1872
- var MAX_ALL_PAGES = 1e3;
1873
1927
  async function createAutomationCommand(repoUrl, promptArg, options, command) {
1874
- const repo = parseAutomationRepo(repoUrl);
1928
+ const repo = parseRepoArg(repoUrl, "repo-url");
1875
1929
  const prompt = await resolvePromptInput(promptArg, options);
1876
1930
  const slackTeamId = options.slackTeamId?.trim();
1877
1931
  const slackChannelId = options.slackChannelId?.trim();
@@ -1918,27 +1972,22 @@ async function createAutomationCommand(repoUrl, promptArg, options, command) {
1918
1972
  async function listAutomationsCommand(options, command) {
1919
1973
  const runtime = getRuntimeOptions(command, options);
1920
1974
  const config = requireConfig(runtime);
1921
- const items = [];
1922
- let cursor2 = options.cursor;
1923
- let nextCursor = null;
1924
- let pageCount = 0;
1925
- do {
1926
- pageCount += 1;
1927
- if (options.all === true && pageCount > MAX_ALL_PAGES) {
1928
- 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 };
1929
1988
  }
1930
- const query = new URLSearchParams();
1931
- if (options.limit) query.set("limit", options.limit);
1932
- if (cursor2) query.set("cursor", cursor2);
1933
- const payload = await automationApiFetch(
1934
- config,
1935
- `/api/automation/schedules${query.size ? `?${query.toString()}` : ""}`
1936
- );
1937
- items.push(...payload.data.items);
1938
- nextCursor = payload.data.nextCursor;
1939
- cursor2 = nextCursor ?? void 0;
1940
- } while (options.all === true && nextCursor);
1941
- const output = { items, nextCursor: options.all === true ? null : nextCursor };
1989
+ );
1990
+ const output = { items, nextCursor };
1942
1991
  if (isJson(command, options)) {
1943
1992
  writeJson(output);
1944
1993
  return;
@@ -1970,14 +2019,6 @@ async function deleteAutomationCommand(id, options, command) {
1970
2019
  }
1971
2020
  console.log(`Deleted automation ${id}.`);
1972
2021
  }
1973
- function parseAutomationRepo(value) {
1974
- if (/^git@/i.test(value) && !/^git@github\.com:/i.test(value)) {
1975
- throw new CliError("user", "repo-url must be owner/name or a GitHub URL.");
1976
- }
1977
- const parsed = parseGithubRepoFullName(value);
1978
- if (!parsed) throw new CliError("user", "repo-url must be owner/name or a GitHub URL.");
1979
- return { owner: parsed.owner, repo: parsed.repo };
1980
- }
1981
2022
  function resolveAutomationModelBackend(rawModel, normalizedModel) {
1982
2023
  if (normalizedModel) {
1983
2024
  const backend = getAgentRuntimeBackendForModel(normalizedModel);
@@ -2386,6 +2427,21 @@ function normalizeUploadedFileOptions(files) {
2386
2427
  function clampPollInterval(ms, minimum) {
2387
2428
  return Math.max(ms, minimum);
2388
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
+ }
2389
2445
 
2390
2446
  // ../../shared/session/no-change-outcome.ts
2391
2447
  function isNoChangesPromptResult(result) {
@@ -3889,20 +3945,6 @@ function applyDisconnectMask(view, mask, now) {
3889
3945
  }
3890
3946
 
3891
3947
  // src/commands/watch.ts
3892
- function parsePollInterval(raw) {
3893
- if (!raw) return DEFAULT_WATCH_POLL_INTERVAL_MS;
3894
- if (!/^\d+$/.test(raw)) {
3895
- throw new CliError("user", "Polling interval must be a non-negative integer.");
3896
- }
3897
- const value = Number(raw);
3898
- if (value < MIN_WATCH_POLL_INTERVAL_MS || value > MAX_WATCH_POLL_INTERVAL_MS) {
3899
- throw new CliError(
3900
- "user",
3901
- `Polling interval must be between ${MIN_WATCH_POLL_INTERVAL_MS} and ${MAX_WATCH_POLL_INTERVAL_MS} milliseconds.`
3902
- );
3903
- }
3904
- return value;
3905
- }
3906
3948
  function parseNonNegativeInteger(raw, name, defaultValue) {
3907
3949
  if (!raw) return defaultValue;
3908
3950
  if (!/^\d+$/.test(raw)) {
@@ -4327,16 +4369,6 @@ function parseEgressAllowlistSourceFile(content, key = EGRESS_ALLOWLIST_SOURCE_P
4327
4369
  }
4328
4370
 
4329
4371
  // src/commands/egress.ts
4330
- function parseRepoArg(value) {
4331
- const parsed = parseGithubRepoFullName(value);
4332
- if (!parsed) throw new CliError("user", "repo must be owner/name or a GitHub URL");
4333
- return {
4334
- ...parsed,
4335
- // Preserve the legacy shorthand contract while URLs and SSH remotes are
4336
- // normalized by the shared parser itself.
4337
- repo: /^[^/:]+\/[^/]+\.git$/.test(value.trim()) ? parsed.repo.slice(0, -4) : parsed.repo
4338
- };
4339
- }
4340
4372
  async function egressSourceSetCommand(sourceRepoArg, options = {}, command) {
4341
4373
  const { config } = resolveBusinessContext(command, options);
4342
4374
  const businessId = await resolveBusinessId(config, options);
@@ -4569,13 +4601,13 @@ function parseCreateConflict(error) {
4569
4601
  let message = error.message;
4570
4602
  let sessionId;
4571
4603
  let sessionUrl;
4572
- try {
4573
- const parsed = JSON.parse(error.body);
4574
- 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;
4575
4608
  if (parsedMessage) message = parsedMessage;
4576
4609
  if (typeof parsed.sessionId === "string") sessionId = parsed.sessionId;
4577
4610
  if (typeof parsed.sessionUrl === "string") sessionUrl = parsed.sessionUrl;
4578
- } catch {
4579
4611
  }
4580
4612
  const location = sessionUrl ? ` (${sessionUrl})` : "";
4581
4613
  const existing = sessionId ? ` Existing verifier session: ${sessionId}${location}.` : "";
@@ -4667,28 +4699,7 @@ async function qaCommand(prUrl, options, command) {
4667
4699
  console.log(`Follow with: arcanist sessions events ${sessionId} --follow --json`);
4668
4700
  }
4669
4701
 
4670
- // src/git.ts
4671
- import { execFileSync as execFileSync2 } from "child_process";
4672
- function git(args) {
4673
- try {
4674
- return execFileSync2("git", args, { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }).trim();
4675
- } catch (err) {
4676
- throw new CliError("user", `git ${args.join(" ")} failed: ${stringifyError(err)}`);
4677
- }
4678
- }
4679
- function currentRepo() {
4680
- const parsed = parseGithubRepoFullName(git(["remote", "get-url", "origin"]));
4681
- if (!parsed) throw new CliError("user", "origin remote must point at a GitHub repository");
4682
- return { owner: parsed.owner, repo: parsed.repo };
4683
- }
4684
-
4685
4702
  // src/commands/repos.ts
4686
- function parseRepoArg2(value) {
4687
- if (value === void 0) return currentRepo();
4688
- const parsed = parseGithubRepoFullName(value);
4689
- if (!parsed) throw new CliError("user", "repo must be owner/name or a GitHub URL");
4690
- return { owner: parsed.owner, repo: parsed.repo };
4691
- }
4692
4703
  async function reposListCommand(options = {}, command) {
4693
4704
  const { config } = resolveBusinessContext(command, options);
4694
4705
  const payload = await apiFetch(
@@ -4704,8 +4715,7 @@ async function reposListCommand(options = {}, command) {
4704
4715
  }
4705
4716
  async function repoBranchesCommand(repoArg, options = {}, command) {
4706
4717
  const { config } = resolveBusinessContext(command, options);
4707
- const repo = parseRepoArg2(repoArg);
4708
- await apiFetch(config, "/api/repos");
4718
+ const repo = parseRepoArgOrCurrent(repoArg);
4709
4719
  const payload = await apiFetch(
4710
4720
  config,
4711
4721
  `/api/repos/${encodeURIComponent(repo.owner)}/${encodeURIComponent(repo.repo)}/branches`
@@ -4716,7 +4726,7 @@ async function repoBranchesCommand(repoArg, options = {}, command) {
4716
4726
  }
4717
4727
  async function repoSkillsCommand(repoArg, options = {}, command) {
4718
4728
  const { config } = resolveBusinessContext(command, options);
4719
- const repo = parseRepoArg2(repoArg);
4729
+ const repo = parseRepoArgOrCurrent(repoArg);
4720
4730
  const payload = await apiFetch(
4721
4731
  config,
4722
4732
  `/api/repos/${encodeURIComponent(repo.owner)}/${encodeURIComponent(repo.repo)}/skills`
@@ -4764,17 +4774,13 @@ async function respondCommand(sessionId, answerArg, options = {}, command) {
4764
4774
  }
4765
4775
  }
4766
4776
  function parseRespondConflict(err) {
4767
- if (!(err instanceof ApiError) || err.status !== 409) return null;
4768
- try {
4769
- const body = JSON.parse(err.body);
4770
- if (body.error !== "session_not_respondable") return null;
4771
- return {
4772
- message: body.reason ? `Session cannot be answered from phase=${body.reason}.` : "Session does not have a pending question.",
4773
- reason: body.reason ?? "session_not_respondable"
4774
- };
4775
- } catch {
4776
- return null;
4777
- }
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
+ };
4778
4784
  }
4779
4785
 
4780
4786
  // src/commands/sandbox.ts
@@ -5327,23 +5333,9 @@ function assertCleanAndPushed() {
5327
5333
  function assertManifestExists(path) {
5328
5334
  if (!existsSync3(path)) throw new CliError("user", `Missing sandbox manifest: ${path}`);
5329
5335
  }
5330
- function parseRepoArg3(value, fallback) {
5331
- if (!value) return fallback();
5332
- const parsed = parseGithubRepoFullName(value);
5333
- if (!parsed) throw new CliError("user", "repo must be owner/name or a GitHub URL");
5334
- return { owner: parsed.owner, repo: parsed.repo };
5335
- }
5336
5336
  function repoPath(repo) {
5337
5337
  return `${repo.owner}/${repo.repo}`;
5338
5338
  }
5339
- function parsePollInterval2(value) {
5340
- if (!value) return DEFAULT_POLL_INTERVAL_MS;
5341
- const parsed = Number(value);
5342
- if (!Number.isInteger(parsed) || parsed < MIN_POLL_INTERVAL_MS) {
5343
- throw new CliError("user", `--poll-interval must be an integer >= ${MIN_POLL_INTERVAL_MS}.`);
5344
- }
5345
- return parsed;
5346
- }
5347
5339
  function printBuildRequest(buildRequest) {
5348
5340
  console.log(
5349
5341
  [
@@ -5615,8 +5607,8 @@ async function sandboxBuildCommand(sourceRepoArg, options = {}, command) {
5615
5607
  const manifestPath = options.manifest ?? DEFAULT_MANIFEST_PATH;
5616
5608
  assertManifestExists(manifestPath);
5617
5609
  if (!options.ref) assertCleanAndPushed();
5618
- const sourceRepo = parseRepoArg3(sourceRepoArg, currentRepo);
5619
- const targetRepo = options.targetRepo ? parseRepoArg3(options.targetRepo, currentRepo) : null;
5610
+ const sourceRepo = parseRepoArgOrCurrent(sourceRepoArg);
5611
+ const targetRepo = options.targetRepo ? parseRepoArgOrCurrent(options.targetRepo) : null;
5620
5612
  const businessId = await resolveBusinessId(config, options);
5621
5613
  const ref = options.ref ?? currentRef();
5622
5614
  let payload;
@@ -5654,7 +5646,11 @@ async function sandboxBuildCommand(sourceRepoArg, options = {}, command) {
5654
5646
  if (!options.wait && !options.follow) return;
5655
5647
  const finalBuild = await waitForBuild(config, businessId, payload.buildRequest.id, {
5656
5648
  follow: options.follow,
5657
- 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
+ }),
5658
5654
  json
5659
5655
  });
5660
5656
  if (json)
@@ -5667,7 +5663,7 @@ async function sandboxBuildCommand(sourceRepoArg, options = {}, command) {
5667
5663
  async function sandboxStatusCommand(repoArg, options = {}, command) {
5668
5664
  const { config } = resolveBusinessContext(command, options);
5669
5665
  const businessId = await resolveBusinessId(config, options);
5670
- const repo = parseRepoArg3(repoArg, currentRepo);
5666
+ const repo = parseRepoArgOrCurrent(repoArg);
5671
5667
  const payload = await apiFetch(
5672
5668
  config,
5673
5669
  `/api/businesses/${encodeURIComponent(businessId)}/repos/${encodeURIComponent(repo.owner)}/${encodeURIComponent(
@@ -5679,9 +5675,9 @@ async function sandboxStatusCommand(repoArg, options = {}, command) {
5679
5675
  async function sandboxHistoryCommand(sourceRepoArg, options = {}, command) {
5680
5676
  const { config } = resolveBusinessContext(command, options);
5681
5677
  const businessId = await resolveBusinessId(config, options);
5682
- const sourceRepo = parseRepoArg3(sourceRepoArg, currentRepo);
5678
+ const sourceRepo = parseRepoArgOrCurrent(sourceRepoArg);
5683
5679
  const params = new URLSearchParams({ sourceRepo: repoPath(sourceRepo) });
5684
- if (options.targetRepo) params.set("targetRepo", repoPath(parseRepoArg3(options.targetRepo, currentRepo)));
5680
+ if (options.targetRepo) params.set("targetRepo", repoPath(parseRepoArgOrCurrent(options.targetRepo)));
5685
5681
  if (options.status) params.set("status", options.status);
5686
5682
  if (options.limit) {
5687
5683
  const limit = Number(options.limit);
@@ -5776,13 +5772,19 @@ async function sandboxLogsCommand(buildId, options = {}, command) {
5776
5772
  }
5777
5773
  if (!options.follow) return;
5778
5774
  if (payload.logs.length > 0) afterSequence = payload.logs[payload.logs.length - 1].sequence;
5779
- 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
+ );
5780
5782
  }
5781
5783
  }
5782
5784
  async function sandboxAssignDefaultCommand(sourceRepoArg, options = {}, command) {
5783
5785
  const { config } = resolveBusinessContext(command, options);
5784
5786
  const businessId = await resolveBusinessId(config, options);
5785
- const sourceRepo = parseRepoArg3(sourceRepoArg, currentRepo);
5787
+ const sourceRepo = parseRepoArgOrCurrent(sourceRepoArg);
5786
5788
  const manifestPath = options.manifest ?? DEFAULT_MANIFEST_PATH;
5787
5789
  const payload = await apiFetch(
5788
5790
  config,
@@ -5799,8 +5801,8 @@ async function sandboxAssignDefaultCommand(sourceRepoArg, options = {}, command)
5799
5801
  async function sandboxAssignRepoCommand(targetRepoArg, sourceRepoArg, options = {}, command) {
5800
5802
  const { config } = resolveBusinessContext(command, options);
5801
5803
  const businessId = await resolveBusinessId(config, options);
5802
- const targetRepo = parseRepoArg3(targetRepoArg, currentRepo);
5803
- const sourceRepo = parseRepoArg3(sourceRepoArg, currentRepo);
5804
+ const targetRepo = parseRepoArgOrCurrent(targetRepoArg);
5805
+ const sourceRepo = parseRepoArgOrCurrent(sourceRepoArg);
5804
5806
  const manifestPath = options.manifest ?? DEFAULT_MANIFEST_PATH;
5805
5807
  const payload = await apiFetch(
5806
5808
  config,
@@ -5838,7 +5840,7 @@ async function sandboxUnassignRepoCommand(targetRepoArg, options = {}, command)
5838
5840
  }
5839
5841
  const { config } = resolveBusinessContext(command, options);
5840
5842
  const businessId = await resolveBusinessId(config, options);
5841
- const targetRepo = parseRepoArg3(targetRepoArg, currentRepo);
5843
+ const targetRepo = parseRepoArgOrCurrent(targetRepoArg);
5842
5844
  if (!options.yes) await confirmOrThrow(`Clear sandbox layer assignment for ${repoPath(targetRepo)}?`);
5843
5845
  const payload = await apiFetch(
5844
5846
  config,
@@ -5851,35 +5853,29 @@ async function sandboxUnassignRepoCommand(targetRepoArg, options = {}, command)
5851
5853
  }
5852
5854
 
5853
5855
  // src/commands/sessions.ts
5854
- var MAX_ALL_PAGES2 = 1e3;
5855
5856
  async function listSessionsCommand(options, command) {
5856
5857
  const runtime = getRuntimeOptions(command, options);
5857
5858
  const config = requireConfig(runtime);
5858
- const sessions2 = [];
5859
- let cursor2 = options.cursor;
5860
- let nextCursor = null;
5861
- let pageCount = 0;
5862
- do {
5863
- pageCount += 1;
5864
- if (options.all === true && pageCount > MAX_ALL_PAGES2) {
5865
- throw new CliError("user", `sessions list --all exceeded ${MAX_ALL_PAGES2} pages without reaching the end.`);
5866
- }
5867
- const query = new URLSearchParams();
5868
- if (options.status) query.set("status", options.status);
5869
- if (options.scope) query.set("scope", options.scope);
5870
- if (options.search) query.set("q", options.search);
5871
- if (options.repo) query.set("repo", options.repo);
5872
- if (options.limit) query.set("limit", options.limit);
5873
- if (cursor2) query.set("cursor", cursor2);
5874
- const payload2 = await apiFetch(
5875
- config,
5876
- `/api/sessions${query.size ? `?${query.toString()}` : ""}`
5877
- );
5878
- sessions2.push(...payload2.sessions);
5879
- nextCursor = payload2.nextCursor;
5880
- cursor2 = nextCursor ?? void 0;
5881
- } while (options.all === true && nextCursor);
5882
- 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 };
5883
5879
  if (isJson(command, options)) {
5884
5880
  writeJson(payload);
5885
5881
  return;
@@ -6021,25 +6017,15 @@ async function stopCommand(sessionId, options = {}, command) {
6021
6017
  });
6022
6018
  }
6023
6019
  function parseStopBlocked(err) {
6024
- if (!(err instanceof ApiError) || err.status !== 409) return null;
6025
- try {
6026
- const body = JSON.parse(err.body);
6027
- if (body.error !== "session_not_stoppable") return null;
6028
- return { reason: body.reason ?? "not_stoppable" };
6029
- } catch {
6030
- return null;
6031
- }
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" };
6032
6023
  }
6033
6024
 
6034
6025
  // src/commands/test-creds.ts
6035
6026
  import { readFileSync as readFileSync4 } from "fs";
6036
- function parseRepoArg4(value) {
6037
- const parsed = parseGithubRepoFullName(value);
6038
- if (!parsed) throw new CliError("user", "repo must be owner/name or a GitHub URL");
6039
- return { owner: parsed.owner, name: parsed.repo };
6040
- }
6041
6027
  function basePath(businessId, repo) {
6042
- 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`;
6043
6029
  }
6044
6030
  function readValue(options) {
6045
6031
  const sources = [options.value, options.valueFile, options.valueStdin].filter((v) => v !== void 0 && v !== false);
@@ -6052,11 +6038,11 @@ function readValue(options) {
6052
6038
  }
6053
6039
  async function listTestCredentialsCommand(repo, options, command) {
6054
6040
  const { config } = resolveBusinessContext(command, options);
6055
- const repoArg = parseRepoArg4(repo);
6041
+ const repoArg = parseRepoArg(repo);
6056
6042
  const payload = await apiFetch(config, basePath(options.business, repoArg));
6057
6043
  emit(command, options, payload, (listPayload) => {
6058
6044
  if (listPayload.credentials.length === 0) {
6059
- console.log(`No test credentials configured for ${repoArg.owner}/${repoArg.name}.`);
6045
+ console.log(`No test credentials configured for ${repoArg.owner}/${repoArg.repo}.`);
6060
6046
  return;
6061
6047
  }
6062
6048
  for (const cred of listPayload.credentials) {
@@ -6067,7 +6053,7 @@ async function listTestCredentialsCommand(repo, options, command) {
6067
6053
  }
6068
6054
  async function setTestCredentialCommand(repo, name, options, command) {
6069
6055
  const { config } = resolveBusinessContext(command, options);
6070
- const repoArg = parseRepoArg4(repo);
6056
+ const repoArg = parseRepoArg(repo);
6071
6057
  const value = readValue(options);
6072
6058
  const payload = await apiFetch(
6073
6059
  config,
@@ -6078,7 +6064,7 @@ async function setTestCredentialCommand(repo, name, options, command) {
6078
6064
  command,
6079
6065
  options,
6080
6066
  payload,
6081
- () => console.log(`Set test credential '${name}' for ${repoArg.owner}/${repoArg.name}.`)
6067
+ () => console.log(`Set test credential '${name}' for ${repoArg.owner}/${repoArg.repo}.`)
6082
6068
  );
6083
6069
  }
6084
6070
  async function deleteTestCredentialCommand(repo, name, options, command) {
@@ -6086,11 +6072,11 @@ async function deleteTestCredentialCommand(repo, name, options, command) {
6086
6072
  throw new CliError("user", "`test-creds delete --json` requires --yes.");
6087
6073
  }
6088
6074
  if (options.yes !== true) {
6089
- const repoArg2 = parseRepoArg4(repo);
6090
- 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}?`);
6091
6077
  }
6092
6078
  const { config } = resolveBusinessContext(command, options);
6093
- const repoArg = parseRepoArg4(repo);
6079
+ const repoArg = parseRepoArg(repo);
6094
6080
  const payload = await apiFetch(
6095
6081
  config,
6096
6082
  `${basePath(options.business, repoArg)}/${encodeURIComponent(name)}`,
@@ -6100,35 +6086,29 @@ async function deleteTestCredentialCommand(repo, name, options, command) {
6100
6086
  command,
6101
6087
  options,
6102
6088
  payload,
6103
- () => console.log(`Deleted test credential '${name}' for ${repoArg.owner}/${repoArg.name}.`)
6089
+ () => console.log(`Deleted test credential '${name}' for ${repoArg.owner}/${repoArg.repo}.`)
6104
6090
  );
6105
6091
  }
6106
6092
 
6107
6093
  // src/commands/tokens.ts
6108
- var MAX_ALL_PAGES3 = 1e3;
6109
6094
  async function listTokensCommand(options, command) {
6110
6095
  const { config } = resolveBusinessContext(command, options);
6111
- const data = [];
6112
- let cursor2 = options.cursor;
6113
- let nextCursor = null;
6114
- let pageCount = 0;
6115
- do {
6116
- pageCount += 1;
6117
- if (options.all === true && pageCount > MAX_ALL_PAGES3) {
6118
- 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 };
6119
6109
  }
6120
- const query = new URLSearchParams();
6121
- if (options.limit) query.set("limit", options.limit);
6122
- if (cursor2) query.set("cursor", cursor2);
6123
- const payload2 = await apiFetch(
6124
- config,
6125
- `/api/cli-tokens${query.size ? `?${query.toString()}` : ""}`
6126
- );
6127
- data.push(...payload2.data);
6128
- nextCursor = payload2.nextCursor;
6129
- cursor2 = nextCursor ?? void 0;
6130
- } while (options.all === true && nextCursor);
6131
- const payload = { data, nextCursor: options.all === true ? null : nextCursor };
6110
+ );
6111
+ const payload = { data, nextCursor };
6132
6112
  emit(command, options, payload, (listPayload) => {
6133
6113
  if (listPayload.data.length === 0) {
6134
6114
  console.log("No CLI tokens found.");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tryarcanist/cli",
3
- "version": "0.1.221",
3
+ "version": "0.1.222",
4
4
  "description": "CLI for Arcanist — create and manage coding agent sessions",
5
5
  "type": "module",
6
6
  "bin": {