consult-llm-mcp 2.5.0 → 2.5.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 CHANGED
@@ -680,21 +680,40 @@ always reliably triggered. See the [example skill](#example-skill) below.
680
680
  **Recommendation:** Start with no custom activation. Use slash commands if you
681
681
  need reliability or custom instructions.
682
682
 
683
- ## Example skill
683
+ ## Installing skills
684
684
 
685
- Here's an example [Claude Code skill](https://code.claude.com/docs/en/skills)
686
- that uses the `consult_llm` MCP tool to create commands like "ask gemini" or
687
- "ask codex". See [skills/consult/SKILL.md](skills/consult/SKILL.md) for the full
688
- content.
685
+ Install all skills globally with a single command:
689
686
 
690
- Save it as `~/.claude/skills/consult-llm/SKILL.md` and you can then use it by
691
- 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.
692
710
 
693
- This one is not strictly necessary either, Claude (or other agent) can infer
694
- from the schema that "Ask gemini" should call this MCP, but it might be helpful
695
- 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.
696
715
 
697
- ## Example slash command
716
+ ## Slash command
698
717
 
699
718
  Here's an example
700
719
  [Claude Code slash command](https://code.claude.com/docs/en/slash-commands) that
@@ -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,29 @@ 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}"`);
54
+ args.push('--json', '--skip-git-repo-check');
55
+ if (config.codexReasoningEffort) {
56
+ args.push('-c', `model_reasoning_effort="${config.codexReasoningEffort}"`);
57
+ }
58
+ // --add-dir is not supported by `codex exec resume`
59
+ if (!threadId) {
60
+ const extraDirs = [
61
+ getMainWorktreePath(),
62
+ ...getExternalDirectories(filePaths),
63
+ ].filter((d) => d !== null);
64
+ for (const dir of extraDirs) {
65
+ args.push('--add-dir', dir);
60
66
  }
61
- args.push('-m', model, fullPrompt);
62
67
  }
68
+ args.push('-m', model);
69
+ if (threadId) {
70
+ args.push(threadId);
71
+ }
72
+ args.push(fullPrompt);
63
73
  const { stdout, stderr, code } = await runCli('codex', args);
64
74
  if (code === 0) {
65
75
  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/version.d.ts CHANGED
@@ -1 +1 @@
1
- export declare const GIT_HASH = "c310fe4";
1
+ export declare const GIT_HASH = "e6c6469";
package/dist/version.js CHANGED
@@ -1 +1 @@
1
- export const GIT_HASH = "c310fe4";
1
+ export const GIT_HASH = "e6c6469";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "consult-llm-mcp",
3
- "version": "2.5.0",
3
+ "version": "2.5.2",
4
4
  "description": "MCP server for consulting powerful AI models",
5
5
  "type": "module",
6
6
  "main": "dist/main.js",