ubume 0.1.0

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 (182) hide show
  1. package/README.md +88 -0
  2. package/bin/codexa-local-harness-bridge.js +3 -0
  3. package/bin/codexa.js +28 -0
  4. package/bin/ubume-local-harness-bridge.js +325 -0
  5. package/bin/ubume.js +398 -0
  6. package/package.json +66 -0
  7. package/src/app.tsx +5759 -0
  8. package/src/commands/handler.ts +889 -0
  9. package/src/config/appVersion.ts +69 -0
  10. package/src/config/buildInfo.ts +3 -0
  11. package/src/config/launchArgs.ts +196 -0
  12. package/src/config/layeredConfig.ts +853 -0
  13. package/src/config/legacyEnv.ts +16 -0
  14. package/src/config/persistence.ts +377 -0
  15. package/src/config/runtimeConfig.ts +558 -0
  16. package/src/config/settings.ts +405 -0
  17. package/src/config/toml-serialize.ts +109 -0
  18. package/src/config/trustStore.ts +84 -0
  19. package/src/config/updateCheckCache.ts +85 -0
  20. package/src/core/README.md +55 -0
  21. package/src/core/agent/loop.ts +464 -0
  22. package/src/core/agent/protocol.ts +345 -0
  23. package/src/core/agent/tools.ts +423 -0
  24. package/src/core/auth/codexAuth.ts +359 -0
  25. package/src/core/codex/codexExecArgs.ts +166 -0
  26. package/src/core/codex/codexLaunch.ts +163 -0
  27. package/src/core/codex/codexPrompt.ts +429 -0
  28. package/src/core/debug/inputDebug.ts +51 -0
  29. package/src/core/debug/localStreamDebug.ts +50 -0
  30. package/src/core/debug/modelStateDebug.ts +35 -0
  31. package/src/core/executables/antigravityExecutable.ts +48 -0
  32. package/src/core/executables/claudeExecutable.ts +63 -0
  33. package/src/core/executables/codexExecutable.ts +160 -0
  34. package/src/core/executables/executableResolver.ts +164 -0
  35. package/src/core/executables/geminiExecutable.ts +78 -0
  36. package/src/core/models/codexCapabilities.ts +97 -0
  37. package/src/core/models/codexModelCapabilities.ts +624 -0
  38. package/src/core/models/codexModelsCacheSeed.ts +153 -0
  39. package/src/core/models/modelSpecs.ts +303 -0
  40. package/src/core/models/providerModelCache.ts +94 -0
  41. package/src/core/perf/profiler.ts +125 -0
  42. package/src/core/perf/renderDebug.ts +398 -0
  43. package/src/core/process/CommandRunner.ts +280 -0
  44. package/src/core/process/processValidation.ts +111 -0
  45. package/src/core/providerLauncher/launcher.ts +220 -0
  46. package/src/core/providerLauncher/registry.ts +354 -0
  47. package/src/core/providerLauncher/types.ts +95 -0
  48. package/src/core/providerLauncher/workspaceConfig.ts +487 -0
  49. package/src/core/providerRuntime/anthropic.ts +580 -0
  50. package/src/core/providerRuntime/antigravity.ts +500 -0
  51. package/src/core/providerRuntime/capabilityProfile.ts +383 -0
  52. package/src/core/providerRuntime/claudeCodeDiscovery.ts +724 -0
  53. package/src/core/providerRuntime/claudeCodeDiscoveryDebug.ts +55 -0
  54. package/src/core/providerRuntime/codexaCupy.ts +97 -0
  55. package/src/core/providerRuntime/codexaNative.ts +425 -0
  56. package/src/core/providerRuntime/contextMetadata.ts +397 -0
  57. package/src/core/providerRuntime/gemini.ts +789 -0
  58. package/src/core/providerRuntime/lmstudio.ts +118 -0
  59. package/src/core/providerRuntime/local.ts +770 -0
  60. package/src/core/providerRuntime/localHarness/runtime.ts +1090 -0
  61. package/src/core/providerRuntime/localOutputBudget.ts +17 -0
  62. package/src/core/providerRuntime/mistralVibe.ts +667 -0
  63. package/src/core/providerRuntime/models.ts +175 -0
  64. package/src/core/providerRuntime/reasoning.ts +20 -0
  65. package/src/core/providerRuntime/registry.ts +286 -0
  66. package/src/core/providerRuntime/types.ts +142 -0
  67. package/src/core/providerRuntime/unsloth.ts +216 -0
  68. package/src/core/providers/codexJsonStream.ts +305 -0
  69. package/src/core/providers/codexSubprocess.ts +378 -0
  70. package/src/core/providers/codexTranscript.ts +695 -0
  71. package/src/core/providers/openaiNative.ts +13 -0
  72. package/src/core/providers/registry.ts +21 -0
  73. package/src/core/providers/types.ts +91 -0
  74. package/src/core/shared/attachments.ts +101 -0
  75. package/src/core/shared/cleanupFastFail.ts +67 -0
  76. package/src/core/shared/clipboard.ts +24 -0
  77. package/src/core/shared/clipboardImage.ts +111 -0
  78. package/src/core/shared/githubDiagnostics.ts +222 -0
  79. package/src/core/shared/hollowResponseFormat.ts +39 -0
  80. package/src/core/terminal/clearFrameBoundary.ts +852 -0
  81. package/src/core/terminal/frameLock.ts +110 -0
  82. package/src/core/terminal/inkRenderReset.ts +123 -0
  83. package/src/core/terminal/startupClear.ts +20 -0
  84. package/src/core/terminal/terminalCapabilities.ts +100 -0
  85. package/src/core/terminal/terminalControl.ts +169 -0
  86. package/src/core/terminal/terminalSanitize.ts +147 -0
  87. package/src/core/terminal/terminalTitle.ts +400 -0
  88. package/src/core/version/channel.ts +27 -0
  89. package/src/core/version/packageManager.ts +119 -0
  90. package/src/core/version/updateCheck.ts +203 -0
  91. package/src/core/workspace/appData.ts +107 -0
  92. package/src/core/workspace/conversationStore.ts +335 -0
  93. package/src/core/workspace/launchContext.ts +259 -0
  94. package/src/core/workspace/planStorage.ts +135 -0
  95. package/src/core/workspace/projectInstructions.ts +54 -0
  96. package/src/core/workspace/scratchDir.ts +64 -0
  97. package/src/core/workspace/workspaceActivity.ts +384 -0
  98. package/src/core/workspace/workspaceGuard.ts +377 -0
  99. package/src/core/workspace/workspaceRoot.ts +47 -0
  100. package/src/exec.ts +73 -0
  101. package/src/headless/execArgs.ts +296 -0
  102. package/src/headless/execRunner.ts +304 -0
  103. package/src/index.tsx +270 -0
  104. package/src/legacyEnvBootstrap.ts +5 -0
  105. package/src/session/appSession.ts +771 -0
  106. package/src/session/chatLifecycle.ts +994 -0
  107. package/src/session/conversation.ts +107 -0
  108. package/src/session/liveRenderScheduler.ts +214 -0
  109. package/src/session/persistedResponse.ts +93 -0
  110. package/src/session/planFlow.ts +159 -0
  111. package/src/session/planTranscript.ts +19 -0
  112. package/src/session/promptRunSchedule.ts +26 -0
  113. package/src/session/types.ts +234 -0
  114. package/src/test/runtimeTestUtils.ts +14 -0
  115. package/src/types/react-dom.d.ts +3 -0
  116. package/src/ui/chrome/ActivityBars.tsx +68 -0
  117. package/src/ui/chrome/ActivityIndicator.tsx +58 -0
  118. package/src/ui/chrome/AnimatedStatusText.tsx +69 -0
  119. package/src/ui/chrome/AppShell.tsx +474 -0
  120. package/src/ui/chrome/BottomComposer.tsx +1135 -0
  121. package/src/ui/chrome/DashCard.tsx +82 -0
  122. package/src/ui/chrome/RunFooter.tsx +65 -0
  123. package/src/ui/chrome/RuntimeStatusBar.tsx +108 -0
  124. package/src/ui/chrome/Spinner.tsx +25 -0
  125. package/src/ui/chrome/TopHeader.tsx +439 -0
  126. package/src/ui/chrome/UpdateAvailableCard.tsx +42 -0
  127. package/src/ui/chrome/busyStatusAnimation.ts +11 -0
  128. package/src/ui/input/commandNormalize.ts +66 -0
  129. package/src/ui/input/focus.ts +73 -0
  130. package/src/ui/input/imageAttachments.ts +18 -0
  131. package/src/ui/input/inputBuffer.ts +203 -0
  132. package/src/ui/input/pastedContent.ts +75 -0
  133. package/src/ui/input/rawArrowKeys.ts +28 -0
  134. package/src/ui/input/slashCommands.ts +43 -0
  135. package/src/ui/input/useStdinRawModeLease.ts +23 -0
  136. package/src/ui/layout.ts +560 -0
  137. package/src/ui/panels/AttachmentImportPanel.tsx +131 -0
  138. package/src/ui/panels/AuthPanel.tsx +149 -0
  139. package/src/ui/panels/BackendPicker.tsx +28 -0
  140. package/src/ui/panels/ModePicker.tsx +31 -0
  141. package/src/ui/panels/ModelPicker.tsx +31 -0
  142. package/src/ui/panels/ModelPickerScreen.tsx +761 -0
  143. package/src/ui/panels/ModelReasoningPicker.tsx +458 -0
  144. package/src/ui/panels/Panel.tsx +51 -0
  145. package/src/ui/panels/PermissionsPanel.tsx +78 -0
  146. package/src/ui/panels/PlanActionPicker.tsx +187 -0
  147. package/src/ui/panels/ProviderPicker.tsx +753 -0
  148. package/src/ui/panels/ProviderSetupPrompt.tsx +52 -0
  149. package/src/ui/panels/ReasoningPicker.tsx +46 -0
  150. package/src/ui/panels/ResumePicker.tsx +90 -0
  151. package/src/ui/panels/SelectionPanel.tsx +138 -0
  152. package/src/ui/panels/SettingsPanel.tsx +156 -0
  153. package/src/ui/panels/TextEntryPanel.tsx +139 -0
  154. package/src/ui/panels/ThemePicker.tsx +32 -0
  155. package/src/ui/panels/ToolApprovalPanel.tsx +46 -0
  156. package/src/ui/panels/UpdatePromptPanel.tsx +236 -0
  157. package/src/ui/panels/responsivePickerViewport.ts +64 -0
  158. package/src/ui/render/Markdown.tsx +331 -0
  159. package/src/ui/render/diffRenderer.ts +116 -0
  160. package/src/ui/render/logoVariants.ts +113 -0
  161. package/src/ui/render/modeDisplay.ts +52 -0
  162. package/src/ui/render/outputPipeline.ts +64 -0
  163. package/src/ui/render/runtimeDisplay.ts +128 -0
  164. package/src/ui/render/terminalAnswerFormat.ts +128 -0
  165. package/src/ui/render/textLayout.ts +392 -0
  166. package/src/ui/theme.tsx +274 -0
  167. package/src/ui/themeFlow.ts +41 -0
  168. package/src/ui/timeline/ActionRequiredBlock.tsx +38 -0
  169. package/src/ui/timeline/AgentBlock.tsx +130 -0
  170. package/src/ui/timeline/StaticIntroItem.tsx +54 -0
  171. package/src/ui/timeline/ThinkingBlock.tsx +100 -0
  172. package/src/ui/timeline/Timeline.tsx +1410 -0
  173. package/src/ui/timeline/TranscriptShell.tsx +302 -0
  174. package/src/ui/timeline/TurnGroup.tsx +673 -0
  175. package/src/ui/timeline/layoutListWindow.ts +145 -0
  176. package/src/ui/timeline/liveViewportWindow.ts +68 -0
  177. package/src/ui/timeline/progressEntries.ts +156 -0
  178. package/src/ui/timeline/runActivityView.ts +37 -0
  179. package/src/ui/timeline/staticTranscriptCache.ts +174 -0
  180. package/src/ui/timeline/streamCoalesce.ts +53 -0
  181. package/src/ui/timeline/timelineMeasure.ts +3273 -0
  182. package/src/ui/useThrottledValue.ts +31 -0
@@ -0,0 +1,359 @@
1
+ import { spawn } from "child_process";
2
+ import { formatCodexLaunchError, resolveCodexExecutable, spawnCodexProcess } from "../executables/codexExecutable.js";
3
+
4
+ export type CodexAuthState = "checking" | "authenticated" | "unauthenticated" | "unknown";
5
+
6
+ export interface CodexAuthProbeResult {
7
+ state: CodexAuthState;
8
+ checkedAt: number;
9
+ rawSummary: string;
10
+ recommendedAction: string;
11
+ }
12
+
13
+ interface CommandResult {
14
+ exitCode: number | null;
15
+ stdout: string;
16
+ stderr: string;
17
+ timedOut: boolean;
18
+ error?: NodeJS.ErrnoException;
19
+ }
20
+
21
+ const JSON_STATUS_FLAGS = ["authenticated", "is_authenticated", "logged_in", "signed_in"] as const;
22
+
23
+ const UNAUTHENTICATED_PATTERNS = [
24
+ "not logged in",
25
+ "not signed in",
26
+ "no active session",
27
+ "login required",
28
+ "sign in required",
29
+ "unauthenticated",
30
+ "authentication required",
31
+ "run `codex login`",
32
+ "run codex login",
33
+ "please login",
34
+ "please log in",
35
+ "session expired",
36
+ "token expired",
37
+ ] as const;
38
+
39
+ const AUTH_FAILURE_PATTERNS = [
40
+ ...UNAUTHENTICATED_PATTERNS,
41
+ "unauthorized",
42
+ "forbidden",
43
+ "invalid token",
44
+ "invalid grant",
45
+ "access denied",
46
+ "401",
47
+ "403",
48
+ ] as const;
49
+
50
+ export function getAuthStateLabel(state: CodexAuthState): string {
51
+ switch (state) {
52
+ case "authenticated":
53
+ return "Authenticated";
54
+ case "unauthenticated":
55
+ return "Signed out";
56
+ case "checking":
57
+ return "Checking";
58
+ default:
59
+ return "Unknown";
60
+ }
61
+ }
62
+
63
+ export function getLoginGuidance(): string {
64
+ return [
65
+ "Sign in to the Ubume neural network to continue.",
66
+ "Run this in your terminal:",
67
+ " codex login",
68
+ "",
69
+ "If you were previously using API-key auth and want ChatGPT subscription auth:",
70
+ " codex logout",
71
+ " codex",
72
+ ].join("\n");
73
+ }
74
+
75
+ export function getLogoutGuidance(): string {
76
+ return [
77
+ "You are managing your Ubume sign-out state.",
78
+ "Run this in your terminal:",
79
+ " codex logout",
80
+ "",
81
+ "After logging out, use /auth status in this UI to refresh state.",
82
+ ].join("\n");
83
+ }
84
+
85
+ export function getAuthStatusMessage(result: CodexAuthProbeResult): string {
86
+ if (result.state === "authenticated") {
87
+ return [
88
+ "Ubume authentication looks healthy.",
89
+ "State: Authenticated",
90
+ `Summary: ${result.rawSummary}`,
91
+ ].join("\n");
92
+ }
93
+
94
+ if (result.state === "unauthenticated") {
95
+ return [
96
+ "Ubume is currently signed out.",
97
+ "State: Signed out",
98
+ `Summary: ${result.rawSummary}`,
99
+ "Recovery:",
100
+ " codex login",
101
+ ].join("\n");
102
+ }
103
+
104
+ if (result.state === "checking") {
105
+ return "Authentication check is currently running.";
106
+ }
107
+
108
+ return [
109
+ "Ubume auth state is unknown. This can happen on unsupported neural versions.",
110
+ `Summary: ${result.rawSummary}`,
111
+ `Recommended action: ${result.recommendedAction}`,
112
+ ].join("\n");
113
+ }
114
+
115
+ export interface RunGateDecision {
116
+ allowRun: boolean;
117
+ blockMessage?: string;
118
+ warningMessage?: string;
119
+ }
120
+
121
+ export interface RunGateDecisionOptions {
122
+ warnOnUnknown?: boolean;
123
+ }
124
+
125
+ export function getRunGateDecision(
126
+ authState: CodexAuthState,
127
+ options: RunGateDecisionOptions = {},
128
+ ): RunGateDecision {
129
+ if (authState === "unauthenticated") {
130
+ return {
131
+ allowRun: false,
132
+ blockMessage: [
133
+ "Run blocked: Ubume is signed out.",
134
+ "Sign in first with your ChatGPT subscription:",
135
+ " codex login",
136
+ ].join("\n"),
137
+ };
138
+ }
139
+
140
+ if (authState === "unknown" || authState === "checking") {
141
+ if (options.warnOnUnknown === false) {
142
+ return { allowRun: true };
143
+ }
144
+
145
+ return {
146
+ allowRun: true,
147
+ warningMessage:
148
+ "Auth state is unknown. Run is allowed, but if this fails, use `codex login` and try again.",
149
+ };
150
+ }
151
+
152
+ return { allowRun: true };
153
+ }
154
+
155
+ export function inferAuthStateFromProbe(
156
+ exitCode: number | null,
157
+ stdout: string,
158
+ stderr: string,
159
+ ): CodexAuthState {
160
+ if (exitCode === 0) return "authenticated";
161
+
162
+ const jsonState = inferAuthStateFromJson(stdout);
163
+ if (jsonState) return jsonState;
164
+
165
+ const output = `${stdout}\n${stderr}`.toLowerCase();
166
+
167
+ if (UNAUTHENTICATED_PATTERNS.some((pattern) => output.includes(pattern))) {
168
+ return "unauthenticated";
169
+ }
170
+
171
+ return "unknown";
172
+ }
173
+
174
+ export function isLikelyAuthFailure(message: string): boolean {
175
+ const lower = message.toLowerCase();
176
+ return AUTH_FAILURE_PATTERNS.some((pattern) => lower.includes(pattern));
177
+ }
178
+
179
+ export async function probeCodexAuthStatus(): Promise<CodexAuthProbeResult> {
180
+ const attempts: string[][] = [
181
+ ["login", "status", "--json"],
182
+ ["login", "status"],
183
+ ];
184
+
185
+ const summaries: string[] = [];
186
+
187
+ for (let index = 0; index < attempts.length; index += 1) {
188
+ const args = attempts[index]!;
189
+ const result = await runCodexCommand(args);
190
+ const summary = summarizeAttempt(args, result);
191
+ summaries.push(summary);
192
+
193
+ if (result.error) {
194
+ if (isCodexUnavailableError(result.error)) {
195
+ return {
196
+ state: "unknown",
197
+ checkedAt: Date.now(),
198
+ rawSummary: summary,
199
+ recommendedAction:
200
+ "Set CODEX_EXECUTABLE to a working Codex command/path, restart Ubume, then run /auth status again.",
201
+ };
202
+ }
203
+
204
+ continue;
205
+ }
206
+
207
+ const inferred = inferAuthStateFromProbe(result.exitCode, result.stdout, result.stderr);
208
+
209
+ if (inferred === "authenticated" || inferred === "unauthenticated") {
210
+ return {
211
+ state: inferred,
212
+ checkedAt: Date.now(),
213
+ rawSummary: summary,
214
+ recommendedAction: inferred === "authenticated" ? "None" : "Run `codex login` and retry.",
215
+ };
216
+ }
217
+ }
218
+
219
+ return {
220
+ state: "unknown",
221
+ checkedAt: Date.now(),
222
+ rawSummary: summaries.join(" | "),
223
+ recommendedAction: "If runs fail, sign in with `codex login`, then retry `/auth status`.",
224
+ };
225
+ }
226
+
227
+ function inferAuthStateFromJson(raw: string): CodexAuthState | null {
228
+ const trimmed = raw.trim();
229
+ if (!trimmed.startsWith("{") || !trimmed.endsWith("}")) return null;
230
+
231
+ try {
232
+ const payload = JSON.parse(trimmed) as Record<string, unknown>;
233
+
234
+ for (const key of JSON_STATUS_FLAGS) {
235
+ const value = payload[key];
236
+ if (typeof value === "boolean") {
237
+ return value ? "authenticated" : "unauthenticated";
238
+ }
239
+ }
240
+
241
+ const status = payload.status;
242
+ if (typeof status === "string") {
243
+ const lower = status.toLowerCase();
244
+ if (lower.includes("auth")) return "authenticated";
245
+ if (lower.includes("unauth")) return "unauthenticated";
246
+ if (lower.includes("signed_out")) return "unauthenticated";
247
+ }
248
+ } catch {
249
+ return null;
250
+ }
251
+
252
+ return null;
253
+ }
254
+
255
+ function runCodexCommand(args: string[], timeoutMs = 6000): Promise<CommandResult> {
256
+ return new Promise<CommandResult>((resolve) => {
257
+ let done = false;
258
+ let proc: ReturnType<typeof spawn> | null = null;
259
+
260
+ let stdout = "";
261
+ let stderr = "";
262
+
263
+ const finish = (result: CommandResult) => {
264
+ if (done) return;
265
+ done = true;
266
+ resolve(result);
267
+ };
268
+
269
+ const startWithExecutable = async () => {
270
+ let executable: string;
271
+ try {
272
+ executable = await resolveCodexExecutable();
273
+ } catch (error) {
274
+ const errno = error as NodeJS.ErrnoException;
275
+ finish({
276
+ exitCode: null,
277
+ stdout,
278
+ stderr: formatCodexLaunchError(errno),
279
+ timedOut: false,
280
+ error: errno,
281
+ });
282
+ return;
283
+ }
284
+
285
+ if (done) return;
286
+ try {
287
+ proc = spawnCodexProcess(executable, args, { stdio: ["ignore", "pipe", "pipe"] });
288
+ } catch (error) {
289
+ const errno = error as NodeJS.ErrnoException;
290
+ clearTimeout(timer);
291
+ finish({
292
+ exitCode: null,
293
+ stdout,
294
+ stderr: formatCodexLaunchError(errno),
295
+ timedOut: false,
296
+ error: errno,
297
+ });
298
+ return;
299
+ }
300
+
301
+ proc.stdout?.on("data", (chunk: Buffer) => {
302
+ stdout += chunk.toString();
303
+ });
304
+
305
+ proc.stderr?.on("data", (chunk: Buffer) => {
306
+ stderr += chunk.toString();
307
+ });
308
+
309
+ proc.on("error", (error) => {
310
+ clearTimeout(timer);
311
+ const errno = error as NodeJS.ErrnoException;
312
+ finish({
313
+ exitCode: null,
314
+ stdout,
315
+ stderr: formatCodexLaunchError(errno),
316
+ timedOut: false,
317
+ error: errno,
318
+ });
319
+ });
320
+
321
+ proc.on("close", (exitCode) => {
322
+ clearTimeout(timer);
323
+ finish({
324
+ exitCode,
325
+ stdout,
326
+ stderr,
327
+ timedOut: false,
328
+ });
329
+ });
330
+ };
331
+
332
+ const timer = setTimeout(() => {
333
+ proc?.kill();
334
+ finish({
335
+ exitCode: null,
336
+ stdout,
337
+ stderr,
338
+ timedOut: true,
339
+ });
340
+ }, timeoutMs);
341
+
342
+ void startWithExecutable();
343
+ });
344
+ }
345
+
346
+ function summarizeAttempt(args: string[], result: CommandResult): string {
347
+ const status =
348
+ result.error?.code ??
349
+ (result.timedOut ? "timeout" : `exit:${result.exitCode ?? "null"}`);
350
+
351
+ const combined = [result.stdout.trim(), result.stderr.trim()].filter(Boolean).join(" | ");
352
+ const output = combined.length > 140 ? `${combined.slice(0, 137)}...` : combined || "no output";
353
+
354
+ return `[${args.join(" ")}] ${status} - ${output}`;
355
+ }
356
+
357
+ function isCodexUnavailableError(error: NodeJS.ErrnoException): boolean {
358
+ return error.code === "ENOENT" || error.code === "EACCES" || error.code === "EPERM";
359
+ }
@@ -0,0 +1,166 @@
1
+ import type { ResolvedRuntimeConfig } from "../../config/runtimeConfig.js";
2
+ import type { CodexCliCapabilities } from "../models/codexCapabilities.js";
3
+ import type { ProviderImageAttachment } from "../providerRuntime/types.js";
4
+
5
+ export interface BuildCodexExecArgsOptions {
6
+ runtime: ResolvedRuntimeConfig;
7
+ cwd: string;
8
+ structuredOutput?: boolean;
9
+ imageAttachments?: readonly ProviderImageAttachment[];
10
+ }
11
+
12
+ export type CodexLaunchStrategy =
13
+ | "direct-flags"
14
+ | "config-overrides"
15
+ | "full-auto"
16
+ | "fail";
17
+
18
+ export type BuildCodexExecArgsResult =
19
+ | { ok: true; args: string[]; strategy: Exclude<CodexLaunchStrategy, "fail"> }
20
+ | { ok: false; error: string; strategy: "fail" };
21
+
22
+ // Prevents newline/null injection into CLI args — rejects the path if it contains unsafe characters.
23
+ function sanitizeWorkingDirectory(cwd: string): string {
24
+ if (cwd.includes("\n") || cwd.includes("\r") || cwd.includes("\0")) {
25
+ return process.cwd();
26
+ }
27
+
28
+ return cwd;
29
+ }
30
+
31
+ function isFullAutoRuntime(runtime: ResolvedRuntimeConfig): boolean {
32
+ return runtime.policy.approvalPolicy === "never" && runtime.policy.sandboxMode === "danger-full-access";
33
+ }
34
+
35
+ function buildCapabilitySummary(capabilities: CodexCliCapabilities): string {
36
+ const supported: string[] = [];
37
+
38
+ if (capabilities.askForApproval) supported.push("--ask-for-approval");
39
+ if (capabilities.sandbox) supported.push("--sandbox");
40
+ if (capabilities.config) supported.push("--config/-c");
41
+ if (capabilities.fullAuto) supported.push("--full-auto");
42
+
43
+ return supported.length > 0 ? supported.join(", ") : "none";
44
+ }
45
+
46
+ function buildRuntimeFailureMessage(runtime: ResolvedRuntimeConfig, capabilities: CodexCliCapabilities): string {
47
+ return [
48
+ "Installed Codex CLI cannot safely apply the requested runtime configuration.",
49
+ `Requested approval policy: ${runtime.policy.approvalPolicy}.`,
50
+ `Requested sandbox mode: ${runtime.policy.sandboxMode}.`,
51
+ `Detected launch controls: ${buildCapabilitySummary(capabilities)}.`,
52
+ "Update Codex or choose a runtime configuration that your installed CLI can represent.",
53
+ ].join("\n");
54
+ }
55
+
56
+ function buildRuntimePolicyArgs(runtime: ResolvedRuntimeConfig, capabilities: CodexCliCapabilities): BuildCodexExecArgsResult {
57
+ const args: string[] = [];
58
+ const missingDirectApproval = !capabilities.askForApproval;
59
+ const missingDirectSandbox = !capabilities.sandbox;
60
+ const runtimePolicy = runtime.policy;
61
+
62
+ if (!missingDirectApproval && !missingDirectSandbox) {
63
+ args.push("--ask-for-approval", runtimePolicy.approvalPolicy);
64
+ args.push("--sandbox", runtimePolicy.sandboxMode);
65
+ return { ok: true, args, strategy: "direct-flags" };
66
+ }
67
+
68
+ if (isFullAutoRuntime(runtime) && capabilities.fullAuto) {
69
+ args.push("--full-auto");
70
+ return { ok: true, args, strategy: "full-auto" };
71
+ }
72
+
73
+ if (capabilities.config) {
74
+ if (capabilities.askForApproval) {
75
+ args.push("--ask-for-approval", runtimePolicy.approvalPolicy);
76
+ } else {
77
+ args.push("-c", `approval_policy=${runtimePolicy.approvalPolicy}`);
78
+ }
79
+
80
+ if (capabilities.sandbox) {
81
+ args.push("--sandbox", runtimePolicy.sandboxMode);
82
+ } else {
83
+ args.push("-c", `sandbox_mode=${runtimePolicy.sandboxMode}`);
84
+ }
85
+
86
+ return { ok: true, args, strategy: "config-overrides" };
87
+ }
88
+
89
+ return {
90
+ ok: false,
91
+ strategy: "fail",
92
+ error: buildRuntimeFailureMessage(runtime, capabilities),
93
+ };
94
+ }
95
+
96
+ export function buildCodexExecArgs(
97
+ options: BuildCodexExecArgsOptions,
98
+ capabilities: CodexCliCapabilities,
99
+ ): BuildCodexExecArgsResult {
100
+ const { runtime } = options;
101
+ const args: string[] = ["exec"];
102
+
103
+ if ((options.imageAttachments?.length ?? 0) > 0 && capabilities.image === false) {
104
+ return {
105
+ ok: false,
106
+ strategy: "fail",
107
+ error: "Installed Codex CLI does not support image attachments. Update Codex or remove the image from the prompt.",
108
+ };
109
+ }
110
+
111
+ if (options.structuredOutput ?? true) {
112
+ args.push("--experimental-json");
113
+ }
114
+
115
+ args.push(
116
+ "--skip-git-repo-check",
117
+ "--cd",
118
+ sanitizeWorkingDirectory(options.cwd),
119
+ "--model",
120
+ runtime.model,
121
+ );
122
+
123
+ if (!capabilities.config) {
124
+ return {
125
+ ok: false,
126
+ strategy: "fail",
127
+ error: [
128
+ `Installed Codex CLI cannot safely apply the selected reasoning level "${runtime.reasoningLevel}".`,
129
+ `Detected launch controls: ${buildCapabilitySummary(capabilities)}.`,
130
+ "This Codex version does not support --config / -c overrides.",
131
+ ].join("\n"),
132
+ };
133
+ }
134
+
135
+ args.push("--config", `model_reasoning_effort=${runtime.reasoningLevel}`);
136
+
137
+ const policyArgs = buildRuntimePolicyArgs(runtime, capabilities);
138
+ if (!policyArgs.ok) {
139
+ return policyArgs;
140
+ }
141
+
142
+ args.push(...policyArgs.args);
143
+
144
+ for (const attachment of options.imageAttachments ?? []) {
145
+ args.push("--image", attachment.path);
146
+ }
147
+
148
+ if (runtime.policy.networkAccess) {
149
+ args.push("--config", `sandbox_workspace_write.network_access=${JSON.stringify(runtime.policy.networkAccess)}`);
150
+ }
151
+
152
+ if (runtime.policy.writableRoots.length > 0) {
153
+ args.push("--config", `sandbox_workspace_write.writable_roots=${JSON.stringify(runtime.policy.writableRoots)}`);
154
+ }
155
+
156
+ if (runtime.policy.serviceTier !== "flex") {
157
+ args.push("--config", `service_tier=${runtime.policy.serviceTier}`);
158
+ }
159
+
160
+ if (runtime.policy.personality !== "none") {
161
+ args.push("--config", `personality=${runtime.policy.personality}`);
162
+ }
163
+
164
+ args.push("-");
165
+ return { ok: true, args, strategy: policyArgs.strategy };
166
+ }
@@ -0,0 +1,163 @@
1
+ import { fileURLToPath } from "url";
2
+ import { buildCodexExecArgs, type BuildCodexExecArgsOptions, type BuildCodexExecArgsResult } from "./codexExecArgs.js";
3
+ import { getCodexCliCapabilities, type CodexCliCapabilities } from "../models/codexCapabilities.js";
4
+ import { resolveCodexExecutable } from "../executables/codexExecutable.js";
5
+ import * as perf from "../perf/profiler.js";
6
+
7
+ // Assumed capability set when probeCapabilities is false — avoids a slow help-output probe on every run.
8
+ const MODERN_CODEX_CLI_CAPABILITIES: CodexCliCapabilities = {
9
+ askForApproval: false,
10
+ sandbox: false,
11
+ config: true,
12
+ fullAuto: false,
13
+ image: true,
14
+ };
15
+
16
+ export interface PreparedCodexExecLaunch {
17
+ executable: string;
18
+ capabilities: CodexCliCapabilities;
19
+ args: string[];
20
+ strategy: BuildCodexExecArgsResult["strategy"];
21
+ responsibleModulePath: string;
22
+ responsibleModuleKind: "src" | "built-artifact" | "other";
23
+ launchContext: {
24
+ launchKind?: string;
25
+ packageRoot?: string;
26
+ launcherScript?: string;
27
+ };
28
+ }
29
+
30
+ interface PrepareCodexExecLaunchDependencies {
31
+ resolveExecutable?: typeof resolveCodexExecutable;
32
+ getCapabilities?: typeof getCodexCliCapabilities;
33
+ diagnosticsLogger?: (message: string) => void;
34
+ }
35
+
36
+ export interface PrepareCodexExecLaunchOptions extends BuildCodexExecArgsOptions {
37
+ probeCapabilities?: boolean;
38
+ codexCommandPath?: string | null;
39
+ }
40
+
41
+ function resolveResponsibleModulePath(moduleUrl: string): string {
42
+ if (moduleUrl.startsWith("file:")) {
43
+ const windowsMatch = moduleUrl.match(/^file:\/\/\/([A-Za-z]:\/.*)/);
44
+ if (windowsMatch) {
45
+ return windowsMatch[1].replace(/\//g, "\\");
46
+ }
47
+ return fileURLToPath(moduleUrl);
48
+ }
49
+
50
+ return moduleUrl;
51
+ }
52
+
53
+ function classifyResponsibleModule(modulePath: string): "src" | "built-artifact" | "other" {
54
+ if (/[\\/]src[\\/]/i.test(modulePath)) {
55
+ return "src";
56
+ }
57
+
58
+ if (/[\\/](dist|build|out|lib)[\\/]/i.test(modulePath)) {
59
+ return "built-artifact";
60
+ }
61
+
62
+ return "other";
63
+ }
64
+
65
+ function shouldLogCodexLaunchDiagnostics(): boolean {
66
+ return process.env.UBUME_DEBUG_CODEX_LAUNCH === "1";
67
+ }
68
+
69
+ function logCodexLaunchDiagnostics(
70
+ options: BuildCodexExecArgsOptions,
71
+ prepared: PreparedCodexExecLaunch,
72
+ logger: ((message: string) => void) | undefined,
73
+ ): void {
74
+ if (!logger || !shouldLogCodexLaunchDiagnostics()) {
75
+ return;
76
+ }
77
+
78
+ const { capabilities, launchContext } = prepared;
79
+ const debugLines = [
80
+ "[ubume] codex launch debug",
81
+ ` responsible module: ${prepared.responsibleModulePath} (${prepared.responsibleModuleKind})`,
82
+ ` launch kind: ${launchContext.launchKind ?? "unknown"}`,
83
+ ` package root: ${launchContext.packageRoot ?? "unknown"}`,
84
+ ` launcher script: ${launchContext.launcherScript ?? "unknown"}`,
85
+ ` resolved executable: ${prepared.executable}`,
86
+ " capabilities:",
87
+ ` askForApproval=${capabilities.askForApproval}`,
88
+ ` sandbox=${capabilities.sandbox}`,
89
+ ` config=${capabilities.config}`,
90
+ ` fullAuto=${capabilities.fullAuto}`,
91
+ ` chosen strategy: ${prepared.strategy}`,
92
+ ` structured output: ${options.structuredOutput ?? true}`,
93
+ ` runtime model: ${options.runtime.model}`,
94
+ ` runtime mode: ${options.runtime.mode}`,
95
+ ` final argv: ${JSON.stringify(prepared.args)}`,
96
+ ];
97
+
98
+ logger(debugLines.join("\n"));
99
+ }
100
+
101
+ export async function prepareCodexExecLaunch(
102
+ options: PrepareCodexExecLaunchOptions,
103
+ responsibleModuleUrl: string,
104
+ dependencies: PrepareCodexExecLaunchDependencies = {},
105
+ ): Promise<BuildCodexExecArgsResult & {
106
+ executable?: string;
107
+ capabilities?: CodexCliCapabilities;
108
+ responsibleModulePath?: string;
109
+ responsibleModuleKind?: PreparedCodexExecLaunch["responsibleModuleKind"];
110
+ launchContext?: PreparedCodexExecLaunch["launchContext"];
111
+ }> {
112
+ const executableResolver = dependencies.resolveExecutable ?? resolveCodexExecutable;
113
+ const capabilityResolver = dependencies.getCapabilities ?? getCodexCliCapabilities;
114
+ const diagnosticsLogger = dependencies.diagnosticsLogger;
115
+ perf.mark("exec_resolve_start");
116
+ const executable = await executableResolver({ configuredPath: options.codexCommandPath });
117
+ perf.mark("exec_resolve_end");
118
+ perf.mark("caps_probe_start");
119
+ const capabilities = options.probeCapabilities
120
+ ? await capabilityResolver(executable)
121
+ : MODERN_CODEX_CLI_CAPABILITIES;
122
+ perf.mark("caps_probe_end");
123
+ const argsResult = buildCodexExecArgs(options, capabilities);
124
+ const responsibleModulePath = resolveResponsibleModulePath(responsibleModuleUrl);
125
+ const responsibleModuleKind = classifyResponsibleModule(responsibleModulePath);
126
+ const launchContext = {
127
+ launchKind: process.env.UBUME_LAUNCH_KIND,
128
+ packageRoot: process.env.UBUME_PACKAGE_ROOT,
129
+ launcherScript: process.env.UBUME_LAUNCHER_SCRIPT,
130
+ };
131
+
132
+ if (!argsResult.ok) {
133
+ return {
134
+ ...argsResult,
135
+ executable,
136
+ capabilities,
137
+ responsibleModulePath,
138
+ responsibleModuleKind,
139
+ launchContext,
140
+ };
141
+ }
142
+
143
+ const prepared: PreparedCodexExecLaunch = {
144
+ executable,
145
+ capabilities,
146
+ args: argsResult.args,
147
+ strategy: argsResult.strategy,
148
+ responsibleModulePath,
149
+ responsibleModuleKind,
150
+ launchContext,
151
+ };
152
+
153
+ logCodexLaunchDiagnostics(options, prepared, diagnosticsLogger);
154
+
155
+ return {
156
+ ...argsResult,
157
+ executable: prepared.executable,
158
+ capabilities: prepared.capabilities,
159
+ responsibleModulePath: prepared.responsibleModulePath,
160
+ responsibleModuleKind: prepared.responsibleModuleKind,
161
+ launchContext: prepared.launchContext,
162
+ };
163
+ }