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 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), `gemini-2.5-pro`, `gemini-3-pro-preview`,
460
- `gemini-3.1-pro-preview`, `deepseek-reasoner`, `gpt-5.3-codex`,
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), `gemini-2.5-pro`, `gemini-3-pro-preview`,
559
- `gemini-3.1-pro-preview`, `deepseek-reasoner`, `gpt-5.3-codex`,
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.2**: OpenAI's latest GPT model
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
- ## Example skill
683
+ ## Installing skills
683
684
 
684
- Here's an example [Claude Code skill](https://code.claude.com/docs/en/skills)
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
- Save it as `~/.claude/skills/consult-llm/SKILL.md` and you can then use it by
690
- typing "ask gemini about X" or "ask codex about X" in Claude Code.
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
- This one is not strictly necessary either, Claude (or other agent) can infer
693
- from the schema that "Ask gemini" should call this MCP, but it might be helpful
694
- in case you want to have more precise control over how the agent calls this MCP.
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
- ## Example slash command
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
- ## Debate skills
726
+ ## Multi-LLM skills
707
727
 
708
- Two skills that orchestrate structured debates between LLMs to find the best
709
- implementation approach before writing code. Both use `thread_id` to maintain
710
- conversation context across rounds, so each LLM remembers the full debate
711
- history without resending everything.
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('exec', 'resume', '--json', '--skip-git-repo-check');
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
- else {
57
- args.push('exec', '--json', '--skip-git-repo-check');
58
- if (config.codexReasoningEffort) {
59
- args.push('-c', `model_reasoning_effort="${config.codexReasoningEffort}"`);
60
- }
61
- args.push('-m', model, fullPrompt);
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
@@ -1,4 +1,8 @@
1
1
  const MODEL_PRICING = {
2
+ 'gpt-5.4': {
3
+ inputCostPerMillion: 2.5,
4
+ outputCostPerMillion: 15.0,
5
+ },
2
6
  'gpt-5.2': {
3
7
  inputCostPerMillion: 1.75,
4
8
  outputCostPerMillion: 14.0,
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
@@ -4,6 +4,7 @@ export const ALL_MODELS = [
4
4
  'gemini-3.1-pro-preview',
5
5
  'deepseek-reasoner',
6
6
  'gpt-5.2',
7
+ 'gpt-5.4',
7
8
  'gpt-5.3-codex',
8
9
  'gpt-5.2-codex',
9
10
  ];
package/dist/version.d.ts CHANGED
@@ -1 +1 @@
1
- export declare const GIT_HASH = "1c9e0da";
1
+ export declare const GIT_HASH = "87afa8e";
package/dist/version.js CHANGED
@@ -1 +1 @@
1
- export const GIT_HASH = "1c9e0da";
1
+ export const GIT_HASH = "87afa8e";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "consult-llm-mcp",
3
- "version": "2.4.2",
3
+ "version": "2.5.1",
4
4
  "description": "MCP server for consulting powerful AI models",
5
5
  "type": "module",
6
6
  "main": "dist/main.js",