consult-llm-mcp 2.4.0 → 2.4.2
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 +10 -5
- package/dist/config.d.ts +8 -0
- package/dist/config.js +30 -4
- package/dist/config.test.js +89 -1
- package/dist/executors/cli-runner.d.ts +17 -0
- package/dist/executors/cli-runner.js +57 -0
- package/dist/executors/codex-cli.js +15 -52
- package/dist/executors/gemini-cli.js +26 -61
- package/dist/llm-cost.js +4 -0
- package/dist/llm.test.js +4 -2
- package/dist/logger.js +2 -2
- package/dist/models.d.ts +1 -1
- package/dist/models.js +1 -0
- package/dist/schema.d.ts +1 -1
- package/dist/schema.js +2 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# Consult LLM MCP
|
|
2
2
|
|
|
3
3
|
An MCP server that lets Claude Code consult stronger AI models (GPT-5.2, Gemini
|
|
4
|
-
3.
|
|
4
|
+
3.1 Pro, DeepSeek Reasoner) when Sonnet has you running in circles and you need
|
|
5
5
|
to bring in the heavy artillery. Supports multi-turn conversations.
|
|
6
6
|
|
|
7
7
|
```
|
|
@@ -27,7 +27,7 @@ to bring in the heavy artillery. Supports multi-turn conversations.
|
|
|
27
27
|
|
|
28
28
|
## Features
|
|
29
29
|
|
|
30
|
-
- Query powerful AI models (GPT-5.2, Gemini 3.
|
|
30
|
+
- Query powerful AI models (GPT-5.2, Gemini 3.1 Pro, DeepSeek Reasoner) with
|
|
31
31
|
relevant files as context
|
|
32
32
|
- Direct queries with optional file context
|
|
33
33
|
- Include git changes for code review and analysis
|
|
@@ -333,7 +333,8 @@ context, but it helps.
|
|
|
333
333
|
#### Gemini CLI
|
|
334
334
|
|
|
335
335
|
Use Gemini's local CLI to take advantage of Google's
|
|
336
|
-
[free quota](https://developers.google.com/gemini-code-assist/resources/quotas#quotas-for-agent-mode-gemini-cli)
|
|
336
|
+
[free quota](https://developers.google.com/gemini-code-assist/resources/quotas#quotas-for-agent-mode-gemini-cli)
|
|
337
|
+
or use your Google AI Pro subscription.
|
|
337
338
|
|
|
338
339
|
**Requirements:**
|
|
339
340
|
|
|
@@ -456,7 +457,8 @@ See the "Using web mode..." example above for a concrete transcript.
|
|
|
456
457
|
- `DEEPSEEK_API_KEY` - Your DeepSeek API key (required for DeepSeek models)
|
|
457
458
|
- `CONSULT_LLM_DEFAULT_MODEL` - Override the default model (optional)
|
|
458
459
|
- Options: `gpt-5.2` (default), `gemini-2.5-pro`, `gemini-3-pro-preview`,
|
|
459
|
-
`deepseek-reasoner`, `gpt-5.3-codex`,
|
|
460
|
+
`gemini-3.1-pro-preview`, `deepseek-reasoner`, `gpt-5.3-codex`,
|
|
461
|
+
`gpt-5.2-codex`
|
|
460
462
|
- `GEMINI_BACKEND` - Backend for Gemini models (optional)
|
|
461
463
|
- Options: `api` (default), `gemini-cli`, `cursor-cli`
|
|
462
464
|
- `OPENAI_BACKEND` - Backend for OpenAI models (optional)
|
|
@@ -554,7 +556,8 @@ models complex questions.
|
|
|
554
556
|
|
|
555
557
|
- **model** (optional): LLM model to use
|
|
556
558
|
- Options: `gpt-5.2` (default), `gemini-2.5-pro`, `gemini-3-pro-preview`,
|
|
557
|
-
`deepseek-reasoner`, `gpt-5.3-codex`,
|
|
559
|
+
`gemini-3.1-pro-preview`, `deepseek-reasoner`, `gpt-5.3-codex`,
|
|
560
|
+
`gpt-5.2-codex`
|
|
558
561
|
|
|
559
562
|
- **task_mode** (optional): Controls the system prompt persona. The calling LLM
|
|
560
563
|
should choose based on the task:
|
|
@@ -589,6 +592,8 @@ models complex questions.
|
|
|
589
592
|
- **gemini-2.5-pro**: Google's Gemini 2.5 Pro ($1.25/$10 per million tokens)
|
|
590
593
|
- **gemini-3-pro-preview**: Google's Gemini 3 Pro Preview ($2/$12 per million
|
|
591
594
|
tokens for prompts ≤200k tokens, $4/$18 for prompts >200k tokens)
|
|
595
|
+
- **gemini-3.1-pro-preview**: Google's Gemini 3.1 Pro Preview ($2/$12 per
|
|
596
|
+
million tokens for prompts ≤200k tokens, $4/$18 for prompts >200k tokens)
|
|
592
597
|
- **deepseek-reasoner**: DeepSeek's reasoning model ($0.55/$2.19 per million
|
|
593
598
|
tokens)
|
|
594
599
|
- **gpt-5.2**: OpenAI's latest GPT model
|
package/dist/config.d.ts
CHANGED
|
@@ -1,4 +1,12 @@
|
|
|
1
1
|
import { z } from 'zod/v4';
|
|
2
|
+
export interface ProviderAvailability {
|
|
3
|
+
geminiApiKey?: string;
|
|
4
|
+
geminiBackend: string;
|
|
5
|
+
openaiApiKey?: string;
|
|
6
|
+
openaiBackend: string;
|
|
7
|
+
deepseekApiKey?: string;
|
|
8
|
+
}
|
|
9
|
+
export declare function filterByAvailability(models: string[], providers: ProviderAvailability): string[];
|
|
2
10
|
/** Build the final model catalog from built-in + extra + allowlist filtering. */
|
|
3
11
|
export declare function buildModelCatalog(builtinModels: readonly string[], extraModelsRaw?: string, allowedModelsRaw?: string): string[];
|
|
4
12
|
export declare const SupportedChatModel: z.ZodEnum<{
|
package/dist/config.js
CHANGED
|
@@ -1,6 +1,21 @@
|
|
|
1
1
|
import { z } from 'zod/v4';
|
|
2
2
|
import { ALL_MODELS } from './models.js';
|
|
3
3
|
import { logToFile } from './logger.js';
|
|
4
|
+
export function filterByAvailability(models, providers) {
|
|
5
|
+
return models.filter((model) => {
|
|
6
|
+
if (model.startsWith('gemini-')) {
|
|
7
|
+
return providers.geminiBackend !== 'api' || !!providers.geminiApiKey;
|
|
8
|
+
}
|
|
9
|
+
if (model.startsWith('gpt-')) {
|
|
10
|
+
return providers.openaiBackend !== 'api' || !!providers.openaiApiKey;
|
|
11
|
+
}
|
|
12
|
+
if (model.startsWith('deepseek-')) {
|
|
13
|
+
return !!providers.deepseekApiKey;
|
|
14
|
+
}
|
|
15
|
+
// Unknown prefix (user-added extra models) — always include
|
|
16
|
+
return true;
|
|
17
|
+
});
|
|
18
|
+
}
|
|
4
19
|
/** Build the final model catalog from built-in + extra + allowlist filtering. */
|
|
5
20
|
export function buildModelCatalog(builtinModels, extraModelsRaw, allowedModelsRaw) {
|
|
6
21
|
const extraModels = extraModelsRaw
|
|
@@ -23,9 +38,20 @@ export function buildModelCatalog(builtinModels, extraModelsRaw, allowedModelsRa
|
|
|
23
38
|
? allAvailable.filter((m) => allowedModels.includes(m))
|
|
24
39
|
: allAvailable;
|
|
25
40
|
}
|
|
26
|
-
|
|
41
|
+
// Resolve backends early (needed for availability filtering)
|
|
42
|
+
const resolvedGeminiBackend = migrateBackendEnv(process.env.GEMINI_BACKEND, process.env.GEMINI_MODE, 'gemini-cli', 'GEMINI_MODE', 'GEMINI_BACKEND');
|
|
43
|
+
const resolvedOpenaiBackend = migrateBackendEnv(process.env.OPENAI_BACKEND, process.env.OPENAI_MODE, 'codex-cli', 'OPENAI_MODE', 'OPENAI_BACKEND');
|
|
44
|
+
// Build catalog, then filter to only available providers
|
|
45
|
+
const catalogModels = buildModelCatalog(ALL_MODELS, process.env.CONSULT_LLM_EXTRA_MODELS, process.env.CONSULT_LLM_ALLOWED_MODELS);
|
|
46
|
+
const enabledModels = filterByAvailability(catalogModels, {
|
|
47
|
+
geminiApiKey: process.env.GEMINI_API_KEY,
|
|
48
|
+
geminiBackend: resolvedGeminiBackend ?? 'api',
|
|
49
|
+
openaiApiKey: process.env.OPENAI_API_KEY,
|
|
50
|
+
openaiBackend: resolvedOpenaiBackend ?? 'api',
|
|
51
|
+
deepseekApiKey: process.env.DEEPSEEK_API_KEY,
|
|
52
|
+
});
|
|
27
53
|
if (enabledModels.length === 0) {
|
|
28
|
-
const msg = 'Invalid environment variables:\n
|
|
54
|
+
const msg = 'Invalid environment variables:\n No models available. Set API keys or configure CLI backends.';
|
|
29
55
|
logToFile(`FATAL ERROR:\n${msg}`);
|
|
30
56
|
console.error(`❌ ${msg}`);
|
|
31
57
|
process.exit(1);
|
|
@@ -62,8 +88,8 @@ const parsedConfig = Config.safeParse({
|
|
|
62
88
|
geminiApiKey: process.env.GEMINI_API_KEY,
|
|
63
89
|
deepseekApiKey: process.env.DEEPSEEK_API_KEY,
|
|
64
90
|
defaultModel: process.env.CONSULT_LLM_DEFAULT_MODEL,
|
|
65
|
-
geminiBackend:
|
|
66
|
-
openaiBackend:
|
|
91
|
+
geminiBackend: resolvedGeminiBackend,
|
|
92
|
+
openaiBackend: resolvedOpenaiBackend,
|
|
67
93
|
codexReasoningEffort: process.env.CODEX_REASONING_EFFORT,
|
|
68
94
|
systemPromptPath: process.env.CONSULT_LLM_SYSTEM_PROMPT_PATH,
|
|
69
95
|
});
|
package/dist/config.test.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { describe, it, expect, vi } from 'vitest';
|
|
2
|
-
import { migrateBackendEnv, buildModelCatalog } from './config.js';
|
|
2
|
+
import { migrateBackendEnv, buildModelCatalog, filterByAvailability, } from './config.js';
|
|
3
3
|
import { ALL_MODELS } from './models.js';
|
|
4
4
|
vi.mock('./logger.js', () => ({ logToFile: vi.fn() }));
|
|
5
5
|
describe('migrateBackendEnv', () => {
|
|
@@ -19,6 +19,94 @@ describe('migrateBackendEnv', () => {
|
|
|
19
19
|
expect(migrateBackendEnv(undefined, 'cli', 'codex-cli', 'OPENAI_MODE', 'OPENAI_BACKEND')).toBe('codex-cli');
|
|
20
20
|
});
|
|
21
21
|
});
|
|
22
|
+
describe('filterByAvailability', () => {
|
|
23
|
+
const allModels = ['gemini-2.5-pro', 'gpt-5.2', 'deepseek-reasoner'];
|
|
24
|
+
it('includes gemini models when using gemini-cli backend', () => {
|
|
25
|
+
const result = filterByAvailability(allModels, {
|
|
26
|
+
geminiBackend: 'gemini-cli',
|
|
27
|
+
openaiBackend: 'api',
|
|
28
|
+
});
|
|
29
|
+
expect(result).toContain('gemini-2.5-pro');
|
|
30
|
+
expect(result).not.toContain('gpt-5.2');
|
|
31
|
+
});
|
|
32
|
+
it('includes gemini models when using cursor-cli backend', () => {
|
|
33
|
+
const result = filterByAvailability(allModels, {
|
|
34
|
+
geminiBackend: 'cursor-cli',
|
|
35
|
+
openaiBackend: 'api',
|
|
36
|
+
});
|
|
37
|
+
expect(result).toContain('gemini-2.5-pro');
|
|
38
|
+
});
|
|
39
|
+
it('includes gemini models when API key is set', () => {
|
|
40
|
+
const result = filterByAvailability(allModels, {
|
|
41
|
+
geminiApiKey: 'key',
|
|
42
|
+
geminiBackend: 'api',
|
|
43
|
+
openaiBackend: 'api',
|
|
44
|
+
});
|
|
45
|
+
expect(result).toContain('gemini-2.5-pro');
|
|
46
|
+
});
|
|
47
|
+
it('excludes gemini models when backend is api and no key', () => {
|
|
48
|
+
const result = filterByAvailability(allModels, {
|
|
49
|
+
geminiBackend: 'api',
|
|
50
|
+
openaiBackend: 'api',
|
|
51
|
+
});
|
|
52
|
+
expect(result).not.toContain('gemini-2.5-pro');
|
|
53
|
+
});
|
|
54
|
+
it('includes gpt models when using codex-cli backend', () => {
|
|
55
|
+
const result = filterByAvailability(allModels, {
|
|
56
|
+
geminiBackend: 'api',
|
|
57
|
+
openaiBackend: 'codex-cli',
|
|
58
|
+
});
|
|
59
|
+
expect(result).toContain('gpt-5.2');
|
|
60
|
+
expect(result).not.toContain('gemini-2.5-pro');
|
|
61
|
+
});
|
|
62
|
+
it('includes gpt models when using cursor-cli backend', () => {
|
|
63
|
+
const result = filterByAvailability(allModels, {
|
|
64
|
+
geminiBackend: 'api',
|
|
65
|
+
openaiBackend: 'cursor-cli',
|
|
66
|
+
});
|
|
67
|
+
expect(result).toContain('gpt-5.2');
|
|
68
|
+
});
|
|
69
|
+
it('includes gpt models when API key is set', () => {
|
|
70
|
+
const result = filterByAvailability(allModels, {
|
|
71
|
+
geminiBackend: 'api',
|
|
72
|
+
openaiApiKey: 'key',
|
|
73
|
+
openaiBackend: 'api',
|
|
74
|
+
});
|
|
75
|
+
expect(result).toContain('gpt-5.2');
|
|
76
|
+
});
|
|
77
|
+
it('excludes gpt models when backend is api and no key', () => {
|
|
78
|
+
const result = filterByAvailability(allModels, {
|
|
79
|
+
geminiBackend: 'api',
|
|
80
|
+
openaiBackend: 'api',
|
|
81
|
+
});
|
|
82
|
+
expect(result).not.toContain('gpt-5.2');
|
|
83
|
+
});
|
|
84
|
+
it('includes deepseek models only when API key is set', () => {
|
|
85
|
+
expect(filterByAvailability(allModels, {
|
|
86
|
+
geminiBackend: 'api',
|
|
87
|
+
openaiBackend: 'api',
|
|
88
|
+
deepseekApiKey: 'key',
|
|
89
|
+
})).toContain('deepseek-reasoner');
|
|
90
|
+
expect(filterByAvailability(allModels, {
|
|
91
|
+
geminiBackend: 'api',
|
|
92
|
+
openaiBackend: 'api',
|
|
93
|
+
})).not.toContain('deepseek-reasoner');
|
|
94
|
+
});
|
|
95
|
+
it('includes unknown-prefix models (user extras)', () => {
|
|
96
|
+
const result = filterByAvailability([...allModels, 'grok-3'], {
|
|
97
|
+
geminiBackend: 'api',
|
|
98
|
+
openaiBackend: 'api',
|
|
99
|
+
});
|
|
100
|
+
expect(result).toContain('grok-3');
|
|
101
|
+
});
|
|
102
|
+
it('returns empty array when no providers are available', () => {
|
|
103
|
+
const result = filterByAvailability(allModels, {
|
|
104
|
+
geminiBackend: 'api',
|
|
105
|
+
openaiBackend: 'api',
|
|
106
|
+
});
|
|
107
|
+
expect(result).toEqual([]);
|
|
108
|
+
});
|
|
109
|
+
});
|
|
22
110
|
describe('buildModelCatalog', () => {
|
|
23
111
|
it('returns all built-in models when no env vars are set', () => {
|
|
24
112
|
const result = buildModelCatalog(ALL_MODELS);
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export interface CliResult {
|
|
2
|
+
stdout: string;
|
|
3
|
+
stderr: string;
|
|
4
|
+
code: number | null;
|
|
5
|
+
duration: number;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Spawn a CLI process and collect its output.
|
|
9
|
+
*
|
|
10
|
+
* Two modes:
|
|
11
|
+
* - **Buffered** (default): collects all stdout into `result.stdout`
|
|
12
|
+
* - **Streaming**: pass `onLine` to process each stdout line as it arrives;
|
|
13
|
+
* `result.stdout` will be empty
|
|
14
|
+
*/
|
|
15
|
+
export declare function runCli(command: string, args: string[], options?: {
|
|
16
|
+
onLine?: (line: string) => void;
|
|
17
|
+
}): Promise<CliResult>;
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { createInterface } from 'node:readline';
|
|
2
|
+
import { spawn } from 'node:child_process';
|
|
3
|
+
import { logCliDebug } from '../logger.js';
|
|
4
|
+
/**
|
|
5
|
+
* Spawn a CLI process and collect its output.
|
|
6
|
+
*
|
|
7
|
+
* Two modes:
|
|
8
|
+
* - **Buffered** (default): collects all stdout into `result.stdout`
|
|
9
|
+
* - **Streaming**: pass `onLine` to process each stdout line as it arrives;
|
|
10
|
+
* `result.stdout` will be empty
|
|
11
|
+
*/
|
|
12
|
+
export function runCli(command, args, options) {
|
|
13
|
+
return new Promise((resolve, reject) => {
|
|
14
|
+
try {
|
|
15
|
+
logCliDebug(`Spawning ${command} CLI`, {
|
|
16
|
+
promptLength: args[args.length - 1]?.length,
|
|
17
|
+
args,
|
|
18
|
+
});
|
|
19
|
+
const child = spawn(command, args, {
|
|
20
|
+
shell: false,
|
|
21
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
22
|
+
});
|
|
23
|
+
let stdout = '';
|
|
24
|
+
let stderr = '';
|
|
25
|
+
const startTime = Date.now();
|
|
26
|
+
child.on('spawn', () => logCliDebug(`${command} CLI process spawned successfully`));
|
|
27
|
+
if (options?.onLine) {
|
|
28
|
+
const rl = createInterface({ input: child.stdout, terminal: false });
|
|
29
|
+
rl.on('line', options.onLine);
|
|
30
|
+
}
|
|
31
|
+
else {
|
|
32
|
+
child.stdout.setEncoding('utf8');
|
|
33
|
+
child.stdout.on('data', (chunk) => (stdout += chunk));
|
|
34
|
+
}
|
|
35
|
+
child.stderr.on('data', (data) => (stderr += data.toString()));
|
|
36
|
+
child.on('close', (code) => {
|
|
37
|
+
const duration = Date.now() - startTime;
|
|
38
|
+
logCliDebug(`${command} CLI process closed`, {
|
|
39
|
+
code,
|
|
40
|
+
duration: `${duration}ms`,
|
|
41
|
+
stdoutLength: stdout.length,
|
|
42
|
+
stderrLength: stderr.length,
|
|
43
|
+
});
|
|
44
|
+
resolve({ stdout, stderr, code, duration });
|
|
45
|
+
});
|
|
46
|
+
child.on('error', (err) => {
|
|
47
|
+
logCliDebug(`Failed to spawn ${command} CLI`, {
|
|
48
|
+
error: err.message,
|
|
49
|
+
});
|
|
50
|
+
reject(new Error(`Failed to spawn ${command} CLI. Is it installed and in PATH? Error: ${err.message}`));
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
catch (err) {
|
|
54
|
+
reject(new Error(`Synchronous error while trying to spawn ${command}: ${err instanceof Error ? err.message : String(err)}`));
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
}
|
|
@@ -1,7 +1,6 @@
|
|
|
1
|
-
import { spawn } from 'node:child_process';
|
|
2
1
|
import { relative } from 'node:path';
|
|
3
2
|
import { config } from '../config.js';
|
|
4
|
-
import {
|
|
3
|
+
import { runCli } from './cli-runner.js';
|
|
5
4
|
export function parseCodexJsonl(output) {
|
|
6
5
|
let threadId;
|
|
7
6
|
const messages = [];
|
|
@@ -61,57 +60,21 @@ export function createCodexExecutor() {
|
|
|
61
60
|
}
|
|
62
61
|
args.push('-m', model, fullPrompt);
|
|
63
62
|
}
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
threadId,
|
|
70
|
-
args,
|
|
71
|
-
});
|
|
72
|
-
const child = spawn('codex', args, {
|
|
73
|
-
shell: false,
|
|
74
|
-
stdio: ['ignore', 'pipe', 'pipe'],
|
|
75
|
-
});
|
|
76
|
-
let stdout = '';
|
|
77
|
-
let stderr = '';
|
|
78
|
-
const startTime = Date.now();
|
|
79
|
-
child.on('spawn', () => logCliDebug('codex CLI process spawned successfully'));
|
|
80
|
-
child.stdout.on('data', (data) => (stdout += data.toString()));
|
|
81
|
-
child.stderr.on('data', (data) => (stderr += data.toString()));
|
|
82
|
-
child.on('close', (code) => {
|
|
83
|
-
const duration = Date.now() - startTime;
|
|
84
|
-
logCliDebug('codex CLI process closed', {
|
|
85
|
-
code,
|
|
86
|
-
duration: `${duration}ms`,
|
|
87
|
-
stdoutLength: stdout.length,
|
|
88
|
-
stderrLength: stderr.length,
|
|
89
|
-
});
|
|
90
|
-
if (code === 0) {
|
|
91
|
-
const parsed = parseCodexJsonl(stdout);
|
|
92
|
-
if (!parsed.response) {
|
|
93
|
-
reject(new Error('No agent_message found in Codex JSONL output'));
|
|
94
|
-
return;
|
|
95
|
-
}
|
|
96
|
-
resolve({
|
|
97
|
-
response: parsed.response,
|
|
98
|
-
usage: null,
|
|
99
|
-
threadId: parsed.threadId,
|
|
100
|
-
});
|
|
101
|
-
}
|
|
102
|
-
else {
|
|
103
|
-
reject(new Error(`Codex CLI exited with code ${code ?? -1}. Error: ${stderr.trim()}`));
|
|
104
|
-
}
|
|
105
|
-
});
|
|
106
|
-
child.on('error', (err) => {
|
|
107
|
-
logCliDebug('Failed to spawn codex CLI', { error: err.message });
|
|
108
|
-
reject(new Error(`Failed to spawn codex CLI. Is it installed and in PATH? Error: ${err.message}`));
|
|
109
|
-
});
|
|
63
|
+
const { stdout, stderr, code } = await runCli('codex', args);
|
|
64
|
+
if (code === 0) {
|
|
65
|
+
const parsed = parseCodexJsonl(stdout);
|
|
66
|
+
if (!parsed.response) {
|
|
67
|
+
throw new Error('No agent_message found in Codex JSONL output');
|
|
110
68
|
}
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
69
|
+
return {
|
|
70
|
+
response: parsed.response,
|
|
71
|
+
usage: null,
|
|
72
|
+
threadId: parsed.threadId,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
else {
|
|
76
|
+
throw new Error(`Codex CLI exited with code ${code ?? -1}. Error: ${stderr.trim()}`);
|
|
77
|
+
}
|
|
115
78
|
},
|
|
116
79
|
};
|
|
117
80
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { spawn } from 'node:child_process';
|
|
2
1
|
import { relative } from 'node:path';
|
|
3
2
|
import { logCliDebug } from '../logger.js';
|
|
3
|
+
import { runCli } from './cli-runner.js';
|
|
4
4
|
export function parseGeminiJson(output) {
|
|
5
5
|
const parsed = JSON.parse(output);
|
|
6
6
|
return {
|
|
@@ -33,70 +33,35 @@ export function createGeminiExecutor() {
|
|
|
33
33
|
args.push('-r', threadId);
|
|
34
34
|
}
|
|
35
35
|
args.push('-p', message);
|
|
36
|
-
|
|
36
|
+
const { stdout, stderr, code } = await runCli('gemini', args);
|
|
37
|
+
if (code === 0) {
|
|
37
38
|
try {
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
});
|
|
48
|
-
let stdout = '';
|
|
49
|
-
let stderr = '';
|
|
50
|
-
const startTime = Date.now();
|
|
51
|
-
child.on('spawn', () => logCliDebug('gemini CLI process spawned successfully'));
|
|
52
|
-
child.stdout.on('data', (data) => (stdout += data.toString()));
|
|
53
|
-
child.stderr.on('data', (data) => (stderr += data.toString()));
|
|
54
|
-
child.on('close', (code) => {
|
|
55
|
-
const duration = Date.now() - startTime;
|
|
56
|
-
logCliDebug('gemini CLI process closed', {
|
|
57
|
-
code,
|
|
58
|
-
duration: `${duration}ms`,
|
|
59
|
-
stdoutLength: stdout.length,
|
|
60
|
-
stderrLength: stderr.length,
|
|
61
|
-
});
|
|
62
|
-
if (code === 0) {
|
|
63
|
-
try {
|
|
64
|
-
const parsed = parseGeminiJson(stdout);
|
|
65
|
-
if (!parsed.response) {
|
|
66
|
-
reject(new Error('No response found in Gemini JSON output'));
|
|
67
|
-
return;
|
|
68
|
-
}
|
|
69
|
-
resolve({
|
|
70
|
-
response: parsed.response,
|
|
71
|
-
usage: null,
|
|
72
|
-
threadId: parsed.sessionId,
|
|
73
|
-
});
|
|
74
|
-
}
|
|
75
|
-
catch {
|
|
76
|
-
logCliDebug('Failed to parse Gemini JSON output', {
|
|
77
|
-
rawOutput: stdout,
|
|
78
|
-
});
|
|
79
|
-
reject(new Error(`Failed to parse Gemini JSON output: ${stdout.slice(0, 200)}`));
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
else {
|
|
83
|
-
if (stderr.includes('RESOURCE_EXHAUSTED')) {
|
|
84
|
-
reject(new Error(`Gemini quota exceeded. Consider using gemini-2.0-flash model. Error: ${stderr.trim()}`));
|
|
85
|
-
}
|
|
86
|
-
else {
|
|
87
|
-
reject(new Error(`Gemini CLI exited with code ${code ?? -1}. Error: ${stderr.trim()}`));
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
});
|
|
91
|
-
child.on('error', (err) => {
|
|
92
|
-
logCliDebug('Failed to spawn gemini CLI', { error: err.message });
|
|
93
|
-
reject(new Error(`Failed to spawn gemini CLI. Is it installed and in PATH? Error: ${err.message}`));
|
|
94
|
-
});
|
|
39
|
+
const parsed = parseGeminiJson(stdout);
|
|
40
|
+
if (!parsed.response) {
|
|
41
|
+
throw new Error('No response found in Gemini JSON output');
|
|
42
|
+
}
|
|
43
|
+
return {
|
|
44
|
+
response: parsed.response,
|
|
45
|
+
usage: null,
|
|
46
|
+
threadId: parsed.sessionId,
|
|
47
|
+
};
|
|
95
48
|
}
|
|
96
49
|
catch (err) {
|
|
97
|
-
|
|
50
|
+
if (err instanceof Error && err.message.includes('No response')) {
|
|
51
|
+
throw err;
|
|
52
|
+
}
|
|
53
|
+
logCliDebug('Failed to parse Gemini JSON output', {
|
|
54
|
+
rawOutput: stdout,
|
|
55
|
+
});
|
|
56
|
+
throw new Error(`Failed to parse Gemini JSON output: ${stdout.slice(0, 200)}`);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
else {
|
|
60
|
+
if (stderr.includes('RESOURCE_EXHAUSTED')) {
|
|
61
|
+
throw new Error(`Gemini quota exceeded. Consider using gemini-2.0-flash model. Error: ${stderr.trim()}`);
|
|
98
62
|
}
|
|
99
|
-
|
|
63
|
+
throw new Error(`Gemini CLI exited with code ${code ?? -1}. Error: ${stderr.trim()}`);
|
|
64
|
+
}
|
|
100
65
|
},
|
|
101
66
|
};
|
|
102
67
|
}
|
package/dist/llm-cost.js
CHANGED
|
@@ -11,6 +11,10 @@ const MODEL_PRICING = {
|
|
|
11
11
|
inputCostPerMillion: 2.0,
|
|
12
12
|
outputCostPerMillion: 12.0,
|
|
13
13
|
},
|
|
14
|
+
'gemini-3.1-pro-preview': {
|
|
15
|
+
inputCostPerMillion: 2.0,
|
|
16
|
+
outputCostPerMillion: 12.0,
|
|
17
|
+
},
|
|
14
18
|
'deepseek-reasoner': {
|
|
15
19
|
inputCostPerMillion: 0.55,
|
|
16
20
|
outputCostPerMillion: 2.19,
|
package/dist/llm.test.js
CHANGED
|
@@ -18,7 +18,7 @@ vi.mock('./logger.js', () => ({
|
|
|
18
18
|
logCliDebug: logCliDebugMock,
|
|
19
19
|
logToFile: vi.fn(),
|
|
20
20
|
}));
|
|
21
|
-
vi.mock('child_process', () => ({ spawn: spawnMock }));
|
|
21
|
+
vi.mock('node:child_process', () => ({ spawn: spawnMock }));
|
|
22
22
|
vi.mock('openai', () => {
|
|
23
23
|
class MockOpenAI {
|
|
24
24
|
chat = {
|
|
@@ -35,7 +35,9 @@ vi.mock('openai', () => {
|
|
|
35
35
|
});
|
|
36
36
|
const createChildProcess = () => {
|
|
37
37
|
const child = new EventEmitter();
|
|
38
|
-
|
|
38
|
+
const stdout = new EventEmitter();
|
|
39
|
+
stdout.setEncoding = vi.fn();
|
|
40
|
+
child.stdout = stdout;
|
|
39
41
|
child.stderr = new EventEmitter();
|
|
40
42
|
child.kill = vi.fn();
|
|
41
43
|
return child;
|
package/dist/logger.js
CHANGED
|
@@ -64,7 +64,7 @@ export function logConfiguration(config) {
|
|
|
64
64
|
}
|
|
65
65
|
export function logCliDebug(message, data) {
|
|
66
66
|
const logMessage = data
|
|
67
|
-
? `
|
|
68
|
-
: `
|
|
67
|
+
? `CLI: ${message}\n${JSON.stringify(data, null, 2)}`
|
|
68
|
+
: `CLI: ${message}`;
|
|
69
69
|
logToFile(logMessage);
|
|
70
70
|
}
|
package/dist/models.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const ALL_MODELS: readonly ["gemini-2.5-pro", "gemini-3-pro-preview", "deepseek-reasoner", "gpt-5.2", "gpt-5.3-codex", "gpt-5.2-codex"];
|
|
1
|
+
export declare const ALL_MODELS: readonly ["gemini-2.5-pro", "gemini-3-pro-preview", "gemini-3.1-pro-preview", "deepseek-reasoner", "gpt-5.2", "gpt-5.3-codex", "gpt-5.2-codex"];
|
package/dist/models.js
CHANGED
package/dist/schema.d.ts
CHANGED
|
@@ -34,7 +34,7 @@ export declare const ConsultLlmArgs: z.ZodObject<{
|
|
|
34
34
|
}, z.core.$strip>;
|
|
35
35
|
export declare const toolSchema: {
|
|
36
36
|
readonly name: "consult_llm";
|
|
37
|
-
readonly description: "Ask a more powerful AI for help with complex problems. Provide your question in the prompt field and always include relevant code files as context.\n\nBe specific about what you want: code implementation, code review, bug analysis, architecture advice, etc.\n\nIMPORTANT: Ask neutral, open-ended questions. Avoid suggesting specific solutions or alternatives in your prompt as this can bias the analysis. Instead of \"Should I use X or Y approach?\", ask \"What's the best approach for this problem?\" Let the consultant LLM provide unbiased recommendations.\n\nFor multi-turn conversations with CLI backends (Codex, Gemini CLI, Cursor CLI), the response includes a [thread_id:xxx] prefix. Extract this ID and pass it as the thread_id parameter in follow-up requests to maintain conversation context.";
|
|
37
|
+
readonly description: "Ask a more powerful AI for help with complex problems. Provide your question in the prompt field and always include relevant code files as context.\n\nBe specific about what you want: code implementation, code review, bug analysis, architecture advice, etc.\n\nIMPORTANT: Do NOT paste file contents into the prompt field. File contents are automatically read and included by the server when you pass file paths in the `files` parameter. The prompt should only contain your question or instructions.\n\nIMPORTANT: Ask neutral, open-ended questions. Avoid suggesting specific solutions or alternatives in your prompt as this can bias the analysis. Instead of \"Should I use X or Y approach?\", ask \"What's the best approach for this problem?\" Let the consultant LLM provide unbiased recommendations.\n\nFor multi-turn conversations with CLI backends (Codex, Gemini CLI, Cursor CLI), the response includes a [thread_id:xxx] prefix. Extract this ID and pass it as the thread_id parameter in follow-up requests to maintain conversation context.";
|
|
38
38
|
readonly inputSchema: z.core.ZodStandardJSONSchemaPayload<z.ZodObject<{
|
|
39
39
|
files: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
40
40
|
prompt: z.ZodString;
|
package/dist/schema.js
CHANGED
|
@@ -55,6 +55,8 @@ export const toolSchema = {
|
|
|
55
55
|
|
|
56
56
|
Be specific about what you want: code implementation, code review, bug analysis, architecture advice, etc.
|
|
57
57
|
|
|
58
|
+
IMPORTANT: Do NOT paste file contents into the prompt field. File contents are automatically read and included by the server when you pass file paths in the \`files\` parameter. The prompt should only contain your question or instructions.
|
|
59
|
+
|
|
58
60
|
IMPORTANT: Ask neutral, open-ended questions. Avoid suggesting specific solutions or alternatives in your prompt as this can bias the analysis. Instead of "Should I use X or Y approach?", ask "What's the best approach for this problem?" Let the consultant LLM provide unbiased recommendations.
|
|
59
61
|
|
|
60
62
|
For multi-turn conversations with CLI backends (Codex, Gemini CLI, Cursor CLI), the response includes a [thread_id:xxx] prefix. Extract this ID and pass it as the thread_id parameter in follow-up requests to maintain conversation context.`,
|
package/dist/version.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const GIT_HASH = "
|
|
1
|
+
export declare const GIT_HASH = "1c9e0da";
|
package/dist/version.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const GIT_HASH = "
|
|
1
|
+
export const GIT_HASH = "1c9e0da";
|