@redstone-md/mapr 0.0.5-alpha → 0.0.6-alpha

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.
package/README.md CHANGED
@@ -10,6 +10,7 @@ This repository is public for source visibility and collaboration. The license r
10
10
 
11
11
  - Bun-only CLI/TUI with interactive setup through `@clack/prompts`
12
12
  - OpenAI and OpenAI-compatible provider support
13
+ - OpenAI Codex family support with fast/reasoning selection
13
14
  - Built-in provider presets for BlackBox AI, Nvidia NIM, and OnlySQ
14
15
  - Model discovery with searchable selection
15
16
  - Automatic context-window detection from provider model metadata when available
@@ -77,6 +78,14 @@ npx @redstone-md/mapr --help
77
78
  - `onlysq` -> `https://api.onlysq.ru/ai/openai`
78
79
  - `custom` -> any other OpenAI-compatible endpoint
79
80
 
81
+ ## OpenAI Auth Modes
82
+
83
+ - `API key` uses the standard OpenAI API at `https://api.openai.com/v1`
84
+ - `Use existing Codex CLI auth` reuses the local `codex login` browser session from `~/.codex/auth.json`
85
+ - Codex CLI auth automatically switches OpenAI requests to `https://chatgpt.com/backend-api/codex`
86
+ - When the local Codex access token expires, Mapr refreshes it through the official OpenAI refresh-token flow before retrying the request
87
+ - If Codex CLI auth is missing, run `codex login` first and then re-run Mapr
88
+
80
89
  ## Usage
81
90
 
82
91
  Interactive:
@@ -105,6 +114,17 @@ List models with detected context sizes when available:
105
114
  npx @redstone-md/mapr --list-models --headless --provider-preset nvidia-nim --api-key secret
106
115
  ```
107
116
 
117
+ List models through an existing Codex CLI login:
118
+
119
+ ```bash
120
+ npx @redstone-md/mapr \
121
+ --headless \
122
+ --list-models \
123
+ --provider-type openai \
124
+ --auth-method codex-cli \
125
+ --codex-home ~/.codex
126
+ ```
127
+
108
128
  Useful flags:
109
129
 
110
130
  - `--max-pages <n>` limits same-origin HTML pages
package/index.ts CHANGED
@@ -9,7 +9,8 @@ import { AiBundleAnalyzer, chunkTextByBytes, deriveChunkSizeBytes } from "./lib/
9
9
  import { getConfigOverrides, parseCliArgs, renderHelpText } from "./lib/cli-args";
10
10
  import { ConfigManager } from "./lib/config";
11
11
  import { BundleFormatter } from "./lib/formatter";
12
- import { renderProgressBar } from "./lib/progress";
12
+ import { renderAdaptiveAnalysisProgressLine, renderProgressBar } from "./lib/progress";
13
+ import { findKnownModelInfo, isCodexModel } from "./lib/provider";
13
14
  import { ReportWriter } from "./lib/reporter";
14
15
  import { BundleScraper } from "./lib/scraper";
15
16
  import { SWARM_AGENT_ORDER } from "./lib/swarm-prompts";
@@ -177,6 +178,7 @@ async function run(): Promise<void> {
177
178
  const analysisConcurrency = await resolveAnalysisConcurrency(headless, args.analysisConcurrency, totalChunks);
178
179
  const totalAgentTasks = Math.max(1, totalChunks * SWARM_AGENT_ORDER.length);
179
180
  let completedAgentTasks = 0;
181
+ const analysisStartedAt = Date.now();
180
182
 
181
183
  const analysisStep = spinner({ indicator: "timer" });
182
184
  analysisStep.start(formatAnalysisProgress(0, totalAgentTasks, `Starting swarm analysis (${analysisConcurrency} lane${analysisConcurrency === 1 ? "" : "s"})`));
@@ -190,7 +192,24 @@ async function run(): Promise<void> {
190
192
  completedAgentTasks += 1;
191
193
  }
192
194
 
193
- const progressLine = formatAnalysisProgress(completedAgentTasks, totalAgentTasks, event.message);
195
+ const progressLine =
196
+ event.stage === "agent" && event.agent
197
+ ? renderAdaptiveAnalysisProgressLine({
198
+ completed: completedAgentTasks,
199
+ total: totalAgentTasks,
200
+ elapsedMs: Date.now() - analysisStartedAt,
201
+ agent: event.agent,
202
+ state: event.state,
203
+ artifactUrl: event.artifactUrl,
204
+ ...(event.chunkIndex !== undefined ? { chunkIndex: event.chunkIndex } : {}),
205
+ ...(event.chunkCount !== undefined ? { chunkCount: event.chunkCount } : {}),
206
+ ...(event.estimatedOutputTokens !== undefined
207
+ ? { estimatedOutputTokens: event.estimatedOutputTokens }
208
+ : {}),
209
+ ...(event.outputTokens !== undefined ? { outputTokens: event.outputTokens } : {}),
210
+ ...(event.tokensPerSecond !== undefined ? { tokensPerSecond: event.tokensPerSecond } : {}),
211
+ })
212
+ : formatAnalysisProgress(completedAgentTasks, totalAgentTasks, event.message);
194
213
  analysisStep.message(progressLine);
195
214
 
196
215
  if (args.verboseAgents && event.stage === "agent" && event.state === "completed") {
@@ -247,12 +266,16 @@ async function run(): Promise<void> {
247
266
  });
248
267
  reportStep.stop(reportStatus === "partial" ? "Partial report written to disk" : "Report written to disk");
249
268
 
269
+ const selectedModelInfo = findKnownModelInfo(config.model);
250
270
  const summaryLines = [
251
271
  reportStatus === "partial" ? `${pc.yellow("Analysis incomplete.")}` : `${pc.green("Analysis complete.")}`,
252
272
  `${pc.bold("Status:")} ${reportStatus === "partial" ? "partial report saved after error" : "complete"}`,
253
273
  `${pc.bold("Target:")} ${scrapeResult.pageUrl}`,
254
274
  `${pc.bold("Provider:")} ${config.providerName} (${config.model})`,
275
+ ...(config.authMethod !== undefined ? [`${pc.bold("Auth:")} ${config.authMethod}`] : []),
255
276
  `${pc.bold("Context size:")} ${config.modelContextSize.toLocaleString()} tokens`,
277
+ ...(config.openAiMode !== undefined ? [`${pc.bold("OpenAI mode:")} ${config.openAiMode}`] : []),
278
+ ...(isCodexModel(config.model) && selectedModelInfo?.usageLimitsNote ? [`${pc.bold("Codex limits:")} ${selectedModelInfo.usageLimitsNote}`] : []),
256
279
  `${pc.bold("Concurrency:")} ${analysisConcurrency}`,
257
280
  `${pc.bold("Local RAG:")} ${args.localRag ? "enabled" : "disabled"}`,
258
281
  `${pc.bold("Pages:")} ${scrapeResult.htmlPages.length}`,
@@ -85,12 +85,14 @@ export class AiBundleAnalyzer {
85
85
  private readonly localRagEnabled: boolean;
86
86
  private readonly analysisConcurrency: number;
87
87
  private readonly onProgress: ((event: AnalysisProgressEvent) => void) | undefined;
88
+ private readonly providerOptions: Record<string, unknown>;
88
89
 
89
90
  public constructor(options: AnalyzerOptions) {
90
91
  this.providerClient = new AiProviderClient(options.providerConfig);
91
92
  this.chunkSizeBytes = options.chunkSizeBytes ?? deriveChunkSizeBytes(options.providerConfig.modelContextSize);
92
93
  this.localRagEnabled = options.localRag ?? false;
93
94
  this.analysisConcurrency = Math.max(1, Math.floor(options.analysisConcurrency ?? 1));
95
+ this.providerOptions = this.providerClient.getProviderOptions();
94
96
  this.onProgress = options.onProgress;
95
97
  }
96
98
 
@@ -271,7 +273,7 @@ export class AiBundleAnalyzer {
271
273
  ].join("\n"),
272
274
  attempts: 4,
273
275
  maxRetries: 2,
274
- providerOptions: { openai: { store: false } },
276
+ providerOptions: this.providerOptions,
275
277
  onRetry: (attempt, error) =>
276
278
  this.emitAgentEvent(
277
279
  "streaming",
@@ -306,7 +308,7 @@ export class AiBundleAnalyzer {
306
308
  ].join("\n"),
307
309
  attempts: 4,
308
310
  maxRetries: 2,
309
- providerOptions: { openai: { store: false } },
311
+ providerOptions: this.providerOptions,
310
312
  onRetry: (attempt, error) =>
311
313
  this.emitAgentEvent(
312
314
  "streaming",
@@ -346,7 +348,7 @@ export class AiBundleAnalyzer {
346
348
  ].join("\n"),
347
349
  attempts: 4,
348
350
  maxRetries: 2,
349
- providerOptions: { openai: { store: false } },
351
+ providerOptions: this.providerOptions,
350
352
  });
351
353
 
352
354
  return finalAnalysisSchema.parse({
package/lib/cli-args.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { z } from "zod";
2
2
 
3
- import { getProviderPreset, providerPresetSchema, providerTypeSchema } from "./provider";
3
+ import { authMethodSchema, getProviderPreset, openAiModeSchema, providerPresetSchema, providerTypeSchema } from "./provider";
4
4
 
5
5
  const rawCliArgsSchema = z.object({
6
6
  help: z.boolean().default(false),
@@ -14,8 +14,11 @@ const rawCliArgsSchema = z.object({
14
14
  output: z.string().min(1).optional(),
15
15
  providerType: providerTypeSchema.optional(),
16
16
  providerPreset: providerPresetSchema.optional(),
17
+ openAiMode: openAiModeSchema.optional(),
18
+ authMethod: authMethodSchema.optional(),
17
19
  providerName: z.string().min(1).optional(),
18
20
  apiKey: z.string().min(1).optional(),
21
+ codexHomePath: z.string().min(1).optional(),
19
22
  baseURL: z.string().url().optional(),
20
23
  model: z.string().min(1).optional(),
21
24
  contextSize: z.number().int().positive().optional(),
@@ -29,8 +32,11 @@ const cliConfigOverrideSchema = z
29
32
  .object({
30
33
  providerType: providerTypeSchema.optional(),
31
34
  providerPreset: providerPresetSchema.optional(),
35
+ openAiMode: openAiModeSchema.optional(),
36
+ authMethod: authMethodSchema.optional(),
32
37
  providerName: z.string().min(1).optional(),
33
38
  apiKey: z.string().min(1).optional(),
39
+ codexHomePath: z.string().min(1).optional(),
34
40
  baseURL: z.string().url().optional(),
35
41
  model: z.string().min(1).optional(),
36
42
  modelContextSize: z.number().int().positive().optional(),
@@ -54,8 +60,11 @@ const optionMap = new Map<string, keyof CliArgs>([
54
60
  ["--output", "output"],
55
61
  ["--provider-type", "providerType"],
56
62
  ["--provider-preset", "providerPreset"],
63
+ ["--openai-mode", "openAiMode"],
64
+ ["--auth-method", "authMethod"],
57
65
  ["--provider-name", "providerName"],
58
66
  ["--api-key", "apiKey"],
67
+ ["--codex-home", "codexHomePath"],
59
68
  ["--base-url", "baseURL"],
60
69
  ["--model", "model"],
61
70
  ["--context-size", "contextSize"],
@@ -117,6 +126,8 @@ export function getConfigOverrides(args: CliArgs) {
117
126
  const overrides: Record<string, unknown> = {};
118
127
 
119
128
  if (args.providerType !== undefined) overrides.providerType = args.providerType;
129
+ if (args.openAiMode !== undefined) overrides.openAiMode = args.openAiMode;
130
+ if (args.authMethod !== undefined) overrides.authMethod = args.authMethod;
120
131
  if (args.providerPreset !== undefined) {
121
132
  const preset = getProviderPreset(args.providerPreset);
122
133
  overrides.providerType = "openai-compatible";
@@ -126,6 +137,7 @@ export function getConfigOverrides(args: CliArgs) {
126
137
  }
127
138
  if (args.providerName !== undefined) overrides.providerName = args.providerName;
128
139
  if (args.apiKey !== undefined) overrides.apiKey = args.apiKey;
140
+ if (args.codexHomePath !== undefined) overrides.codexHomePath = args.codexHomePath;
129
141
  if (args.baseURL !== undefined) overrides.baseURL = args.baseURL;
130
142
  if (args.model !== undefined) overrides.model = args.model;
131
143
  if (args.contextSize !== undefined) overrides.modelContextSize = args.contextSize;
@@ -151,8 +163,11 @@ export function renderHelpText(): string {
151
163
  "Provider options:",
152
164
  " --provider-type <type> openai | openai-compatible",
153
165
  " --provider-preset <preset> custom | blackbox | nvidia-nim | onlysq",
166
+ " --openai-mode <mode> fast | reasoning",
167
+ " --auth-method <method> api-key | codex-cli",
154
168
  " --provider-name <name> Display name for the provider",
155
169
  " --api-key <key> Provider API key",
170
+ " --codex-home <path> Path to the local Codex CLI home, defaults to ~/.codex",
156
171
  " --base-url <url> Base URL for the provider",
157
172
  " --model <id> Model identifier",
158
173
  " --context-size <tokens> Model context window, for example 128000 or 512000",
@@ -0,0 +1,275 @@
1
+ import { readFile, writeFile } from "fs/promises";
2
+ import { homedir } from "os";
3
+ import { join } from "path";
4
+ import { z } from "zod";
5
+
6
+ export const CODEX_REFRESH_CLIENT_ID = "app_EMoamEEZ73f0CkXaXp7hrann";
7
+ export const DEFAULT_CODEX_HOME_PATH = join(homedir(), ".codex");
8
+ export const DEFAULT_CHATGPT_CODEX_BASE_URL = "https://chatgpt.com/backend-api/codex";
9
+ const DEFAULT_REFRESH_ENDPOINT = "https://auth.openai.com/oauth/token";
10
+ const TOKEN_REFRESH_LEEWAY_MS = 5 * 60 * 1000;
11
+
12
+ const codexTokenSchema = z
13
+ .object({
14
+ id_token: z.string().min(1),
15
+ access_token: z.string().min(1),
16
+ refresh_token: z.string().min(1),
17
+ account_id: z.string().min(1).nullable().optional(),
18
+ })
19
+ .strict();
20
+
21
+ const codexAuthFileSchema = z
22
+ .object({
23
+ auth_mode: z.string().optional(),
24
+ OPENAI_API_KEY: z.string().nullable().optional(),
25
+ tokens: codexTokenSchema.optional(),
26
+ last_refresh: z.string().min(1).optional(),
27
+ })
28
+ .strict();
29
+
30
+ const refreshResponseSchema = z
31
+ .object({
32
+ id_token: z.string().min(1).optional(),
33
+ access_token: z.string().min(1).optional(),
34
+ refresh_token: z.string().min(1).optional(),
35
+ })
36
+ .passthrough();
37
+
38
+ type FetchLike = (input: string | URL | Request, init?: RequestInit) => Promise<Response>;
39
+ type HeaderLike = Headers | Array<[string, string]> | Record<string, string>;
40
+
41
+ export interface CodexCliAuthState {
42
+ accessToken: string;
43
+ refreshToken: string;
44
+ accountId: string;
45
+ planType?: string;
46
+ expiresAt?: number;
47
+ idToken: string;
48
+ authFilePath: string;
49
+ }
50
+
51
+ function parseJwtPayload(token: string): Record<string, unknown> {
52
+ const payload = token.split(".")[1];
53
+ if (!payload) {
54
+ throw new Error("Codex token is malformed.");
55
+ }
56
+
57
+ const normalizedPayload = payload.replace(/-/g, "+").replace(/_/g, "/");
58
+ const padding = normalizedPayload.length % 4 === 0 ? "" : "=".repeat(4 - (normalizedPayload.length % 4));
59
+ const decoded = Buffer.from(`${normalizedPayload}${padding}`, "base64").toString("utf8");
60
+ return z.record(z.string(), z.unknown()).parse(JSON.parse(decoded) as unknown);
61
+ }
62
+
63
+ function extractString(value: unknown): string | undefined {
64
+ return typeof value === "string" && value.trim().length > 0 ? value : undefined;
65
+ }
66
+
67
+ function extractAccountId(idToken: string, fallbackAccountId?: string): string | undefined {
68
+ const payload = parseJwtPayload(idToken);
69
+ const auth = z.record(z.string(), z.unknown()).safeParse(payload["https://api.openai.com/auth"]);
70
+ if (auth.success) {
71
+ return extractString(auth.data.chatgpt_account_id) ?? fallbackAccountId;
72
+ }
73
+
74
+ return fallbackAccountId;
75
+ }
76
+
77
+ function extractPlanType(idToken: string): string | undefined {
78
+ const payload = parseJwtPayload(idToken);
79
+ const auth = z.record(z.string(), z.unknown()).safeParse(payload["https://api.openai.com/auth"]);
80
+ if (!auth.success) {
81
+ return undefined;
82
+ }
83
+
84
+ return extractString(auth.data.chatgpt_plan_type)?.toLowerCase();
85
+ }
86
+
87
+ function extractExpiresAt(accessToken: string): number | undefined {
88
+ const payload = parseJwtPayload(accessToken);
89
+ const exp = payload.exp;
90
+ return typeof exp === "number" && Number.isFinite(exp) ? exp * 1000 : undefined;
91
+ }
92
+
93
+ function mergeHeaders(input: HeaderLike | undefined, overrides: Record<string, string>): Headers {
94
+ const headers = new Headers(input);
95
+ for (const [key, value] of Object.entries(overrides)) {
96
+ headers.set(key, value);
97
+ }
98
+ return headers;
99
+ }
100
+
101
+ export class CodexCliAuthManager {
102
+ private readonly codexHomePath: string;
103
+ private readonly fetcher: FetchLike;
104
+ private refreshPromise: Promise<CodexCliAuthState> | null = null;
105
+
106
+ public constructor(options: { codexHomePath?: string; fetcher?: FetchLike } = {}) {
107
+ this.codexHomePath = options.codexHomePath ?? DEFAULT_CODEX_HOME_PATH;
108
+ this.fetcher = options.fetcher ?? fetch;
109
+ }
110
+
111
+ public getCodexHomePath(): string {
112
+ return this.codexHomePath;
113
+ }
114
+
115
+ public getAuthFilePath(): string {
116
+ return join(this.codexHomePath, "auth.json");
117
+ }
118
+
119
+ public async loadState(): Promise<CodexCliAuthState> {
120
+ const raw = await readFile(this.getAuthFilePath(), "utf8");
121
+ const parsed = codexAuthFileSchema.parse(JSON.parse(raw) as unknown);
122
+ if (!parsed.tokens) {
123
+ throw new Error(`Codex CLI auth was not found at ${this.getAuthFilePath()}. Run \`codex login\` first.`);
124
+ }
125
+
126
+ const accountId = extractAccountId(parsed.tokens.id_token, parsed.tokens.account_id ?? undefined);
127
+ if (!accountId) {
128
+ throw new Error("Codex CLI auth is missing a ChatGPT account identifier.");
129
+ }
130
+
131
+ return {
132
+ accessToken: parsed.tokens.access_token,
133
+ refreshToken: parsed.tokens.refresh_token,
134
+ accountId,
135
+ ...(extractPlanType(parsed.tokens.id_token) !== undefined
136
+ ? { planType: extractPlanType(parsed.tokens.id_token)! }
137
+ : {}),
138
+ ...(extractExpiresAt(parsed.tokens.access_token) !== undefined
139
+ ? { expiresAt: extractExpiresAt(parsed.tokens.access_token)! }
140
+ : {}),
141
+ idToken: parsed.tokens.id_token,
142
+ authFilePath: this.getAuthFilePath(),
143
+ };
144
+ }
145
+
146
+ public async ensureFreshState(): Promise<CodexCliAuthState> {
147
+ const state = await this.loadState();
148
+ if (!this.shouldRefresh(state)) {
149
+ return state;
150
+ }
151
+
152
+ return this.refreshState();
153
+ }
154
+
155
+ public async buildAuthHeaders(): Promise<Record<string, string>> {
156
+ const state = await this.ensureFreshState();
157
+ return {
158
+ Authorization: `Bearer ${state.accessToken}`,
159
+ "ChatGPT-Account-Id": state.accountId,
160
+ Accept: "application/json",
161
+ };
162
+ }
163
+
164
+ public createAuthenticatedFetch(baseFetch: FetchLike = this.fetcher): FetchLike {
165
+ return async (input, init) => {
166
+ const sourceRequest = input instanceof Request ? input : new Request(String(input), init);
167
+ const requestBody =
168
+ sourceRequest.method === "GET" || sourceRequest.method === "HEAD"
169
+ ? undefined
170
+ : await sourceRequest.clone().arrayBuffer();
171
+ const execute = async (forceRefresh = false): Promise<Response> => {
172
+ const state = forceRefresh ? await this.refreshState() : await this.ensureFreshState();
173
+ const headers = mergeHeaders(sourceRequest.headers, {
174
+ Authorization: `Bearer ${state.accessToken}`,
175
+ "ChatGPT-Account-Id": state.accountId,
176
+ });
177
+ const request = new Request(sourceRequest, {
178
+ headers,
179
+ ...(requestBody !== undefined ? { body: requestBody.slice(0) } : {}),
180
+ });
181
+
182
+ const response = await baseFetch(request);
183
+
184
+ if (response.status !== 401 || forceRefresh) {
185
+ return response;
186
+ }
187
+
188
+ return execute(true);
189
+ };
190
+
191
+ return execute(false);
192
+ };
193
+ }
194
+
195
+ public async refreshState(): Promise<CodexCliAuthState> {
196
+ if (!this.refreshPromise) {
197
+ this.refreshPromise = this.refreshStateInternal().finally(() => {
198
+ this.refreshPromise = null;
199
+ });
200
+ }
201
+
202
+ return this.refreshPromise;
203
+ }
204
+
205
+ private shouldRefresh(state: CodexCliAuthState): boolean {
206
+ return state.expiresAt !== undefined && state.expiresAt - Date.now() <= TOKEN_REFRESH_LEEWAY_MS;
207
+ }
208
+
209
+ private async refreshStateInternal(): Promise<CodexCliAuthState> {
210
+ const current = await this.loadState();
211
+ const response = await this.fetcher(DEFAULT_REFRESH_ENDPOINT, {
212
+ method: "POST",
213
+ headers: {
214
+ "Content-Type": "application/json",
215
+ Accept: "application/json",
216
+ },
217
+ body: JSON.stringify({
218
+ client_id: CODEX_REFRESH_CLIENT_ID,
219
+ grant_type: "refresh_token",
220
+ refresh_token: current.refreshToken,
221
+ }),
222
+ });
223
+
224
+ const bodyText = await response.text();
225
+ if (!response.ok) {
226
+ let message = bodyText.trim();
227
+ try {
228
+ const parsedError = z.record(z.string(), z.unknown()).parse(JSON.parse(bodyText) as unknown);
229
+ const nestedError = parsedError.error;
230
+ if (typeof nestedError === "string") {
231
+ message = nestedError;
232
+ } else if (nestedError && typeof nestedError === "object" && "message" in nestedError) {
233
+ message = extractString((nestedError as Record<string, unknown>).message) ?? message;
234
+ }
235
+ } catch {
236
+ // Keep the raw response body when the backend did not return JSON.
237
+ }
238
+
239
+ throw new Error(`Codex CLI auth refresh failed: ${response.status} ${message || response.statusText}`);
240
+ }
241
+
242
+ const refresh = refreshResponseSchema.parse(JSON.parse(bodyText) as unknown);
243
+ const nextIdToken = refresh.id_token ?? current.idToken;
244
+ const nextAccessToken = refresh.access_token ?? current.accessToken;
245
+ const nextRefreshToken = refresh.refresh_token ?? current.refreshToken;
246
+ const nextAccountId = extractAccountId(nextIdToken, current.accountId);
247
+ if (!nextAccountId) {
248
+ throw new Error("Codex CLI auth refresh returned a token without a ChatGPT account identifier.");
249
+ }
250
+
251
+ const persisted = {
252
+ auth_mode: "chatgpt",
253
+ OPENAI_API_KEY: null,
254
+ tokens: {
255
+ id_token: nextIdToken,
256
+ access_token: nextAccessToken,
257
+ refresh_token: nextRefreshToken,
258
+ account_id: nextAccountId,
259
+ },
260
+ last_refresh: new Date().toISOString(),
261
+ };
262
+
263
+ await writeFile(this.getAuthFilePath(), `${JSON.stringify(persisted, null, 2)}\n`, "utf8");
264
+
265
+ return {
266
+ accessToken: nextAccessToken,
267
+ refreshToken: nextRefreshToken,
268
+ accountId: nextAccountId,
269
+ ...(extractPlanType(nextIdToken) !== undefined ? { planType: extractPlanType(nextIdToken)! } : {}),
270
+ ...(extractExpiresAt(nextAccessToken) !== undefined ? { expiresAt: extractExpiresAt(nextAccessToken)! } : {}),
271
+ idToken: nextIdToken,
272
+ authFilePath: this.getAuthFilePath(),
273
+ };
274
+ }
275
+ }
@@ -0,0 +1,74 @@
1
+ import { select, text } from "@clack/prompts";
2
+ import { z } from "zod";
3
+
4
+ import { DEFAULT_CODEX_HOME_PATH } from "./codex-auth";
5
+ import { DEFAULT_MODEL_CONTEXT_SIZE } from "./provider";
6
+
7
+ type ExitIfCancelled = <T>(value: T) => T;
8
+
9
+ export async function promptForContextSize(defaultValue: number, exitIfCancelled: ExitIfCancelled): Promise<number> {
10
+ const rawValue = exitIfCancelled(
11
+ await text({
12
+ message: "Model context size in tokens",
13
+ placeholder: String(DEFAULT_MODEL_CONTEXT_SIZE),
14
+ initialValue: String(defaultValue),
15
+ validate(value) {
16
+ const parsed = z.coerce.number().int().positive().safeParse(value);
17
+ return parsed.success ? undefined : "Context size must be a positive integer.";
18
+ },
19
+ }),
20
+ );
21
+
22
+ return z.coerce.number().int().positive().parse(rawValue);
23
+ }
24
+
25
+ export async function promptForAuthMethod<T>(
26
+ initialMethod: "api-key" | "codex-cli" | undefined,
27
+ exitIfCancelled: ExitIfCancelled,
28
+ ): Promise<"api-key" | "codex-cli"> {
29
+ return exitIfCancelled(
30
+ await select({
31
+ message: "OpenAI auth method",
32
+ initialValue: initialMethod ?? "api-key",
33
+ options: [
34
+ { value: "api-key", label: "API key", hint: "Use a standard OpenAI API key" },
35
+ { value: "codex-cli", label: "Use existing Codex CLI auth", hint: "Reuse `codex login` browser sign-in" },
36
+ ],
37
+ }),
38
+ ) as "api-key" | "codex-cli";
39
+ }
40
+
41
+ export async function promptForCodexMode(
42
+ initialMode: "fast" | "reasoning" | undefined,
43
+ exitIfCancelled: ExitIfCancelled,
44
+ ): Promise<"fast" | "reasoning"> {
45
+ return exitIfCancelled(
46
+ await select({
47
+ message: "Codex mode",
48
+ initialValue: initialMode ?? "reasoning",
49
+ options: [
50
+ { value: "fast", label: "Fast", hint: "Prefer mini / lower-latency Codex variant" },
51
+ { value: "reasoning", label: "Reasoning", hint: "Prefer max / deeper reasoning Codex variant" },
52
+ ],
53
+ }),
54
+ ) as "fast" | "reasoning";
55
+ }
56
+
57
+ export async function promptForCodexHomePath(
58
+ initialPath: string | undefined,
59
+ exitIfCancelled: ExitIfCancelled,
60
+ ): Promise<string> {
61
+ return z.string().trim().min(1).parse(
62
+ exitIfCancelled(
63
+ await text({
64
+ message: "Codex CLI home path",
65
+ placeholder: DEFAULT_CODEX_HOME_PATH,
66
+ initialValue: initialPath ?? DEFAULT_CODEX_HOME_PATH,
67
+ validate(value) {
68
+ const parsed = z.string().trim().min(1).safeParse(value);
69
+ return parsed.success ? undefined : "Codex home path is required.";
70
+ },
71
+ }),
72
+ ),
73
+ );
74
+ }