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 +30 -11
- package/dist/executors/codex-cli.js +21 -11
- 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/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
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
|
-
##
|
|
683
|
+
## Installing skills
|
|
684
684
|
|
|
685
|
-
|
|
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
|
-
|
|
691
|
-
|
|
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
|
-
|
|
694
|
-
from the schema that "
|
|
695
|
-
|
|
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
|
-
##
|
|
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('
|
|
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
|
-
|
|
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 = "
|
|
1
|
+
export declare const GIT_HASH = "e6c6469";
|
package/dist/version.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const GIT_HASH = "
|
|
1
|
+
export const GIT_HASH = "e6c6469";
|