@tryarcanist/cli 0.1.204 → 0.1.206

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 +413 -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}`);
@@ -2095,6 +2402,7 @@ function createFlattenState() {
2095
2402
  toolCallIndexById: /* @__PURE__ */ new Map(),
2096
2403
  agentProgressIndexByKey: /* @__PURE__ */ new Map(),
2097
2404
  questionIndexById: /* @__PURE__ */ new Map(),
2405
+ narrationIndex: null,
2098
2406
  malformedSearchBlockedPromptIds: /* @__PURE__ */ new Set(),
2099
2407
  malformedSearchBlockedWithoutPrompt: false
2100
2408
  };
@@ -2426,6 +2734,23 @@ function applyTodoUpdate(data, state) {
2426
2734
  }
2427
2735
  }
2428
2736
  }
2737
+ function projectNarration(data, state) {
2738
+ const text = typeof data?.text === "string" ? data.text.trim() : "";
2739
+ if (!text) return;
2740
+ const promptId = resolvePromptId(data);
2741
+ const event = {
2742
+ type: "narration",
2743
+ id: "narration",
2744
+ text,
2745
+ ...promptId ? { promptId } : {}
2746
+ };
2747
+ if (state.narrationIndex === null) {
2748
+ state.narrationIndex = state.merged.length;
2749
+ state.merged.push(event);
2750
+ } else {
2751
+ state.merged[state.narrationIndex] = event;
2752
+ }
2753
+ }
2429
2754
  function flattenSessionEvents(raw) {
2430
2755
  const state = createFlattenState();
2431
2756
  const normalizedEvents = normalizeRawSessionEvents(raw);
@@ -2499,6 +2824,9 @@ function flattenSessionEvents(raw) {
2499
2824
  case "question":
2500
2825
  projectQuestion(data, state);
2501
2826
  break;
2827
+ case "narration":
2828
+ projectNarration(data, state);
2829
+ break;
2502
2830
  }
2503
2831
  }
2504
2832
  return suppressMalformedSearchSegments(state);
@@ -2828,6 +3156,9 @@ ${event.answer ? `**Answer:** ${event.answer}
2828
3156
  return "";
2829
3157
  case "agent_progress":
2830
3158
  return `*[${event.label}]*
3159
+ `;
3160
+ case "narration":
3161
+ return `*[Currently: ${event.text}]*
2831
3162
  `;
2832
3163
  case "raw_agent_runtime":
2833
3164
  return "";
@@ -3126,6 +3457,10 @@ function resolveModelAndBackend(options) {
3126
3457
  );
3127
3458
  }
3128
3459
  agentRuntimeBackend = options.backend;
3460
+ } else if (options.model !== void 0) {
3461
+ const normalizedModel = extractModelId(options.model);
3462
+ const derived = normalizedModel ? getAgentRuntimeBackendForModel(normalizedModel) : void 0;
3463
+ if (derived) agentRuntimeBackend = derived;
3129
3464
  }
3130
3465
  if (options.model !== void 0) {
3131
3466
  const normalizedModel = extractModelId(options.model);
@@ -3936,10 +4271,10 @@ async function qaCommand(prUrl, options, command) {
3936
4271
  }
3937
4272
 
3938
4273
  // src/git.ts
3939
- import { execFileSync } from "child_process";
4274
+ import { execFileSync as execFileSync2 } from "child_process";
3940
4275
  function git(args) {
3941
4276
  try {
3942
- return execFileSync("git", args, { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }).trim();
4277
+ return execFileSync2("git", args, { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }).trim();
3943
4278
  } catch (err) {
3944
4279
  throw new CliError("user", `git ${args.join(" ")} failed: ${stringifyError(err)}`);
3945
4280
  }
@@ -4046,7 +4381,7 @@ function parseRespondConflict(err) {
4046
4381
  }
4047
4382
 
4048
4383
  // src/commands/sandbox.ts
4049
- import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
4384
+ import { existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
4050
4385
  import { dirname as dirname2 } from "path";
4051
4386
 
4052
4387
  // ../../shared/sandbox-layer/parser.ts
@@ -4593,7 +4928,7 @@ function assertCleanAndPushed() {
4593
4928
  throw new CliError("user", "Current commit is not pushed; push before building the sandbox layer.");
4594
4929
  }
4595
4930
  function assertManifestExists(path) {
4596
- if (!existsSync2(path)) throw new CliError("user", `Missing sandbox manifest: ${path}`);
4931
+ if (!existsSync3(path)) throw new CliError("user", `Missing sandbox manifest: ${path}`);
4597
4932
  }
4598
4933
  function parseRepoArg3(value, fallback) {
4599
4934
  if (!value) return fallback();
@@ -4824,9 +5159,9 @@ function sourceBody(sourceRepo, manifestPath) {
4824
5159
  }
4825
5160
  async function sandboxInitCommand(options = {}, command) {
4826
5161
  const runtime = getRuntimeOptions(command, options);
4827
- if (existsSync2(DEFAULT_MANIFEST_PATH)) throw new CliError("conflict", `${DEFAULT_MANIFEST_PATH} already exists`);
5162
+ if (existsSync3(DEFAULT_MANIFEST_PATH)) throw new CliError("conflict", `${DEFAULT_MANIFEST_PATH} already exists`);
4828
5163
  const layerPath = ".arcanist/sandbox.layer.Dockerfile";
4829
- if (existsSync2(layerPath)) throw new CliError("conflict", `${layerPath} already exists`);
5164
+ if (existsSync3(layerPath)) throw new CliError("conflict", `${layerPath} already exists`);
4830
5165
  mkdirSync2(dirname2(DEFAULT_MANIFEST_PATH), { recursive: true });
4831
5166
  writeFileSync2(
4832
5167
  DEFAULT_MANIFEST_PATH,
@@ -5124,7 +5459,7 @@ async function listSessionsCommand(options, command) {
5124
5459
  const runtime = getRuntimeOptions(command, options);
5125
5460
  const config = requireConfig(runtime);
5126
5461
  const sessions2 = [];
5127
- let cursor = options.cursor;
5462
+ let cursor2 = options.cursor;
5128
5463
  let nextCursor = null;
5129
5464
  let pageCount = 0;
5130
5465
  do {
@@ -5138,14 +5473,14 @@ async function listSessionsCommand(options, command) {
5138
5473
  if (options.search) query.set("q", options.search);
5139
5474
  if (options.repo) query.set("repo", options.repo);
5140
5475
  if (options.limit) query.set("limit", options.limit);
5141
- if (cursor) query.set("cursor", cursor);
5476
+ if (cursor2) query.set("cursor", cursor2);
5142
5477
  const payload2 = await apiFetch(
5143
5478
  config,
5144
5479
  `/api/sessions${query.size ? `?${query.toString()}` : ""}`
5145
5480
  );
5146
5481
  sessions2.push(...payload2.sessions);
5147
5482
  nextCursor = payload2.nextCursor;
5148
- cursor = nextCursor ?? void 0;
5483
+ cursor2 = nextCursor ?? void 0;
5149
5484
  } while (options.all === true && nextCursor);
5150
5485
  const payload = { sessions: sessions2, nextCursor: options.all === true ? null : nextCursor };
5151
5486
  if (isJson(command, options)) {
@@ -5377,7 +5712,7 @@ var MAX_ALL_PAGES3 = 1e3;
5377
5712
  async function listTokensCommand(options, command) {
5378
5713
  const { config } = resolveBusinessContext(command, options);
5379
5714
  const data = [];
5380
- let cursor = options.cursor;
5715
+ let cursor2 = options.cursor;
5381
5716
  let nextCursor = null;
5382
5717
  let pageCount = 0;
5383
5718
  do {
@@ -5387,14 +5722,14 @@ async function listTokensCommand(options, command) {
5387
5722
  }
5388
5723
  const query = new URLSearchParams();
5389
5724
  if (options.limit) query.set("limit", options.limit);
5390
- if (cursor) query.set("cursor", cursor);
5725
+ if (cursor2) query.set("cursor", cursor2);
5391
5726
  const payload2 = await apiFetch(
5392
5727
  config,
5393
5728
  `/api/cli-tokens${query.size ? `?${query.toString()}` : ""}`
5394
5729
  );
5395
5730
  data.push(...payload2.data);
5396
5731
  nextCursor = payload2.nextCursor;
5397
- cursor = nextCursor ?? void 0;
5732
+ cursor2 = nextCursor ?? void 0;
5398
5733
  } while (options.all === true && nextCursor);
5399
5734
  const payload = { data, nextCursor: options.all === true ? null : nextCursor };
5400
5735
  emit(command, options, payload, (listPayload) => {
@@ -5628,6 +5963,52 @@ Examples:
5628
5963
  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
5964
  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
5965
  codex.command("logout").description("Deactivate the selector and remove the stored Codex subscription auth for your user").action((options, command) => codexLogoutCommand(options, command));
5966
+ var grok = program.command("grok").description("Grok (xAI) subscription (bring-your-own-subscription) commands");
5967
+ 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(
5968
+ "after",
5969
+ `
5970
+ Runs the Grok CLI device-authorization login locally under a temporary HOME, then uploads the
5971
+ resulting auth.json to Arcanist (encrypted, per user) and activates the selector. Your workspace
5972
+ must have Grok subscription auth enabled. The credential is never written to your default ~/.grok.
5973
+ Grok BYOS session execution is not live yet; the credential is stored for when it ships.
5974
+
5975
+ Examples:
5976
+ arcanist grok login
5977
+ arcanist grok login --grok-path /usr/local/bin/grok
5978
+ `
5979
+ ).action(
5980
+ (options, command) => agentSubscriptionLoginCommand(GROK_SUBSCRIPTION_CLI_CONFIG, { ...options, binPath: options.grokPath }, command)
5981
+ );
5982
+ grok.command("use <state>").description("Turn using your saved Grok subscription auth on or off (state: on|off)").action(
5983
+ (state, options, command) => agentSubscriptionUseCommand(GROK_SUBSCRIPTION_CLI_CONFIG, state, options, command)
5984
+ );
5985
+ 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));
5986
+ 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));
5987
+ var cursor = program.command("cursor").description("Cursor subscription (bring-your-own-subscription) commands");
5988
+ cursor.command("login").description("Authenticate a Cursor subscription and store it for future Cursor BYOS sessions").option(
5989
+ "--cursor-path <path>",
5990
+ "Path to the cursor-agent executable (default: cursor-agent on PATH, or ARCANIST_CURSOR_AGENT_BIN)"
5991
+ ).addHelpText(
5992
+ "after",
5993
+ `
5994
+ Runs the Cursor CLI login locally under a temporary HOME/CURSOR_CONFIG_DIR, then uploads the
5995
+ captured auth credential to Arcanist (encrypted, per user) and activates the selector. Your
5996
+ workspace must have Cursor subscription auth enabled. Cursor's login-token location is not
5997
+ formally documented; if capture fails, use a Cursor API key in Settings instead. Cursor BYOS
5998
+ session execution is not live yet; the credential is stored for when it ships.
5999
+
6000
+ Examples:
6001
+ arcanist cursor login
6002
+ arcanist cursor login --cursor-path /usr/local/bin/cursor-agent
6003
+ `
6004
+ ).action(
6005
+ (options, command) => agentSubscriptionLoginCommand(CURSOR_SUBSCRIPTION_CLI_CONFIG, { ...options, binPath: options.cursorPath }, command)
6006
+ );
6007
+ cursor.command("use <state>").description("Turn using your saved Cursor subscription auth on or off (state: on|off)").action(
6008
+ (state, options, command) => agentSubscriptionUseCommand(CURSOR_SUBSCRIPTION_CLI_CONFIG, state, options, command)
6009
+ );
6010
+ 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));
6011
+ 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
6012
  var sessions = program.command("sessions").description("Session commands");
5632
6013
  addCreateOptions(sessions.command("create").description("Create a session and send a prompt")).action(
5633
6014
  (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.206",
4
4
  "description": "CLI for Arcanist — create and manage coding agent sessions",
5
5
  "type": "module",
6
6
  "bin": {