Skip to content

Commit 5009b88

Browse files
Srikanthmvtscghostincli
authored andcommitted
Integrated mistral vibe cli
1 parent 5d6acfb commit 5009b88

File tree

11 files changed

+900
-0
lines changed

11 files changed

+900
-0
lines changed

src/infra/engines/core/registry.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import cursorEngine from '../providers/cursor/index.js';
1212
import ccrEngine from '../providers/ccr/index.js';
1313
import opencodeEngine from '../providers/opencode/index.js';
1414
import auggieEngine from '../providers/auggie/index.js';
15+
import mistralEngine from '../providers/mistral/index.js';
1516

1617
/**
1718
* Engine Registry - Singleton that manages all available engines
@@ -38,6 +39,7 @@ class EngineRegistry {
3839
ccrEngine,
3940
opencodeEngine,
4041
auggieEngine,
42+
mistralEngine,
4143
// Add new engines here
4244
];
4345

src/infra/engines/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,4 @@ export * as claude from './providers/claude/index.js';
88
export * as ccr from './providers/ccr/index.js';
99
export * as opencode from './providers/opencode/index.js';
1010
export * as auggie from './providers/auggie/index.js';
11+
export * as mistral from './providers/mistral/index.js';
Lines changed: 201 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,201 @@
1+
import { stat, rm, writeFile, mkdir } from 'node:fs/promises';
2+
import * as path from 'node:path';
3+
import { homedir } from 'node:os';
4+
5+
import { expandHomeDir } from '../../../../shared/utils/index.js';
6+
import { metadata } from './metadata.js';
7+
8+
/**
9+
* Check if CLI is installed
10+
*/
11+
async function isCliInstalled(command: string): Promise<boolean> {
12+
try {
13+
// Resolve command using Bun.which() to handle Windows .cmd files
14+
const resolvedCommand = Bun.which(command);
15+
16+
// If command is not found in PATH, it's not installed
17+
if (!resolvedCommand) {
18+
return false;
19+
}
20+
21+
const proc = Bun.spawn([resolvedCommand, '--help'], {
22+
stdout: 'pipe',
23+
stderr: 'pipe',
24+
stdin: 'ignore',
25+
});
26+
27+
// Set a timeout
28+
const timeout = new Promise<never>((_, reject) =>
29+
setTimeout(() => reject(new Error('Timeout')), 3000)
30+
);
31+
32+
const exitCode = await Promise.race([proc.exited, timeout]);
33+
const stdout = await new Response(proc.stdout).text();
34+
const stderr = await new Response(proc.stderr).text();
35+
const out = `${stdout}\n${stderr}`;
36+
37+
// Check for error messages indicating command not found
38+
if (/not recognized as an internal or external command/i.test(out)) return false;
39+
if (/command not found/i.test(out)) return false;
40+
if (/No such file or directory/i.test(out)) return false;
41+
42+
// If exit code is 0 or we get help output, CLI is installed
43+
if (typeof exitCode === 'number' && exitCode === 0) return true;
44+
// Even if exit code is non-zero, if we got help output, CLI exists
45+
if (/usage:|vibe \[-h\]/i.test(out)) return true;
46+
47+
return false;
48+
} catch {
49+
return false;
50+
}
51+
}
52+
53+
export interface MistralAuthOptions {
54+
mistralConfigDir?: string;
55+
}
56+
57+
export function resolveMistralConfigDir(options?: MistralAuthOptions): string {
58+
if (options?.mistralConfigDir) {
59+
return expandHomeDir(options.mistralConfigDir);
60+
}
61+
62+
if (process.env.MISTRAL_CONFIG_DIR) {
63+
return expandHomeDir(process.env.MISTRAL_CONFIG_DIR);
64+
}
65+
66+
// Authentication is shared globally
67+
return path.join(homedir(), '.codemachine', 'mistral');
68+
}
69+
70+
/**
71+
* Gets the path to the credentials file
72+
* Mistral Vibe stores it at ~/.vibe/.env
73+
*/
74+
export function getCredentialsPath(configDir: string): string {
75+
// Mistral Vibe uses ~/.vibe/.env for API key
76+
const vibeDir = path.join(homedir(), '.vibe');
77+
return path.join(vibeDir, '.env');
78+
}
79+
80+
/**
81+
* Gets paths to all Mistral-related files that need to be cleaned up
82+
* CodeMachine should not manage Vibe's credentials - it only checks if they exist.
83+
*/
84+
export function getMistralAuthPaths(configDir: string): string[] {
85+
// Only return CodeMachine-specific paths, not Vibe's actual credentials
86+
// Mistral Vibe manages its own credentials at ~/.vibe/.env
87+
return [
88+
// Add any CodeMachine-specific auth files here if needed in the future
89+
// For now, we don't manage any CodeMachine-specific Mistral auth files
90+
];
91+
}
92+
93+
/**
94+
* Checks if Mistral is authenticated
95+
*/
96+
export async function isAuthenticated(options?: MistralAuthOptions): Promise<boolean> {
97+
// Check if token is set via environment variable
98+
if (process.env.MISTRAL_API_KEY) {
99+
return true;
100+
}
101+
102+
const credPath = getCredentialsPath(resolveMistralConfigDir(options));
103+
104+
try {
105+
await stat(credPath);
106+
return true;
107+
} catch (_error) {
108+
return false;
109+
}
110+
}
111+
112+
/**
113+
* Ensures Mistral is authenticated, running setup-token if needed
114+
*/
115+
export async function ensureAuth(options?: MistralAuthOptions): Promise<boolean> {
116+
// Check if token is already set via environment variable
117+
if (process.env.MISTRAL_API_KEY) {
118+
return true;
119+
}
120+
121+
const configDir = resolveMistralConfigDir(options);
122+
const credPath = getCredentialsPath(configDir);
123+
124+
// If already authenticated, nothing to do
125+
try {
126+
await stat(credPath);
127+
return true;
128+
} catch {
129+
// Credentials file doesn't exist
130+
}
131+
132+
if (process.env.CODEMACHINE_SKIP_AUTH === '1') {
133+
// Create a placeholder for testing/dry-run mode
134+
const vibeDir = path.dirname(credPath);
135+
await mkdir(vibeDir, { recursive: true });
136+
await writeFile(credPath, 'MISTRAL_API_KEY=placeholder', { encoding: 'utf8' });
137+
return true;
138+
}
139+
140+
// Check if CLI is installed
141+
const cliInstalled = await isCliInstalled(metadata.cliBinary);
142+
if (!cliInstalled) {
143+
console.error(`\n────────────────────────────────────────────────────────────`);
144+
console.error(` ⚠️ ${metadata.name} CLI Not Installed`);
145+
console.error(`────────────────────────────────────────────────────────────`);
146+
console.error(`\nThe '${metadata.cliBinary}' command is not available.`);
147+
console.error(`Please install ${metadata.name} CLI first:\n`);
148+
console.error(` ${metadata.installCommand}\n`);
149+
console.error(`────────────────────────────────────────────────────────────\n`);
150+
throw new Error(`${metadata.name} CLI is not installed.`);
151+
}
152+
153+
// Mistral Vibe CLI manages its own authentication
154+
// We should not interfere with its credentials file (~/.vibe/.env)
155+
// Instead, guide the user to authenticate via Vibe CLI directly
156+
console.log(`\n────────────────────────────────────────────────────────────`);
157+
console.log(` 🔐 ${metadata.name} Authentication`);
158+
console.log(`────────────────────────────────────────────────────────────`);
159+
console.log(`\n${metadata.name} CLI manages its own authentication.`);
160+
console.log(`\nTo authenticate with ${metadata.name}:\n`);
161+
console.log(`1. Run the Vibe CLI directly to set up your API key:`);
162+
console.log(` vibe\n`);
163+
console.log(`2. When prompted, enter your API key from: https://console.mistral.ai/api-keys`);
164+
console.log(`3. Vibe will store your credentials at ~/.vibe/.env`);
165+
console.log(`4. After authentication, CodeMachine will automatically detect it.\n`);
166+
console.log(`Alternatively, you can set the MISTRAL_API_KEY environment variable:\n`);
167+
console.log(` export MISTRAL_API_KEY=<your-api-key>\n`);
168+
console.log(`────────────────────────────────────────────────────────────\n`);
169+
170+
throw new Error('Authentication incomplete. Please authenticate via Vibe CLI or set MISTRAL_API_KEY environment variable.');
171+
}
172+
173+
/**
174+
* Clears all Mistral authentication data
175+
* CodeMachine does not manage Vibe's credentials - it only checks if they exist.
176+
* To clear Vibe's credentials, users should do so directly via the Vibe CLI or manually.
177+
*/
178+
export async function clearAuth(options?: MistralAuthOptions): Promise<void> {
179+
const configDir = resolveMistralConfigDir(options);
180+
const authPaths = getMistralAuthPaths(configDir);
181+
182+
// Remove only CodeMachine-specific auth files (if any)
183+
// We do NOT delete ~/.vibe/.env as that belongs to Mistral Vibe CLI
184+
await Promise.all(
185+
authPaths.map(async (authPath) => {
186+
try {
187+
await rm(authPath, { force: true });
188+
} catch (_error) {
189+
// Ignore removal errors; treat as cleared
190+
}
191+
}),
192+
);
193+
}
194+
195+
/**
196+
* Returns the next auth menu action based on current auth state
197+
*/
198+
export async function nextAuthMenuAction(options?: MistralAuthOptions): Promise<'login' | 'logout'> {
199+
return (await isAuthenticated(options)) ? 'logout' : 'login';
200+
}
201+
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
/**
2+
* Mistral engine configuration and model mapping
3+
*/
4+
5+
export interface MistralConfig {
6+
/**
7+
* Model to use for Mistral execution
8+
* Can be a Mistral model name (devstral-2, mistral-large, mistral-medium, etc.)
9+
* or a generic model name that will be mapped (gpt-5-codex, gpt-4, etc.)
10+
*/
11+
model?: string;
12+
13+
/**
14+
* Working directory for execution
15+
*/
16+
workingDir: string;
17+
18+
/**
19+
* Optional custom Mistral config directory
20+
* Defaults to ~/.codemachine/mistral
21+
*/
22+
mistralConfigDir?: string;
23+
}
24+
25+
/**
26+
* Available Mistral models
27+
*/
28+
export const MISTRAL_MODELS = {
29+
DEVSTRAL_2: 'devstral-2',
30+
MISTRAL_LARGE: 'mistral-large',
31+
MISTRAL_MEDIUM: 'mistral-medium',
32+
MISTRAL_SMALL: 'mistral-small',
33+
} as const;
34+
35+
/**
36+
* Model mapping from generic model names to Mistral models
37+
* This allows using config with 'gpt-5-codex' or 'gpt-4' to map to Mistral models
38+
*/
39+
export const MODEL_MAPPING: Record<string, string> = {
40+
// Map common model names to Mistral equivalents
41+
'gpt-5-codex': MISTRAL_MODELS.DEVSTRAL_2,
42+
'gpt-4': MISTRAL_MODELS.MISTRAL_LARGE,
43+
'gpt-4-turbo': MISTRAL_MODELS.MISTRAL_LARGE,
44+
'gpt-3.5-turbo': MISTRAL_MODELS.MISTRAL_SMALL,
45+
'o1-preview': MISTRAL_MODELS.DEVSTRAL_2,
46+
'o1-mini': MISTRAL_MODELS.MISTRAL_LARGE,
47+
};
48+
49+
/**
50+
* Resolves a model name to a Mistral model
51+
* Returns undefined if the model should use Mistral's default
52+
*/
53+
export function resolveModel(model?: string): string | undefined {
54+
if (!model) {
55+
return undefined;
56+
}
57+
58+
// Check if it's in our mapping
59+
if (model in MODEL_MAPPING) {
60+
return MODEL_MAPPING[model];
61+
}
62+
63+
// If it's already a Mistral model name, return it
64+
if (model.startsWith('mistral-') || model.startsWith('devstral-')) {
65+
return model;
66+
}
67+
68+
// Otherwise, return undefined to use Mistral's default
69+
return undefined;
70+
}
71+
72+
/**
73+
* Default timeout for Mistral operations (30 minutes)
74+
*/
75+
export const DEFAULT_TIMEOUT = 1800000;
76+
77+
/**
78+
* Environment variable names
79+
*/
80+
export const ENV = {
81+
MISTRAL_CONFIG_DIR: 'MISTRAL_CONFIG_DIR',
82+
SKIP_MISTRAL: 'CODEMACHINE_SKIP_MISTRAL',
83+
SKIP_AUTH: 'CODEMACHINE_SKIP_AUTH',
84+
PLAIN_LOGS: 'CODEMACHINE_PLAIN_LOGS',
85+
} as const;
86+
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
export interface MistralCommandOptions {
2+
workingDir: string;
3+
prompt: string;
4+
model?: string;
5+
}
6+
7+
export interface MistralCommand {
8+
command: string;
9+
args: string[];
10+
}
11+
12+
/**
13+
* Model mapping from config models to Mistral model names
14+
* If model is not in this map, it will be passed as-is to Mistral
15+
*/
16+
const MODEL_MAP: Record<string, string> = {
17+
'gpt-5-codex': 'devstral-2', // Map to Devstral 2
18+
'gpt-4': 'mistral-large', // Map to Mistral Large
19+
'gpt-4-turbo': 'mistral-large',
20+
'gpt-3.5-turbo': 'mistral-small',
21+
'o1-preview': 'devstral-2',
22+
'o1-mini': 'mistral-large',
23+
};
24+
25+
/**
26+
* Maps a model name from config to Mistral's model naming convention
27+
* Returns undefined if the model should use Mistral's default
28+
*/
29+
function mapModel(model?: string): string | undefined {
30+
if (!model) {
31+
return undefined;
32+
}
33+
34+
// If it's in our mapping, use the mapped value
35+
if (model in MODEL_MAP) {
36+
return MODEL_MAP[model];
37+
}
38+
39+
// If it's already a Mistral model name, pass it through
40+
if (model.startsWith('mistral-') || model.startsWith('devstral-')) {
41+
return model;
42+
}
43+
44+
// Otherwise, don't use a model flag and let Mistral use its default
45+
return undefined;
46+
}
47+
48+
export function buildMistralExecCommand(options: MistralCommandOptions): MistralCommand {
49+
// Mistral Vibe CLI doesn't support --model flag
50+
// Model selection is done via agent configuration files at ~/.vibe/agents/
51+
// For now, we'll use the default model configured in Vibe
52+
53+
// Base args for Mistral Vibe CLI in programmatic mode
54+
// -p: programmatic mode (send prompt, auto-approve tools, output response, exit)
55+
// The prompt will be passed as an argument to -p
56+
// --auto-approve: automatically approve all tool executions
57+
// --output streaming: output newline-delimited JSON per message
58+
const args: string[] = [
59+
'-p',
60+
options.prompt, // Pass prompt as argument to -p (required by Mistral Vibe)
61+
'--auto-approve',
62+
'--output',
63+
'streaming',
64+
];
65+
66+
// Note: Model selection is not supported via CLI flags in Mistral Vibe
67+
// Users need to configure models via agent config files at ~/.vibe/agents/NAME.toml
68+
// or use the default model configured in ~/.vibe/config.toml
69+
70+
// Call vibe directly - prompt is passed as argument to -p flag
71+
return {
72+
command: 'vibe',
73+
args,
74+
};
75+
}
76+

0 commit comments

Comments
 (0)