@golba98/codexa 1.0.3 → 1.0.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +17 -18
- package/bin/codexa.js +62 -144
- package/package.json +4 -3
- package/src/app.tsx +642 -303
- package/src/commands/handler.ts +7 -18
- package/src/config/buildInfo.ts +2 -2
- package/src/config/layeredConfig.ts +1 -1
- package/src/config/runtimeConfig.ts +1 -1
- package/src/config/settings.ts +1 -1
- package/src/config/trustStore.ts +1 -1
- package/src/core/README.md +52 -0
- package/src/core/agent/loop.ts +282 -0
- package/src/core/agent/protocol.ts +211 -0
- package/src/core/agent/tools.ts +414 -0
- package/src/core/{codexExecArgs.ts → codex/codexExecArgs.ts} +2 -2
- package/src/core/{codexLaunch.ts → codex/codexLaunch.ts} +3 -3
- package/src/core/{codexPrompt.ts → codex/codexPrompt.ts} +3 -3
- package/src/core/debug/modelStateDebug.ts +34 -0
- package/src/core/executables/antigravityExecutable.ts +48 -0
- package/src/core/executables/executableResolver.ts +5 -1
- package/src/core/models/codexModelCapabilities.ts +57 -4
- package/src/core/models/codexModelsCacheSeed.ts +153 -0
- package/src/core/models/providerModelCache.ts +106 -0
- package/src/core/perf/renderDebug.ts +10 -6
- package/src/core/process/CommandRunner.ts +12 -1
- package/src/core/providerLauncher/registry.ts +64 -18
- package/src/core/providerLauncher/types.ts +12 -9
- package/src/core/providerLauncher/workspaceConfig.ts +41 -26
- package/src/core/providerRuntime/anthropic.ts +10 -6
- package/src/core/providerRuntime/antigravity.ts +461 -0
- package/src/core/providerRuntime/claudeCodeDiscovery.ts +724 -446
- package/src/core/providerRuntime/claudeCodeDiscoveryDebug.ts +55 -0
- package/src/core/providerRuntime/contextMetadata.ts +12 -24
- package/src/core/providerRuntime/local.ts +129 -51
- package/src/core/providerRuntime/mistralVibe.ts +663 -0
- package/src/core/providerRuntime/models.ts +40 -15
- package/src/core/providerRuntime/reasoning.ts +9 -6
- package/src/core/providerRuntime/registry.ts +76 -4
- package/src/core/providerRuntime/types.ts +20 -14
- package/src/core/providers/codexSubprocess.ts +2 -2
- package/src/core/providers/types.ts +1 -1
- package/src/core/{attachments.ts → shared/attachments.ts} +27 -4
- package/src/core/{cleanupFastFail.ts → shared/cleanupFastFail.ts} +1 -1
- package/src/core/{hollowResponseFormat.ts → shared/hollowResponseFormat.ts} +1 -1
- package/src/core/terminal/clearFrameBoundary.ts +814 -0
- package/src/core/terminal/frameLock.ts +109 -0
- package/src/core/terminal/inkRenderReset.ts +123 -0
- package/src/core/terminal/terminalControl.ts +22 -0
- package/src/core/terminal/terminalTitle.ts +16 -102
- package/src/core/{channel.ts → version/channel.ts} +1 -1
- package/src/core/{updateCheck.ts → version/updateCheck.ts} +10 -5
- package/src/core/{launchContext.ts → workspace/launchContext.ts} +9 -16
- package/src/core/{planStorage.ts → workspace/planStorage.ts} +2 -2
- package/src/core/{workspaceGuard.ts → workspace/workspaceGuard.ts} +97 -13
- package/src/headless/execRunner.ts +2 -2
- package/src/index.tsx +66 -98
- package/src/session/appSession.ts +10 -7
- package/src/session/chatLifecycle.ts +1 -1
- package/src/session/liveRenderScheduler.ts +1 -1
- package/src/session/types.ts +1 -1
- package/src/test/runtimeTestUtils.ts +90 -0
- package/src/ui/{ActivityBars.tsx → chrome/ActivityBars.tsx} +1 -1
- package/src/ui/{ActivityIndicator.tsx → chrome/ActivityIndicator.tsx} +3 -3
- package/src/ui/{AnimatedStatusText.tsx → chrome/AnimatedStatusText.tsx} +3 -3
- package/src/ui/chrome/AppShell.tsx +672 -0
- package/src/ui/{BottomComposer.tsx → chrome/BottomComposer.tsx} +159 -158
- package/src/ui/{DashCard.tsx → chrome/DashCard.tsx} +2 -2
- package/src/ui/{RunFooter.tsx → chrome/RunFooter.tsx} +3 -3
- package/src/ui/chrome/RuntimeStatusBar.tsx +108 -0
- package/src/ui/{Spinner.tsx → chrome/Spinner.tsx} +1 -1
- package/src/ui/{TopHeader.tsx → chrome/TopHeader.tsx} +54 -38
- package/src/ui/{UpdateAvailableCard.tsx → chrome/UpdateAvailableCard.tsx} +5 -5
- package/src/ui/{commandNormalize.ts → input/commandNormalize.ts} +1 -1
- package/src/ui/{focus.ts → input/focus.ts} +1 -1
- package/src/ui/{inputBuffer.ts → input/inputBuffer.ts} +3 -3
- package/src/ui/layout.ts +298 -24
- package/src/ui/{AttachmentImportPanel.tsx → panels/AttachmentImportPanel.tsx} +3 -3
- package/src/ui/{AuthPanel.tsx → panels/AuthPanel.tsx} +5 -5
- package/src/ui/{BackendPicker.tsx → panels/BackendPicker.tsx} +2 -2
- package/src/ui/{ModePicker.tsx → panels/ModePicker.tsx} +2 -2
- package/src/ui/{ModelPicker.tsx → panels/ModelPicker.tsx} +2 -2
- package/src/ui/{ModelPickerScreen.tsx → panels/ModelPickerScreen.tsx} +224 -42
- package/src/ui/{ModelReasoningPicker.tsx → panels/ModelReasoningPicker.tsx} +5 -5
- package/src/ui/{Panel.tsx → panels/Panel.tsx} +1 -1
- package/src/ui/{PermissionsPanel.tsx → panels/PermissionsPanel.tsx} +3 -3
- package/src/ui/{PlanActionPicker.tsx → panels/PlanActionPicker.tsx} +2 -2
- package/src/ui/{PlanReviewPanel.tsx → panels/PlanReviewPanel.tsx} +5 -5
- package/src/ui/panels/ProviderPicker.tsx +737 -0
- package/src/ui/{ReasoningPicker.tsx → panels/ReasoningPicker.tsx} +3 -3
- package/src/ui/{SelectionPanel.tsx → panels/SelectionPanel.tsx} +5 -1
- package/src/ui/{SettingsPanel.tsx → panels/SettingsPanel.tsx} +4 -4
- package/src/ui/{TextEntryPanel.tsx → panels/TextEntryPanel.tsx} +4 -4
- package/src/ui/{ThemePicker.tsx → panels/ThemePicker.tsx} +2 -2
- package/src/ui/{UpdatePromptPanel.tsx → panels/UpdatePromptPanel.tsx} +17 -23
- package/src/ui/{Markdown.tsx → render/Markdown.tsx} +2 -2
- package/src/ui/{diffRenderer.ts → render/diffRenderer.ts} +1 -1
- package/src/ui/{logoVariants.ts → render/logoVariants.ts} +4 -8
- package/src/ui/{modeDisplay.ts → render/modeDisplay.ts} +2 -2
- package/src/ui/{outputPipeline.ts → render/outputPipeline.ts} +2 -2
- package/src/ui/{runtimeDisplay.ts → render/runtimeDisplay.ts} +23 -10
- package/src/ui/{textLayout.ts → render/textLayout.ts} +15 -4
- package/src/ui/{ActionRequiredBlock.tsx → timeline/ActionRequiredBlock.tsx} +3 -3
- package/src/ui/{AgentBlock.tsx → timeline/AgentBlock.tsx} +10 -10
- package/src/ui/{StaticIntroItem.tsx → timeline/StaticIntroItem.tsx} +2 -2
- package/src/ui/{ThinkingBlock.tsx → timeline/ThinkingBlock.tsx} +4 -4
- package/src/ui/{Timeline.tsx → timeline/Timeline.tsx} +1620 -1471
- package/src/ui/timeline/TranscriptShell.tsx +322 -0
- package/src/ui/{TurnGroup.tsx → timeline/TurnGroup.tsx} +15 -15
- package/src/ui/timeline/layoutListWindow.ts +145 -0
- package/src/ui/{progressEntries.ts → timeline/progressEntries.ts} +3 -3
- package/src/ui/{runActivityView.ts → timeline/runActivityView.ts} +1 -1
- package/src/ui/{timelineMeasure.ts → timeline/timelineMeasure.ts} +206 -134
- package/src/core/codex.ts +0 -124
- package/src/ui/AppShell.tsx +0 -706
- package/src/ui/ProviderPicker.tsx +0 -321
- package/src/ui/StaticTranscriptItem.tsx +0 -56
- /package/src/core/{inputDebug.ts → debug/inputDebug.ts} +0 -0
- /package/src/core/{clipboard.ts → shared/clipboard.ts} +0 -0
- /package/src/core/{githubDiagnostics.ts → shared/githubDiagnostics.ts} +0 -0
- /package/src/core/{projectInstructions.ts → workspace/projectInstructions.ts} +0 -0
- /package/src/core/{workspaceActivity.ts → workspace/workspaceActivity.ts} +0 -0
- /package/src/core/{workspaceRoot.ts → workspace/workspaceRoot.ts} +0 -0
- /package/src/ui/{busyStatusAnimation.ts → chrome/busyStatusAnimation.ts} +0 -0
- /package/src/ui/{slashCommands.ts → input/slashCommands.ts} +0 -0
- /package/src/ui/{terminalAnswerFormat.ts → render/terminalAnswerFormat.ts} +0 -0
|
@@ -0,0 +1,663 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "fs";
|
|
2
|
+
import { readdir, readFile } from "fs/promises";
|
|
3
|
+
import { homedir } from "os";
|
|
4
|
+
import { dirname, join, resolve } from "path";
|
|
5
|
+
import { parseTomlDocument } from "../../config/layeredConfig.js";
|
|
6
|
+
import { runCommand, runShellCommand, type CommandResult, type CommandStreamHandlers, type CommandSpec } from "../process/CommandRunner.js";
|
|
7
|
+
import { normalizeExecutableValue } from "../process/processValidation.js";
|
|
8
|
+
import { sanitizeTerminalOutput } from "../terminal/terminalSanitize.js";
|
|
9
|
+
import { launchProviderCli, type LaunchProviderCliOptions, type ProviderLaunchResult } from "../providerLauncher/launcher.js";
|
|
10
|
+
import type { ProviderConfig } from "../providerLauncher/types.js";
|
|
11
|
+
import type { BackendRunHandlers } from "../providers/types.js";
|
|
12
|
+
import type { ProviderChatRequest, ProviderModel, ProviderModelDiscoveryResult, ProviderRouteValidationResult, ProviderRuntime } from "./types.js";
|
|
13
|
+
|
|
14
|
+
const VIBE_LOOKUP_TIMEOUT_MS = 5_000;
|
|
15
|
+
const VIBE_RUN_TIMEOUT_MS = 600_000;
|
|
16
|
+
export const VIBE_DEFAULT_MODEL_LABEL = "Vibe default";
|
|
17
|
+
|
|
18
|
+
export const MISTRAL_VIBE_MISSING_MESSAGE =
|
|
19
|
+
"`vibe` is not available. Install Mistral Vibe CLI and authenticate it with `vibe --setup`, then try again.";
|
|
20
|
+
|
|
21
|
+
export const MISTRAL_VIBE_AUTH_MESSAGE =
|
|
22
|
+
"Mistral Vibe CLI is not authenticated. Run `vibe` in a terminal and sign in, then retry.";
|
|
23
|
+
|
|
24
|
+
type CommandResultSubset = Pick<CommandResult, "status" | "exitCode" | "stdout">;
|
|
25
|
+
type ShellCommandRunner = (
|
|
26
|
+
command: string,
|
|
27
|
+
options: { cwd: string; timeoutMs?: number },
|
|
28
|
+
) => { result: Promise<CommandResultSubset> };
|
|
29
|
+
type DirectCommandRunner = (
|
|
30
|
+
spec: { executable: string; args: string[]; cwd: string; timeoutMs?: number },
|
|
31
|
+
) => { result: Promise<CommandResultSubset> };
|
|
32
|
+
|
|
33
|
+
export interface VibeModelDetection {
|
|
34
|
+
modelId: string;
|
|
35
|
+
source: "environment" | "project-config" | "user-config" | "default";
|
|
36
|
+
configPath: string | null;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function readActiveModel(filePath: string): string | null {
|
|
40
|
+
if (!existsSync(filePath)) return null;
|
|
41
|
+
try {
|
|
42
|
+
const parsed = parseTomlDocument(readFileSync(filePath, "utf-8"));
|
|
43
|
+
return typeof parsed.active_model === "string" && parsed.active_model.trim()
|
|
44
|
+
? parsed.active_model.trim()
|
|
45
|
+
: null;
|
|
46
|
+
} catch {
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function findProjectVibeConfig(cwd: string, vibeHome: string): string | null {
|
|
52
|
+
let current = resolve(cwd);
|
|
53
|
+
const stopDirectory = dirname(resolve(vibeHome));
|
|
54
|
+
|
|
55
|
+
while (current !== stopDirectory) {
|
|
56
|
+
const candidate = join(current, ".vibe", "config.toml");
|
|
57
|
+
if (existsSync(candidate)) return candidate;
|
|
58
|
+
const parent = dirname(current);
|
|
59
|
+
if (parent === current) break;
|
|
60
|
+
current = parent;
|
|
61
|
+
}
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function detectVibeActiveModel(options: {
|
|
66
|
+
cwd?: string;
|
|
67
|
+
env?: NodeJS.ProcessEnv;
|
|
68
|
+
homeDirectory?: string;
|
|
69
|
+
} = {}): VibeModelDetection {
|
|
70
|
+
const env = options.env ?? process.env;
|
|
71
|
+
const environmentModel = env.VIBE_ACTIVE_MODEL?.trim();
|
|
72
|
+
if (environmentModel) {
|
|
73
|
+
return { modelId: environmentModel, source: "environment", configPath: null };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const homeDirectory = options.homeDirectory ?? homedir();
|
|
77
|
+
const vibeHome = env.VIBE_HOME?.trim() || join(homeDirectory, ".vibe");
|
|
78
|
+
const projectConfig = findProjectVibeConfig(options.cwd ?? process.cwd(), vibeHome);
|
|
79
|
+
if (projectConfig) {
|
|
80
|
+
const projectModel = readActiveModel(projectConfig);
|
|
81
|
+
if (projectModel) {
|
|
82
|
+
return { modelId: projectModel, source: "project-config", configPath: projectConfig };
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const userConfig = join(vibeHome, "config.toml");
|
|
87
|
+
const userModel = readActiveModel(userConfig);
|
|
88
|
+
if (userModel) {
|
|
89
|
+
return { modelId: userModel, source: "user-config", configPath: userConfig };
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return { modelId: VIBE_DEFAULT_MODEL_LABEL, source: "default", configPath: null };
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
interface VibeConfigModelEntry {
|
|
96
|
+
name: string;
|
|
97
|
+
alias: string;
|
|
98
|
+
provider: string | null;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Vibe resolves `active_model` (and the VIBE_ACTIVE_MODEL override) against the
|
|
102
|
+
// model *alias*, which defaults to the model name when omitted — so the alias is
|
|
103
|
+
// the id Codexa must select and pass back.
|
|
104
|
+
function readVibeModelEntries(filePath: string): VibeConfigModelEntry[] {
|
|
105
|
+
if (!existsSync(filePath)) return [];
|
|
106
|
+
try {
|
|
107
|
+
const parsed = parseTomlDocument(readFileSync(filePath, "utf-8"));
|
|
108
|
+
const rawModels = (parsed as Record<string, unknown>).models;
|
|
109
|
+
if (!Array.isArray(rawModels)) return [];
|
|
110
|
+
const entries: VibeConfigModelEntry[] = [];
|
|
111
|
+
for (const raw of rawModels) {
|
|
112
|
+
if (!raw || typeof raw !== "object") continue;
|
|
113
|
+
const record = raw as Record<string, unknown>;
|
|
114
|
+
const name = typeof record.name === "string" ? record.name.trim() : "";
|
|
115
|
+
if (!name) continue;
|
|
116
|
+
const alias = typeof record.alias === "string" && record.alias.trim() ? record.alias.trim() : name;
|
|
117
|
+
entries.push({
|
|
118
|
+
name,
|
|
119
|
+
alias,
|
|
120
|
+
provider: typeof record.provider === "string" && record.provider.trim() ? record.provider.trim() : null,
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
return entries;
|
|
124
|
+
} catch {
|
|
125
|
+
return [];
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function vibeEntryToProviderModel(entry: VibeConfigModelEntry): ProviderModel {
|
|
130
|
+
return {
|
|
131
|
+
id: entry.alias,
|
|
132
|
+
modelId: entry.alias,
|
|
133
|
+
label: entry.alias,
|
|
134
|
+
description: entry.provider ? `${entry.name} via ${entry.provider}` : entry.name,
|
|
135
|
+
defaultReasoningLevel: null,
|
|
136
|
+
supportedReasoningLevels: null,
|
|
137
|
+
source: "config",
|
|
138
|
+
raw: entry,
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export function listVibeConfiguredModels(options: {
|
|
143
|
+
cwd?: string;
|
|
144
|
+
env?: NodeJS.ProcessEnv;
|
|
145
|
+
homeDirectory?: string;
|
|
146
|
+
} = {}): { models: ProviderModel[]; configPath: string | null } {
|
|
147
|
+
const env = options.env ?? process.env;
|
|
148
|
+
const homeDirectory = options.homeDirectory ?? homedir();
|
|
149
|
+
const vibeHome = env.VIBE_HOME?.trim() || join(homeDirectory, ".vibe");
|
|
150
|
+
const projectConfig = findProjectVibeConfig(options.cwd ?? process.cwd(), vibeHome);
|
|
151
|
+
const userConfig = join(vibeHome, "config.toml");
|
|
152
|
+
|
|
153
|
+
const seen = new Set<string>();
|
|
154
|
+
const models: ProviderModel[] = [];
|
|
155
|
+
let configPath: string | null = null;
|
|
156
|
+
for (const candidate of [projectConfig, userConfig]) {
|
|
157
|
+
if (!candidate) continue;
|
|
158
|
+
const entries = readVibeModelEntries(candidate);
|
|
159
|
+
if (entries.length > 0 && !configPath) configPath = candidate;
|
|
160
|
+
for (const entry of entries) {
|
|
161
|
+
if (seen.has(entry.alias)) continue;
|
|
162
|
+
seen.add(entry.alias);
|
|
163
|
+
models.push(vibeEntryToProviderModel(entry));
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
return { models, configPath };
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export function discoverMistralVibeModels(cwd = process.cwd()): ProviderModelDiscoveryResult {
|
|
170
|
+
const detected = detectVibeActiveModel({ cwd });
|
|
171
|
+
const listed = listVibeConfiguredModels({ cwd });
|
|
172
|
+
|
|
173
|
+
// The active model must be first: registry consumers read models[0] as the default route model.
|
|
174
|
+
const models: ProviderModel[] = [];
|
|
175
|
+
const activeFromList = listed.models.find((model) => model.modelId === detected.modelId);
|
|
176
|
+
if (activeFromList) {
|
|
177
|
+
models.push(activeFromList, ...listed.models.filter((model) => model !== activeFromList));
|
|
178
|
+
} else {
|
|
179
|
+
models.push({
|
|
180
|
+
id: detected.modelId,
|
|
181
|
+
modelId: detected.modelId,
|
|
182
|
+
label: detected.modelId,
|
|
183
|
+
description: "Active model reported by Mistral Vibe configuration.",
|
|
184
|
+
defaultReasoningLevel: null,
|
|
185
|
+
supportedReasoningLevels: null,
|
|
186
|
+
source: detected.source === "default" ? "fallback" : "config",
|
|
187
|
+
raw: {
|
|
188
|
+
source: detected.source,
|
|
189
|
+
configPath: detected.configPath,
|
|
190
|
+
},
|
|
191
|
+
}, ...listed.models);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
return {
|
|
195
|
+
status: "ready",
|
|
196
|
+
providerId: "mistral",
|
|
197
|
+
backendKind: "mistral-vibe-cli-auth",
|
|
198
|
+
models,
|
|
199
|
+
diagnostics: {
|
|
200
|
+
selectedModel: detected.modelId,
|
|
201
|
+
modelSource: detected.source,
|
|
202
|
+
configPath: detected.configPath ?? listed.configPath,
|
|
203
|
+
modelCount: models.length,
|
|
204
|
+
},
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function firstOutputLine(result: CommandResultSubset): string | null {
|
|
209
|
+
if (result.status !== "completed" || result.exitCode !== 0) return null;
|
|
210
|
+
return result.stdout.split(/[\r\n]+/).map((line) => line.trim()).find(Boolean) ?? null;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
export async function resolveVibeExecutable(options: {
|
|
214
|
+
cwd?: string;
|
|
215
|
+
platform?: NodeJS.Platform;
|
|
216
|
+
runShellCommandImpl?: ShellCommandRunner;
|
|
217
|
+
runCommandImpl?: DirectCommandRunner;
|
|
218
|
+
} = {}): Promise<string | null> {
|
|
219
|
+
const cwd = options.cwd ?? process.cwd();
|
|
220
|
+
const platform = options.platform ?? process.platform;
|
|
221
|
+
let candidate: string | null;
|
|
222
|
+
|
|
223
|
+
if (platform === "win32") {
|
|
224
|
+
const runner = (options.runCommandImpl ?? runCommand as DirectCommandRunner)({
|
|
225
|
+
executable: "where.exe",
|
|
226
|
+
args: ["vibe"],
|
|
227
|
+
cwd,
|
|
228
|
+
timeoutMs: VIBE_LOOKUP_TIMEOUT_MS,
|
|
229
|
+
});
|
|
230
|
+
candidate = firstOutputLine(await runner.result);
|
|
231
|
+
} else {
|
|
232
|
+
const runner = (options.runShellCommandImpl ?? runShellCommand as ShellCommandRunner)(
|
|
233
|
+
"command -v vibe",
|
|
234
|
+
{ cwd, timeoutMs: VIBE_LOOKUP_TIMEOUT_MS },
|
|
235
|
+
);
|
|
236
|
+
candidate = firstOutputLine(await runner.result);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
if (!candidate) return null;
|
|
240
|
+
try {
|
|
241
|
+
return normalizeExecutableValue(candidate, {
|
|
242
|
+
label: "Mistral Vibe executable",
|
|
243
|
+
cwd,
|
|
244
|
+
allowBareExecutable: true,
|
|
245
|
+
});
|
|
246
|
+
} catch {
|
|
247
|
+
return null;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
export async function launchMistralVibeCli(
|
|
252
|
+
provider: ProviderConfig,
|
|
253
|
+
options: LaunchProviderCliOptions & {
|
|
254
|
+
resolveExecutable?: (cwd: string) => Promise<string | null>;
|
|
255
|
+
},
|
|
256
|
+
): Promise<ProviderLaunchResult> {
|
|
257
|
+
const resolveExecutable = options.resolveExecutable
|
|
258
|
+
?? ((cwd: string) => resolveVibeExecutable({ cwd }));
|
|
259
|
+
const executable = await resolveExecutable(options.cwd);
|
|
260
|
+
if (!executable) {
|
|
261
|
+
return { status: "missing-command", message: MISTRAL_VIBE_MISSING_MESSAGE };
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
return launchProviderCli({
|
|
265
|
+
...provider,
|
|
266
|
+
launchCommand: { executable, args: [] },
|
|
267
|
+
}, options);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// ─── Session continuation ────────────────────────────────────────────────────
|
|
271
|
+
// Vibe persists every programmatic run as a session directory; passing the last
|
|
272
|
+
// session id back via --resume keeps conversation context across Codexa turns.
|
|
273
|
+
|
|
274
|
+
const activeVibeSessions = new Map<string, string>();
|
|
275
|
+
|
|
276
|
+
export function getMistralVibeSessionId(workspaceRoot: string): string | null {
|
|
277
|
+
return activeVibeSessions.get(resolve(workspaceRoot)) ?? null;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
export function resetMistralVibeSession(workspaceRoot?: string): void {
|
|
281
|
+
if (workspaceRoot === undefined) {
|
|
282
|
+
activeVibeSessions.clear();
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
activeVibeSessions.delete(resolve(workspaceRoot));
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
export async function findLatestVibeSession(options: {
|
|
289
|
+
workspaceRoot: string;
|
|
290
|
+
sinceMs: number;
|
|
291
|
+
env?: NodeJS.ProcessEnv;
|
|
292
|
+
homeDirectory?: string;
|
|
293
|
+
}): Promise<string | null> {
|
|
294
|
+
try {
|
|
295
|
+
const env = options.env ?? process.env;
|
|
296
|
+
const homeDirectory = options.homeDirectory ?? homedir();
|
|
297
|
+
const vibeHome = env.VIBE_HOME?.trim() || join(homeDirectory, ".vibe");
|
|
298
|
+
const sessionRoot = join(vibeHome, "logs", "session");
|
|
299
|
+
const workspaceRoot = resolve(options.workspaceRoot);
|
|
300
|
+
const entries = await readdir(sessionRoot);
|
|
301
|
+
|
|
302
|
+
let latestStart = -Infinity;
|
|
303
|
+
let latestSessionId: string | null = null;
|
|
304
|
+
for (const entry of entries) {
|
|
305
|
+
if (!entry.startsWith("session_")) continue;
|
|
306
|
+
try {
|
|
307
|
+
const meta = JSON.parse(await readFile(join(sessionRoot, entry, "meta.json"), "utf-8")) as {
|
|
308
|
+
session_id?: unknown;
|
|
309
|
+
start_time?: unknown;
|
|
310
|
+
environment?: { working_directory?: unknown };
|
|
311
|
+
};
|
|
312
|
+
if (typeof meta.session_id !== "string" || typeof meta.start_time !== "string") continue;
|
|
313
|
+
if (meta.environment?.working_directory !== workspaceRoot) continue;
|
|
314
|
+
const startMs = Date.parse(meta.start_time);
|
|
315
|
+
// 5s slack absorbs clock skew between Codexa's spawn timestamp and vibe's own start_time.
|
|
316
|
+
if (!Number.isFinite(startMs) || startMs < options.sinceMs - 5_000) continue;
|
|
317
|
+
if (startMs > latestStart) {
|
|
318
|
+
latestStart = startMs;
|
|
319
|
+
latestSessionId = meta.session_id;
|
|
320
|
+
}
|
|
321
|
+
} catch {
|
|
322
|
+
continue;
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
return latestSessionId;
|
|
326
|
+
} catch {
|
|
327
|
+
return null;
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// ─── Streaming output parsing ────────────────────────────────────────────────
|
|
332
|
+
|
|
333
|
+
function extractVibeText(content: unknown): string {
|
|
334
|
+
if (typeof content === "string") return content;
|
|
335
|
+
if (Array.isArray(content)) {
|
|
336
|
+
return content
|
|
337
|
+
.map((item) => {
|
|
338
|
+
if (typeof item === "string") return item;
|
|
339
|
+
if (item && typeof item === "object" && typeof (item as { text?: unknown }).text === "string") {
|
|
340
|
+
return (item as { text: string }).text;
|
|
341
|
+
}
|
|
342
|
+
return "";
|
|
343
|
+
})
|
|
344
|
+
.filter(Boolean)
|
|
345
|
+
.join("\n");
|
|
346
|
+
}
|
|
347
|
+
return "";
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
function truncateForActivity(value: string, max = 120): string {
|
|
351
|
+
const collapsed = value.replace(/\s+/g, " ").trim();
|
|
352
|
+
return collapsed.length > max ? `${collapsed.slice(0, max - 1)}…` : collapsed;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
export interface VibeStreamParser {
|
|
356
|
+
push: (chunk: string) => void;
|
|
357
|
+
flush: () => void;
|
|
358
|
+
finalText: () => string;
|
|
359
|
+
assistantText: () => string;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// Parses `vibe -p --output streaming` stdout: one JSON LLMMessage per line, followed
|
|
363
|
+
// by the final assistant text repeated as plain text (which must not be emitted twice).
|
|
364
|
+
export function createVibeStreamParser(
|
|
365
|
+
handlers: BackendRunHandlers,
|
|
366
|
+
options: { startAfterUserPrompt?: string } = {},
|
|
367
|
+
): VibeStreamParser {
|
|
368
|
+
let lineBuf = "";
|
|
369
|
+
let accumulated = "";
|
|
370
|
+
const plainLines: string[] = [];
|
|
371
|
+
let sequence = 0;
|
|
372
|
+
const runningTools = new Map<string, { command: string; startedAt: number }>();
|
|
373
|
+
const resumePrompt = options.startAfterUserPrompt?.trim() || null;
|
|
374
|
+
let acceptingCurrentTurn = resumePrompt === null;
|
|
375
|
+
|
|
376
|
+
const handleMessage = (message: Record<string, unknown>) => {
|
|
377
|
+
const role = message.role;
|
|
378
|
+
if (!acceptingCurrentTurn) {
|
|
379
|
+
if (role === "user" && extractVibeText(message.content).trim() === resumePrompt) {
|
|
380
|
+
acceptingCurrentTurn = true;
|
|
381
|
+
}
|
|
382
|
+
return;
|
|
383
|
+
}
|
|
384
|
+
if (role !== "assistant" && role !== "tool") return;
|
|
385
|
+
|
|
386
|
+
if (role === "tool") {
|
|
387
|
+
const toolCallId = typeof message.tool_call_id === "string" ? message.tool_call_id : null;
|
|
388
|
+
const running = toolCallId ? runningTools.get(toolCallId) : undefined;
|
|
389
|
+
if (toolCallId && running) {
|
|
390
|
+
runningTools.delete(toolCallId);
|
|
391
|
+
handlers.onToolActivity?.({
|
|
392
|
+
id: toolCallId,
|
|
393
|
+
command: running.command,
|
|
394
|
+
status: "completed",
|
|
395
|
+
startedAt: running.startedAt,
|
|
396
|
+
completedAt: Date.now(),
|
|
397
|
+
summary: truncateForActivity(extractVibeText(message.content)) || null,
|
|
398
|
+
});
|
|
399
|
+
}
|
|
400
|
+
return;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
const reasoning = extractVibeText(message.reasoning_content);
|
|
404
|
+
if (reasoning.trim()) {
|
|
405
|
+
handlers.onProgress?.({
|
|
406
|
+
id: `vibe-reasoning-${++sequence}`,
|
|
407
|
+
source: "reasoning",
|
|
408
|
+
text: reasoning,
|
|
409
|
+
});
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
const content = extractVibeText(message.content);
|
|
413
|
+
if (content.trim()) {
|
|
414
|
+
const chunk = accumulated ? `\n\n${content}` : content;
|
|
415
|
+
accumulated += chunk;
|
|
416
|
+
handlers.onAssistantDelta?.(chunk);
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
if (Array.isArray(message.tool_calls)) {
|
|
420
|
+
for (const call of message.tool_calls) {
|
|
421
|
+
if (!call || typeof call !== "object") continue;
|
|
422
|
+
const record = call as { id?: unknown; function?: { name?: unknown; arguments?: unknown } };
|
|
423
|
+
const name = typeof record.function?.name === "string" ? record.function.name : "tool";
|
|
424
|
+
const args = typeof record.function?.arguments === "string" ? record.function.arguments : "";
|
|
425
|
+
const id = typeof record.id === "string" ? record.id : `vibe-tool-${++sequence}`;
|
|
426
|
+
const activity = {
|
|
427
|
+
id,
|
|
428
|
+
command: truncateForActivity(args ? `${name}(${args})` : name),
|
|
429
|
+
startedAt: Date.now(),
|
|
430
|
+
};
|
|
431
|
+
runningTools.set(id, activity);
|
|
432
|
+
handlers.onToolActivity?.({ ...activity, status: "running" });
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
};
|
|
436
|
+
|
|
437
|
+
const handleLine = (rawLine: string) => {
|
|
438
|
+
const line = sanitizeTerminalOutput(rawLine).trim();
|
|
439
|
+
if (!line) return;
|
|
440
|
+
try {
|
|
441
|
+
const parsed = JSON.parse(line) as unknown;
|
|
442
|
+
if (parsed && typeof parsed === "object" && typeof (parsed as { role?: unknown }).role === "string") {
|
|
443
|
+
handleMessage(parsed as Record<string, unknown>);
|
|
444
|
+
return;
|
|
445
|
+
}
|
|
446
|
+
} catch {
|
|
447
|
+
// Not a JSON message: either a pre-stream notice or the trailing plain-text
|
|
448
|
+
// duplicate of the final answer. Collected as fallback, never streamed.
|
|
449
|
+
}
|
|
450
|
+
plainLines.push(line);
|
|
451
|
+
};
|
|
452
|
+
|
|
453
|
+
return {
|
|
454
|
+
push: (chunk) => {
|
|
455
|
+
lineBuf += chunk;
|
|
456
|
+
const lines = lineBuf.split("\n");
|
|
457
|
+
lineBuf = lines.pop() ?? "";
|
|
458
|
+
for (const line of lines) handleLine(line);
|
|
459
|
+
},
|
|
460
|
+
flush: () => {
|
|
461
|
+
if (!lineBuf) return;
|
|
462
|
+
const rest = lineBuf;
|
|
463
|
+
lineBuf = "";
|
|
464
|
+
handleLine(rest);
|
|
465
|
+
},
|
|
466
|
+
finalText: () => accumulated || plainLines.join("\n").trim(),
|
|
467
|
+
assistantText: () => accumulated,
|
|
468
|
+
};
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
// ─── In-Codexa run adapter ───────────────────────────────────────────────────
|
|
472
|
+
|
|
473
|
+
type VibeCommandRunner = (
|
|
474
|
+
spec: CommandSpec,
|
|
475
|
+
handlers: CommandStreamHandlers,
|
|
476
|
+
) => { result: Promise<CommandResult>; cancel: () => void };
|
|
477
|
+
|
|
478
|
+
export interface RunMistralVibeDeps {
|
|
479
|
+
runCommandImpl?: VibeCommandRunner;
|
|
480
|
+
resolveExecutable?: (cwd: string) => Promise<string | null>;
|
|
481
|
+
env?: NodeJS.ProcessEnv;
|
|
482
|
+
findSessionImpl?: typeof findLatestVibeSession;
|
|
483
|
+
now?: () => number;
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
function isVibeAuthFailure(stderr: string): boolean {
|
|
487
|
+
return /api key|401|unauthorized|authentication/i.test(stderr);
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
export function runMistralVibe(
|
|
491
|
+
request: ProviderChatRequest,
|
|
492
|
+
handlers: BackendRunHandlers,
|
|
493
|
+
deps: RunMistralVibeDeps = {},
|
|
494
|
+
): () => void {
|
|
495
|
+
let cancelled = false;
|
|
496
|
+
let currentCancel: (() => void) | null = null;
|
|
497
|
+
const now = deps.now ?? Date.now;
|
|
498
|
+
const env = deps.env ?? process.env;
|
|
499
|
+
const runImpl = deps.runCommandImpl ?? (runCommand as VibeCommandRunner);
|
|
500
|
+
const resolveExecutable = deps.resolveExecutable ?? ((cwd: string) => resolveVibeExecutable({ cwd }));
|
|
501
|
+
const findSessionImpl = deps.findSessionImpl ?? findLatestVibeSession;
|
|
502
|
+
const workspaceRoot = request.workspaceRoot;
|
|
503
|
+
|
|
504
|
+
handlers.onProgress?.({
|
|
505
|
+
id: "mistral-route",
|
|
506
|
+
source: "stdout",
|
|
507
|
+
text: "Starting Mistral Vibe CLI",
|
|
508
|
+
});
|
|
509
|
+
|
|
510
|
+
const runAttempt = (executable: string, resumeSessionId: string | null) => {
|
|
511
|
+
const args = ["-p", "--output", "streaming", "--trust", "--auto-approve", "--workdir", workspaceRoot];
|
|
512
|
+
if (resumeSessionId) args.push("--resume", resumeSessionId);
|
|
513
|
+
|
|
514
|
+
const modelId = request.route.modelId?.trim();
|
|
515
|
+
const spawnEnv: NodeJS.ProcessEnv = { ...env };
|
|
516
|
+
if (modelId && modelId !== VIBE_DEFAULT_MODEL_LABEL) {
|
|
517
|
+
spawnEnv.VIBE_ACTIVE_MODEL = modelId;
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
const parser = createVibeStreamParser(
|
|
521
|
+
handlers,
|
|
522
|
+
resumeSessionId ? { startAfterUserPrompt: request.prompt } : undefined,
|
|
523
|
+
);
|
|
524
|
+
const spawnedAt = now();
|
|
525
|
+
const runner = runImpl(
|
|
526
|
+
{
|
|
527
|
+
executable,
|
|
528
|
+
args,
|
|
529
|
+
cwd: workspaceRoot,
|
|
530
|
+
env: spawnEnv,
|
|
531
|
+
timeoutMs: VIBE_RUN_TIMEOUT_MS,
|
|
532
|
+
stdinData: request.prompt,
|
|
533
|
+
},
|
|
534
|
+
{
|
|
535
|
+
onStdout: (chunk) => {
|
|
536
|
+
if (!cancelled) parser.push(chunk);
|
|
537
|
+
},
|
|
538
|
+
onProcessLifecycle: (event) => {
|
|
539
|
+
handlers.onProcessLifecycle?.(event === "cancel" ? "cleanup" : event);
|
|
540
|
+
},
|
|
541
|
+
},
|
|
542
|
+
);
|
|
543
|
+
currentCancel = runner.cancel;
|
|
544
|
+
|
|
545
|
+
runner.result.then((result) => {
|
|
546
|
+
if (cancelled || result.status === "canceled") return;
|
|
547
|
+
parser.flush();
|
|
548
|
+
|
|
549
|
+
if (result.status !== "completed" || result.exitCode !== 0) {
|
|
550
|
+
if (isVibeAuthFailure(result.stderr)) {
|
|
551
|
+
handlers.onError(MISTRAL_VIBE_AUTH_MESSAGE, result.stderr);
|
|
552
|
+
return;
|
|
553
|
+
}
|
|
554
|
+
if (result.status === "spawn_error" && result.errorCode === "ENOENT") {
|
|
555
|
+
handlers.onError(MISTRAL_VIBE_MISSING_MESSAGE);
|
|
556
|
+
return;
|
|
557
|
+
}
|
|
558
|
+
if (resumeSessionId && /session|not found|resume/i.test(result.stderr)) {
|
|
559
|
+
resetMistralVibeSession(workspaceRoot);
|
|
560
|
+
handlers.onProgress?.({
|
|
561
|
+
id: "vibe-resume-retry",
|
|
562
|
+
source: "stderr",
|
|
563
|
+
text: "Saved Vibe session could not be resumed; retrying with a fresh session.",
|
|
564
|
+
});
|
|
565
|
+
runAttempt(executable, null);
|
|
566
|
+
return;
|
|
567
|
+
}
|
|
568
|
+
handlers.onError(
|
|
569
|
+
result.userMessage || "Mistral Vibe execution failed.",
|
|
570
|
+
result.stderr.trim() || undefined,
|
|
571
|
+
);
|
|
572
|
+
return;
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
const finalText = parser.finalText() || sanitizeTerminalOutput(result.stdout).trim();
|
|
576
|
+
if (!parser.assistantText() && finalText) {
|
|
577
|
+
handlers.onAssistantDelta?.(finalText);
|
|
578
|
+
}
|
|
579
|
+
handlers.onFinalAnswerObserved?.(finalText);
|
|
580
|
+
handlers.onResponse(finalText);
|
|
581
|
+
|
|
582
|
+
void findSessionImpl({ workspaceRoot, sinceMs: spawnedAt, env })
|
|
583
|
+
.then((sessionId) => {
|
|
584
|
+
if (sessionId) activeVibeSessions.set(resolve(workspaceRoot), sessionId);
|
|
585
|
+
})
|
|
586
|
+
.catch(() => undefined);
|
|
587
|
+
}).catch((error) => {
|
|
588
|
+
if (cancelled) return;
|
|
589
|
+
handlers.onError(error instanceof Error ? error.message : "Mistral Vibe execution failed.");
|
|
590
|
+
});
|
|
591
|
+
};
|
|
592
|
+
|
|
593
|
+
void (async () => {
|
|
594
|
+
const executable = await resolveExecutable(workspaceRoot);
|
|
595
|
+
if (cancelled) return;
|
|
596
|
+
if (!executable) {
|
|
597
|
+
handlers.onError(MISTRAL_VIBE_MISSING_MESSAGE);
|
|
598
|
+
return;
|
|
599
|
+
}
|
|
600
|
+
runAttempt(executable, getMistralVibeSessionId(workspaceRoot));
|
|
601
|
+
})();
|
|
602
|
+
|
|
603
|
+
return () => {
|
|
604
|
+
cancelled = true;
|
|
605
|
+
currentCancel?.();
|
|
606
|
+
};
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
export async function validateMistralVibeRoute(options: {
|
|
610
|
+
cwd: string;
|
|
611
|
+
resolveExecutable?: (cwd: string) => Promise<string | null>;
|
|
612
|
+
} = { cwd: process.cwd() }): Promise<ProviderRouteValidationResult> {
|
|
613
|
+
const resolveExecutable = options.resolveExecutable ?? ((cwd: string) => resolveVibeExecutable({ cwd }));
|
|
614
|
+
const executable = await resolveExecutable(options.cwd);
|
|
615
|
+
if (!executable) {
|
|
616
|
+
return {
|
|
617
|
+
status: "not-configured",
|
|
618
|
+
providerId: "mistral",
|
|
619
|
+
backendKind: "unavailable",
|
|
620
|
+
message: MISTRAL_VIBE_MISSING_MESSAGE,
|
|
621
|
+
diagnostics: { resolvedCommand: null },
|
|
622
|
+
};
|
|
623
|
+
}
|
|
624
|
+
const detected = detectVibeActiveModel({ cwd: options.cwd });
|
|
625
|
+
return {
|
|
626
|
+
status: "ready",
|
|
627
|
+
providerId: "mistral",
|
|
628
|
+
backendKind: "mistral-vibe-cli-auth",
|
|
629
|
+
message: "Mistral Vibe CLI is available.",
|
|
630
|
+
diagnostics: {
|
|
631
|
+
resolvedCommand: executable,
|
|
632
|
+
selectedModel: detected.modelId,
|
|
633
|
+
modelSource: detected.source,
|
|
634
|
+
},
|
|
635
|
+
};
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
export const mistralVibeRuntime: ProviderRuntime = {
|
|
639
|
+
providerId: "mistral",
|
|
640
|
+
label: "Mistral Vibe CLI",
|
|
641
|
+
modelPickerLabel: "Mistral Vibe",
|
|
642
|
+
backendKind: "mistral-vibe-cli-auth",
|
|
643
|
+
routeAvailable: true,
|
|
644
|
+
routeStatus: "Uses the Mistral Vibe CLI (vibe -p) with your existing vibe authentication.",
|
|
645
|
+
routeSetupMessage: MISTRAL_VIBE_MISSING_MESSAGE,
|
|
646
|
+
launchAvailable: true,
|
|
647
|
+
validateRoute: async ({ workspaceRoot }) => validateMistralVibeRoute({ cwd: workspaceRoot }),
|
|
648
|
+
discoverModels: () => discoverMistralVibeModels(),
|
|
649
|
+
refreshModels: async ({ cwd }) => {
|
|
650
|
+
const executable = await resolveVibeExecutable({ cwd });
|
|
651
|
+
if (!executable) {
|
|
652
|
+
return {
|
|
653
|
+
status: "not-configured",
|
|
654
|
+
providerId: "mistral",
|
|
655
|
+
backendKind: "unavailable",
|
|
656
|
+
models: [],
|
|
657
|
+
message: MISTRAL_VIBE_MISSING_MESSAGE,
|
|
658
|
+
};
|
|
659
|
+
}
|
|
660
|
+
return discoverMistralVibeModels(cwd);
|
|
661
|
+
},
|
|
662
|
+
run: (request, handlers) => runMistralVibe(request, handlers),
|
|
663
|
+
};
|