@golba98/codexa 1.0.1

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 (226) hide show
  1. package/README.md +320 -0
  2. package/bin/codexa.js +445 -0
  3. package/package.json +45 -0
  4. package/scripts/audit-codexa-capabilities.mjs +466 -0
  5. package/scripts/smoke-terminal-bench.mjs +35 -0
  6. package/src/app.tsx +4561 -0
  7. package/src/appRenderStability.test.ts +131 -0
  8. package/src/commands/handler.test.ts +643 -0
  9. package/src/commands/handler.ts +875 -0
  10. package/src/config/launchArgs.test.ts +158 -0
  11. package/src/config/launchArgs.ts +186 -0
  12. package/src/config/layeredConfig.test.ts +143 -0
  13. package/src/config/layeredConfig.ts +836 -0
  14. package/src/config/persistence.test.ts +110 -0
  15. package/src/config/persistence.ts +311 -0
  16. package/src/config/runtimeConfig.test.ts +218 -0
  17. package/src/config/runtimeConfig.ts +554 -0
  18. package/src/config/settings.test.ts +155 -0
  19. package/src/config/settings.ts +401 -0
  20. package/src/config/toml-serialize.ts +98 -0
  21. package/src/config/trustStore.test.ts +29 -0
  22. package/src/config/trustStore.ts +68 -0
  23. package/src/core/attachments.test.ts +155 -0
  24. package/src/core/attachments.ts +71 -0
  25. package/src/core/auth/codexAuth.test.ts +68 -0
  26. package/src/core/auth/codexAuth.ts +359 -0
  27. package/src/core/cleanupFastFail.test.ts +76 -0
  28. package/src/core/cleanupFastFail.ts +67 -0
  29. package/src/core/clipboard.ts +24 -0
  30. package/src/core/codex.ts +124 -0
  31. package/src/core/codexExecArgs.test.ts +195 -0
  32. package/src/core/codexExecArgs.ts +152 -0
  33. package/src/core/codexLaunch.test.ts +205 -0
  34. package/src/core/codexLaunch.ts +162 -0
  35. package/src/core/codexPrompt.test.ts +252 -0
  36. package/src/core/codexPrompt.ts +428 -0
  37. package/src/core/executables/claudeExecutable.ts +63 -0
  38. package/src/core/executables/codexExecutable.test.ts +212 -0
  39. package/src/core/executables/codexExecutable.ts +159 -0
  40. package/src/core/executables/executableResolver.test.ts +129 -0
  41. package/src/core/executables/executableResolver.ts +138 -0
  42. package/src/core/executables/geminiExecutable.test.ts +116 -0
  43. package/src/core/executables/geminiExecutable.ts +78 -0
  44. package/src/core/executables/pathSanityScan.test.ts +47 -0
  45. package/src/core/githubDiagnostics.test.ts +92 -0
  46. package/src/core/githubDiagnostics.ts +222 -0
  47. package/src/core/hollowResponseFormat.test.ts +58 -0
  48. package/src/core/hollowResponseFormat.ts +39 -0
  49. package/src/core/inputDebug.ts +51 -0
  50. package/src/core/launchContext.test.ts +157 -0
  51. package/src/core/launchContext.ts +266 -0
  52. package/src/core/models/codexCapabilities.test.ts +45 -0
  53. package/src/core/models/codexCapabilities.ts +95 -0
  54. package/src/core/models/codexModelCapabilities.test.ts +246 -0
  55. package/src/core/models/codexModelCapabilities.ts +571 -0
  56. package/src/core/models/modelSpecs.test.ts +283 -0
  57. package/src/core/models/modelSpecs.ts +300 -0
  58. package/src/core/perf/profiler.ts +125 -0
  59. package/src/core/perf/renderDebug.test.ts +230 -0
  60. package/src/core/perf/renderDebug.ts +373 -0
  61. package/src/core/planStorage.test.ts +143 -0
  62. package/src/core/planStorage.ts +141 -0
  63. package/src/core/process/CommandRunner.test.ts +105 -0
  64. package/src/core/process/CommandRunner.ts +269 -0
  65. package/src/core/process/processValidation.ts +101 -0
  66. package/src/core/projectInstructions.test.ts +50 -0
  67. package/src/core/projectInstructions.ts +54 -0
  68. package/src/core/providerLauncher/launcher.test.ts +238 -0
  69. package/src/core/providerLauncher/launcher.ts +203 -0
  70. package/src/core/providerLauncher/registry.test.ts +324 -0
  71. package/src/core/providerLauncher/registry.ts +253 -0
  72. package/src/core/providerLauncher/types.ts +84 -0
  73. package/src/core/providerLauncher/workspaceConfig.test.ts +638 -0
  74. package/src/core/providerLauncher/workspaceConfig.ts +407 -0
  75. package/src/core/providerRuntime/anthropic.test.ts +1120 -0
  76. package/src/core/providerRuntime/anthropic.ts +576 -0
  77. package/src/core/providerRuntime/capabilityProfile.test.ts +311 -0
  78. package/src/core/providerRuntime/capabilityProfile.ts +288 -0
  79. package/src/core/providerRuntime/claudeCodeDiscovery.ts +446 -0
  80. package/src/core/providerRuntime/contextMetadata.test.ts +468 -0
  81. package/src/core/providerRuntime/contextMetadata.ts +409 -0
  82. package/src/core/providerRuntime/gemini.test.ts +437 -0
  83. package/src/core/providerRuntime/gemini.ts +784 -0
  84. package/src/core/providerRuntime/lmstudio.test.ts +168 -0
  85. package/src/core/providerRuntime/lmstudio.ts +118 -0
  86. package/src/core/providerRuntime/local.test.ts +787 -0
  87. package/src/core/providerRuntime/local.ts +754 -0
  88. package/src/core/providerRuntime/models.ts +150 -0
  89. package/src/core/providerRuntime/reasoning.ts +17 -0
  90. package/src/core/providerRuntime/registry.test.ts +233 -0
  91. package/src/core/providerRuntime/registry.ts +203 -0
  92. package/src/core/providerRuntime/types.ts +103 -0
  93. package/src/core/providers/codexJsonStream.test.ts +148 -0
  94. package/src/core/providers/codexJsonStream.ts +305 -0
  95. package/src/core/providers/codexSubprocess.test.ts +68 -0
  96. package/src/core/providers/codexSubprocess.ts +372 -0
  97. package/src/core/providers/codexTranscript.test.ts +284 -0
  98. package/src/core/providers/codexTranscript.ts +695 -0
  99. package/src/core/providers/openaiNative.ts +13 -0
  100. package/src/core/providers/registry.ts +21 -0
  101. package/src/core/providers/types.ts +59 -0
  102. package/src/core/terminal/terminalCapabilities.test.ts +93 -0
  103. package/src/core/terminal/terminalCapabilities.ts +100 -0
  104. package/src/core/terminal/terminalControl.test.ts +75 -0
  105. package/src/core/terminal/terminalControl.ts +147 -0
  106. package/src/core/terminal/terminalSanitize.test.ts +22 -0
  107. package/src/core/terminal/terminalSanitize.ts +147 -0
  108. package/src/core/terminal/terminalSelection.test.ts +42 -0
  109. package/src/core/terminal/terminalSelection.ts +66 -0
  110. package/src/core/terminal/terminalTitle.test.ts +328 -0
  111. package/src/core/terminal/terminalTitle.ts +483 -0
  112. package/src/core/workspaceActivity.test.ts +163 -0
  113. package/src/core/workspaceActivity.ts +380 -0
  114. package/src/core/workspaceGuard.test.ts +151 -0
  115. package/src/core/workspaceGuard.ts +288 -0
  116. package/src/core/workspaceRoot.test.ts +23 -0
  117. package/src/core/workspaceRoot.ts +47 -0
  118. package/src/exec.test.ts +13 -0
  119. package/src/exec.ts +72 -0
  120. package/src/headless/execArgs.test.ts +147 -0
  121. package/src/headless/execArgs.ts +294 -0
  122. package/src/headless/execRunner.test.ts +434 -0
  123. package/src/headless/execRunner.ts +304 -0
  124. package/src/index.test.tsx +618 -0
  125. package/src/index.tsx +296 -0
  126. package/src/session/appSession.test.ts +897 -0
  127. package/src/session/appSession.ts +761 -0
  128. package/src/session/chatLifecycle.test.ts +64 -0
  129. package/src/session/chatLifecycle.ts +951 -0
  130. package/src/session/liveRenderScheduler.test.ts +201 -0
  131. package/src/session/liveRenderScheduler.ts +214 -0
  132. package/src/session/planFlow.test.ts +103 -0
  133. package/src/session/planFlow.ts +149 -0
  134. package/src/session/planTranscript.test.ts +65 -0
  135. package/src/session/planTranscript.ts +15 -0
  136. package/src/session/promptRunSchedule.test.ts +36 -0
  137. package/src/session/promptRunSchedule.ts +26 -0
  138. package/src/session/types.ts +228 -0
  139. package/src/test/runtimeTestUtils.ts +14 -0
  140. package/src/types/react-dom.d.ts +3 -0
  141. package/src/ui/ActionRequiredBlock.tsx +38 -0
  142. package/src/ui/ActivityBars.tsx +68 -0
  143. package/src/ui/ActivityIndicator.test.tsx +58 -0
  144. package/src/ui/ActivityIndicator.tsx +58 -0
  145. package/src/ui/AgentBlock.test.ts +6 -0
  146. package/src/ui/AgentBlock.tsx +130 -0
  147. package/src/ui/AnimatedStatusText.test.ts +16 -0
  148. package/src/ui/AnimatedStatusText.tsx +69 -0
  149. package/src/ui/AppShell.test.tsx +1739 -0
  150. package/src/ui/AppShell.tsx +698 -0
  151. package/src/ui/AttachmentImportPanel.test.tsx +204 -0
  152. package/src/ui/AttachmentImportPanel.tsx +98 -0
  153. package/src/ui/AuthPanel.tsx +113 -0
  154. package/src/ui/BackendPicker.tsx +28 -0
  155. package/src/ui/BottomComposer.test.ts +674 -0
  156. package/src/ui/BottomComposer.tsx +1028 -0
  157. package/src/ui/CodexLogo.tsx +55 -0
  158. package/src/ui/DashCard.tsx +82 -0
  159. package/src/ui/Markdown.test.ts +157 -0
  160. package/src/ui/Markdown.tsx +310 -0
  161. package/src/ui/ModePicker.tsx +27 -0
  162. package/src/ui/ModelPicker.tsx +31 -0
  163. package/src/ui/ModelPickerProviderScope.test.tsx +411 -0
  164. package/src/ui/ModelPickerScreen.test.tsx +99 -0
  165. package/src/ui/ModelPickerScreen.tsx +416 -0
  166. package/src/ui/ModelPickerState.test.tsx +151 -0
  167. package/src/ui/ModelReasoningPicker.test.tsx +447 -0
  168. package/src/ui/ModelReasoningPicker.tsx +458 -0
  169. package/src/ui/Panel.tsx +51 -0
  170. package/src/ui/PermissionsPanel.tsx +78 -0
  171. package/src/ui/PlanActionPicker.tsx +119 -0
  172. package/src/ui/PlanReviewPanel.test.tsx +267 -0
  173. package/src/ui/PlanReviewPanel.tsx +212 -0
  174. package/src/ui/PromptCardBorder.test.tsx +161 -0
  175. package/src/ui/ProviderPicker.test.tsx +289 -0
  176. package/src/ui/ProviderPicker.tsx +321 -0
  177. package/src/ui/ProviderShortcut.test.tsx +143 -0
  178. package/src/ui/ReasoningPicker.tsx +46 -0
  179. package/src/ui/RunFooter.tsx +65 -0
  180. package/src/ui/SelectionPanel.tsx +67 -0
  181. package/src/ui/SettingsPanel.test.tsx +233 -0
  182. package/src/ui/SettingsPanel.tsx +156 -0
  183. package/src/ui/Spinner.tsx +25 -0
  184. package/src/ui/StaticIntroItem.tsx +54 -0
  185. package/src/ui/StaticTranscriptItem.tsx +56 -0
  186. package/src/ui/TextEntryPanel.tsx +139 -0
  187. package/src/ui/ThemePicker.tsx +31 -0
  188. package/src/ui/ThinkingBlock.tsx +100 -0
  189. package/src/ui/Timeline.test.ts +2067 -0
  190. package/src/ui/Timeline.tsx +1472 -0
  191. package/src/ui/TimelineNavigation.test.tsx +201 -0
  192. package/src/ui/TopHeader.test.tsx +239 -0
  193. package/src/ui/TopHeader.tsx +257 -0
  194. package/src/ui/TurnGroup.test.tsx +365 -0
  195. package/src/ui/TurnGroup.tsx +657 -0
  196. package/src/ui/busyStatusAnimation.test.ts +30 -0
  197. package/src/ui/busyStatusAnimation.ts +11 -0
  198. package/src/ui/commandNormalize.test.ts +142 -0
  199. package/src/ui/commandNormalize.ts +66 -0
  200. package/src/ui/diffRenderer.test.ts +102 -0
  201. package/src/ui/diffRenderer.ts +116 -0
  202. package/src/ui/focus.ts +61 -0
  203. package/src/ui/focusFlow.test.tsx +1098 -0
  204. package/src/ui/inputBuffer.test.ts +151 -0
  205. package/src/ui/inputBuffer.ts +203 -0
  206. package/src/ui/layout.test.ts +145 -0
  207. package/src/ui/layout.ts +287 -0
  208. package/src/ui/modeDisplay.test.ts +42 -0
  209. package/src/ui/modeDisplay.ts +52 -0
  210. package/src/ui/outputPipeline.ts +64 -0
  211. package/src/ui/progressEntries.ts +156 -0
  212. package/src/ui/runActivityView.test.ts +89 -0
  213. package/src/ui/runActivityView.ts +37 -0
  214. package/src/ui/runLifecycleView.test.tsx +237 -0
  215. package/src/ui/slashCommands.ts +41 -0
  216. package/src/ui/statusRenderIsolation.test.tsx +654 -0
  217. package/src/ui/terminalAnswerFormat.test.ts +19 -0
  218. package/src/ui/terminalAnswerFormat.ts +128 -0
  219. package/src/ui/textLayout.test.ts +18 -0
  220. package/src/ui/textLayout.ts +338 -0
  221. package/src/ui/theme.tsx +395 -0
  222. package/src/ui/themeFlow.test.ts +53 -0
  223. package/src/ui/themeFlow.ts +41 -0
  224. package/src/ui/timelineMeasure.ts +3088 -0
  225. package/src/ui/timelineMeasureCache.test.ts +986 -0
  226. package/src/ui/useThrottledValue.ts +31 -0
@@ -0,0 +1,571 @@
1
+ import { type ChildProcess } from "child_process";
2
+ import { APP_NAME, APP_VERSION, DEFAULT_MODEL, LEGACY_FALLBACK_MODELS, formatReasoningLabel } from "../../config/settings.js";
3
+ import { resolveCodexExecutable, spawnCodexProcess } from "../executables/codexExecutable.js";
4
+
5
+ export type ModelCapabilitySource = "runtime" | "fallback";
6
+ export type ModelCapabilityStatus = "ready" | "fallback";
7
+
8
+ export interface ReasoningEffortCapability {
9
+ id: string;
10
+ label: string;
11
+ description: string | null;
12
+ }
13
+
14
+ export interface CodexModelCapability {
15
+ id: string;
16
+ model: string;
17
+ label: string;
18
+ description: string | null;
19
+ available: boolean;
20
+ hidden: boolean;
21
+ isDefault: boolean;
22
+ defaultReasoningLevel: string | null;
23
+ supportedReasoningLevels: readonly ReasoningEffortCapability[] | null;
24
+ reasoningLevelCount: number | null;
25
+ source: ModelCapabilitySource;
26
+ raw: unknown;
27
+ }
28
+
29
+ export interface CodexModelCapabilities {
30
+ status: ModelCapabilityStatus;
31
+ source: ModelCapabilitySource;
32
+ models: readonly CodexModelCapability[];
33
+ discoveredAt: number;
34
+ executable: string | null;
35
+ error: string | null;
36
+ }
37
+
38
+ export interface DiscoverCodexModelCapabilitiesOptions {
39
+ executable?: string;
40
+ includeHidden?: boolean;
41
+ timeoutMs?: number;
42
+ now?: () => number;
43
+ }
44
+
45
+ export interface GetCodexModelCapabilitiesOptions extends DiscoverCodexModelCapabilitiesOptions {
46
+ forceRefresh?: boolean;
47
+ ttlMs?: number;
48
+ resolveExecutable?: typeof resolveCodexExecutable;
49
+ discover?: typeof discoverCodexModelCapabilities;
50
+ }
51
+
52
+ interface JsonRpcResponse {
53
+ id?: string | number | null;
54
+ result?: unknown;
55
+ error?: unknown;
56
+ }
57
+
58
+ interface ModelListResponse {
59
+ data?: unknown;
60
+ nextCursor?: unknown;
61
+ }
62
+
63
+ interface AppServerRequest {
64
+ id: number;
65
+ method: string;
66
+ params: Record<string, unknown>;
67
+ }
68
+
69
+ interface CapabilityCacheEntry {
70
+ expiresAt: number;
71
+ promise: Promise<CodexModelCapabilities>;
72
+ }
73
+
74
+ const DEFAULT_DISCOVERY_TIMEOUT_MS = 12000;
75
+ const DEFAULT_CACHE_TTL_MS = 5 * 60 * 1000;
76
+ const MODEL_LIST_LIMIT = 100;
77
+
78
+ const capabilityCache = new Map<string, CapabilityCacheEntry>();
79
+
80
+ function isRecord(value: unknown): value is Record<string, unknown> {
81
+ return typeof value === "object" && value !== null && !Array.isArray(value);
82
+ }
83
+
84
+ function normalizeString(value: unknown): string | null {
85
+ return typeof value === "string" && value.trim().length > 0 ? value.trim() : null;
86
+ }
87
+
88
+ function normalizeBoolean(value: unknown, fallback = false): boolean {
89
+ return typeof value === "boolean" ? value : fallback;
90
+ }
91
+
92
+ function getErrorMessage(error: unknown): string {
93
+ if (error instanceof Error) {
94
+ return error.message;
95
+ }
96
+
97
+ if (typeof error === "string") {
98
+ return error;
99
+ }
100
+
101
+ try {
102
+ return JSON.stringify(error);
103
+ } catch {
104
+ return "Unknown model capability discovery error";
105
+ }
106
+ }
107
+
108
+ function createReasoningEffortCapability(raw: unknown): ReasoningEffortCapability | null {
109
+ if (typeof raw === "string") {
110
+ const id = raw.trim();
111
+ return id ? { id, label: formatReasoningLabel(id), description: null } : null;
112
+ }
113
+
114
+ if (!isRecord(raw)) {
115
+ return null;
116
+ }
117
+
118
+ const id = normalizeString(raw.reasoningEffort ?? raw.reasoning_effort ?? raw.id);
119
+ if (!id) {
120
+ return null;
121
+ }
122
+
123
+ return {
124
+ id,
125
+ label: formatReasoningLabel(id),
126
+ description: normalizeString(raw.description),
127
+ };
128
+ }
129
+
130
+ function normalizeRuntimeModel(raw: unknown): CodexModelCapability | null {
131
+ if (!isRecord(raw)) {
132
+ return null;
133
+ }
134
+
135
+ // Accept both "model" and "id" fields — Codex runtime has used both across versions.
136
+ const model = normalizeString(raw.model ?? raw.id);
137
+ const id = normalizeString(raw.id ?? raw.model) ?? model;
138
+ if (!model || !id) {
139
+ return null;
140
+ }
141
+
142
+ // Accept both camelCase and snake_case reasoning effort fields.
143
+ const rawReasoning = Array.isArray(raw.supportedReasoningEfforts)
144
+ ? raw.supportedReasoningEfforts
145
+ : Array.isArray(raw.supported_reasoning_efforts)
146
+ ? raw.supported_reasoning_efforts
147
+ : null;
148
+ const supportedReasoningLevels = rawReasoning
149
+ ? rawReasoning.map(createReasoningEffortCapability).filter((item): item is ReasoningEffortCapability => Boolean(item))
150
+ : null;
151
+ const defaultReasoningLevel = normalizeString(raw.defaultReasoningEffort ?? raw.default_reasoning_effort);
152
+
153
+ return {
154
+ id,
155
+ model,
156
+ label: normalizeString(raw.displayName ?? raw.display_name) ?? model,
157
+ description: normalizeString(raw.description),
158
+ available: true,
159
+ hidden: normalizeBoolean(raw.hidden),
160
+ isDefault: normalizeBoolean(raw.isDefault ?? raw.is_default),
161
+ defaultReasoningLevel,
162
+ supportedReasoningLevels,
163
+ reasoningLevelCount: supportedReasoningLevels ? supportedReasoningLevels.length : null,
164
+ source: "runtime",
165
+ raw,
166
+ };
167
+ }
168
+
169
+ function dedupeModels(models: CodexModelCapability[]): CodexModelCapability[] {
170
+ const seen = new Set<string>();
171
+ const deduped: CodexModelCapability[] = [];
172
+
173
+ for (const model of models) {
174
+ const key = model.model.toLowerCase();
175
+ if (seen.has(key)) {
176
+ continue;
177
+ }
178
+ seen.add(key);
179
+ deduped.push(model);
180
+ }
181
+
182
+ return deduped;
183
+ }
184
+
185
+ export function normalizeCodexModelListResponses(
186
+ responses: readonly ModelListResponse[],
187
+ options: { discoveredAt?: number; executable?: string | null } = {},
188
+ ): CodexModelCapabilities {
189
+ const models = dedupeModels(
190
+ responses.flatMap((response) => {
191
+ const rawModels = Array.isArray(response.data) ? response.data : [];
192
+ return rawModels
193
+ .map(normalizeRuntimeModel)
194
+ .filter((item): item is CodexModelCapability => Boolean(item));
195
+ }),
196
+ );
197
+
198
+ if (models.length === 0) {
199
+ throw new Error("Codex model discovery returned no usable models.");
200
+ }
201
+
202
+ return {
203
+ status: "ready",
204
+ source: "runtime",
205
+ models,
206
+ discoveredAt: options.discoveredAt ?? Date.now(),
207
+ executable: options.executable ?? null,
208
+ error: null,
209
+ };
210
+ }
211
+
212
+ export function createFallbackModelCapabilities(
213
+ error: unknown = null,
214
+ options: { discoveredAt?: number; executable?: string | null } = {},
215
+ ): CodexModelCapabilities {
216
+ return {
217
+ status: "fallback",
218
+ source: "fallback",
219
+ models: LEGACY_FALLBACK_MODELS.map((model) => ({
220
+ id: model,
221
+ model,
222
+ label: model,
223
+ description: null,
224
+ available: false,
225
+ hidden: false,
226
+ isDefault: model === DEFAULT_MODEL,
227
+ defaultReasoningLevel: null,
228
+ supportedReasoningLevels: null,
229
+ reasoningLevelCount: null,
230
+ source: "fallback",
231
+ raw: null,
232
+ })),
233
+ discoveredAt: options.discoveredAt ?? Date.now(),
234
+ executable: options.executable ?? null,
235
+ error: error ? getErrorMessage(error) : null,
236
+ };
237
+ }
238
+
239
+ function writeJsonLine(proc: ChildProcess, message: AppServerRequest): void {
240
+ proc.stdin?.write(`${JSON.stringify(message)}\n`);
241
+ }
242
+
243
+ function asModelListResponse(value: unknown): ModelListResponse {
244
+ if (!isRecord(value)) {
245
+ throw new Error("Codex model/list returned a non-object result.");
246
+ }
247
+
248
+ return {
249
+ data: value.data,
250
+ nextCursor: value.nextCursor ?? value.next_cursor, // accept both field name styles
251
+ };
252
+ }
253
+
254
+ async function requestModelListFromAppServer(
255
+ executable: string,
256
+ options: Required<Pick<DiscoverCodexModelCapabilitiesOptions, "includeHidden" | "timeoutMs">>,
257
+ ): Promise<ModelListResponse[]> {
258
+ return new Promise<ModelListResponse[]>((resolve, reject) => {
259
+ let proc: ReturnType<typeof spawnCodexProcess>;
260
+ try {
261
+ proc = spawnCodexProcess(executable, ["app-server", "--listen", "stdio://"], {
262
+ stdio: ["pipe", "pipe", "pipe"],
263
+ });
264
+ } catch (error) {
265
+ reject(error);
266
+ return;
267
+ }
268
+
269
+ let stdoutBuffer = "";
270
+ let stderr = "";
271
+ let settled = false;
272
+ let nextRequestId = 1;
273
+ let activeModelListRequestId: number | null = null;
274
+ const responses: ModelListResponse[] = [];
275
+
276
+ const cleanup = () => {
277
+ clearTimeout(timer);
278
+ proc.stdout?.removeAllListeners();
279
+ proc.stderr?.removeAllListeners();
280
+ proc.removeAllListeners();
281
+ if (!proc.killed) {
282
+ try {
283
+ proc.kill();
284
+ } catch {
285
+ // Best-effort shutdown.
286
+ }
287
+ }
288
+ };
289
+
290
+ const finish = (callback: () => void) => {
291
+ if (settled) {
292
+ return;
293
+ }
294
+ settled = true;
295
+ cleanup();
296
+ callback();
297
+ };
298
+
299
+ const fail = (error: unknown) => {
300
+ finish(() => reject(error));
301
+ };
302
+
303
+ const requestModels = (cursor: string | null = null) => {
304
+ const id = ++nextRequestId;
305
+ activeModelListRequestId = id;
306
+ writeJsonLine(proc, {
307
+ id,
308
+ method: "model/list",
309
+ params: {
310
+ includeHidden: options.includeHidden,
311
+ limit: MODEL_LIST_LIMIT,
312
+ cursor,
313
+ },
314
+ });
315
+ };
316
+
317
+ const handleResponse = (message: JsonRpcResponse) => {
318
+ if (message.error) {
319
+ fail(new Error(`Codex app-server request failed: ${getErrorMessage(message.error)}`));
320
+ return;
321
+ }
322
+
323
+ if (message.id === 1) {
324
+ requestModels();
325
+ return;
326
+ }
327
+
328
+ if (message.id === activeModelListRequestId) {
329
+ let response: ModelListResponse;
330
+ try {
331
+ response = asModelListResponse(message.result);
332
+ } catch (error) {
333
+ fail(error);
334
+ return;
335
+ }
336
+
337
+ responses.push(response);
338
+ const nextCursor = normalizeString(response.nextCursor);
339
+ if (nextCursor) {
340
+ requestModels(nextCursor);
341
+ return;
342
+ }
343
+
344
+ finish(() => resolve(responses));
345
+ }
346
+ };
347
+
348
+ const processStdout = (flush = false) => {
349
+ while (true) {
350
+ const newlineIndex = stdoutBuffer.indexOf("\n");
351
+ if (newlineIndex < 0) {
352
+ if (flush && stdoutBuffer.trim()) {
353
+ const line = stdoutBuffer;
354
+ stdoutBuffer = "";
355
+ parseLine(line);
356
+ }
357
+ return;
358
+ }
359
+
360
+ const line = stdoutBuffer.slice(0, newlineIndex).replace(/\r$/, "");
361
+ stdoutBuffer = stdoutBuffer.slice(newlineIndex + 1);
362
+ parseLine(line);
363
+ }
364
+ };
365
+
366
+ const parseLine = (line: string) => {
367
+ if (!line.trim()) {
368
+ return;
369
+ }
370
+
371
+ try {
372
+ handleResponse(JSON.parse(line) as JsonRpcResponse);
373
+ } catch (error) {
374
+ fail(new Error(`Unable to parse Codex app-server response: ${getErrorMessage(error)}`));
375
+ }
376
+ };
377
+
378
+ const timer = setTimeout(() => {
379
+ fail(new Error(`Timed out waiting for Codex model discovery after ${options.timeoutMs}ms.`));
380
+ }, options.timeoutMs);
381
+
382
+ proc.stdout?.on("data", (chunk: Buffer) => {
383
+ if (settled) {
384
+ return;
385
+ }
386
+ stdoutBuffer += chunk.toString("utf8");
387
+ processStdout(false);
388
+ });
389
+
390
+ proc.stderr?.on("data", (chunk: Buffer) => {
391
+ stderr += chunk.toString("utf8");
392
+ });
393
+
394
+ proc.on("error", (error) => {
395
+ fail(error);
396
+ });
397
+
398
+ proc.on("close", (exitCode) => {
399
+ if (settled) {
400
+ return;
401
+ }
402
+ processStdout(true);
403
+ if (settled) {
404
+ return;
405
+ }
406
+ const stderrSummary = stderr.trim() ? ` stderr: ${stderr.trim().slice(0, 300)}` : "";
407
+ fail(new Error(`Codex app-server exited before model discovery completed (code ${exitCode}).${stderrSummary}`));
408
+ });
409
+
410
+ writeJsonLine(proc, {
411
+ id: 1,
412
+ method: "initialize",
413
+ params: {
414
+ clientInfo: {
415
+ name: APP_NAME.toLowerCase(),
416
+ title: APP_NAME,
417
+ version: APP_VERSION,
418
+ },
419
+ capabilities: {
420
+ experimentalApi: true,
421
+ },
422
+ },
423
+ });
424
+ });
425
+ }
426
+
427
+ export async function discoverCodexModelCapabilities(
428
+ options: DiscoverCodexModelCapabilitiesOptions = {},
429
+ ): Promise<CodexModelCapabilities> {
430
+ const executable = options.executable ?? await resolveCodexExecutable();
431
+ const includeHidden = options.includeHidden ?? false;
432
+ const timeoutMs = options.timeoutMs ?? DEFAULT_DISCOVERY_TIMEOUT_MS;
433
+ const discoveredAt = options.now?.() ?? Date.now();
434
+ const responses = await requestModelListFromAppServer(executable, { includeHidden, timeoutMs });
435
+
436
+ return normalizeCodexModelListResponses(responses, {
437
+ discoveredAt,
438
+ executable,
439
+ });
440
+ }
441
+
442
+ // Cache chain: TTL-based in-memory cache → live discovery → fallback model list on error.
443
+ // Failed discoveries are evicted from the cache so retries are possible.
444
+ export async function getCodexModelCapabilities(
445
+ options: GetCodexModelCapabilitiesOptions = {},
446
+ ): Promise<CodexModelCapabilities> {
447
+ const now = options.now?.() ?? Date.now();
448
+ const ttlMs = options.ttlMs ?? DEFAULT_CACHE_TTL_MS;
449
+ let executable: string | null = null;
450
+
451
+ try {
452
+ executable = options.executable ?? await (options.resolveExecutable ?? resolveCodexExecutable)();
453
+ const cacheKey = `${executable}|hidden:${options.includeHidden ?? false}`;
454
+ const cached = capabilityCache.get(cacheKey);
455
+ if (!options.forceRefresh && cached && cached.expiresAt > now) {
456
+ return cached.promise;
457
+ }
458
+
459
+ const discover = options.discover ?? discoverCodexModelCapabilities;
460
+ const promise = discover({
461
+ executable,
462
+ includeHidden: options.includeHidden,
463
+ timeoutMs: options.timeoutMs,
464
+ now: () => now,
465
+ }).catch((error) => createFallbackModelCapabilities(error, { discoveredAt: now, executable }));
466
+
467
+ capabilityCache.set(cacheKey, {
468
+ expiresAt: now + ttlMs,
469
+ promise,
470
+ });
471
+
472
+ const result = await promise;
473
+ if (result.status === "fallback") {
474
+ capabilityCache.delete(cacheKey);
475
+ }
476
+
477
+ return result;
478
+ } catch (error) {
479
+ return createFallbackModelCapabilities(error, { discoveredAt: now, executable });
480
+ }
481
+ }
482
+
483
+ export function clearCodexModelCapabilityCache(): void {
484
+ capabilityCache.clear();
485
+ }
486
+
487
+ export function getSelectableModelCapabilities(
488
+ capabilities: CodexModelCapabilities,
489
+ ): readonly CodexModelCapability[] {
490
+ return capabilities.models.filter((model) => !model.hidden);
491
+ }
492
+
493
+ export function findModelCapability(
494
+ capabilities: CodexModelCapabilities | null | undefined,
495
+ model: string,
496
+ ): CodexModelCapability | null {
497
+ if (!capabilities) {
498
+ return null;
499
+ }
500
+
501
+ const normalized = model.toLowerCase();
502
+ return capabilities.models.find((candidate) =>
503
+ candidate.model.toLowerCase() === normalized || candidate.id.toLowerCase() === normalized
504
+ ) ?? null;
505
+ }
506
+
507
+ export function isModelSelectable(
508
+ capabilities: CodexModelCapabilities | null | undefined,
509
+ model: string,
510
+ ): boolean {
511
+ const found = findModelCapability(capabilities, model);
512
+ return Boolean(found && !found.hidden);
513
+ }
514
+
515
+ export function getPreferredModelFromCapabilities(
516
+ capabilities: CodexModelCapabilities,
517
+ currentModel: string,
518
+ ): string {
519
+ if (isModelSelectable(capabilities, currentModel)) {
520
+ return currentModel;
521
+ }
522
+
523
+ const selectable = getSelectableModelCapabilities(capabilities);
524
+ return selectable.find((model) => model.isDefault)?.model
525
+ ?? selectable[0]?.model
526
+ ?? currentModel
527
+ ?? DEFAULT_MODEL;
528
+ }
529
+
530
+ export function normalizeReasoningForModelCapabilities(
531
+ model: string,
532
+ currentReasoning: string,
533
+ capabilities: CodexModelCapabilities | null | undefined,
534
+ ): string {
535
+ const capability = findModelCapability(capabilities, model);
536
+ const supported = capability?.supportedReasoningLevels;
537
+ if (!supported || supported.length === 0) {
538
+ return currentReasoning;
539
+ }
540
+
541
+ if (supported.some((item) => item.id === currentReasoning)) {
542
+ return currentReasoning;
543
+ }
544
+
545
+ if (capability.defaultReasoningLevel && supported.some((item) => item.id === capability.defaultReasoningLevel)) {
546
+ return capability.defaultReasoningLevel;
547
+ }
548
+
549
+ return supported[0]!.id;
550
+ }
551
+
552
+ export function formatModelCapabilitiesList(
553
+ capabilities: CodexModelCapabilities,
554
+ currentModel: string,
555
+ ): string {
556
+ const list = getSelectableModelCapabilities(capabilities)
557
+ .map((model, index) => {
558
+ const active = model.model === currentModel || model.id === currentModel ? " *" : "";
559
+ const reasoning = model.reasoningLevelCount === null
560
+ ? "reasoning metadata unknown"
561
+ : `${model.reasoningLevelCount} reasoning ${model.reasoningLevelCount === 1 ? "level" : "levels"}`;
562
+ return ` ${index + 1}. ${model.label} (${model.model}) - ${reasoning}${active}`;
563
+ })
564
+ .join("\n");
565
+
566
+ const source = capabilities.status === "ready"
567
+ ? "Detected from Codex runtime."
568
+ : `Fallback list; runtime discovery failed${capabilities.error ? `: ${capabilities.error}` : "."}`;
569
+
570
+ return `${source}\n${list || " - none"}`;
571
+ }