@tryarcanist/cli 0.1.186 → 0.1.188

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 (3) hide show
  1. package/README.md +39 -0
  2. package/dist/index.js +219 -8
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -137,6 +137,45 @@ ARCANIST_TOKEN=arc_... arcanist auth whoami --json
137
137
 
138
138
  JSON mode prints the raw `/api/auth/whoami` API response. Fields currently include `userId`, `email`, `tokenId`, `tokenScope`, and `authMode`.
139
139
 
140
+ ### `arcanist codex login`
141
+
142
+ Authenticates a Codex/ChatGPT subscription and stores it for your Codex sessions, then activates it (turns on "use subscription auth for OpenAI sessions"). Runs the Codex CLI device-authorization login locally under a temporary `CODEX_HOME`, then uploads the resulting `auth.json` to Arcanist (encrypted, per user). The credential is never written to your default `~/.codex`. Requires a write-scoped token and a workspace with Codex subscription auth enabled. If activation fails the credential is still saved; run `arcanist codex use on`.
143
+
144
+ ```bash
145
+ arcanist codex login
146
+ arcanist codex login --codex-path /usr/local/bin/codex
147
+ ```
148
+
149
+ `--codex-path <path>` overrides the `codex` executable used (default: `codex` on `PATH`, or `ARCANIST_CODEX_BIN`).
150
+
151
+ ### `arcanist codex use <on|off>`
152
+
153
+ Turns using your saved Codex subscription auth for OpenAI sessions on or off, without changing the stored credential. `arcanist codex login` turns it on automatically; use this to toggle it later.
154
+
155
+ ```bash
156
+ arcanist codex use on
157
+ arcanist codex use off
158
+ ```
159
+
160
+ ### `arcanist codex status`
161
+
162
+ Shows whether your workspace is eligible for Codex subscription auth and whether an `auth.json` is currently saved.
163
+
164
+ ```bash
165
+ arcanist codex status
166
+ arcanist codex status --json
167
+ ```
168
+
169
+ JSON mode returns `{eligible, credential: {isSet, lastValidationStatus?, lastValidationReasonCode?}}`.
170
+
171
+ ### `arcanist codex logout`
172
+
173
+ Deactivates the selector and removes the stored Codex subscription auth for your user (write-scoped token).
174
+
175
+ ```bash
176
+ arcanist codex logout
177
+ ```
178
+
140
179
  ### `arcanist sessions create <repo-url> [prompt]`
141
180
 
142
181
  Creates a new session and sends the initial prompt.
package/dist/index.js CHANGED
@@ -221,9 +221,9 @@ import { randomUUID } from "crypto";
221
221
  import { createInterface } from "readline/promises";
222
222
 
223
223
  // src/config.ts
224
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
224
+ import { existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from "fs";
225
225
  import { homedir } from "os";
226
- import { dirname, join } from "path";
226
+ import { dirname, join, resolve } from "path";
227
227
  var CONFIG_DIR = join(homedir(), ".arcanist");
228
228
  var CONFIG_FILE = join(CONFIG_DIR, "config.json");
229
229
  var PROJECT_CONFIG_FILE = ".arcanist-cli.json";
@@ -330,12 +330,34 @@ function validateAndNormalizeConfig(config) {
330
330
  function findGitRoot(cwd) {
331
331
  let current = cwd;
332
332
  while (true) {
333
- if (existsSync(join(current, ".git"))) return current;
333
+ if (hasGitMetadata(current)) return current;
334
334
  const parent = dirname(current);
335
335
  if (parent === current) return null;
336
336
  current = parent;
337
337
  }
338
338
  }
339
+ function hasGitMetadata(repoPath2) {
340
+ const gitPath = join(repoPath2, ".git");
341
+ if (!existsSync(gitPath)) return false;
342
+ try {
343
+ const gitStat = statSync(gitPath);
344
+ if (gitStat.isDirectory()) {
345
+ return existsSync(join(gitPath, "HEAD")) && existsSync(join(gitPath, "config"));
346
+ }
347
+ if (!gitStat.isFile()) return false;
348
+ const gitDir = readGitDirPointer(gitPath);
349
+ if (!gitDir) return false;
350
+ return existsSync(join(resolve(repoPath2, gitDir), "HEAD"));
351
+ } catch {
352
+ return false;
353
+ }
354
+ }
355
+ function readGitDirPointer(gitFilePath) {
356
+ const gitDirLine = readFileSync(gitFilePath, "utf-8").split(/\r?\n/).find((line) => line.startsWith("gitdir:"));
357
+ if (!gitDirLine) return null;
358
+ const gitDir = gitDirLine.slice("gitdir:".length).trim();
359
+ return gitDir || null;
360
+ }
339
361
  function parseProjectConfig(configPath) {
340
362
  let parsed;
341
363
  try {
@@ -434,7 +456,7 @@ async function readHiddenPrompt(prompt) {
434
456
  if (!process.stdin.isTTY || !process.stdout.isTTY) {
435
457
  throw new CliError("user", "No interactive terminal available. Re-run with --token-stdin or set ARCANIST_TOKEN.");
436
458
  }
437
- return new Promise((resolve, reject) => {
459
+ return new Promise((resolve2, reject) => {
438
460
  const stdin = process.stdin;
439
461
  const inputChars = [];
440
462
  let ansiCarry = "";
@@ -450,7 +472,7 @@ async function readHiddenPrompt(prompt) {
450
472
  settled = true;
451
473
  cleanup();
452
474
  process.stdout.write("\n");
453
- resolve(inputChars.join(""));
475
+ resolve2(inputChars.join(""));
454
476
  };
455
477
  const fail2 = (error) => {
456
478
  if (settled) return;
@@ -605,6 +627,10 @@ function asNonEmptyString(value) {
605
627
 
606
628
  // ../../shared/constants/models.ts
607
629
  var OpenAIModel = {
630
+ GPT56: "gpt-5.6",
631
+ GPT56Sol: "gpt-5.6-sol",
632
+ GPT56Terra: "gpt-5.6-terra",
633
+ GPT56Luna: "gpt-5.6-luna",
608
634
  GPT55: "gpt-5.5",
609
635
  GPT54: "gpt-5.4",
610
636
  GPT54Pro: "gpt-5.4-pro",
@@ -654,6 +680,48 @@ var MODEL_REGISTRY = [
654
680
  },
655
681
  sessionStart: { eligible: true, isDefault: true }
656
682
  },
683
+ {
684
+ id: OpenAIModel.GPT56,
685
+ name: "GPT-5.6",
686
+ provider: "openai",
687
+ backends: [CODEX_AGENT_RUNTIME_BACKEND],
688
+ contextWindow: 105e4,
689
+ // Verified 2026-07-09 against OpenAI's GPT-5.6 migration guide and model
690
+ // catalog: the alias routes to gpt-5.6-sol and supports max effort.
691
+ reasoning: { efforts: ["none", "low", "medium", "high", "xhigh", "max"], default: "medium" },
692
+ costTracked: false,
693
+ sessionStart: { eligible: true }
694
+ },
695
+ {
696
+ id: OpenAIModel.GPT56Sol,
697
+ name: "GPT-5.6 Sol",
698
+ provider: "openai",
699
+ backends: [CODEX_AGENT_RUNTIME_BACKEND],
700
+ contextWindow: 105e4,
701
+ reasoning: { efforts: ["none", "low", "medium", "high", "xhigh", "max"], default: "medium" },
702
+ costTracked: false,
703
+ sessionStart: { eligible: true }
704
+ },
705
+ {
706
+ id: OpenAIModel.GPT56Terra,
707
+ name: "GPT-5.6 Terra",
708
+ provider: "openai",
709
+ backends: [CODEX_AGENT_RUNTIME_BACKEND],
710
+ contextWindow: 105e4,
711
+ reasoning: { efforts: ["none", "low", "medium", "high", "xhigh", "max"], default: "medium" },
712
+ costTracked: false,
713
+ sessionStart: { eligible: true }
714
+ },
715
+ {
716
+ id: OpenAIModel.GPT56Luna,
717
+ name: "GPT-5.6 Luna",
718
+ provider: "openai",
719
+ backends: [CODEX_AGENT_RUNTIME_BACKEND],
720
+ contextWindow: 105e4,
721
+ reasoning: { efforts: ["none", "low", "medium", "high", "xhigh", "max"], default: "medium" },
722
+ costTracked: false,
723
+ sessionStart: { eligible: true }
724
+ },
657
725
  {
658
726
  id: OpenAIModel.GPT55,
659
727
  name: "GPT-5.5",
@@ -1178,9 +1246,136 @@ function formatTime(value) {
1178
1246
  return typeof value === "number" ? new Date(value).toISOString() : "none";
1179
1247
  }
1180
1248
 
1249
+ // src/commands/codex.ts
1250
+ import { spawn } from "child_process";
1251
+ import { mkdtemp, readFile, rm } from "fs/promises";
1252
+ import { tmpdir } from "os";
1253
+ import { join as join2 } from "path";
1254
+ var CODEX_SUBSCRIPTION_PATH = "/api/settings/codex-subscription";
1255
+ var CODEX_SUBSCRIPTION_AUTH_JSON_PATH = "/api/settings/codex-subscription/auth-json";
1256
+ var CODEX_SUBSCRIPTION_ENABLED_PATH = "/api/settings/codex-subscription/enabled";
1257
+ function resolveCodexPath(optionPath) {
1258
+ return optionPath?.trim() || process.env.ARCANIST_CODEX_BIN?.trim() || "codex";
1259
+ }
1260
+ function runCodexDeviceLogin(codexPath, codexHome) {
1261
+ return new Promise((resolve2, reject) => {
1262
+ const child = spawn(codexPath, ["login", "--device-auth"], {
1263
+ stdio: "inherit",
1264
+ env: { ...process.env, CODEX_HOME: codexHome }
1265
+ });
1266
+ child.on("error", (err) => {
1267
+ if (err.code === "ENOENT") {
1268
+ reject(
1269
+ new CliError("user", `Could not find the \`${codexPath}\` executable.`, {
1270
+ hint: "Install the Codex CLI, or point at it with --codex-path <path> or ARCANIST_CODEX_BIN."
1271
+ })
1272
+ );
1273
+ return;
1274
+ }
1275
+ reject(new CliError("user", `Failed to launch \`${codexPath} login --device-auth\`: ${err.message}`));
1276
+ });
1277
+ child.on("close", (code) => {
1278
+ if (code === 0) {
1279
+ resolve2();
1280
+ return;
1281
+ }
1282
+ reject(
1283
+ new CliError("user", `\`${codexPath} login --device-auth\` exited with code ${code ?? "unknown"}.`, {
1284
+ hint: "Complete the device approval in your browser, then re-run `arcanist codex login`."
1285
+ })
1286
+ );
1287
+ });
1288
+ });
1289
+ }
1290
+ async function setCodexSubscriptionEnabled(config, enabled) {
1291
+ return apiFetch(config, CODEX_SUBSCRIPTION_ENABLED_PATH, {
1292
+ method: "PUT",
1293
+ body: JSON.stringify({ enabled })
1294
+ });
1295
+ }
1296
+ async function codexLoginCommand(options, command) {
1297
+ const { config } = resolveBusinessContext(command, options);
1298
+ const codexPath = resolveCodexPath(options.codexPath);
1299
+ const codexHome = await mkdtemp(join2(tmpdir(), "arcanist-codex-"));
1300
+ try {
1301
+ await runCodexDeviceLogin(codexPath, codexHome);
1302
+ let authJson;
1303
+ try {
1304
+ authJson = await readFile(join2(codexHome, "auth.json"), "utf8");
1305
+ } catch {
1306
+ throw new CliError("user", "Codex login completed but no auth.json was written.", {
1307
+ hint: "Verify `codex login --device-auth` succeeds on its own, then re-run `arcanist codex login`."
1308
+ });
1309
+ }
1310
+ if (!authJson.trim()) {
1311
+ throw new CliError("user", "Codex login produced an empty auth.json.");
1312
+ }
1313
+ const state = await apiFetch(config, CODEX_SUBSCRIPTION_AUTH_JSON_PATH, {
1314
+ method: "PUT",
1315
+ body: JSON.stringify({ authJson })
1316
+ });
1317
+ let activated = false;
1318
+ let activationError;
1319
+ try {
1320
+ await setCodexSubscriptionEnabled(config, true);
1321
+ activated = true;
1322
+ } catch (err) {
1323
+ activationError = err instanceof Error ? err.message : String(err);
1324
+ }
1325
+ emit(command, options, { ...state, useCodexSubscription: activated }, (payload) => {
1326
+ console.log(
1327
+ activated ? "Codex subscription auth saved and activated for OpenAI sessions." : "Codex subscription auth saved."
1328
+ );
1329
+ if (payload.lastValidationStatus) console.log(`Status: ${payload.lastValidationStatus}`);
1330
+ if (!activated) {
1331
+ console.log(`Could not activate it automatically${activationError ? ` (${activationError})` : ""}.`);
1332
+ console.log("Run `arcanist codex use on` to start using it.");
1333
+ }
1334
+ });
1335
+ } finally {
1336
+ await rm(codexHome, { recursive: true, force: true }).catch(() => {
1337
+ });
1338
+ }
1339
+ }
1340
+ async function codexUseCommand(state, options, command) {
1341
+ const normalized = state.trim().toLowerCase();
1342
+ if (normalized !== "on" && normalized !== "off") {
1343
+ throw new CliError("user", "Usage: arcanist codex use <on|off>");
1344
+ }
1345
+ const enabled = normalized === "on";
1346
+ const { config } = resolveBusinessContext(command, options);
1347
+ const result = await setCodexSubscriptionEnabled(config, enabled);
1348
+ emit(
1349
+ command,
1350
+ options,
1351
+ result,
1352
+ () => console.log(
1353
+ result.useCodexSubscription ? "Codex subscription auth is now used for OpenAI sessions." : "Codex subscription auth is no longer used for OpenAI sessions."
1354
+ )
1355
+ );
1356
+ }
1357
+ async function codexStatusCommand(options, command) {
1358
+ const { config } = resolveBusinessContext(command, options);
1359
+ const payload = await apiFetch(config, CODEX_SUBSCRIPTION_PATH);
1360
+ emit(command, options, payload, (state) => {
1361
+ console.log(`Eligible: ${state.eligible ? "yes" : "no"}`);
1362
+ console.log(`Auth.json saved: ${state.credential.isSet ? "yes" : "no"}`);
1363
+ if (state.credential.lastValidationStatus) console.log(`Status: ${state.credential.lastValidationStatus}`);
1364
+ });
1365
+ }
1366
+ async function codexLogoutCommand(options, command) {
1367
+ const { config } = resolveBusinessContext(command, options);
1368
+ await setCodexSubscriptionEnabled(config, false).catch(() => {
1369
+ });
1370
+ const payload = await apiFetch(config, CODEX_SUBSCRIPTION_AUTH_JSON_PATH, {
1371
+ method: "DELETE"
1372
+ });
1373
+ emit(command, options, payload, () => console.log("Codex subscription auth deactivated and cleared."));
1374
+ }
1375
+
1181
1376
  // ../../shared/utils/timing.ts
1182
1377
  function sleep(ms) {
1183
- return new Promise((resolve) => setTimeout(resolve, ms));
1378
+ return new Promise((resolve2) => setTimeout(resolve2, ms));
1184
1379
  }
1185
1380
 
1186
1381
  // ../../shared/session/phase.ts
@@ -1229,7 +1424,7 @@ function isWatchTerminal(phase) {
1229
1424
  }
1230
1425
 
1231
1426
  // src/uploads.ts
1232
- import { readFile } from "fs/promises";
1427
+ import { readFile as readFile2 } from "fs/promises";
1233
1428
  import { basename, extname } from "path";
1234
1429
 
1235
1430
  // ../../shared/constants/uploads.ts
@@ -1332,7 +1527,7 @@ async function resolveUploadedFileOptions(files) {
1332
1527
  paths.map(async (path) => {
1333
1528
  const name = basename(path);
1334
1529
  try {
1335
- return { name, content: await readFile(path, "utf8") };
1530
+ return { name, content: await readFile2(path, "utf8") };
1336
1531
  } catch (err) {
1337
1532
  const message = stringifyError(err);
1338
1533
  throw new CliError("user", `Failed to read uploaded file ${path}: ${message}`);
@@ -5282,6 +5477,22 @@ Examples:
5282
5477
  ARCANIST_TOKEN=arc_... arcanist auth whoami --json
5283
5478
  `
5284
5479
  ).action((options, command) => whoamiCommand(options, command));
5480
+ var codex = program.command("codex").description("Codex subscription (bring-your-own-subscription) commands");
5481
+ codex.command("login").description("Authenticate a Codex/ChatGPT subscription and store it for your Codex sessions").option("--codex-path <path>", "Path to the codex executable (default: codex on PATH, or ARCANIST_CODEX_BIN)").addHelpText(
5482
+ "after",
5483
+ `
5484
+ Runs the Codex CLI device-authorization login locally under a temporary CODEX_HOME, then uploads
5485
+ the resulting auth.json to Arcanist (encrypted, per user) and activates it. Your workspace must have
5486
+ Codex subscription auth enabled. The credential is never written to your default ~/.codex.
5487
+
5488
+ Examples:
5489
+ arcanist codex login
5490
+ arcanist codex login --codex-path /usr/local/bin/codex
5491
+ `
5492
+ ).action((options, command) => codexLoginCommand(options, command));
5493
+ 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));
5494
+ codex.command("status").description("Show whether your workspace is eligible and whether a Codex subscription auth is saved").action((options, command) => codexStatusCommand(options, command));
5495
+ codex.command("logout").description("Deactivate the selector and remove the stored Codex subscription auth for your user").action((options, command) => codexLogoutCommand(options, command));
5285
5496
  var sessions = program.command("sessions").description("Session commands");
5286
5497
  addCreateOptions(sessions.command("create").description("Create a session and send a prompt")).action(
5287
5498
  (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.186",
3
+ "version": "0.1.188",
4
4
  "description": "CLI for Arcanist — create and manage coding agent sessions",
5
5
  "type": "module",
6
6
  "bin": {