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,111 @@
1
+ import { existsSync } from "fs";
2
+ import { isAbsolute, resolve } from "path";
3
+
4
+ const CONTROL_CHARACTER_PATTERN = /[\u0000-\u001f\u007f]/;
5
+ const BARE_EXECUTABLE_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
6
+ const SHELL_METACHARACTER_PATTERN = /[;&|<>`$]/;
7
+ const WINDOWS_BATCH_METACHARACTER_PATTERN = /[;&|<>`$^%!]/;
8
+
9
+ export interface ExecutableValidationOptions {
10
+ label: string;
11
+ cwd?: string;
12
+ requireExistingPath?: boolean;
13
+ allowBareExecutable?: boolean;
14
+ }
15
+
16
+ function stripBalancedWrappingQuotes(value: string): string {
17
+ if (value.length < 2) return value;
18
+ const first = value[0];
19
+ const last = value[value.length - 1];
20
+ if ((first === `"` && last === `"`) || (first === `'` && last === `'`)) {
21
+ return value.slice(1, -1).trim();
22
+ }
23
+ return value;
24
+ }
25
+
26
+ function looksLikePath(value: string): boolean {
27
+ return /[\\/]/.test(value) || /^[A-Za-z]:/.test(value);
28
+ }
29
+
30
+ function isWindowsAbsolutePath(value: string): boolean {
31
+ return /^[A-Za-z]:[\\/]/.test(value);
32
+ }
33
+
34
+ function hasWindowsBatchExtension(value: string): boolean {
35
+ return /\.(?:cmd|bat)$/i.test(value);
36
+ }
37
+
38
+ function validateCommonExecutableSyntax(value: string, label: string): void {
39
+ if (!value) {
40
+ throw new Error(`${label} is empty.`);
41
+ }
42
+ if (CONTROL_CHARACTER_PATTERN.test(value)) {
43
+ throw new Error(`${label} contains control characters and cannot be used as an executable.`);
44
+ }
45
+ if (value.includes(`"`) || value.includes(`'`)) {
46
+ throw new Error(`${label} must be a command/path only, without embedded quotes.`);
47
+ }
48
+ if (SHELL_METACHARACTER_PATTERN.test(value)) {
49
+ throw new Error(`${label} contains shell metacharacters and cannot be used as an executable.`);
50
+ }
51
+ }
52
+
53
+ export function normalizeExecutableValue(
54
+ rawValue: string,
55
+ options: ExecutableValidationOptions,
56
+ ): string {
57
+ const allowBareExecutable = options.allowBareExecutable ?? true;
58
+ const requireExistingPath = options.requireExistingPath ?? false;
59
+ const cwd = options.cwd ?? process.cwd();
60
+ const trimmed = stripBalancedWrappingQuotes(rawValue.trim());
61
+
62
+ validateCommonExecutableSyntax(trimmed, options.label);
63
+
64
+ if (!looksLikePath(trimmed)) {
65
+ if (!allowBareExecutable) {
66
+ throw new Error(`${options.label} must be an executable path.`);
67
+ }
68
+ if (!BARE_EXECUTABLE_PATTERN.test(trimmed)) {
69
+ throw new Error(`${options.label} must be a single executable name without arguments.`);
70
+ }
71
+ return trimmed;
72
+ }
73
+
74
+ const normalized = isAbsolute(trimmed) || isWindowsAbsolutePath(trimmed) ? trimmed : resolve(cwd, trimmed);
75
+ validateCommonExecutableSyntax(normalized, options.label);
76
+
77
+ if (requireExistingPath && !existsSync(normalized)) {
78
+ throw new Error(
79
+ `${options.label} path does not exist: "${normalized}"\n` +
80
+ `Check the path is correct and the file is accessible, or unset ${options.label}.`,
81
+ );
82
+ }
83
+
84
+ return normalized;
85
+ }
86
+
87
+ export function validateExecutableForSpawn(
88
+ executable: string,
89
+ options: ExecutableValidationOptions,
90
+ ): string {
91
+ return normalizeExecutableValue(executable, {
92
+ ...options,
93
+ requireExistingPath: options.requireExistingPath ?? false,
94
+ });
95
+ }
96
+
97
+ export function validateWindowsBatchExecutableForCmd(
98
+ executable: string,
99
+ label: string,
100
+ ): void {
101
+ if (!hasWindowsBatchExtension(executable)) return;
102
+ if (WINDOWS_BATCH_METACHARACTER_PATTERN.test(executable)) {
103
+ throw new Error(`${label} contains characters that are unsafe for cmd.exe batch launch.`);
104
+ }
105
+ }
106
+
107
+ export function validateWindowsBatchArgumentForCmd(argument: string, label: string): void {
108
+ if (WINDOWS_BATCH_METACHARACTER_PATTERN.test(argument)) {
109
+ throw new Error(`${label} contains characters that are unsafe for cmd.exe batch launch.`);
110
+ }
111
+ }
@@ -0,0 +1,220 @@
1
+ import { spawn, type ChildProcess } from "child_process";
2
+ import { accessSync, constants, existsSync } from "fs";
3
+ import { delimiter, join } from "path";
4
+ import { buildSpawnSpec } from "../executables/executableResolver.js";
5
+ import { normalizeExecutableValue } from "../process/processValidation.js";
6
+ import type { ProviderConfig, ProviderLaunchCommand } from "./types.js";
7
+
8
+ export interface ProviderLaunchSpec {
9
+ executable: string;
10
+ args: string[];
11
+ cwd: string;
12
+ }
13
+
14
+ export type ProviderLaunchResult =
15
+ | { status: "completed"; exitCode: number | null; signal: NodeJS.Signals | null; message: string }
16
+ | { status: "disabled"; message: string }
17
+ | { status: "missing-command"; message: string }
18
+ | { status: "spawn-error"; message: string; errorCode?: string };
19
+
20
+ interface RawModeStream {
21
+ isRaw?: boolean;
22
+ setRawMode?: (enabled: boolean) => unknown;
23
+ }
24
+
25
+ export interface LaunchProviderCliOptions {
26
+ cwd: string;
27
+ stdin?: RawModeStream | null;
28
+ beforeLaunch?: () => void;
29
+ afterLaunch?: () => void;
30
+ spawnImpl?: typeof spawn;
31
+ commandExists?: (executable: string) => Promise<boolean> | boolean;
32
+ }
33
+
34
+ function formatCommand(command: ProviderLaunchCommand): string {
35
+ return [command.executable, ...command.args].join(" ");
36
+ }
37
+
38
+ export function buildProviderLaunchSpec(provider: ProviderConfig, cwd: string): ProviderLaunchSpec | ProviderLaunchResult {
39
+ if (!provider.enabled) {
40
+ return {
41
+ status: "disabled",
42
+ message: `${provider.displayName} is disabled. Configure a command in Ubume's provider settings before launching it.`,
43
+ };
44
+ }
45
+
46
+ if (!provider.launchCommand?.executable.trim()) {
47
+ return {
48
+ status: "missing-command",
49
+ message: `${provider.displayName} does not have a launch command configured.`,
50
+ };
51
+ }
52
+
53
+ try {
54
+ return {
55
+ executable: normalizeExecutableValue(provider.launchCommand.executable, {
56
+ label: `${provider.displayName} launch command`,
57
+ cwd,
58
+ }),
59
+ args: provider.launchCommand.args,
60
+ cwd,
61
+ };
62
+ } catch (error) {
63
+ const message = error instanceof Error ? error.message : "Invalid launch command.";
64
+ return {
65
+ status: "spawn-error",
66
+ message: `${provider.displayName} has an unsafe launch command. ${message}`,
67
+ };
68
+ }
69
+ }
70
+
71
+ function setRawMode(stdin: RawModeStream | null | undefined, enabled: boolean): void {
72
+ try {
73
+ stdin?.setRawMode?.(enabled);
74
+ } catch {
75
+ // Some test streams and redirected terminals do not support raw-mode changes.
76
+ }
77
+ }
78
+
79
+ function executableLooksLikePath(executable: string): boolean {
80
+ return /[\\/]/.test(executable) || /^[A-Za-z]:/.test(executable);
81
+ }
82
+
83
+ function canAccessExecutable(path: string): boolean {
84
+ try {
85
+ if (process.platform === "win32") {
86
+ return existsSync(path);
87
+ }
88
+ accessSync(path, constants.X_OK);
89
+ return true;
90
+ } catch {
91
+ return false;
92
+ }
93
+ }
94
+
95
+ function getPathExecutableCandidates(executable: string): string[] {
96
+ if (process.platform !== "win32" || /\.[A-Za-z0-9_-]+$/.test(executable)) {
97
+ return [executable];
98
+ }
99
+
100
+ const pathExt = process.env.PATHEXT?.split(";").filter(Boolean) ?? [".COM", ".EXE", ".BAT", ".CMD"];
101
+ return [executable, ...pathExt.map((ext) => `${executable}${ext.toLowerCase()}`)];
102
+ }
103
+
104
+ export async function commandExistsOnPath(executable: string): Promise<boolean> {
105
+ let candidate: string;
106
+ try {
107
+ candidate = normalizeExecutableValue(executable, {
108
+ label: "Provider launch command",
109
+ allowBareExecutable: true,
110
+ });
111
+ } catch {
112
+ return false;
113
+ }
114
+
115
+ const trimmed = candidate.trim();
116
+ if (executableLooksLikePath(trimmed)) {
117
+ return canAccessExecutable(trimmed);
118
+ }
119
+
120
+ const pathEntries = process.env.PATH?.split(delimiter).filter(Boolean) ?? [];
121
+ for (const dir of pathEntries) {
122
+ for (const name of getPathExecutableCandidates(trimmed)) {
123
+ if (canAccessExecutable(join(dir, name))) return true;
124
+ }
125
+ }
126
+ return false;
127
+ }
128
+
129
+ function formatSpawnError(provider: ProviderConfig, executable: string, error: NodeJS.ErrnoException): ProviderLaunchResult {
130
+ if (error.code === "ENOENT") {
131
+ return {
132
+ status: "missing-command",
133
+ message: `${provider.displayName} could not be launched because \`${executable}\` is not installed or not available on PATH.`,
134
+ };
135
+ }
136
+
137
+ if (error.code === "EACCES" || error.code === "EPERM") {
138
+ return {
139
+ status: "spawn-error",
140
+ errorCode: error.code,
141
+ message: `${provider.displayName} could not be launched because permission was denied for \`${executable}\`.`,
142
+ };
143
+ }
144
+
145
+ return {
146
+ status: "spawn-error",
147
+ errorCode: error.code,
148
+ message: `${provider.displayName} could not be launched. ${error.message}`,
149
+ };
150
+ }
151
+
152
+ export async function launchProviderCli(
153
+ provider: ProviderConfig,
154
+ options: LaunchProviderCliOptions,
155
+ ): Promise<ProviderLaunchResult> {
156
+ const spec = buildProviderLaunchSpec(provider, options.cwd);
157
+ if ("status" in spec) return spec;
158
+
159
+ const spawnImpl = options.spawnImpl ?? spawn;
160
+ const commandExists = options.commandExists ?? commandExistsOnPath;
161
+ const available = await commandExists(spec.executable);
162
+ if (!available) {
163
+ return {
164
+ status: "missing-command",
165
+ message: `${provider.displayName} could not be launched because \`${spec.executable}\` is not installed or not available on PATH.`,
166
+ };
167
+ }
168
+
169
+ const wasRaw = Boolean(options.stdin?.isRaw);
170
+ options.beforeLaunch?.();
171
+ setRawMode(options.stdin, false);
172
+
173
+ try {
174
+ return await new Promise<ProviderLaunchResult>((resolve) => {
175
+ let child: ChildProcess;
176
+ try {
177
+ const spawnSpec = buildSpawnSpec(spec.executable, spec.args);
178
+ if (!spawnSpec.executable) {
179
+ resolve({ status: "spawn-error", message: "Executable path is empty after resolution." });
180
+ return;
181
+ }
182
+ child = spawnImpl(spawnSpec.executable, spawnSpec.args, {
183
+ cwd: spec.cwd,
184
+ shell: false,
185
+ stdio: "inherit",
186
+ });
187
+ } catch (error) {
188
+ resolve(formatSpawnError(provider, spec.executable, error as NodeJS.ErrnoException));
189
+ return;
190
+ }
191
+
192
+ child.once("error", (error: NodeJS.ErrnoException) => {
193
+ resolve(formatSpawnError(provider, spec.executable, error));
194
+ });
195
+
196
+ child.once("close", (exitCode, signal) => {
197
+ resolve({
198
+ status: "completed",
199
+ exitCode,
200
+ signal,
201
+ message: `${provider.displayName} launch finished${exitCode === null ? "" : ` with exit code ${exitCode}`}.`,
202
+ });
203
+ });
204
+ });
205
+ } finally {
206
+ if (wasRaw) {
207
+ setRawMode(options.stdin, true);
208
+ }
209
+ options.afterLaunch?.();
210
+ }
211
+ }
212
+
213
+ export function describeProviderLaunch(provider: ProviderConfig): string {
214
+ if (!provider.enabled) {
215
+ return `${provider.displayName} is disabled.`;
216
+ }
217
+ return provider.launchCommand
218
+ ? `${provider.displayName}: ${formatCommand(provider.launchCommand)}`
219
+ : `${provider.displayName} has no launch command configured.`;
220
+ }
@@ -0,0 +1,354 @@
1
+ import type {
2
+ ProviderConfig,
3
+ ProviderId,
4
+ ProviderBackendType,
5
+ ProviderLaunchCommand,
6
+ ProviderWorkspaceConfig,
7
+ ProviderWorkspaceOverride,
8
+ } from "./types.js";
9
+ import { DEFAULT_MODEL } from "../../config/settings.js";
10
+ import { isLocalDevChannel } from "../version/channel.js";
11
+ import {
12
+ getDefaultRouteModel,
13
+ getProviderRouteSetupMessage,
14
+ getProviderRuntime,
15
+ isProviderRoutableInUbume,
16
+ isProviderRouteConfigured,
17
+ } from "../providerRuntime/registry.js";
18
+ import { normalizeGeminiModelId } from "../providerRuntime/models.js";
19
+ import { ANTIGRAVITY_DEFAULT_MODEL_ID } from "../providerRuntime/antigravity.js";
20
+ import { discoverMistralVibeModels } from "../providerRuntime/mistralVibe.js";
21
+ import { setLocalProviderConfig } from "../providerRuntime/local.js";
22
+ import { CODEXA_NATIVE_MODEL_ID, discoverCodexaNativeModels } from "../providerRuntime/codexaNative.js";
23
+ import { CODEXA_CUPY_MODEL_ID, discoverCodexaCupyModels } from "../providerRuntime/codexaCupy.js";
24
+ import { formatContextLength, resolveModelContextLengthCached } from "../providerRuntime/contextMetadata.js";
25
+ import { resolveModelCapabilityProfileCached } from "../providerRuntime/capabilityProfile.js";
26
+
27
+ // Google/Gemini remains a recognized legacy config value so existing workspace
28
+ // files can be migrated, but it is no longer a selectable Ubume provider.
29
+ const ALL_PROVIDER_ORDER: readonly ProviderId[] = ["openai", "anthropic", "mistral", "codexa-native", "codexa-cupy", "local", "antigravity"];
30
+ const KNOWN_PROVIDER_IDS: readonly ProviderId[] = ["openai", "anthropic", "google", "mistral", "local", "codexa-native", "codexa-cupy", "antigravity"];
31
+
32
+ export function getProviderOrder(env: NodeJS.ProcessEnv = process.env): readonly ProviderId[] {
33
+ if (isLocalDevChannel(env)) {
34
+ return ALL_PROVIDER_ORDER;
35
+ }
36
+ return ALL_PROVIDER_ORDER.filter((id) => id !== "codexa-native" && id !== "codexa-cupy");
37
+ }
38
+
39
+ const DEFAULT_PROVIDER_ID: ProviderId = "openai";
40
+
41
+ type ProviderDefault = Omit<ProviderConfig, "currentModel" | "enabled" | "statusLabel" | "launchCommand" | "isDefault"> & {
42
+ currentModel: (activeModel: string) => string;
43
+ enabled: boolean;
44
+ launchCommand: ProviderLaunchCommand | null;
45
+ };
46
+
47
+ const DEFAULT_PROVIDERS: Record<ProviderId, ProviderDefault> = {
48
+ openai: {
49
+ id: "openai",
50
+ displayName: "OpenAI",
51
+ currentModel: (activeModel) => activeModel,
52
+ backendType: "codex-cli-auth",
53
+ routeMode: "in-ubume",
54
+ enabled: true,
55
+ launchCommand: { executable: "codex", args: [] },
56
+ isActiveRoute: false,
57
+ routeUnavailableReason: null,
58
+ },
59
+ anthropic: {
60
+ id: "anthropic",
61
+ displayName: "Anthropic",
62
+ currentModel: () => "Claude Code default",
63
+ backendType: "claude-code-auth",
64
+ routeMode: "in-ubume",
65
+ enabled: true,
66
+ launchCommand: { executable: "claude", args: [] },
67
+ isActiveRoute: false,
68
+ routeUnavailableReason: null,
69
+ },
70
+ google: {
71
+ id: "google",
72
+ displayName: "Google",
73
+ currentModel: () => "gemini-3-flash-preview",
74
+ backendType: "gemini-cli-auth",
75
+ routeMode: "in-ubume",
76
+ enabled: true,
77
+ launchCommand: { executable: "gemini", args: [] },
78
+ isActiveRoute: false,
79
+ routeUnavailableReason: null,
80
+ },
81
+ local: {
82
+ id: "local",
83
+ displayName: "Local",
84
+ currentModel: () => "Local default",
85
+ backendType: "local-openai-compatible",
86
+ routeMode: "in-ubume",
87
+ enabled: false,
88
+ launchCommand: null,
89
+ isActiveRoute: false,
90
+ routeUnavailableReason: "Local provider unavailable. Start LM Studio, load a model, and enable the local server.",
91
+ },
92
+ "codexa-native": {
93
+ id: "codexa-native",
94
+ displayName: "ubume-PyTorch",
95
+ currentModel: () => CODEXA_NATIVE_MODEL_ID,
96
+ backendType: "codexa-native-pytorch",
97
+ routeMode: "in-ubume",
98
+ enabled: false,
99
+ launchCommand: null,
100
+ isActiveRoute: false,
101
+ routeUnavailableReason: "Codexa Native is only available on ubume-dev.",
102
+ },
103
+ "codexa-cupy": {
104
+ id: "codexa-cupy",
105
+ displayName: "CuPy",
106
+ currentModel: () => CODEXA_CUPY_MODEL_ID,
107
+ backendType: "codexa-cupy",
108
+ routeMode: "in-ubume",
109
+ enabled: false,
110
+ launchCommand: null,
111
+ isActiveRoute: false,
112
+ routeUnavailableReason: "CuPy is only available on ubume-dev.",
113
+ },
114
+ mistral: {
115
+ id: "mistral",
116
+ displayName: "Mistral Vibe CLI",
117
+ currentModel: () => "Vibe default",
118
+ backendType: "mistral-vibe-cli-auth",
119
+ routeMode: "in-ubume",
120
+ enabled: true,
121
+ launchCommand: { executable: "vibe", args: [] },
122
+ isActiveRoute: false,
123
+ routeUnavailableReason: null,
124
+ },
125
+ antigravity: {
126
+ id: "antigravity",
127
+ displayName: "Antigravity",
128
+ currentModel: () => ANTIGRAVITY_DEFAULT_MODEL_ID,
129
+ backendType: "antigravity-cli-auth",
130
+ routeMode: "in-ubume",
131
+ enabled: true,
132
+ launchCommand: { executable: "agy", args: [] },
133
+ isActiveRoute: false,
134
+ routeUnavailableReason: null,
135
+ },
136
+ };
137
+
138
+ function isProviderId(value: unknown): value is ProviderId {
139
+ return typeof value === "string" && KNOWN_PROVIDER_IDS.includes(value as ProviderId);
140
+ }
141
+
142
+ function normalizeLaunchCommand(value: ProviderWorkspaceOverride["command"] | undefined): ProviderLaunchCommand | null | undefined {
143
+ if (value === undefined) return undefined;
144
+ if (value === null) return null;
145
+ if (typeof value === "string") {
146
+ const executable = value.trim();
147
+ return executable ? { executable, args: [] } : null;
148
+ }
149
+ if (typeof value.executable !== "string") return undefined;
150
+ const executable = value.executable.trim();
151
+ if (!executable) return null;
152
+ return {
153
+ executable,
154
+ args: Array.isArray(value.args)
155
+ ? value.args.filter((arg): arg is string => typeof arg === "string")
156
+ : [],
157
+ };
158
+ }
159
+
160
+ function applyOverride(
161
+ provider: ProviderConfig,
162
+ override: ProviderWorkspaceOverride | undefined,
163
+ ): ProviderConfig {
164
+ if (!override) return provider;
165
+
166
+ const launchCommand = normalizeLaunchCommand(override.command);
167
+ const hasConfiguredCommand = launchCommand !== undefined;
168
+ const nextCommand = hasConfiguredCommand ? launchCommand : provider.launchCommand;
169
+ const nextEnabled = typeof override.enabled === "boolean"
170
+ ? provider.id === "local" ? provider.enabled && override.enabled : override.enabled
171
+ : provider.enabled;
172
+
173
+ const overrideModel = typeof override.currentModel === "string" && override.currentModel.trim()
174
+ ? override.currentModel.trim()
175
+ : null;
176
+
177
+ return {
178
+ ...provider,
179
+ currentModel: overrideModel && provider.id !== "local"
180
+ ? provider.id === "google" ? normalizeGeminiModelId(overrideModel) : overrideModel
181
+ : provider.currentModel,
182
+ enabled: nextEnabled,
183
+ launchCommand: nextCommand,
184
+ statusLabel: !nextEnabled
185
+ ? "Disabled"
186
+ : provider.routeMode === "launch-only"
187
+ ? provider.statusLabel
188
+ : (provider.routeUnavailableReason ? "Needs config" : "Enabled"),
189
+ };
190
+ }
191
+
192
+ export function getDefaultProviderId(
193
+ config: ProviderWorkspaceConfig | null | undefined,
194
+ env: NodeJS.ProcessEnv = process.env,
195
+ ): ProviderId {
196
+ const providerId = config?.workspaceDefaultProviderId;
197
+ const isAvailable = isProviderId(providerId)
198
+ && providerId !== "google"
199
+ && (providerId !== "codexa-native" || isLocalDevChannel(env));
200
+ return isAvailable ? providerId : DEFAULT_PROVIDER_ID;
201
+ }
202
+
203
+ export function getActiveRouteProviderId(
204
+ config: ProviderWorkspaceConfig | null | undefined,
205
+ env: NodeJS.ProcessEnv = process.env,
206
+ ): ProviderId {
207
+ const providerId = config?.activeRoute?.providerId;
208
+ return isProviderId(providerId) && providerId !== "google" && isProviderRoutableInUbume(providerId, env)
209
+ ? providerId
210
+ : DEFAULT_PROVIDER_ID;
211
+ }
212
+
213
+ export function buildProviderRegistry(options: {
214
+ activeModel: string;
215
+ workspaceRoot?: string;
216
+ workspaceConfig?: ProviderWorkspaceConfig | null;
217
+ diagnostics?: Record<string, Record<string, string | number | boolean | null>>;
218
+ routeErrors?: Record<string, string>;
219
+ env?: NodeJS.ProcessEnv;
220
+ }): ProviderConfig[] {
221
+ const env = options.env ?? process.env;
222
+ const defaultProviderId = getDefaultProviderId(options.workspaceConfig, env);
223
+ const activeRouteProviderId = getActiveRouteProviderId(options.workspaceConfig, env);
224
+ const providerOrder = getProviderOrder(env);
225
+
226
+ return providerOrder.map((id) => {
227
+ if (id === "local") {
228
+ setLocalProviderConfig(options.workspaceConfig?.providers?.local);
229
+ }
230
+ const defaults = DEFAULT_PROVIDERS[id];
231
+ const runtime = getProviderRuntime(id);
232
+ const discovery = id === "mistral"
233
+ ? discoverMistralVibeModels(options.workspaceRoot ?? process.cwd())
234
+ : id === "codexa-native"
235
+ ? discoverCodexaNativeModels(undefined, env)
236
+ : id === "codexa-cupy"
237
+ ? discoverCodexaCupyModels(undefined, env)
238
+ : runtime.discoverModels();
239
+
240
+ const activeRoute = options.workspaceConfig?.activeRoute;
241
+ const isThisActive = activeRoute?.providerId === id;
242
+
243
+ let currentModelLabel = isThisActive && activeRoute
244
+ ? activeRoute.modelId
245
+ : getDefaultRouteModel(id, id === "openai" ? DEFAULT_MODEL : defaults.currentModel(options.activeModel));
246
+
247
+ if (id === "google") {
248
+ const hasGoogleOverride = options.workspaceConfig?.providers?.google?.currentModel !== undefined;
249
+ const geminiRoute = isThisActive || !hasGoogleOverride ? activeRoute : null;
250
+ const selection = geminiRoute?.modelSelection;
251
+ if (selection) {
252
+ if (selection.kind === "auto") {
253
+ currentModelLabel = `Auto (${selection.family === "gemini-3" ? "Gemini 3" : "Gemini 2.5"})`;
254
+ } else {
255
+ currentModelLabel = normalizeGeminiModelId(selection.modelId);
256
+ }
257
+ } else {
258
+ currentModelLabel = normalizeGeminiModelId(currentModelLabel);
259
+ }
260
+ }
261
+
262
+ if (id === "local") {
263
+ const selectedModel = typeof discovery.diagnostics?.selectedModel === "string" && discovery.diagnostics.selectedModel.trim()
264
+ ? discovery.diagnostics.selectedModel.trim()
265
+ : discovery.models[0]?.modelId;
266
+ if (selectedModel) {
267
+ currentModelLabel = selectedModel;
268
+ }
269
+ }
270
+
271
+ if (id === "mistral") {
272
+ currentModelLabel = discovery.models[0]?.modelId ?? "Vibe default";
273
+ }
274
+
275
+ const rawMetadataForModel = discovery.models.find((model) => model.modelId === currentModelLabel)?.raw;
276
+ const contextMetadata = resolveModelContextLengthCached({
277
+ providerId: id,
278
+ modelId: currentModelLabel,
279
+ providerConfig: options.workspaceConfig?.providers?.[id],
280
+ rawMetadata: rawMetadataForModel,
281
+ });
282
+ const contextSource = contextMetadata.source === "known-registry" ? "registry" : contextMetadata.source;
283
+ const capabilityProfile = resolveModelCapabilityProfileCached({
284
+ providerId: id,
285
+ modelId: currentModelLabel,
286
+ providerConfig: options.workspaceConfig?.providers?.[id],
287
+ rawMetadata: rawMetadataForModel,
288
+ });
289
+
290
+ const routeUnavailableReason: string | null = runtime.routeAvailable
291
+ ? (isProviderRouteConfigured(id, env)
292
+ ? null
293
+ : (options.routeErrors?.[id]
294
+ ?? discovery.message
295
+ ?? getProviderRouteSetupMessage(id)))
296
+ : runtime.routeStatus;
297
+
298
+ const enabled = id === "codexa-native" || id === "codexa-cupy"
299
+ ? isLocalDevChannel(env)
300
+ : id === "local"
301
+ ? discovery.status === "ready"
302
+ : defaults.enabled;
303
+
304
+ const availabilityStatus = options.diagnostics?.[id]?.availabilityStatus;
305
+ const statusLabel = id === "mistral"
306
+ ? availabilityStatus === "checking"
307
+ ? "Checking"
308
+ : availabilityStatus === "unavailable"
309
+ ? "Missing"
310
+ : routeUnavailableReason
311
+ ? "Needs config"
312
+ : "Enabled"
313
+ : id === "local"
314
+ ? (discovery.status === "ready" ? "Enabled" : "Disabled")
315
+ : !enabled
316
+ ? "Disabled"
317
+ : routeUnavailableReason
318
+ ? "Needs config"
319
+ : "Enabled";
320
+
321
+ const provider: ProviderConfig = {
322
+ id,
323
+ displayName: defaults.displayName,
324
+ currentModel: currentModelLabel,
325
+ contextLengthLabel: formatContextLength(contextMetadata.contextLength),
326
+ contextLengthSource: contextSource,
327
+ capabilityProfile,
328
+ // Keep the provider's stable backend identity even when discovery reports
329
+ // missing local model files. Availability is represented separately by
330
+ // statusLabel and routeUnavailableReason.
331
+ backendType: id === "codexa-native" || id === "codexa-cupy"
332
+ ? defaults.backendType
333
+ : discovery.backendKind as ProviderBackendType,
334
+ routeMode: runtime.routeAvailable ? "in-ubume" : "launch-only",
335
+ enabled,
336
+ statusLabel,
337
+ launchCommand: defaults.launchCommand ? { ...defaults.launchCommand, args: [...defaults.launchCommand.args] } : null,
338
+ isDefault: id === defaultProviderId,
339
+ isActiveRoute: id === activeRouteProviderId,
340
+ routeUnavailableReason,
341
+ routeDiagnostics: options.diagnostics?.[id],
342
+ };
343
+
344
+ return applyOverride(provider, options.workspaceConfig?.providers?.[id]);
345
+ });
346
+ }
347
+
348
+ export function findProvider(providers: readonly ProviderConfig[], providerId: ProviderId): ProviderConfig | null {
349
+ return providers.find((provider) => provider.id === providerId) ?? null;
350
+ }
351
+
352
+ export function isKnownProviderId(value: string): value is ProviderId {
353
+ return isProviderId(value);
354
+ }