@tryarcanist/cli 0.1.204 → 0.1.205

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 +389 -32
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -4,6 +4,13 @@
4
4
  import { createRequire as createRequire2 } from "module";
5
5
  import { Command } from "commander";
6
6
 
7
+ // src/commands/agent-subscription.ts
8
+ import { execFileSync, spawn } from "child_process";
9
+ import { existsSync as existsSync2, rmSync } from "fs";
10
+ import { mkdtemp, readFile, rm } from "fs/promises";
11
+ import { tmpdir } from "os";
12
+ import { join as join2 } from "path";
13
+
7
14
  // src/api.ts
8
15
  import { createRequire } from "module";
9
16
 
@@ -604,6 +611,259 @@ function isControlCharacter(char) {
604
611
  return code < ASCII_FIRST_PRINTABLE || code === ASCII_DELETE;
605
612
  }
606
613
 
614
+ // src/commands/agent-subscription.ts
615
+ var GROK_SUBSCRIPTION_CLI_CONFIG = {
616
+ harness: "grok",
617
+ displayName: "Grok",
618
+ defaultBin: "grok",
619
+ binEnvVar: "ARCANIST_GROK_BIN",
620
+ binOption: "grok-path",
621
+ // RFC 8628 device flow: prints a verification URL + code, usable from a
622
+ // terminal without a local browser.
623
+ loginArgs: ["login", "--device-auth"],
624
+ loginEnv: () => ({}),
625
+ authJsonCandidates: [join2(".grok", "auth.json")],
626
+ // Grok Build's auth.json is issuer-keyed (observed 2026-07-15 against the
627
+ // installed CLI's embedded docs): {"https://accounts.x.ai/sign-in": {"key":
628
+ // "…"}} — so the credential fields live one level down and the token field
629
+ // is named `key`. Token detection scans nested objects.
630
+ tokenFields: ["access_token", "refresh_token", "key"],
631
+ installHint: "Install the Grok CLI (https://x.ai/cli), or point at it with --grok-path <path> or ARCANIST_GROK_BIN."
632
+ };
633
+ var CURSOR_SUBSCRIPTION_CLI_CONFIG = {
634
+ harness: "cursor",
635
+ displayName: "Cursor",
636
+ defaultBin: "cursor-agent",
637
+ binEnvVar: "ARCANIST_CURSOR_AGENT_BIN",
638
+ binOption: "cursor-path",
639
+ loginArgs: ["login"],
640
+ // Cursor's CLI stores login state under its config dir; the location is not
641
+ // formally documented, so pin every documented override at the throwaway
642
+ // HOME and search the known spots afterwards.
643
+ loginEnv: (tempHome) => ({
644
+ CURSOR_CONFIG_DIR: join2(tempHome, ".cursor"),
645
+ XDG_CONFIG_HOME: join2(tempHome, ".config")
646
+ }),
647
+ authJsonCandidates: [
648
+ join2(".cursor", "auth.json"),
649
+ join2(".cursor", "cli-config.json"),
650
+ join2(".config", "cursor", "auth.json"),
651
+ join2(".config", "cursor", "cli-config.json")
652
+ ],
653
+ tokenFields: ["accessToken", "access_token", "refreshToken", "refresh_token", "token"],
654
+ installHint: "Install the Cursor CLI (https://cursor.com/docs/cli/installation), or point at it with --cursor-path <path> or ARCANIST_CURSOR_AGENT_BIN. If login-token capture keeps failing, use a Cursor API key in Settings instead.",
655
+ // Entry names observed 2026-07-15 after `cursor-agent login` on macOS
656
+ // (login keychain, account "cursor-user").
657
+ darwinKeychain: {
658
+ entries: [
659
+ { service: "cursor-access-token", field: "accessToken", required: true },
660
+ { service: "cursor-refresh-token", field: "refreshToken", required: false }
661
+ ]
662
+ }
663
+ };
664
+ function subscriptionBasePath(harness) {
665
+ return `/api/settings/agent-subscriptions/${harness}`;
666
+ }
667
+ function resolveVendorBin(config, optionPath) {
668
+ return optionPath?.trim() || process.env[config.binEnvVar]?.trim() || config.defaultBin;
669
+ }
670
+ function runVendorLogin(config, binPath, tempHome) {
671
+ return new Promise((resolve2, reject) => {
672
+ const child = spawn(binPath, config.loginArgs, {
673
+ stdio: "inherit",
674
+ env: tempHome ? { ...process.env, HOME: tempHome, ...config.loginEnv(tempHome) } : process.env
675
+ });
676
+ child.on("error", (err) => {
677
+ if (err.code === "ENOENT") {
678
+ reject(new CliError("user", `Could not find the \`${binPath}\` executable.`, { hint: config.installHint }));
679
+ return;
680
+ }
681
+ reject(new CliError("user", `Failed to launch \`${binPath} ${config.loginArgs.join(" ")}\`: ${err.message}`));
682
+ });
683
+ child.on("close", (code) => {
684
+ if (code === 0) {
685
+ resolve2();
686
+ return;
687
+ }
688
+ reject(
689
+ new CliError("user", `\`${binPath} ${config.loginArgs.join(" ")}\` exited with code ${code ?? "unknown"}.`, {
690
+ hint: `Complete the ${config.displayName} login, then re-run \`arcanist ${config.harness} login\`.`
691
+ })
692
+ );
693
+ });
694
+ });
695
+ }
696
+ function hasTokenField(value, tokenFields, depth = 0) {
697
+ if (!value || typeof value !== "object" || Array.isArray(value) || depth > 2) return false;
698
+ const record = value;
699
+ if (tokenFields.some((field) => typeof record[field] === "string" && record[field].trim().length > 0)) {
700
+ return true;
701
+ }
702
+ return Object.values(record).some((nested) => hasTokenField(nested, tokenFields, depth + 1));
703
+ }
704
+ function readDarwinKeychainAuthJson(config) {
705
+ const record = {};
706
+ for (const entry of config.darwinKeychain?.entries ?? []) {
707
+ let value = "";
708
+ try {
709
+ value = execFileSync("security", ["find-generic-password", "-s", entry.service, "-w"], {
710
+ encoding: "utf8"
711
+ }).trim();
712
+ } catch {
713
+ }
714
+ if (value) {
715
+ record[entry.field] = value;
716
+ } else if (entry.required) {
717
+ throw new CliError(
718
+ "user",
719
+ `${config.displayName} login completed but the '${entry.service}' Keychain item could not be read.`,
720
+ {
721
+ hint: `Approve the macOS Keychain access prompt if one appeared, then re-run \`arcanist ${config.harness} login\`.`
722
+ }
723
+ );
724
+ }
725
+ }
726
+ return JSON.stringify(record);
727
+ }
728
+ async function readLoginAuthJson(config, tempHome) {
729
+ for (const candidate of config.authJsonCandidates) {
730
+ const path = join2(tempHome, candidate);
731
+ if (!existsSync2(path)) continue;
732
+ let raw;
733
+ try {
734
+ raw = await readFile(path, "utf8");
735
+ } catch {
736
+ continue;
737
+ }
738
+ try {
739
+ if (hasTokenField(JSON.parse(raw), config.tokenFields)) return raw;
740
+ } catch {
741
+ }
742
+ }
743
+ throw new CliError("user", `${config.displayName} login completed but no auth credential file was found.`, {
744
+ hint: `Verify \`${config.defaultBin} ${config.loginArgs.join(" ")}\` succeeds on its own, then re-run \`arcanist ${config.harness} login\`.`
745
+ });
746
+ }
747
+ async function setSubscriptionEnabled(apiConfig, harness, enabled) {
748
+ return apiFetch(apiConfig, `${subscriptionBasePath(harness)}/enabled`, {
749
+ method: "PUT",
750
+ body: JSON.stringify({ enabled })
751
+ });
752
+ }
753
+ async function agentSubscriptionLoginCommand(config, options, command) {
754
+ const { config: apiConfig } = resolveBusinessContext(command, options);
755
+ const binPath = resolveVendorBin(config, options.binPath);
756
+ let tempHome;
757
+ const handleSigint = () => {
758
+ if (tempHome) {
759
+ try {
760
+ rmSync(tempHome, { recursive: true, force: true });
761
+ } catch {
762
+ }
763
+ }
764
+ process.exit(EXIT_CODE_INTERRUPTED);
765
+ };
766
+ process.on("SIGINT", handleSigint);
767
+ try {
768
+ const captureViaKeychain = process.platform === "darwin" && config.darwinKeychain !== void 0;
769
+ let authJson;
770
+ if (captureViaKeychain) {
771
+ await runVendorLogin(config, binPath, null);
772
+ authJson = readDarwinKeychainAuthJson(config);
773
+ } else {
774
+ tempHome = await mkdtemp(join2(tmpdir(), `arcanist-${config.harness}-`));
775
+ await runVendorLogin(config, binPath, tempHome);
776
+ authJson = await readLoginAuthJson(config, tempHome);
777
+ }
778
+ const state = await apiFetch(
779
+ apiConfig,
780
+ `${subscriptionBasePath(config.harness)}/auth-json`,
781
+ {
782
+ method: "PUT",
783
+ body: JSON.stringify({ authJson })
784
+ }
785
+ );
786
+ let activated = false;
787
+ let activationError;
788
+ try {
789
+ await setSubscriptionEnabled(apiConfig, config.harness, true);
790
+ activated = true;
791
+ } catch (err) {
792
+ activationError = err instanceof Error ? err.message : String(err);
793
+ }
794
+ emit(command, options, { ...state, enabled: activated }, (payload) => {
795
+ console.log(
796
+ activated ? `${config.displayName} subscription auth saved and activated.` : `${config.displayName} subscription auth saved.`
797
+ );
798
+ if (payload.lastValidationStatus) console.log(`Status: ${payload.lastValidationStatus}`);
799
+ if (!activated) {
800
+ console.log(`Could not activate it automatically${activationError ? ` (${activationError})` : ""}.`);
801
+ console.log(`Run \`arcanist ${config.harness} use on\` to start using it.`);
802
+ }
803
+ console.log(
804
+ `Note: ${config.displayName} BYOS session execution is not live yet; the credential is stored for when it ships.`
805
+ );
806
+ if (captureViaKeychain) {
807
+ console.log(
808
+ `Your local ${config.defaultBin} login also remains active (Keychain-backed); run \`${config.defaultBin} logout\` to remove it.`
809
+ );
810
+ }
811
+ });
812
+ } finally {
813
+ try {
814
+ if (tempHome) {
815
+ await rm(tempHome, { recursive: true, force: true }).catch(() => {
816
+ });
817
+ }
818
+ } finally {
819
+ process.off("SIGINT", handleSigint);
820
+ }
821
+ }
822
+ }
823
+ async function agentSubscriptionUseCommand(config, state, options, command) {
824
+ const normalized = state.trim().toLowerCase();
825
+ if (normalized !== "on" && normalized !== "off") {
826
+ throw new CliError("user", `Usage: arcanist ${config.harness} use <on|off>`);
827
+ }
828
+ const enabled = normalized === "on";
829
+ const { config: apiConfig } = resolveBusinessContext(command, options);
830
+ const result = await setSubscriptionEnabled(apiConfig, config.harness, enabled);
831
+ emit(
832
+ command,
833
+ options,
834
+ result,
835
+ () => console.log(
836
+ result.enabled ? `${config.displayName} subscription auth is now selected for ${config.displayName} sessions.` : `${config.displayName} subscription auth is no longer selected.`
837
+ )
838
+ );
839
+ }
840
+ async function agentSubscriptionStatusCommand(config, options, command) {
841
+ const { config: apiConfig } = resolveBusinessContext(command, options);
842
+ const payload = await apiFetch(apiConfig, subscriptionBasePath(config.harness));
843
+ emit(command, options, payload, (state) => {
844
+ console.log(`Eligible: ${state.eligible ? "yes" : "no"}`);
845
+ console.log(`Auth saved: ${state.credential.isSet ? "yes" : "no"}`);
846
+ console.log(`Selected: ${state.enabled ? "yes" : "no"}`);
847
+ if (state.credential.lastValidationStatus) console.log(`Status: ${state.credential.lastValidationStatus}`);
848
+ });
849
+ }
850
+ async function agentSubscriptionLogoutCommand(config, options, command) {
851
+ const { config: apiConfig } = resolveBusinessContext(command, options);
852
+ await setSubscriptionEnabled(apiConfig, config.harness, false).catch(() => {
853
+ });
854
+ const payload = await apiFetch(
855
+ apiConfig,
856
+ `${subscriptionBasePath(config.harness)}/auth-json`,
857
+ { method: "DELETE" }
858
+ );
859
+ emit(
860
+ command,
861
+ options,
862
+ payload,
863
+ () => console.log(`${config.displayName} subscription auth deactivated and cleared.`)
864
+ );
865
+ }
866
+
607
867
  // src/commands/auth.ts
608
868
  async function whoamiCommand(options, command) {
609
869
  const { config } = resolveBusinessContext(command, options);
@@ -618,16 +878,19 @@ async function whoamiCommand(options, command) {
618
878
  var CODEX_AGENT_RUNTIME_BACKEND = "codex";
619
879
  var CLAUDE_CODE_AGENT_RUNTIME_BACKEND = "claude_code";
620
880
  var OPENCODE_AGENT_RUNTIME_BACKEND = "opencode";
881
+ var CURSOR_AGENT_RUNTIME_BACKEND = "cursor";
621
882
  var AGENT_RUNTIME_BACKENDS = [
622
883
  CODEX_AGENT_RUNTIME_BACKEND,
623
884
  CLAUDE_CODE_AGENT_RUNTIME_BACKEND,
624
- OPENCODE_AGENT_RUNTIME_BACKEND
885
+ OPENCODE_AGENT_RUNTIME_BACKEND,
886
+ CURSOR_AGENT_RUNTIME_BACKEND
625
887
  ];
626
888
  var AGENT_RUNTIME_BACKEND_NAMES = {
627
889
  [CODEX_AGENT_RUNTIME_BACKEND]: "Codex",
628
890
  [CLAUDE_CODE_AGENT_RUNTIME_BACKEND]: "Claude Code",
629
891
  // "opencode" is intentionally lowercase to match the project's brand name.
630
- [OPENCODE_AGENT_RUNTIME_BACKEND]: "opencode"
892
+ [OPENCODE_AGENT_RUNTIME_BACKEND]: "opencode",
893
+ [CURSOR_AGENT_RUNTIME_BACKEND]: "Cursor"
631
894
  };
632
895
  function isAgentRuntimeBackend(value) {
633
896
  return AGENT_RUNTIME_BACKENDS.includes(value);
@@ -669,10 +932,18 @@ var AnthropicModel = {
669
932
  var BasetenModel = {
670
933
  KimiK27Code: "kimi-k2.7-code"
671
934
  };
935
+ var XaiModel = {
936
+ Grok45: "grok-4.5"
937
+ };
938
+ var CursorModel = {
939
+ Composer25: "composer-2.5"
940
+ };
672
941
  var MODEL_PROVIDERS_SET = /* @__PURE__ */ new Set([
673
942
  "openai",
674
943
  "anthropic",
675
- "baseten"
944
+ "baseten",
945
+ "xai",
946
+ "cursor"
676
947
  ]);
677
948
  var BACKEND_DESKTOP_IMAGE_FEEDBACK_CONFIGS = {
678
949
  [CODEX_AGENT_RUNTIME_BACKEND]: {
@@ -986,12 +1257,47 @@ var MODEL_REGISTRY = [
986
1257
  },
987
1258
  pricing: { inputPerMillion: 0.95, outputPerMillion: 4, cacheReadPerMillion: 0.16 },
988
1259
  sessionStart: { eligible: true, isDefault: true }
1260
+ },
1261
+ {
1262
+ id: XaiModel.Grok45,
1263
+ name: "Grok 4.5",
1264
+ provider: "xai",
1265
+ backends: [OPENCODE_AGENT_RUNTIME_BACKEND],
1266
+ contextWindow: 5e5,
1267
+ // No reasoning config on purpose: xAI's chat-completions reference
1268
+ // (verified 2026-07-14, https://docs.x.ai/docs/api-reference) documents
1269
+ // `reasoning_effort` as supported only by grok-4.3, so grok-4.5 is
1270
+ // classified no-reasoning per the registry rule. Pricing/context verified
1271
+ // 2026-07-14 against https://docs.x.ai/docs/pricing ($2/M in, $6/M out,
1272
+ // 500k context); no cached-input rate is documented, so none is recorded.
1273
+ pricing: { inputPerMillion: 2, outputPerMillion: 6 },
1274
+ sessionStart: { eligible: true }
1275
+ },
1276
+ {
1277
+ id: CursorModel.Composer25,
1278
+ name: "Composer 2.5",
1279
+ provider: "cursor",
1280
+ backends: [CURSOR_AGENT_RUNTIME_BACKEND],
1281
+ // Cursor bills Composer usage through the user's Cursor account (API key
1282
+ // or subscription); Arcanist does not meter it, so no registry pricing is
1283
+ // recorded (costTracked: false requires pricing to stay undefined). No
1284
+ // reasoning config: Cursor encodes effort in distinct model ids
1285
+ // (`--list-models`), not a per-request parameter. Context window is not
1286
+ // published for Composer 2.5 (checked cursor.com/docs/models 2026-07-15),
1287
+ // so none is recorded.
1288
+ costTracked: false,
1289
+ sessionStart: { eligible: true, isDefault: true },
1290
+ // Internal probe: hidden from the public model picker; selectable via
1291
+ // CLI/API only while the cursor backend is gated.
1292
+ visibility: "internal_probe"
989
1293
  }
990
1294
  ];
991
1295
  var MODEL_PROVIDER_NAMES = {
992
1296
  openai: "OpenAI",
993
1297
  anthropic: "Anthropic",
994
- baseten: "Baseten"
1298
+ baseten: "Baseten",
1299
+ xai: "xAI",
1300
+ cursor: "Cursor"
995
1301
  };
996
1302
  function buildSessionStartModelIdsByBackend() {
997
1303
  const byBackend = Object.fromEntries(AGENT_RUNTIME_BACKENDS.map((backend) => [backend, []]));
@@ -1030,7 +1336,8 @@ var VALID_SESSION_START_MODEL_IDS_BY_BACKEND = {
1030
1336
  [CLAUDE_CODE_AGENT_RUNTIME_BACKEND]: new Set(
1031
1337
  SESSION_START_MODEL_IDS_BY_BACKEND[CLAUDE_CODE_AGENT_RUNTIME_BACKEND]
1032
1338
  ),
1033
- [OPENCODE_AGENT_RUNTIME_BACKEND]: new Set(SESSION_START_MODEL_IDS_BY_BACKEND[OPENCODE_AGENT_RUNTIME_BACKEND])
1339
+ [OPENCODE_AGENT_RUNTIME_BACKEND]: new Set(SESSION_START_MODEL_IDS_BY_BACKEND[OPENCODE_AGENT_RUNTIME_BACKEND]),
1340
+ [CURSOR_AGENT_RUNTIME_BACKEND]: new Set(SESSION_START_MODEL_IDS_BY_BACKEND[CURSOR_AGENT_RUNTIME_BACKEND])
1034
1341
  };
1035
1342
  var MODEL_CONTEXT_WINDOWS = {
1036
1343
  ...Object.fromEntries(
@@ -1231,7 +1538,7 @@ async function listAutomationsCommand(options, command) {
1231
1538
  const runtime = getRuntimeOptions(command, options);
1232
1539
  const config = requireConfig(runtime);
1233
1540
  const items = [];
1234
- let cursor = options.cursor;
1541
+ let cursor2 = options.cursor;
1235
1542
  let nextCursor = null;
1236
1543
  let pageCount = 0;
1237
1544
  do {
@@ -1241,14 +1548,14 @@ async function listAutomationsCommand(options, command) {
1241
1548
  }
1242
1549
  const query = new URLSearchParams();
1243
1550
  if (options.limit) query.set("limit", options.limit);
1244
- if (cursor) query.set("cursor", cursor);
1551
+ if (cursor2) query.set("cursor", cursor2);
1245
1552
  const payload = await automationApiFetch(
1246
1553
  config,
1247
1554
  `/api/automation/schedules${query.size ? `?${query.toString()}` : ""}`
1248
1555
  );
1249
1556
  items.push(...payload.data.items);
1250
1557
  nextCursor = payload.data.nextCursor;
1251
- cursor = nextCursor ?? void 0;
1558
+ cursor2 = nextCursor ?? void 0;
1252
1559
  } while (options.all === true && nextCursor);
1253
1560
  const output = { items, nextCursor: options.all === true ? null : nextCursor };
1254
1561
  if (isJson(command, options)) {
@@ -1341,11 +1648,11 @@ function formatTime(value) {
1341
1648
  }
1342
1649
 
1343
1650
  // src/commands/codex.ts
1344
- import { spawn } from "child_process";
1345
- import { rmSync } from "fs";
1346
- import { mkdtemp, readFile, rm } from "fs/promises";
1347
- import { tmpdir } from "os";
1348
- import { join as join2 } from "path";
1651
+ import { spawn as spawn2 } from "child_process";
1652
+ import { rmSync as rmSync2 } from "fs";
1653
+ import { mkdtemp as mkdtemp2, readFile as readFile2, rm as rm2 } from "fs/promises";
1654
+ import { tmpdir as tmpdir2 } from "os";
1655
+ import { join as join3 } from "path";
1349
1656
  var CODEX_SUBSCRIPTION_PATH = "/api/settings/codex-subscription";
1350
1657
  var CODEX_SUBSCRIPTION_AUTH_JSON_PATH = "/api/settings/codex-subscription/auth-json";
1351
1658
  var CODEX_SUBSCRIPTION_ENABLED_PATH = "/api/settings/codex-subscription/enabled";
@@ -1354,7 +1661,7 @@ function resolveCodexPath(optionPath) {
1354
1661
  }
1355
1662
  function runCodexDeviceLogin(codexPath, codexHome) {
1356
1663
  return new Promise((resolve2, reject) => {
1357
- const child = spawn(codexPath, ["login", "--device-auth"], {
1664
+ const child = spawn2(codexPath, ["login", "--device-auth"], {
1358
1665
  stdio: "inherit",
1359
1666
  env: { ...process.env, CODEX_HOME: codexHome }
1360
1667
  });
@@ -1395,7 +1702,7 @@ async function codexLoginCommand(options, command) {
1395
1702
  const handleSigint = () => {
1396
1703
  if (codexHome) {
1397
1704
  try {
1398
- rmSync(codexHome, { recursive: true, force: true });
1705
+ rmSync2(codexHome, { recursive: true, force: true });
1399
1706
  } catch {
1400
1707
  }
1401
1708
  }
@@ -1403,11 +1710,11 @@ async function codexLoginCommand(options, command) {
1403
1710
  };
1404
1711
  process.on("SIGINT", handleSigint);
1405
1712
  try {
1406
- codexHome = await mkdtemp(join2(tmpdir(), "arcanist-codex-"));
1713
+ codexHome = await mkdtemp2(join3(tmpdir2(), "arcanist-codex-"));
1407
1714
  await runCodexDeviceLogin(codexPath, codexHome);
1408
1715
  let authJson;
1409
1716
  try {
1410
- authJson = await readFile(join2(codexHome, "auth.json"), "utf8");
1717
+ authJson = await readFile2(join3(codexHome, "auth.json"), "utf8");
1411
1718
  } catch {
1412
1719
  throw new CliError("user", "Codex login completed but no auth.json was written.", {
1413
1720
  hint: "Verify `codex login --device-auth` succeeds on its own, then re-run `arcanist codex login`."
@@ -1441,7 +1748,7 @@ async function codexLoginCommand(options, command) {
1441
1748
  } finally {
1442
1749
  try {
1443
1750
  if (codexHome) {
1444
- await rm(codexHome, { recursive: true, force: true }).catch(() => {
1751
+ await rm2(codexHome, { recursive: true, force: true }).catch(() => {
1445
1752
  });
1446
1753
  }
1447
1754
  } finally {
@@ -1536,7 +1843,7 @@ function isWatchTerminal(phase) {
1536
1843
  }
1537
1844
 
1538
1845
  // src/uploads.ts
1539
- import { readFile as readFile2 } from "fs/promises";
1846
+ import { readFile as readFile3 } from "fs/promises";
1540
1847
  import { basename, extname } from "path";
1541
1848
 
1542
1849
  // ../../shared/constants/uploads.ts
@@ -1639,7 +1946,7 @@ async function resolveUploadedFileOptions(files) {
1639
1946
  paths.map(async (path) => {
1640
1947
  const name = basename(path);
1641
1948
  try {
1642
- return { name, content: await readFile2(path, "utf8") };
1949
+ return { name, content: await readFile3(path, "utf8") };
1643
1950
  } catch (err) {
1644
1951
  const message = stringifyError(err);
1645
1952
  throw new CliError("user", `Failed to read uploaded file ${path}: ${message}`);
@@ -3126,6 +3433,10 @@ function resolveModelAndBackend(options) {
3126
3433
  );
3127
3434
  }
3128
3435
  agentRuntimeBackend = options.backend;
3436
+ } else if (options.model !== void 0) {
3437
+ const normalizedModel = extractModelId(options.model);
3438
+ const derived = normalizedModel ? getAgentRuntimeBackendForModel(normalizedModel) : void 0;
3439
+ if (derived) agentRuntimeBackend = derived;
3129
3440
  }
3130
3441
  if (options.model !== void 0) {
3131
3442
  const normalizedModel = extractModelId(options.model);
@@ -3936,10 +4247,10 @@ async function qaCommand(prUrl, options, command) {
3936
4247
  }
3937
4248
 
3938
4249
  // src/git.ts
3939
- import { execFileSync } from "child_process";
4250
+ import { execFileSync as execFileSync2 } from "child_process";
3940
4251
  function git(args) {
3941
4252
  try {
3942
- return execFileSync("git", args, { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }).trim();
4253
+ return execFileSync2("git", args, { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }).trim();
3943
4254
  } catch (err) {
3944
4255
  throw new CliError("user", `git ${args.join(" ")} failed: ${stringifyError(err)}`);
3945
4256
  }
@@ -4046,7 +4357,7 @@ function parseRespondConflict(err) {
4046
4357
  }
4047
4358
 
4048
4359
  // src/commands/sandbox.ts
4049
- import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
4360
+ import { existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
4050
4361
  import { dirname as dirname2 } from "path";
4051
4362
 
4052
4363
  // ../../shared/sandbox-layer/parser.ts
@@ -4593,7 +4904,7 @@ function assertCleanAndPushed() {
4593
4904
  throw new CliError("user", "Current commit is not pushed; push before building the sandbox layer.");
4594
4905
  }
4595
4906
  function assertManifestExists(path) {
4596
- if (!existsSync2(path)) throw new CliError("user", `Missing sandbox manifest: ${path}`);
4907
+ if (!existsSync3(path)) throw new CliError("user", `Missing sandbox manifest: ${path}`);
4597
4908
  }
4598
4909
  function parseRepoArg3(value, fallback) {
4599
4910
  if (!value) return fallback();
@@ -4824,9 +5135,9 @@ function sourceBody(sourceRepo, manifestPath) {
4824
5135
  }
4825
5136
  async function sandboxInitCommand(options = {}, command) {
4826
5137
  const runtime = getRuntimeOptions(command, options);
4827
- if (existsSync2(DEFAULT_MANIFEST_PATH)) throw new CliError("conflict", `${DEFAULT_MANIFEST_PATH} already exists`);
5138
+ if (existsSync3(DEFAULT_MANIFEST_PATH)) throw new CliError("conflict", `${DEFAULT_MANIFEST_PATH} already exists`);
4828
5139
  const layerPath = ".arcanist/sandbox.layer.Dockerfile";
4829
- if (existsSync2(layerPath)) throw new CliError("conflict", `${layerPath} already exists`);
5140
+ if (existsSync3(layerPath)) throw new CliError("conflict", `${layerPath} already exists`);
4830
5141
  mkdirSync2(dirname2(DEFAULT_MANIFEST_PATH), { recursive: true });
4831
5142
  writeFileSync2(
4832
5143
  DEFAULT_MANIFEST_PATH,
@@ -5124,7 +5435,7 @@ async function listSessionsCommand(options, command) {
5124
5435
  const runtime = getRuntimeOptions(command, options);
5125
5436
  const config = requireConfig(runtime);
5126
5437
  const sessions2 = [];
5127
- let cursor = options.cursor;
5438
+ let cursor2 = options.cursor;
5128
5439
  let nextCursor = null;
5129
5440
  let pageCount = 0;
5130
5441
  do {
@@ -5138,14 +5449,14 @@ async function listSessionsCommand(options, command) {
5138
5449
  if (options.search) query.set("q", options.search);
5139
5450
  if (options.repo) query.set("repo", options.repo);
5140
5451
  if (options.limit) query.set("limit", options.limit);
5141
- if (cursor) query.set("cursor", cursor);
5452
+ if (cursor2) query.set("cursor", cursor2);
5142
5453
  const payload2 = await apiFetch(
5143
5454
  config,
5144
5455
  `/api/sessions${query.size ? `?${query.toString()}` : ""}`
5145
5456
  );
5146
5457
  sessions2.push(...payload2.sessions);
5147
5458
  nextCursor = payload2.nextCursor;
5148
- cursor = nextCursor ?? void 0;
5459
+ cursor2 = nextCursor ?? void 0;
5149
5460
  } while (options.all === true && nextCursor);
5150
5461
  const payload = { sessions: sessions2, nextCursor: options.all === true ? null : nextCursor };
5151
5462
  if (isJson(command, options)) {
@@ -5377,7 +5688,7 @@ var MAX_ALL_PAGES3 = 1e3;
5377
5688
  async function listTokensCommand(options, command) {
5378
5689
  const { config } = resolveBusinessContext(command, options);
5379
5690
  const data = [];
5380
- let cursor = options.cursor;
5691
+ let cursor2 = options.cursor;
5381
5692
  let nextCursor = null;
5382
5693
  let pageCount = 0;
5383
5694
  do {
@@ -5387,14 +5698,14 @@ async function listTokensCommand(options, command) {
5387
5698
  }
5388
5699
  const query = new URLSearchParams();
5389
5700
  if (options.limit) query.set("limit", options.limit);
5390
- if (cursor) query.set("cursor", cursor);
5701
+ if (cursor2) query.set("cursor", cursor2);
5391
5702
  const payload2 = await apiFetch(
5392
5703
  config,
5393
5704
  `/api/cli-tokens${query.size ? `?${query.toString()}` : ""}`
5394
5705
  );
5395
5706
  data.push(...payload2.data);
5396
5707
  nextCursor = payload2.nextCursor;
5397
- cursor = nextCursor ?? void 0;
5708
+ cursor2 = nextCursor ?? void 0;
5398
5709
  } while (options.all === true && nextCursor);
5399
5710
  const payload = { data, nextCursor: options.all === true ? null : nextCursor };
5400
5711
  emit(command, options, payload, (listPayload) => {
@@ -5628,6 +5939,52 @@ Examples:
5628
5939
  codex.command("use <state>").description("Turn using your saved Codex subscription auth for OpenAI sessions on or off (state: on|off)").action((state, options, command) => codexUseCommand(state, options, command));
5629
5940
  codex.command("status").description("Show whether your workspace is eligible and whether a Codex subscription auth is saved").action((options, command) => codexStatusCommand(options, command));
5630
5941
  codex.command("logout").description("Deactivate the selector and remove the stored Codex subscription auth for your user").action((options, command) => codexLogoutCommand(options, command));
5942
+ var grok = program.command("grok").description("Grok (xAI) subscription (bring-your-own-subscription) commands");
5943
+ grok.command("login").description("Authenticate a Grok (xAI) subscription and store it for future Grok BYOS sessions").option("--grok-path <path>", "Path to the grok executable (default: grok on PATH, or ARCANIST_GROK_BIN)").addHelpText(
5944
+ "after",
5945
+ `
5946
+ Runs the Grok CLI device-authorization login locally under a temporary HOME, then uploads the
5947
+ resulting auth.json to Arcanist (encrypted, per user) and activates the selector. Your workspace
5948
+ must have Grok subscription auth enabled. The credential is never written to your default ~/.grok.
5949
+ Grok BYOS session execution is not live yet; the credential is stored for when it ships.
5950
+
5951
+ Examples:
5952
+ arcanist grok login
5953
+ arcanist grok login --grok-path /usr/local/bin/grok
5954
+ `
5955
+ ).action(
5956
+ (options, command) => agentSubscriptionLoginCommand(GROK_SUBSCRIPTION_CLI_CONFIG, { ...options, binPath: options.grokPath }, command)
5957
+ );
5958
+ grok.command("use <state>").description("Turn using your saved Grok subscription auth on or off (state: on|off)").action(
5959
+ (state, options, command) => agentSubscriptionUseCommand(GROK_SUBSCRIPTION_CLI_CONFIG, state, options, command)
5960
+ );
5961
+ grok.command("status").description("Show whether your workspace is eligible and whether a Grok subscription auth is saved").action((options, command) => agentSubscriptionStatusCommand(GROK_SUBSCRIPTION_CLI_CONFIG, options, command));
5962
+ grok.command("logout").description("Deactivate the selector and remove the stored Grok subscription auth for your user").action((options, command) => agentSubscriptionLogoutCommand(GROK_SUBSCRIPTION_CLI_CONFIG, options, command));
5963
+ var cursor = program.command("cursor").description("Cursor subscription (bring-your-own-subscription) commands");
5964
+ cursor.command("login").description("Authenticate a Cursor subscription and store it for future Cursor BYOS sessions").option(
5965
+ "--cursor-path <path>",
5966
+ "Path to the cursor-agent executable (default: cursor-agent on PATH, or ARCANIST_CURSOR_AGENT_BIN)"
5967
+ ).addHelpText(
5968
+ "after",
5969
+ `
5970
+ Runs the Cursor CLI login locally under a temporary HOME/CURSOR_CONFIG_DIR, then uploads the
5971
+ captured auth credential to Arcanist (encrypted, per user) and activates the selector. Your
5972
+ workspace must have Cursor subscription auth enabled. Cursor's login-token location is not
5973
+ formally documented; if capture fails, use a Cursor API key in Settings instead. Cursor BYOS
5974
+ session execution is not live yet; the credential is stored for when it ships.
5975
+
5976
+ Examples:
5977
+ arcanist cursor login
5978
+ arcanist cursor login --cursor-path /usr/local/bin/cursor-agent
5979
+ `
5980
+ ).action(
5981
+ (options, command) => agentSubscriptionLoginCommand(CURSOR_SUBSCRIPTION_CLI_CONFIG, { ...options, binPath: options.cursorPath }, command)
5982
+ );
5983
+ cursor.command("use <state>").description("Turn using your saved Cursor subscription auth on or off (state: on|off)").action(
5984
+ (state, options, command) => agentSubscriptionUseCommand(CURSOR_SUBSCRIPTION_CLI_CONFIG, state, options, command)
5985
+ );
5986
+ 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));
5987
+ 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));
5631
5988
  var sessions = program.command("sessions").description("Session commands");
5632
5989
  addCreateOptions(sessions.command("create").description("Create a session and send a prompt")).action(
5633
5990
  (repoUrl, prompt, options, command) => createCommand(repoUrl, prompt, options, command)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tryarcanist/cli",
3
- "version": "0.1.204",
3
+ "version": "0.1.205",
4
4
  "description": "CLI for Arcanist — create and manage coding agent sessions",
5
5
  "type": "module",
6
6
  "bin": {