ai-cli-mcp 2.6.0 → 2.7.0
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/CHANGELOG.md +7 -0
- package/CONTRIBUTING.md +10 -0
- package/dist/__tests__/cli-builder.test.js +278 -0
- package/dist/cli-builder.js +145 -0
- package/dist/cli-parse.js +85 -0
- package/dist/cli.js +111 -0
- package/dist/server.js +24 -139
- package/docs/development.md +77 -6
- package/package.json +4 -2
- package/src/__tests__/cli-builder.test.ts +345 -0
- package/src/cli-builder.ts +190 -0
- package/src/cli-parse.ts +96 -0
- package/src/cli.ts +121 -0
- package/src/server.ts +24 -186
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
2
|
+
import { resolve as pathResolve, isAbsolute } from 'node:path';
|
|
3
|
+
|
|
4
|
+
// Model alias mappings for user-friendly model names
|
|
5
|
+
export const MODEL_ALIASES: Record<string, string> = {
|
|
6
|
+
'claude-ultra': 'opus',
|
|
7
|
+
'codex-ultra': 'gpt-5.3-codex',
|
|
8
|
+
'gemini-ultra': 'gemini-3-pro-preview'
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export const ALLOWED_REASONING_EFFORTS = new Set(['low', 'medium', 'high', 'xhigh']);
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Resolves model aliases to their full model names
|
|
15
|
+
* @param model - The model name or alias to resolve
|
|
16
|
+
* @returns The full model name, or the original value if no alias exists
|
|
17
|
+
*/
|
|
18
|
+
export function resolveModelAlias(model: string): string {
|
|
19
|
+
return MODEL_ALIASES[model] || model;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Validates and normalizes reasoning effort parameter.
|
|
24
|
+
* @returns normalized reasoning effort string, or '' if not applicable
|
|
25
|
+
* @throws Error for invalid values (plain Error, not MCP-specific)
|
|
26
|
+
*/
|
|
27
|
+
export function getReasoningEffort(model: string, rawValue: unknown): string {
|
|
28
|
+
if (typeof rawValue !== 'string') {
|
|
29
|
+
return '';
|
|
30
|
+
}
|
|
31
|
+
const trimmed = rawValue.trim();
|
|
32
|
+
if (!trimmed) {
|
|
33
|
+
return '';
|
|
34
|
+
}
|
|
35
|
+
const normalized = trimmed.toLowerCase();
|
|
36
|
+
if (!ALLOWED_REASONING_EFFORTS.has(normalized)) {
|
|
37
|
+
throw new Error(
|
|
38
|
+
`Invalid reasoning_effort: ${rawValue}. Allowed values: low, medium, high, xhigh.`
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
if (!model.startsWith('gpt-')) {
|
|
42
|
+
throw new Error(
|
|
43
|
+
'reasoning_effort is only supported for Codex models (gpt-*).'
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
return normalized;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface CliCommand {
|
|
50
|
+
cliPath: string;
|
|
51
|
+
args: string[];
|
|
52
|
+
cwd: string;
|
|
53
|
+
agent: 'claude' | 'codex' | 'gemini';
|
|
54
|
+
prompt: string;
|
|
55
|
+
resolvedModel: string;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export interface BuildCliCommandOptions {
|
|
59
|
+
prompt?: string;
|
|
60
|
+
prompt_file?: string;
|
|
61
|
+
workFolder: string;
|
|
62
|
+
model?: string;
|
|
63
|
+
session_id?: string;
|
|
64
|
+
reasoning_effort?: string;
|
|
65
|
+
cliPaths: { claude: string; codex: string; gemini: string };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Build a CLI command from the given options.
|
|
70
|
+
* This is a pure function (aside from filesystem reads for prompt_file / workFolder validation).
|
|
71
|
+
* @throws Error on validation failures
|
|
72
|
+
*/
|
|
73
|
+
export function buildCliCommand(options: BuildCliCommandOptions): CliCommand {
|
|
74
|
+
// Validate workFolder
|
|
75
|
+
if (!options.workFolder || typeof options.workFolder !== 'string') {
|
|
76
|
+
throw new Error('Missing or invalid required parameter: workFolder');
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Validate prompt / prompt_file
|
|
80
|
+
const hasPrompt = !!options.prompt && typeof options.prompt === 'string' && options.prompt.trim() !== '';
|
|
81
|
+
const hasPromptFile = !!options.prompt_file && typeof options.prompt_file === 'string' && options.prompt_file.trim() !== '';
|
|
82
|
+
|
|
83
|
+
if (!hasPrompt && !hasPromptFile) {
|
|
84
|
+
throw new Error('Either prompt or prompt_file must be provided');
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (hasPrompt && hasPromptFile) {
|
|
88
|
+
throw new Error('Cannot specify both prompt and prompt_file. Please use only one.');
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Determine prompt
|
|
92
|
+
let prompt: string;
|
|
93
|
+
if (hasPrompt) {
|
|
94
|
+
prompt = options.prompt!;
|
|
95
|
+
} else {
|
|
96
|
+
const promptFilePath = isAbsolute(options.prompt_file!)
|
|
97
|
+
? options.prompt_file!
|
|
98
|
+
: pathResolve(options.workFolder, options.prompt_file!);
|
|
99
|
+
|
|
100
|
+
if (!existsSync(promptFilePath)) {
|
|
101
|
+
throw new Error(`Prompt file does not exist: ${promptFilePath}`);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
try {
|
|
105
|
+
prompt = readFileSync(promptFilePath, 'utf-8');
|
|
106
|
+
} catch (error: any) {
|
|
107
|
+
throw new Error(`Failed to read prompt file: ${error.message}`);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Resolve workFolder
|
|
112
|
+
const cwd = pathResolve(options.workFolder);
|
|
113
|
+
if (!existsSync(cwd)) {
|
|
114
|
+
throw new Error(`Working folder does not exist: ${options.workFolder}`);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Resolve model
|
|
118
|
+
const rawModel = options.model || '';
|
|
119
|
+
const resolvedModel = resolveModelAlias(rawModel);
|
|
120
|
+
|
|
121
|
+
// Special handling for codex-ultra: default to high reasoning effort if not specified
|
|
122
|
+
let reasoningEffortArg: string | undefined = options.reasoning_effort;
|
|
123
|
+
if (rawModel === 'codex-ultra' && !reasoningEffortArg) {
|
|
124
|
+
reasoningEffortArg = 'xhigh';
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const reasoningEffort = getReasoningEffort(resolvedModel, reasoningEffortArg);
|
|
128
|
+
|
|
129
|
+
// Determine agent
|
|
130
|
+
let agent: 'codex' | 'claude' | 'gemini';
|
|
131
|
+
if (resolvedModel.startsWith('gpt-')) {
|
|
132
|
+
agent = 'codex';
|
|
133
|
+
} else if (resolvedModel.startsWith('gemini')) {
|
|
134
|
+
agent = 'gemini';
|
|
135
|
+
} else {
|
|
136
|
+
agent = 'claude';
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Build CLI path and args
|
|
140
|
+
let cliPath: string;
|
|
141
|
+
let args: string[];
|
|
142
|
+
|
|
143
|
+
if (agent === 'codex') {
|
|
144
|
+
cliPath = options.cliPaths.codex;
|
|
145
|
+
|
|
146
|
+
if (options.session_id && typeof options.session_id === 'string') {
|
|
147
|
+
args = ['exec', 'resume', options.session_id];
|
|
148
|
+
} else {
|
|
149
|
+
args = ['exec'];
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (reasoningEffort) {
|
|
153
|
+
args.push('-c', `model_reasoning_effort=${reasoningEffort}`);
|
|
154
|
+
}
|
|
155
|
+
if (resolvedModel) {
|
|
156
|
+
args.push('--model', resolvedModel);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
args.push('--full-auto', '--json', prompt);
|
|
160
|
+
|
|
161
|
+
} else if (agent === 'gemini') {
|
|
162
|
+
cliPath = options.cliPaths.gemini;
|
|
163
|
+
args = ['-y', '--output-format', 'json'];
|
|
164
|
+
|
|
165
|
+
if (options.session_id && typeof options.session_id === 'string') {
|
|
166
|
+
args.push('-r', options.session_id);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
if (resolvedModel) {
|
|
170
|
+
args.push('--model', resolvedModel);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
args.push(prompt);
|
|
174
|
+
|
|
175
|
+
} else {
|
|
176
|
+
cliPath = options.cliPaths.claude;
|
|
177
|
+
args = ['--dangerously-skip-permissions', '--output-format', 'stream-json', '--verbose'];
|
|
178
|
+
|
|
179
|
+
if (options.session_id && typeof options.session_id === 'string') {
|
|
180
|
+
args.push('-r', options.session_id);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
args.push('-p', prompt);
|
|
184
|
+
if (resolvedModel) {
|
|
185
|
+
args.push('--model', resolvedModel);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
return { cliPath, args, cwd, agent, prompt, resolvedModel };
|
|
190
|
+
}
|
package/src/cli-parse.ts
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { parseClaudeOutput, parseCodexOutput, parseGeminiOutput } from './parsers.js';
|
|
3
|
+
|
|
4
|
+
const AGENTS = ['claude', 'codex', 'gemini'] as const;
|
|
5
|
+
type Agent = typeof AGENTS[number];
|
|
6
|
+
|
|
7
|
+
const USAGE = `Usage: npm run -s cli.run.parse -- --agent <claude|codex|gemini>
|
|
8
|
+
|
|
9
|
+
Reads raw CLI output from stdin and outputs parsed JSON to stdout.
|
|
10
|
+
|
|
11
|
+
Options:
|
|
12
|
+
--agent Agent type: claude, codex, or gemini (required)
|
|
13
|
+
--help Show this help message
|
|
14
|
+
|
|
15
|
+
Examples:
|
|
16
|
+
npm run -s cli.run -- --model sonnet --workFolder /tmp --prompt "hi" > raw.txt
|
|
17
|
+
npm run -s cli.run.parse -- --agent claude < raw.txt
|
|
18
|
+
|
|
19
|
+
# Or pipe directly
|
|
20
|
+
npm run -s cli.run -- --model sonnet --workFolder /tmp --prompt "hi" | npm run -s cli.run.parse -- --agent claude
|
|
21
|
+
`;
|
|
22
|
+
|
|
23
|
+
function parseArgs(argv: string[]): Record<string, string> {
|
|
24
|
+
const result: Record<string, string> = {};
|
|
25
|
+
for (let i = 0; i < argv.length; i++) {
|
|
26
|
+
const arg = argv[i];
|
|
27
|
+
if (!arg.startsWith('--')) continue;
|
|
28
|
+
|
|
29
|
+
const eqIdx = arg.indexOf('=');
|
|
30
|
+
if (eqIdx !== -1) {
|
|
31
|
+
result[arg.slice(2, eqIdx)] = arg.slice(eqIdx + 1);
|
|
32
|
+
} else {
|
|
33
|
+
const key = arg.slice(2);
|
|
34
|
+
const next = argv[i + 1];
|
|
35
|
+
if (next !== undefined && !next.startsWith('--')) {
|
|
36
|
+
result[key] = next;
|
|
37
|
+
i++;
|
|
38
|
+
} else {
|
|
39
|
+
result[key] = '';
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return result;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function readStdin(): Promise<string> {
|
|
47
|
+
return new Promise((resolve, reject) => {
|
|
48
|
+
const chunks: Buffer[] = [];
|
|
49
|
+
process.stdin.on('data', (chunk) => chunks.push(chunk));
|
|
50
|
+
process.stdin.on('end', () => resolve(Buffer.concat(chunks).toString('utf-8')));
|
|
51
|
+
process.stdin.on('error', reject);
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async function main(): Promise<void> {
|
|
56
|
+
const args = parseArgs(process.argv.slice(2));
|
|
57
|
+
|
|
58
|
+
if ('help' in args) {
|
|
59
|
+
process.stdout.write(USAGE);
|
|
60
|
+
process.exit(0);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const agent = args.agent as Agent;
|
|
64
|
+
if (!agent || !AGENTS.includes(agent)) {
|
|
65
|
+
process.stderr.write(`Error: --agent is required (claude, codex, or gemini)\n\n`);
|
|
66
|
+
process.stderr.write(USAGE);
|
|
67
|
+
process.exit(1);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const input = await readStdin();
|
|
71
|
+
|
|
72
|
+
if (!input.trim()) {
|
|
73
|
+
process.stderr.write('Error: no input received from stdin\n');
|
|
74
|
+
process.exit(1);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
let parsed: any = null;
|
|
78
|
+
switch (agent) {
|
|
79
|
+
case 'claude':
|
|
80
|
+
parsed = parseClaudeOutput(input);
|
|
81
|
+
break;
|
|
82
|
+
case 'codex':
|
|
83
|
+
parsed = parseCodexOutput(input);
|
|
84
|
+
break;
|
|
85
|
+
case 'gemini':
|
|
86
|
+
parsed = parseGeminiOutput(input);
|
|
87
|
+
break;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
process.stdout.write(JSON.stringify(parsed, null, 2) + '\n');
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
main().catch((err) => {
|
|
94
|
+
process.stderr.write(`Fatal error: ${err.message}\n`);
|
|
95
|
+
process.exit(1);
|
|
96
|
+
});
|
package/src/cli.ts
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { spawn } from 'node:child_process';
|
|
3
|
+
import { buildCliCommand } from './cli-builder.js';
|
|
4
|
+
import { findClaudeCli, findCodexCli, findGeminiCli } from './server.js';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Minimal argv parser. No external dependencies.
|
|
8
|
+
* Supports: --key value, --key=value
|
|
9
|
+
*/
|
|
10
|
+
function parseArgs(argv: string[]): Record<string, string> {
|
|
11
|
+
const result: Record<string, string> = {};
|
|
12
|
+
for (let i = 0; i < argv.length; i++) {
|
|
13
|
+
const arg = argv[i];
|
|
14
|
+
if (!arg.startsWith('--')) continue;
|
|
15
|
+
|
|
16
|
+
const eqIdx = arg.indexOf('=');
|
|
17
|
+
if (eqIdx !== -1) {
|
|
18
|
+
// --key=value
|
|
19
|
+
result[arg.slice(2, eqIdx)] = arg.slice(eqIdx + 1);
|
|
20
|
+
} else {
|
|
21
|
+
// --key value
|
|
22
|
+
const key = arg.slice(2);
|
|
23
|
+
const next = argv[i + 1];
|
|
24
|
+
if (next !== undefined && !next.startsWith('--')) {
|
|
25
|
+
result[key] = next;
|
|
26
|
+
i++;
|
|
27
|
+
} else {
|
|
28
|
+
result[key] = '';
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return result;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const USAGE = `Usage: npm run -s cli.run -- --model <model> --workFolder <path> --prompt "..." [options]
|
|
36
|
+
|
|
37
|
+
Options:
|
|
38
|
+
--model Model name or alias (e.g. sonnet, opus, gpt-5.2-codex, gemini-2.5-pro)
|
|
39
|
+
--workFolder Working directory (absolute path)
|
|
40
|
+
--prompt Prompt string (mutually exclusive with --prompt_file)
|
|
41
|
+
--prompt_file Path to a file containing the prompt
|
|
42
|
+
--session_id Session ID to resume
|
|
43
|
+
--reasoning_effort Codex only: low, medium, high, xhigh
|
|
44
|
+
--help Show this help message
|
|
45
|
+
|
|
46
|
+
Raw CLI output goes to stdout. Use cli.run.parse to parse the output:
|
|
47
|
+
npm run -s cli.run -- ... > raw.txt
|
|
48
|
+
npm run -s cli.run.parse -- --agent claude < raw.txt
|
|
49
|
+
`;
|
|
50
|
+
|
|
51
|
+
async function main(): Promise<void> {
|
|
52
|
+
const args = parseArgs(process.argv.slice(2));
|
|
53
|
+
|
|
54
|
+
if ('help' in args) {
|
|
55
|
+
process.stdout.write(USAGE);
|
|
56
|
+
process.exit(0);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (!args.workFolder) {
|
|
60
|
+
process.stderr.write('Error: --workFolder is required\n\n');
|
|
61
|
+
process.stderr.write(USAGE);
|
|
62
|
+
process.exit(1);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (!args.prompt && !args.prompt_file) {
|
|
66
|
+
process.stderr.write('Error: --prompt or --prompt_file is required\n\n');
|
|
67
|
+
process.stderr.write(USAGE);
|
|
68
|
+
process.exit(1);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Resolve CLI paths
|
|
72
|
+
const cliPaths = {
|
|
73
|
+
claude: findClaudeCli(),
|
|
74
|
+
codex: findCodexCli(),
|
|
75
|
+
gemini: findGeminiCli(),
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
// Build command
|
|
79
|
+
let cmd;
|
|
80
|
+
try {
|
|
81
|
+
cmd = buildCliCommand({
|
|
82
|
+
prompt: args.prompt || undefined,
|
|
83
|
+
prompt_file: args.prompt_file || undefined,
|
|
84
|
+
workFolder: args.workFolder,
|
|
85
|
+
model: args.model || undefined,
|
|
86
|
+
session_id: args.session_id || undefined,
|
|
87
|
+
reasoning_effort: args.reasoning_effort || undefined,
|
|
88
|
+
cliPaths,
|
|
89
|
+
});
|
|
90
|
+
} catch (error: any) {
|
|
91
|
+
process.stderr.write(`Error: ${error.message}\n`);
|
|
92
|
+
process.exit(1);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Log agent info to stderr (does not pollute stdout)
|
|
96
|
+
process.stderr.write(`[cli.run] agent=${cmd.agent} model=${cmd.resolvedModel || '(default)'}\n`);
|
|
97
|
+
|
|
98
|
+
// Spawn foreground process — raw output passthrough
|
|
99
|
+
const child = spawn(cmd.cliPath, cmd.args, {
|
|
100
|
+
cwd: cmd.cwd,
|
|
101
|
+
stdio: 'inherit',
|
|
102
|
+
detached: false,
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
const exitCode = await new Promise<number>((resolve) => {
|
|
106
|
+
child.on('close', (code) => {
|
|
107
|
+
resolve(code ?? 1);
|
|
108
|
+
});
|
|
109
|
+
child.on('error', (err) => {
|
|
110
|
+
process.stderr.write(`Process error: ${err.message}\n`);
|
|
111
|
+
resolve(1);
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
process.exit(exitCode);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
main().catch((err) => {
|
|
119
|
+
process.stderr.write(`Fatal error: ${err.message}\n`);
|
|
120
|
+
process.exit(1);
|
|
121
|
+
});
|
package/src/server.ts
CHANGED
|
@@ -9,48 +9,16 @@ import {
|
|
|
9
9
|
type ServerResult,
|
|
10
10
|
} from '@modelcontextprotocol/sdk/types.js';
|
|
11
11
|
import { spawn, ChildProcess } from 'node:child_process';
|
|
12
|
-
import { existsSync
|
|
12
|
+
import { existsSync } from 'node:fs';
|
|
13
13
|
import { homedir } from 'node:os';
|
|
14
|
-
import { join
|
|
14
|
+
import { join } from 'node:path';
|
|
15
15
|
import * as path from 'path';
|
|
16
16
|
import { parseCodexOutput, parseClaudeOutput, parseGeminiOutput } from './parsers.js';
|
|
17
|
+
import { buildCliCommand } from './cli-builder.js';
|
|
17
18
|
|
|
18
19
|
// Server version - update this when releasing new versions
|
|
19
20
|
const SERVER_VERSION = "2.2.0";
|
|
20
21
|
|
|
21
|
-
// Model alias mappings for user-friendly model names
|
|
22
|
-
const MODEL_ALIASES: Record<string, string> = {
|
|
23
|
-
'claude-ultra': 'opus',
|
|
24
|
-
'codex-ultra': 'gpt-5.3-codex',
|
|
25
|
-
'gemini-ultra': 'gemini-3-pro-preview'
|
|
26
|
-
};
|
|
27
|
-
|
|
28
|
-
const ALLOWED_REASONING_EFFORTS = new Set(['low', 'medium', 'high', 'xhigh']);
|
|
29
|
-
|
|
30
|
-
function getReasoningEffort(model: string, rawValue: unknown): string {
|
|
31
|
-
if (typeof rawValue !== 'string') {
|
|
32
|
-
return '';
|
|
33
|
-
}
|
|
34
|
-
const trimmed = rawValue.trim();
|
|
35
|
-
if (!trimmed) {
|
|
36
|
-
return '';
|
|
37
|
-
}
|
|
38
|
-
const normalized = trimmed.toLowerCase();
|
|
39
|
-
if (!ALLOWED_REASONING_EFFORTS.has(normalized)) {
|
|
40
|
-
throw new McpError(
|
|
41
|
-
ErrorCode.InvalidParams,
|
|
42
|
-
`Invalid reasoning_effort: ${rawValue}. Allowed values: low, medium, high, xhigh.`
|
|
43
|
-
);
|
|
44
|
-
}
|
|
45
|
-
if (!model.startsWith('gpt-')) {
|
|
46
|
-
throw new McpError(
|
|
47
|
-
ErrorCode.InvalidParams,
|
|
48
|
-
'reasoning_effort is only supported for Codex models (gpt-*).'
|
|
49
|
-
);
|
|
50
|
-
}
|
|
51
|
-
return normalized;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
22
|
// Define debugMode globally using const
|
|
55
23
|
const debugMode = process.env.MCP_CLAUDE_DEBUG === 'true';
|
|
56
24
|
|
|
@@ -226,36 +194,8 @@ export function findClaudeCli(): string {
|
|
|
226
194
|
return cliName;
|
|
227
195
|
}
|
|
228
196
|
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
*/
|
|
232
|
-
interface ClaudeCodeArgs {
|
|
233
|
-
prompt?: string;
|
|
234
|
-
prompt_file?: string;
|
|
235
|
-
workFolder: string;
|
|
236
|
-
model?: string;
|
|
237
|
-
session_id?: string;
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
/**
|
|
241
|
-
* Interface for Codex tool arguments
|
|
242
|
-
*/
|
|
243
|
-
interface CodexArgs {
|
|
244
|
-
prompt?: string;
|
|
245
|
-
prompt_file?: string;
|
|
246
|
-
workFolder: string;
|
|
247
|
-
model?: string; // Codex model id (e.g., gpt-5.2-codex)
|
|
248
|
-
reasoning_effort?: string;
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
/**
|
|
252
|
-
* Resolves model aliases to their full model names
|
|
253
|
-
* @param model - The model name or alias to resolve
|
|
254
|
-
* @returns The full model name, or the original value if no alias exists
|
|
255
|
-
*/
|
|
256
|
-
export function resolveModelAlias(model: string): string {
|
|
257
|
-
return MODEL_ALIASES[model] || model;
|
|
258
|
-
}
|
|
197
|
+
// Re-export resolveModelAlias for backward compatibility
|
|
198
|
+
export { resolveModelAlias } from './cli-builder.js';
|
|
259
199
|
|
|
260
200
|
// Ensure spawnAsync is defined correctly *before* the class
|
|
261
201
|
export async function spawnAsync(command: string, args: string[], options?: { timeout?: number, cwd?: string }): Promise<{ stdout: string; stderr: string }> {
|
|
@@ -512,135 +452,33 @@ export class ClaudeCodeServer {
|
|
|
512
452
|
* Handle run tool - starts Claude or Codex process and returns PID immediately
|
|
513
453
|
*/
|
|
514
454
|
private async handleRun(toolArguments: any): Promise<ServerResult> {
|
|
515
|
-
// Validate workFolder is required
|
|
516
|
-
if (!toolArguments.workFolder || typeof toolArguments.workFolder !== 'string') {
|
|
517
|
-
throw new McpError(ErrorCode.InvalidParams, 'Missing or invalid required parameter: workFolder');
|
|
518
|
-
}
|
|
519
|
-
|
|
520
|
-
// Validate that either prompt or prompt_file is provided
|
|
521
|
-
const hasPrompt = toolArguments.prompt && typeof toolArguments.prompt === 'string' && toolArguments.prompt.trim() !== '';
|
|
522
|
-
const hasPromptFile = toolArguments.prompt_file && typeof toolArguments.prompt_file === 'string' && toolArguments.prompt_file.trim() !== '';
|
|
523
|
-
|
|
524
|
-
if (!hasPrompt && !hasPromptFile) {
|
|
525
|
-
throw new McpError(ErrorCode.InvalidParams, 'Either prompt or prompt_file must be provided');
|
|
526
|
-
}
|
|
527
|
-
|
|
528
|
-
if (hasPrompt && hasPromptFile) {
|
|
529
|
-
throw new McpError(ErrorCode.InvalidParams, 'Cannot specify both prompt and prompt_file. Please use only one.');
|
|
530
|
-
}
|
|
531
|
-
|
|
532
|
-
// Determine the prompt to use
|
|
533
|
-
let prompt: string;
|
|
534
|
-
if (hasPrompt) {
|
|
535
|
-
prompt = toolArguments.prompt;
|
|
536
|
-
} else {
|
|
537
|
-
// Read prompt from file
|
|
538
|
-
const promptFilePath = path.isAbsolute(toolArguments.prompt_file)
|
|
539
|
-
? toolArguments.prompt_file
|
|
540
|
-
: pathResolve(toolArguments.workFolder, toolArguments.prompt_file);
|
|
541
|
-
|
|
542
|
-
if (!existsSync(promptFilePath)) {
|
|
543
|
-
throw new McpError(ErrorCode.InvalidParams, `Prompt file does not exist: ${promptFilePath}`);
|
|
544
|
-
}
|
|
545
|
-
|
|
546
|
-
try {
|
|
547
|
-
prompt = readFileSync(promptFilePath, 'utf-8');
|
|
548
|
-
} catch (error: any) {
|
|
549
|
-
throw new McpError(ErrorCode.InvalidParams, `Failed to read prompt file: ${error.message}`);
|
|
550
|
-
}
|
|
551
|
-
}
|
|
552
|
-
|
|
553
|
-
// Determine working directory
|
|
554
|
-
const resolvedCwd = pathResolve(toolArguments.workFolder);
|
|
555
|
-
if (!existsSync(resolvedCwd)) {
|
|
556
|
-
throw new McpError(ErrorCode.InvalidParams, `Working folder does not exist: ${toolArguments.workFolder}`);
|
|
557
|
-
}
|
|
558
|
-
const effectiveCwd = resolvedCwd;
|
|
559
|
-
|
|
560
455
|
// Print version on first use
|
|
561
456
|
if (isFirstToolUse) {
|
|
562
457
|
console.error(`ai_cli_mcp v${SERVER_VERSION} started at ${serverStartupTime}`);
|
|
563
458
|
isFirstToolUse = false;
|
|
564
459
|
}
|
|
565
460
|
|
|
566
|
-
//
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
agent = 'claude';
|
|
461
|
+
// Build CLI command (validation + args assembly)
|
|
462
|
+
let cmd;
|
|
463
|
+
try {
|
|
464
|
+
cmd = buildCliCommand({
|
|
465
|
+
prompt: toolArguments.prompt,
|
|
466
|
+
prompt_file: toolArguments.prompt_file,
|
|
467
|
+
workFolder: toolArguments.workFolder,
|
|
468
|
+
model: toolArguments.model,
|
|
469
|
+
session_id: toolArguments.session_id,
|
|
470
|
+
reasoning_effort: toolArguments.reasoning_effort,
|
|
471
|
+
cliPaths: {
|
|
472
|
+
claude: this.claudeCliPath,
|
|
473
|
+
codex: this.codexCliPath,
|
|
474
|
+
gemini: this.geminiCliPath,
|
|
475
|
+
},
|
|
476
|
+
});
|
|
477
|
+
} catch (error: any) {
|
|
478
|
+
throw new McpError(ErrorCode.InvalidParams, error.message);
|
|
585
479
|
}
|
|
586
480
|
|
|
587
|
-
|
|
588
|
-
let processArgs: string[];
|
|
589
|
-
|
|
590
|
-
if (agent === 'codex') {
|
|
591
|
-
// Handle Codex
|
|
592
|
-
cliPath = this.codexCliPath;
|
|
593
|
-
|
|
594
|
-
// Use 'exec resume' if session_id is provided, otherwise use 'exec'
|
|
595
|
-
if (toolArguments.session_id && typeof toolArguments.session_id === 'string') {
|
|
596
|
-
processArgs = ['exec', 'resume', toolArguments.session_id];
|
|
597
|
-
} else {
|
|
598
|
-
processArgs = ['exec'];
|
|
599
|
-
}
|
|
600
|
-
|
|
601
|
-
// Handle Codex models.
|
|
602
|
-
if (reasoningEffort) {
|
|
603
|
-
processArgs.push('-c', `model_reasoning_effort=${reasoningEffort}`);
|
|
604
|
-
}
|
|
605
|
-
if (resolvedModel) {
|
|
606
|
-
processArgs.push('--model', resolvedModel);
|
|
607
|
-
}
|
|
608
|
-
|
|
609
|
-
processArgs.push('--full-auto', '--json', prompt);
|
|
610
|
-
|
|
611
|
-
} else if (agent === 'gemini') {
|
|
612
|
-
// Handle Gemini
|
|
613
|
-
cliPath = this.geminiCliPath;
|
|
614
|
-
processArgs = ['-y', '--output-format', 'json'];
|
|
615
|
-
|
|
616
|
-
// Add session_id if provided
|
|
617
|
-
if (toolArguments.session_id && typeof toolArguments.session_id === 'string') {
|
|
618
|
-
processArgs.push('-r', toolArguments.session_id);
|
|
619
|
-
}
|
|
620
|
-
|
|
621
|
-
// Add model if specified
|
|
622
|
-
if (resolvedModel) {
|
|
623
|
-
processArgs.push('--model', resolvedModel);
|
|
624
|
-
}
|
|
625
|
-
|
|
626
|
-
// Add prompt as positional argument
|
|
627
|
-
processArgs.push(prompt);
|
|
628
|
-
|
|
629
|
-
} else {
|
|
630
|
-
// Handle Claude (default)
|
|
631
|
-
cliPath = this.claudeCliPath;
|
|
632
|
-
processArgs = ['--dangerously-skip-permissions', '--output-format', 'stream-json', '--verbose'];
|
|
633
|
-
|
|
634
|
-
// Add session_id if provided (Claude only)
|
|
635
|
-
if (toolArguments.session_id && typeof toolArguments.session_id === 'string') {
|
|
636
|
-
processArgs.push('-r', toolArguments.session_id);
|
|
637
|
-
}
|
|
638
|
-
|
|
639
|
-
processArgs.push('-p', prompt);
|
|
640
|
-
if (resolvedModel) {
|
|
641
|
-
processArgs.push('--model', resolvedModel);
|
|
642
|
-
}
|
|
643
|
-
}
|
|
481
|
+
const { cliPath, args: processArgs, cwd: effectiveCwd, agent, prompt } = cmd;
|
|
644
482
|
|
|
645
483
|
// Spawn process without waiting
|
|
646
484
|
const childProcess = spawn(cliPath, processArgs, {
|