consult-llm-mcp 2.4.2 → 2.5.1
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 +65 -23
- package/dist/executors/codex-cli.js +19 -12
- package/dist/executors/gemini-cli.js +9 -0
- package/dist/external-dirs.d.ts +6 -0
- package/dist/external-dirs.js +19 -0
- package/dist/external-dirs.test.d.ts +1 -0
- package/dist/external-dirs.test.js +23 -0
- package/dist/git-worktree.d.ts +9 -0
- package/dist/git-worktree.js +40 -0
- package/dist/git-worktree.test.d.ts +1 -0
- package/dist/git-worktree.test.js +35 -0
- package/dist/llm-cost.js +4 -0
- package/dist/models.d.ts +1 -1
- package/dist/models.js +1 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -456,9 +456,9 @@ See the "Using web mode..." example above for a concrete transcript.
|
|
|
456
456
|
mode)
|
|
457
457
|
- `DEEPSEEK_API_KEY` - Your DeepSeek API key (required for DeepSeek models)
|
|
458
458
|
- `CONSULT_LLM_DEFAULT_MODEL` - Override the default model (optional)
|
|
459
|
-
- Options: `gpt-5.2` (default), `
|
|
460
|
-
`gemini-3.1-pro-preview`, `deepseek-reasoner`,
|
|
461
|
-
`gpt-5.2-codex`
|
|
459
|
+
- Options: `gpt-5.2` (default), `gpt-5.4`, `gemini-2.5-pro`,
|
|
460
|
+
`gemini-3-pro-preview`, `gemini-3.1-pro-preview`, `deepseek-reasoner`,
|
|
461
|
+
`gpt-5.3-codex`, `gpt-5.2-codex`
|
|
462
462
|
- `GEMINI_BACKEND` - Backend for Gemini models (optional)
|
|
463
463
|
- Options: `api` (default), `gemini-cli`, `cursor-cli`
|
|
464
464
|
- `OPENAI_BACKEND` - Backend for OpenAI models (optional)
|
|
@@ -555,9 +555,9 @@ models complex questions.
|
|
|
555
555
|
- All files are added as context with file paths and code blocks
|
|
556
556
|
|
|
557
557
|
- **model** (optional): LLM model to use
|
|
558
|
-
- Options: `gpt-5.2` (default), `
|
|
559
|
-
`gemini-3.1-pro-preview`, `deepseek-reasoner`,
|
|
560
|
-
`gpt-5.2-codex`
|
|
558
|
+
- Options: `gpt-5.2` (default), `gpt-5.4`, `gemini-2.5-pro`,
|
|
559
|
+
`gemini-3-pro-preview`, `gemini-3.1-pro-preview`, `deepseek-reasoner`,
|
|
560
|
+
`gpt-5.3-codex`, `gpt-5.2-codex`
|
|
561
561
|
|
|
562
562
|
- **task_mode** (optional): Controls the system prompt persona. The calling LLM
|
|
563
563
|
should choose based on the task:
|
|
@@ -596,7 +596,8 @@ models complex questions.
|
|
|
596
596
|
million tokens for prompts ≤200k tokens, $4/$18 for prompts >200k tokens)
|
|
597
597
|
- **deepseek-reasoner**: DeepSeek's reasoning model ($0.55/$2.19 per million
|
|
598
598
|
tokens)
|
|
599
|
-
- **gpt-5.
|
|
599
|
+
- **gpt-5.4**: OpenAI's GPT-5.4 model ($2.50/$15 per million tokens)
|
|
600
|
+
- **gpt-5.2**: OpenAI's GPT-5.2 model ($1.75/$14 per million tokens)
|
|
600
601
|
- **gpt-5.3-codex**: OpenAI's Codex model based on GPT-5.3
|
|
601
602
|
- **gpt-5.2-codex**: OpenAI's Codex model based on GPT-5.2
|
|
602
603
|
|
|
@@ -679,21 +680,40 @@ always reliably triggered. See the [example skill](#example-skill) below.
|
|
|
679
680
|
**Recommendation:** Start with no custom activation. Use slash commands if you
|
|
680
681
|
need reliability or custom instructions.
|
|
681
682
|
|
|
682
|
-
##
|
|
683
|
+
## Installing skills
|
|
683
684
|
|
|
684
|
-
|
|
685
|
-
that uses the `consult_llm` MCP tool to create commands like "ask gemini" or
|
|
686
|
-
"ask codex". See [skills/consult/SKILL.md](skills/consult/SKILL.md) for the full
|
|
687
|
-
content.
|
|
685
|
+
Install all skills globally with a single command:
|
|
688
686
|
|
|
689
|
-
|
|
690
|
-
|
|
687
|
+
```bash
|
|
688
|
+
curl -fsSL https://raw.githubusercontent.com/raine/consult-llm-mcp/main/scripts/install-skills | bash
|
|
689
|
+
```
|
|
690
|
+
|
|
691
|
+
This installs skills for all detected platforms:
|
|
692
|
+
|
|
693
|
+
- **Claude Code** → `~/.claude/skills/`
|
|
694
|
+
- **OpenCode** → `~/.config/opencode/skills/`
|
|
695
|
+
- **Codex** → `~/.codex/skills/`
|
|
696
|
+
|
|
697
|
+
To uninstall:
|
|
698
|
+
|
|
699
|
+
```bash
|
|
700
|
+
curl -fsSL https://raw.githubusercontent.com/raine/consult-llm-mcp/main/scripts/install-skills | bash -s uninstall
|
|
701
|
+
```
|
|
702
|
+
|
|
703
|
+
## Skills
|
|
704
|
+
|
|
705
|
+
### consult
|
|
706
|
+
|
|
707
|
+
An example [Claude Code skill](https://code.claude.com/docs/en/skills) that uses
|
|
708
|
+
the `consult_llm` MCP tool to create commands like "ask gemini" or "ask codex".
|
|
709
|
+
See [skills/consult/SKILL.md](skills/consult/SKILL.md) for the full content.
|
|
691
710
|
|
|
692
|
-
|
|
693
|
-
from the schema that "
|
|
694
|
-
|
|
711
|
+
Type "ask gemini about X" or "ask codex about X" in Claude Code. This is not
|
|
712
|
+
strictly necessary since Claude can infer from the schema that "ask gemini"
|
|
713
|
+
should call this MCP, but it gives more precise control over how the agent calls
|
|
714
|
+
this MCP.
|
|
695
715
|
|
|
696
|
-
##
|
|
716
|
+
## Slash command
|
|
697
717
|
|
|
698
718
|
Here's an example
|
|
699
719
|
[Claude Code slash command](https://code.claude.com/docs/en/slash-commands) that
|
|
@@ -703,12 +723,34 @@ for the full content.
|
|
|
703
723
|
Save it as `~/.claude/commands/consult.md` and you can then use it by typing
|
|
704
724
|
`/consult ask gemini about X` or `/consult ask codex about X` in Claude Code.
|
|
705
725
|
|
|
706
|
-
##
|
|
726
|
+
## Multi-LLM skills
|
|
707
727
|
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
728
|
+
Skills that orchestrate multi-turn conversations between LLMs. All use
|
|
729
|
+
`thread_id` to maintain conversation context across rounds, so each LLM
|
|
730
|
+
remembers the full history without resending everything.
|
|
731
|
+
|
|
732
|
+
### collab
|
|
733
|
+
|
|
734
|
+
**Collaborative ideation.** Gemini and Codex independently brainstorm ideas,
|
|
735
|
+
then build on each other's suggestions across multiple rounds. Unlike debate,
|
|
736
|
+
the tone is cooperative — refining and combining rather than critiquing. Claude
|
|
737
|
+
synthesizes the strongest ideas into a plan and implements. See
|
|
738
|
+
[skills/collab/SKILL.md](skills/collab/SKILL.md).
|
|
739
|
+
|
|
740
|
+
```
|
|
741
|
+
> /collab how should we handle offline sync for the mobile app
|
|
742
|
+
```
|
|
743
|
+
|
|
744
|
+
### collab-vs
|
|
745
|
+
|
|
746
|
+
**Claude brainstorms with one LLM.** Claude and an opponent (Gemini or Codex)
|
|
747
|
+
take turns building on each other's ideas. Like collab, but Claude participates
|
|
748
|
+
directly instead of moderating. See
|
|
749
|
+
[skills/collab-vs/SKILL.md](skills/collab-vs/SKILL.md).
|
|
750
|
+
|
|
751
|
+
```
|
|
752
|
+
> /collab-vs --gemini how should we handle offline sync for the mobile app
|
|
753
|
+
```
|
|
712
754
|
|
|
713
755
|
### debate
|
|
714
756
|
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { relative } from 'node:path';
|
|
2
2
|
import { config } from '../config.js';
|
|
3
|
+
import { getExternalDirectories } from '../external-dirs.js';
|
|
4
|
+
import { getMainWorktreePath } from '../git-worktree.js';
|
|
3
5
|
import { runCli } from './cli-runner.js';
|
|
4
6
|
export function parseCodexJsonl(output) {
|
|
5
7
|
let threadId;
|
|
@@ -45,21 +47,26 @@ export function createCodexExecutor() {
|
|
|
45
47
|
const fullPrompt = threadId
|
|
46
48
|
? message // On resume, include files but skip system prompt
|
|
47
49
|
: `${systemPrompt}\n\n${message}`;
|
|
48
|
-
const args = [];
|
|
50
|
+
const args = ['exec'];
|
|
49
51
|
if (threadId) {
|
|
50
|
-
args.push('
|
|
51
|
-
if (config.codexReasoningEffort) {
|
|
52
|
-
args.push('-c', `model_reasoning_effort="${config.codexReasoningEffort}"`);
|
|
53
|
-
}
|
|
54
|
-
args.push('-m', model, threadId, fullPrompt);
|
|
52
|
+
args.push('resume');
|
|
55
53
|
}
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
54
|
+
args.push('--json', '--skip-git-repo-check');
|
|
55
|
+
if (config.codexReasoningEffort) {
|
|
56
|
+
args.push('-c', `model_reasoning_effort="${config.codexReasoningEffort}"`);
|
|
57
|
+
}
|
|
58
|
+
const extraDirs = [
|
|
59
|
+
getMainWorktreePath(),
|
|
60
|
+
...getExternalDirectories(filePaths),
|
|
61
|
+
].filter((d) => d !== null);
|
|
62
|
+
for (const dir of extraDirs) {
|
|
63
|
+
args.push('--add-dir', dir);
|
|
64
|
+
}
|
|
65
|
+
args.push('-m', model);
|
|
66
|
+
if (threadId) {
|
|
67
|
+
args.push(threadId);
|
|
62
68
|
}
|
|
69
|
+
args.push(fullPrompt);
|
|
63
70
|
const { stdout, stderr, code } = await runCli('codex', args);
|
|
64
71
|
if (code === 0) {
|
|
65
72
|
const parsed = parseCodexJsonl(stdout);
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { relative } from 'node:path';
|
|
2
|
+
import { getExternalDirectories } from '../external-dirs.js';
|
|
3
|
+
import { getMainWorktreePath } from '../git-worktree.js';
|
|
2
4
|
import { logCliDebug } from '../logger.js';
|
|
3
5
|
import { runCli } from './cli-runner.js';
|
|
4
6
|
export function parseGeminiJson(output) {
|
|
@@ -29,6 +31,13 @@ export function createGeminiExecutor() {
|
|
|
29
31
|
? messageWithFiles
|
|
30
32
|
: `${systemPrompt}\n\n${messageWithFiles}`;
|
|
31
33
|
const args = ['-m', model, '-o', 'json'];
|
|
34
|
+
const extraDirs = [
|
|
35
|
+
getMainWorktreePath(),
|
|
36
|
+
...getExternalDirectories(filePaths),
|
|
37
|
+
].filter((d) => d !== null);
|
|
38
|
+
for (const dir of extraDirs) {
|
|
39
|
+
args.push('--include-directories', dir);
|
|
40
|
+
}
|
|
32
41
|
if (threadId) {
|
|
33
42
|
args.push('-r', threadId);
|
|
34
43
|
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Given a list of absolute file paths, return the unique parent directories
|
|
3
|
+
* of files that live outside `cwd`. A directory is "external" when the
|
|
4
|
+
* relative path from `cwd` starts with `..`.
|
|
5
|
+
*/
|
|
6
|
+
export declare function getExternalDirectories(filePaths: string[] | undefined, cwd?: string): string[];
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { dirname, resolve, relative } from 'node:path';
|
|
2
|
+
/**
|
|
3
|
+
* Given a list of absolute file paths, return the unique parent directories
|
|
4
|
+
* of files that live outside `cwd`. A directory is "external" when the
|
|
5
|
+
* relative path from `cwd` starts with `..`.
|
|
6
|
+
*/
|
|
7
|
+
export function getExternalDirectories(filePaths, cwd = process.cwd()) {
|
|
8
|
+
if (!filePaths || filePaths.length === 0)
|
|
9
|
+
return [];
|
|
10
|
+
const dirs = new Set();
|
|
11
|
+
for (const filePath of filePaths) {
|
|
12
|
+
const abs = resolve(filePath);
|
|
13
|
+
const rel = relative(cwd, abs);
|
|
14
|
+
if (rel.startsWith('..')) {
|
|
15
|
+
dirs.add(dirname(abs));
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
return [...dirs];
|
|
19
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { getExternalDirectories } from './external-dirs.js';
|
|
3
|
+
describe('getExternalDirectories', () => {
|
|
4
|
+
it('returns empty array for no files', () => {
|
|
5
|
+
expect(getExternalDirectories(undefined, '/project')).toEqual([]);
|
|
6
|
+
expect(getExternalDirectories([], '/project')).toEqual([]);
|
|
7
|
+
});
|
|
8
|
+
it('returns empty array when all files are within cwd', () => {
|
|
9
|
+
expect(getExternalDirectories(['/project/src/a.ts', '/project/lib/b.ts'], '/project')).toEqual([]);
|
|
10
|
+
});
|
|
11
|
+
it('returns parent directories of external files', () => {
|
|
12
|
+
expect(getExternalDirectories(['/other/docs/readme.md'], '/project')).toEqual(['/other/docs']);
|
|
13
|
+
});
|
|
14
|
+
it('deduplicates directories', () => {
|
|
15
|
+
expect(getExternalDirectories(['/other/docs/a.md', '/other/docs/b.md'], '/project')).toEqual(['/other/docs']);
|
|
16
|
+
});
|
|
17
|
+
it('returns multiple directories for files in different locations', () => {
|
|
18
|
+
const result = getExternalDirectories(['/other/a.ts', '/another/b.ts', '/project/c.ts'], '/project');
|
|
19
|
+
expect(result).toHaveLength(2);
|
|
20
|
+
expect(result).toContain('/other');
|
|
21
|
+
expect(result).toContain('/another');
|
|
22
|
+
});
|
|
23
|
+
});
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Detect if we're running inside a git worktree and return the main
|
|
3
|
+
* worktree path. Returns `null` when we're already in the main worktree
|
|
4
|
+
* (or not in a git repo at all). The result is cached for the process
|
|
5
|
+
* lifetime since the worktree layout doesn't change at runtime.
|
|
6
|
+
*/
|
|
7
|
+
export declare function getMainWorktreePath(): string | null;
|
|
8
|
+
/** Reset the cache – useful for tests. */
|
|
9
|
+
export declare function _resetCache(): void;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { execSync } from 'node:child_process';
|
|
2
|
+
import { resolve } from 'node:path';
|
|
3
|
+
let cachedMainWorktreePath;
|
|
4
|
+
/**
|
|
5
|
+
* Detect if we're running inside a git worktree and return the main
|
|
6
|
+
* worktree path. Returns `null` when we're already in the main worktree
|
|
7
|
+
* (or not in a git repo at all). The result is cached for the process
|
|
8
|
+
* lifetime since the worktree layout doesn't change at runtime.
|
|
9
|
+
*/
|
|
10
|
+
export function getMainWorktreePath() {
|
|
11
|
+
if (cachedMainWorktreePath !== undefined)
|
|
12
|
+
return cachedMainWorktreePath;
|
|
13
|
+
cachedMainWorktreePath = detectMainWorktreePath();
|
|
14
|
+
return cachedMainWorktreePath;
|
|
15
|
+
}
|
|
16
|
+
function detectMainWorktreePath() {
|
|
17
|
+
try {
|
|
18
|
+
// --git-dir returns the .git path for the current worktree
|
|
19
|
+
// --git-common-dir returns the shared .git path of the main worktree
|
|
20
|
+
// In the main worktree they are identical; in a linked worktree they differ.
|
|
21
|
+
// This works from any subdirectory.
|
|
22
|
+
const output = execSync('git rev-parse --git-dir --git-common-dir', {
|
|
23
|
+
encoding: 'utf8',
|
|
24
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
25
|
+
}).trim();
|
|
26
|
+
const [gitDir, commonDir] = output.split('\n');
|
|
27
|
+
if (!gitDir || !commonDir || gitDir === commonDir)
|
|
28
|
+
return null;
|
|
29
|
+
// The common dir is the .git directory of the main worktree.
|
|
30
|
+
// Its parent is the main worktree root.
|
|
31
|
+
return resolve(commonDir, '..');
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
/** Reset the cache – useful for tests. */
|
|
38
|
+
export function _resetCache() {
|
|
39
|
+
cachedMainWorktreePath = undefined;
|
|
40
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import { getMainWorktreePath, _resetCache } from './git-worktree.js';
|
|
3
|
+
const execSyncMock = vi.hoisted(() => vi.fn());
|
|
4
|
+
vi.mock('child_process', () => ({
|
|
5
|
+
execSync: execSyncMock,
|
|
6
|
+
}));
|
|
7
|
+
beforeEach(() => {
|
|
8
|
+
_resetCache();
|
|
9
|
+
execSyncMock.mockReset();
|
|
10
|
+
});
|
|
11
|
+
afterEach(() => {
|
|
12
|
+
_resetCache();
|
|
13
|
+
});
|
|
14
|
+
describe('getMainWorktreePath', () => {
|
|
15
|
+
it('returns null when git-dir and git-common-dir are identical (main worktree)', () => {
|
|
16
|
+
execSyncMock.mockReturnValue('/repo/.git\n/repo/.git\n');
|
|
17
|
+
expect(getMainWorktreePath()).toBeNull();
|
|
18
|
+
});
|
|
19
|
+
it('returns main worktree path when in a linked worktree', () => {
|
|
20
|
+
execSyncMock.mockReturnValue('/repo/.git/worktrees/my-branch\n/repo/.git\n');
|
|
21
|
+
expect(getMainWorktreePath()).toBe('/repo');
|
|
22
|
+
});
|
|
23
|
+
it('caches the result across calls', () => {
|
|
24
|
+
execSyncMock.mockReturnValue('/repo/.git/worktrees/my-branch\n/repo/.git\n');
|
|
25
|
+
getMainWorktreePath();
|
|
26
|
+
getMainWorktreePath();
|
|
27
|
+
expect(execSyncMock).toHaveBeenCalledTimes(1);
|
|
28
|
+
});
|
|
29
|
+
it('returns null when git command fails', () => {
|
|
30
|
+
execSyncMock.mockImplementation(() => {
|
|
31
|
+
throw new Error('not a git repo');
|
|
32
|
+
});
|
|
33
|
+
expect(getMainWorktreePath()).toBeNull();
|
|
34
|
+
});
|
|
35
|
+
});
|
package/dist/llm-cost.js
CHANGED
package/dist/models.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
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"];
|
|
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.4", "gpt-5.3-codex", "gpt-5.2-codex"];
|
package/dist/models.js
CHANGED
package/dist/version.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const GIT_HASH = "
|
|
1
|
+
export declare const GIT_HASH = "87afa8e";
|
package/dist/version.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const GIT_HASH = "
|
|
1
|
+
export const GIT_HASH = "87afa8e";
|