ai-cli-mcp 2.11.0 → 2.13.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/.github/workflows/publish.yml +25 -0
- package/CHANGELOG.md +23 -0
- package/README.ja.md +112 -8
- package/README.md +112 -9
- package/dist/__tests__/app-cli.test.js +293 -0
- package/dist/__tests__/cli-bin-smoke.test.js +58 -0
- package/dist/__tests__/cli-builder.test.js +37 -0
- package/dist/__tests__/cli-process-service.test.js +279 -0
- package/dist/__tests__/cli-utils.test.js +140 -0
- package/dist/__tests__/error-cases.test.js +2 -1
- package/dist/__tests__/mcp-contract.test.js +343 -0
- package/dist/__tests__/parsers.test.js +37 -1
- package/dist/__tests__/process-management.test.js +15 -8
- package/dist/__tests__/server.test.js +29 -3
- package/dist/__tests__/wait.test.js +31 -0
- package/dist/app/cli.js +304 -0
- package/dist/app/mcp.js +366 -0
- package/dist/bin/ai-cli-mcp.js +6 -0
- package/dist/bin/ai-cli.js +10 -0
- package/dist/cli-builder.js +15 -6
- package/dist/cli-parse.js +8 -5
- package/dist/cli-process-service.js +332 -0
- package/dist/cli-utils.js +159 -88
- package/dist/cli.js +4 -3
- package/dist/model-catalog.js +53 -0
- package/dist/parsers.js +55 -0
- package/dist/process-service.js +201 -0
- package/dist/server.js +4 -578
- package/docs/cli-architecture.md +275 -0
- package/package.json +4 -3
- package/server.json +1 -1
- package/src/__tests__/app-cli.test.ts +370 -0
- package/src/__tests__/cli-bin-smoke.test.ts +75 -0
- package/src/__tests__/cli-builder.test.ts +47 -0
- package/src/__tests__/cli-process-service.test.ts +334 -0
- package/src/__tests__/cli-utils.test.ts +166 -0
- package/src/__tests__/error-cases.test.ts +3 -4
- package/src/__tests__/mcp-contract.test.ts +422 -0
- package/src/__tests__/parsers.test.ts +44 -1
- package/src/__tests__/process-management.test.ts +15 -9
- package/src/__tests__/server.test.ts +27 -6
- package/src/__tests__/wait.test.ts +38 -0
- package/src/app/cli.ts +373 -0
- package/src/app/mcp.ts +402 -0
- package/src/bin/ai-cli-mcp.ts +7 -0
- package/src/bin/ai-cli.ts +11 -0
- package/src/cli-builder.ts +19 -10
- package/src/cli-parse.ts +8 -5
- package/src/cli-process-service.ts +418 -0
- package/src/cli-utils.ts +205 -99
- package/src/cli.ts +4 -3
- package/src/model-catalog.ts +64 -0
- package/src/parsers.ts +61 -0
- package/src/process-service.ts +263 -0
- package/src/server.ts +4 -668
package/src/app/mcp.ts
ADDED
|
@@ -0,0 +1,402 @@
|
|
|
1
|
+
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
2
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
3
|
+
import {
|
|
4
|
+
CallToolRequestSchema,
|
|
5
|
+
ErrorCode,
|
|
6
|
+
ListToolsRequestSchema,
|
|
7
|
+
McpError,
|
|
8
|
+
type ServerResult,
|
|
9
|
+
} from '@modelcontextprotocol/sdk/types.js';
|
|
10
|
+
import { spawn } from 'node:child_process';
|
|
11
|
+
import { debugLog, findClaudeCli, findCodexCli, findForgeCli, findGeminiCli } from '../cli-utils.js';
|
|
12
|
+
import { getModelParameterDescription, getSupportedModelsDescription } from '../model-catalog.js';
|
|
13
|
+
import { ProcessService } from '../process-service.js';
|
|
14
|
+
|
|
15
|
+
// Server version - update this when releasing new versions
|
|
16
|
+
const SERVER_VERSION = "2.2.0";
|
|
17
|
+
|
|
18
|
+
// Track if this is the first tool use for version printing
|
|
19
|
+
let isFirstToolUse = true;
|
|
20
|
+
|
|
21
|
+
// Capture server startup time when the module loads
|
|
22
|
+
const serverStartupTime = new Date().toISOString();
|
|
23
|
+
|
|
24
|
+
// Ensure spawnAsync is defined correctly *before* the class
|
|
25
|
+
export async function spawnAsync(command: string, args: string[], options?: { timeout?: number, cwd?: string }): Promise<{ stdout: string; stderr: string }> {
|
|
26
|
+
return new Promise((resolve, reject) => {
|
|
27
|
+
debugLog(`[Spawn] Running command: ${command} ${args.join(' ')}`);
|
|
28
|
+
const process = spawn(command, args, {
|
|
29
|
+
shell: false,
|
|
30
|
+
timeout: options?.timeout,
|
|
31
|
+
cwd: options?.cwd,
|
|
32
|
+
stdio: ['ignore', 'pipe', 'pipe']
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
let stdout = '';
|
|
36
|
+
let stderr = '';
|
|
37
|
+
|
|
38
|
+
process.stdout.on('data', (data) => { stdout += data.toString(); });
|
|
39
|
+
process.stderr.on('data', (data) => {
|
|
40
|
+
stderr += data.toString();
|
|
41
|
+
debugLog(`[Spawn Stderr Chunk] ${data.toString()}`);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
process.on('error', (error: NodeJS.ErrnoException) => {
|
|
45
|
+
debugLog(`[Spawn Error Event] Full error object:`, error);
|
|
46
|
+
let errorMessage = `Spawn error: ${error.message}`;
|
|
47
|
+
if (error.path) {
|
|
48
|
+
errorMessage += ` | Path: ${error.path}`;
|
|
49
|
+
}
|
|
50
|
+
if (error.syscall) {
|
|
51
|
+
errorMessage += ` | Syscall: ${error.syscall}`;
|
|
52
|
+
}
|
|
53
|
+
errorMessage += `\nStderr: ${stderr.trim()}`;
|
|
54
|
+
reject(new Error(errorMessage));
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
process.on('close', (code) => {
|
|
58
|
+
debugLog(`[Spawn Close] Exit code: ${code}`);
|
|
59
|
+
debugLog(`[Spawn Stderr Full] ${stderr.trim()}`);
|
|
60
|
+
debugLog(`[Spawn Stdout Full] ${stdout.trim()}`);
|
|
61
|
+
if (code === 0) {
|
|
62
|
+
resolve({ stdout, stderr });
|
|
63
|
+
} else {
|
|
64
|
+
reject(new Error(`Command failed with exit code ${code}\nStderr: ${stderr.trim()}\nStdout: ${stdout.trim()}`));
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export class ClaudeCodeServer {
|
|
71
|
+
private server: Server;
|
|
72
|
+
private claudeCliPath: string;
|
|
73
|
+
private codexCliPath: string;
|
|
74
|
+
private geminiCliPath: string;
|
|
75
|
+
private forgeCliPath: string;
|
|
76
|
+
private processService: ProcessService;
|
|
77
|
+
private sigintHandler?: () => Promise<void>;
|
|
78
|
+
private packageVersion: string;
|
|
79
|
+
|
|
80
|
+
constructor() {
|
|
81
|
+
this.claudeCliPath = findClaudeCli();
|
|
82
|
+
this.codexCliPath = findCodexCli();
|
|
83
|
+
this.geminiCliPath = findGeminiCli();
|
|
84
|
+
this.forgeCliPath = findForgeCli();
|
|
85
|
+
console.error(`[Setup] Using Claude CLI command/path: ${this.claudeCliPath}`);
|
|
86
|
+
console.error(`[Setup] Using Codex CLI command/path: ${this.codexCliPath}`);
|
|
87
|
+
console.error(`[Setup] Using Gemini CLI command/path: ${this.geminiCliPath}`);
|
|
88
|
+
console.error(`[Setup] Using Forge CLI command/path: ${this.forgeCliPath}`);
|
|
89
|
+
this.packageVersion = SERVER_VERSION;
|
|
90
|
+
this.processService = new ProcessService({
|
|
91
|
+
cliPaths: {
|
|
92
|
+
claude: this.claudeCliPath,
|
|
93
|
+
codex: this.codexCliPath,
|
|
94
|
+
gemini: this.geminiCliPath,
|
|
95
|
+
forge: this.forgeCliPath,
|
|
96
|
+
},
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
this.server = new Server(
|
|
100
|
+
{
|
|
101
|
+
name: 'ai_cli_mcp',
|
|
102
|
+
version: SERVER_VERSION,
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
capabilities: {
|
|
106
|
+
tools: {},
|
|
107
|
+
},
|
|
108
|
+
}
|
|
109
|
+
);
|
|
110
|
+
|
|
111
|
+
this.setupToolHandlers();
|
|
112
|
+
|
|
113
|
+
this.server.onerror = (error) => console.error('[Error]', error);
|
|
114
|
+
this.sigintHandler = async () => {
|
|
115
|
+
await this.server.close();
|
|
116
|
+
process.exit(0);
|
|
117
|
+
};
|
|
118
|
+
process.on('SIGINT', this.sigintHandler);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
private setupToolHandlers(): void {
|
|
122
|
+
this.server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
123
|
+
tools: [
|
|
124
|
+
{
|
|
125
|
+
name: 'run',
|
|
126
|
+
description: `AI Agent Runner: Starts a Claude, Codex, Gemini, or Forge CLI process in the background and returns a PID immediately. Use list_processes and get_result to monitor progress.
|
|
127
|
+
|
|
128
|
+
• File ops: Create, read, (fuzzy) edit, move, copy, delete, list files, analyze/ocr images, file content analysis
|
|
129
|
+
• Code: Generate / analyse / refactor / fix
|
|
130
|
+
• Git: Stage ▸ commit ▸ push ▸ tag (any workflow)
|
|
131
|
+
• Terminal: Run any CLI cmd or open URLs
|
|
132
|
+
• Web search + summarise content on-the-fly
|
|
133
|
+
• Multi-step workflows & GitHub integration
|
|
134
|
+
|
|
135
|
+
**IMPORTANT**: This tool now returns immediately with a PID. Use other tools to check status and get results.
|
|
136
|
+
|
|
137
|
+
**Supported models**:
|
|
138
|
+
${getSupportedModelsDescription()}
|
|
139
|
+
|
|
140
|
+
**Prompt input**: You must provide EITHER prompt (string) OR prompt_file (file path), but not both.
|
|
141
|
+
|
|
142
|
+
**Prompt tips**
|
|
143
|
+
1. Be concise, explicit & step-by-step for complex tasks.
|
|
144
|
+
2. Check process status with list_processes
|
|
145
|
+
3. Get results with get_result using the returned PID
|
|
146
|
+
4. Kill long-running processes with kill_process if needed
|
|
147
|
+
|
|
148
|
+
`,
|
|
149
|
+
inputSchema: {
|
|
150
|
+
type: 'object',
|
|
151
|
+
properties: {
|
|
152
|
+
prompt: {
|
|
153
|
+
type: 'string',
|
|
154
|
+
description: 'The detailed natural language prompt for the agent to execute. Either this or prompt_file is required.',
|
|
155
|
+
},
|
|
156
|
+
prompt_file: {
|
|
157
|
+
type: 'string',
|
|
158
|
+
description: 'Path to a file containing the prompt. Either this or prompt is required. Must be an absolute path or relative to workFolder.',
|
|
159
|
+
},
|
|
160
|
+
workFolder: {
|
|
161
|
+
type: 'string',
|
|
162
|
+
description: 'The working directory for the agent execution. Must be an absolute path.',
|
|
163
|
+
},
|
|
164
|
+
model: {
|
|
165
|
+
type: 'string',
|
|
166
|
+
description: getModelParameterDescription(),
|
|
167
|
+
},
|
|
168
|
+
reasoning_effort: {
|
|
169
|
+
type: 'string',
|
|
170
|
+
description: 'Reasoning control for Claude and Codex. Claude uses --effort with "low", "medium", "high". Codex uses model_reasoning_effort with "low", "medium", "high", "xhigh". Forge does not support reasoning_effort in this integration.',
|
|
171
|
+
},
|
|
172
|
+
session_id: {
|
|
173
|
+
type: 'string',
|
|
174
|
+
description: 'Optional session ID to resume a previous session. Supported for: haiku, sonnet, opus, gemini-2.5-pro, gemini-2.5-flash, gemini-3.1-pro-preview, gemini-3-pro-preview, gemini-3-flash-preview, forge.',
|
|
175
|
+
},
|
|
176
|
+
},
|
|
177
|
+
required: ['workFolder'],
|
|
178
|
+
},
|
|
179
|
+
},
|
|
180
|
+
{
|
|
181
|
+
name: 'list_processes',
|
|
182
|
+
description: 'List all running and completed AI agent processes. Returns a simple list with PID, agent type, and status for each process.',
|
|
183
|
+
inputSchema: {
|
|
184
|
+
type: 'object',
|
|
185
|
+
properties: {},
|
|
186
|
+
},
|
|
187
|
+
},
|
|
188
|
+
{
|
|
189
|
+
name: 'get_result',
|
|
190
|
+
description: 'Get the current output and status of an AI agent process by PID. Returns the output from the agent including session_id (if applicable), along with process metadata.',
|
|
191
|
+
inputSchema: {
|
|
192
|
+
type: 'object',
|
|
193
|
+
properties: {
|
|
194
|
+
pid: {
|
|
195
|
+
type: 'number',
|
|
196
|
+
description: 'The process ID returned by run tool.',
|
|
197
|
+
},
|
|
198
|
+
verbose: {
|
|
199
|
+
type: 'boolean',
|
|
200
|
+
description: 'Optional: If true, returns detailed execution information including tool usage history. Defaults to false.',
|
|
201
|
+
}
|
|
202
|
+
},
|
|
203
|
+
required: ['pid'],
|
|
204
|
+
},
|
|
205
|
+
},
|
|
206
|
+
{
|
|
207
|
+
name: 'wait',
|
|
208
|
+
description: 'Wait for multiple AI agent processes to complete and return their results. Blocks until all specified PIDs finish or timeout occurs.',
|
|
209
|
+
inputSchema: {
|
|
210
|
+
type: 'object',
|
|
211
|
+
properties: {
|
|
212
|
+
pids: {
|
|
213
|
+
type: 'array',
|
|
214
|
+
items: { type: 'number' },
|
|
215
|
+
description: 'List of process IDs to wait for (returned by the run tool).',
|
|
216
|
+
},
|
|
217
|
+
timeout: {
|
|
218
|
+
type: 'number',
|
|
219
|
+
description: 'Optional: Maximum time to wait in seconds. Defaults to 180 (3 minutes).',
|
|
220
|
+
},
|
|
221
|
+
},
|
|
222
|
+
required: ['pids'],
|
|
223
|
+
},
|
|
224
|
+
},
|
|
225
|
+
{
|
|
226
|
+
name: 'kill_process',
|
|
227
|
+
description: 'Terminate a running AI agent process by PID.',
|
|
228
|
+
inputSchema: {
|
|
229
|
+
type: 'object',
|
|
230
|
+
properties: {
|
|
231
|
+
pid: {
|
|
232
|
+
type: 'number',
|
|
233
|
+
description: 'The process ID to terminate.',
|
|
234
|
+
},
|
|
235
|
+
},
|
|
236
|
+
required: ['pid'],
|
|
237
|
+
},
|
|
238
|
+
},
|
|
239
|
+
{
|
|
240
|
+
name: 'cleanup_processes',
|
|
241
|
+
description: 'Remove all completed and failed processes from the process list to free up memory.',
|
|
242
|
+
inputSchema: {
|
|
243
|
+
type: 'object',
|
|
244
|
+
properties: {},
|
|
245
|
+
},
|
|
246
|
+
}
|
|
247
|
+
],
|
|
248
|
+
}));
|
|
249
|
+
|
|
250
|
+
this.server.setRequestHandler(CallToolRequestSchema, async (args): Promise<ServerResult> => {
|
|
251
|
+
debugLog('[Debug] Handling CallToolRequest:', args);
|
|
252
|
+
|
|
253
|
+
const toolName = args.params.name;
|
|
254
|
+
const toolArguments = args.params.arguments || {};
|
|
255
|
+
|
|
256
|
+
switch (toolName) {
|
|
257
|
+
case 'run':
|
|
258
|
+
return this.handleRun(toolArguments);
|
|
259
|
+
case 'list_processes':
|
|
260
|
+
return this.handleListProcesses();
|
|
261
|
+
case 'get_result':
|
|
262
|
+
return this.handleGetResult(toolArguments);
|
|
263
|
+
case 'wait':
|
|
264
|
+
return this.handleWait(toolArguments);
|
|
265
|
+
case 'kill_process':
|
|
266
|
+
return this.handleKillProcess(toolArguments);
|
|
267
|
+
case 'cleanup_processes':
|
|
268
|
+
return this.handleCleanupProcesses();
|
|
269
|
+
default:
|
|
270
|
+
throw new McpError(ErrorCode.MethodNotFound, `Tool ${toolName} not found`);
|
|
271
|
+
}
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
private async handleRun(toolArguments: any): Promise<ServerResult> {
|
|
276
|
+
if (isFirstToolUse) {
|
|
277
|
+
console.error(`ai_cli_mcp v${SERVER_VERSION} started at ${serverStartupTime}`);
|
|
278
|
+
isFirstToolUse = false;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
try {
|
|
282
|
+
const result = this.processService.startProcess({
|
|
283
|
+
prompt: toolArguments.prompt,
|
|
284
|
+
prompt_file: toolArguments.prompt_file,
|
|
285
|
+
workFolder: toolArguments.workFolder,
|
|
286
|
+
model: toolArguments.model,
|
|
287
|
+
session_id: toolArguments.session_id,
|
|
288
|
+
reasoning_effort: toolArguments.reasoning_effort,
|
|
289
|
+
});
|
|
290
|
+
return {
|
|
291
|
+
content: [{
|
|
292
|
+
type: 'text',
|
|
293
|
+
text: JSON.stringify(result, null, 2)
|
|
294
|
+
}]
|
|
295
|
+
};
|
|
296
|
+
} catch (error: any) {
|
|
297
|
+
const code = /Failed to start/.test(error.message) ? ErrorCode.InternalError : ErrorCode.InvalidParams;
|
|
298
|
+
throw new McpError(code, error.message);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
private async handleListProcesses(): Promise<ServerResult> {
|
|
303
|
+
return {
|
|
304
|
+
content: [{
|
|
305
|
+
type: 'text',
|
|
306
|
+
text: JSON.stringify(this.processService.listProcesses(), null, 2)
|
|
307
|
+
}]
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
private async handleGetResult(toolArguments: any): Promise<ServerResult> {
|
|
312
|
+
if (!toolArguments.pid || typeof toolArguments.pid !== 'number') {
|
|
313
|
+
throw new McpError(ErrorCode.InvalidParams, 'Missing or invalid required parameter: pid');
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
const pid = toolArguments.pid;
|
|
317
|
+
const verbose = !!toolArguments.verbose;
|
|
318
|
+
try {
|
|
319
|
+
const response = this.processService.getProcessResult(pid, verbose);
|
|
320
|
+
return {
|
|
321
|
+
content: [{
|
|
322
|
+
type: 'text',
|
|
323
|
+
text: JSON.stringify(response, null, 2)
|
|
324
|
+
}]
|
|
325
|
+
};
|
|
326
|
+
} catch (error: any) {
|
|
327
|
+
const code = /not found/.test(error.message) ? ErrorCode.InvalidParams : ErrorCode.InternalError;
|
|
328
|
+
throw new McpError(code, error.message);
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
private async handleWait(toolArguments: any): Promise<ServerResult> {
|
|
333
|
+
if (!toolArguments.pids || !Array.isArray(toolArguments.pids) || toolArguments.pids.length === 0) {
|
|
334
|
+
throw new McpError(ErrorCode.InvalidParams, 'Missing or invalid required parameter: pids (must be a non-empty array of numbers)');
|
|
335
|
+
}
|
|
336
|
+
try {
|
|
337
|
+
const results = await this.processService.waitForProcesses(
|
|
338
|
+
toolArguments.pids,
|
|
339
|
+
typeof toolArguments.timeout === 'number' ? toolArguments.timeout : 180
|
|
340
|
+
);
|
|
341
|
+
return {
|
|
342
|
+
content: [{
|
|
343
|
+
type: 'text',
|
|
344
|
+
text: JSON.stringify(results, null, 2)
|
|
345
|
+
}]
|
|
346
|
+
};
|
|
347
|
+
} catch (error: any) {
|
|
348
|
+
const code = /not found/.test(error.message) ? ErrorCode.InvalidParams : ErrorCode.InternalError;
|
|
349
|
+
throw new McpError(code, error.message);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
private async handleKillProcess(toolArguments: any): Promise<ServerResult> {
|
|
354
|
+
if (!toolArguments.pid || typeof toolArguments.pid !== 'number') {
|
|
355
|
+
throw new McpError(ErrorCode.InvalidParams, 'Missing or invalid required parameter: pid');
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
const pid = toolArguments.pid;
|
|
359
|
+
try {
|
|
360
|
+
const response = this.processService.killProcess(pid);
|
|
361
|
+
return {
|
|
362
|
+
content: [{
|
|
363
|
+
type: 'text',
|
|
364
|
+
text: JSON.stringify(response, null, 2)
|
|
365
|
+
}]
|
|
366
|
+
};
|
|
367
|
+
} catch (error: any) {
|
|
368
|
+
const code = /not found/.test(error.message) ? ErrorCode.InvalidParams : ErrorCode.InternalError;
|
|
369
|
+
const message = code === ErrorCode.InternalError
|
|
370
|
+
? `Failed to terminate process: ${error.message}`
|
|
371
|
+
: error.message;
|
|
372
|
+
throw new McpError(code, message);
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
private async handleCleanupProcesses(): Promise<ServerResult> {
|
|
377
|
+
return {
|
|
378
|
+
content: [{
|
|
379
|
+
type: 'text',
|
|
380
|
+
text: JSON.stringify(this.processService.cleanupProcesses(), null, 2)
|
|
381
|
+
}]
|
|
382
|
+
};
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
async run(): Promise<void> {
|
|
386
|
+
const transport = new StdioServerTransport();
|
|
387
|
+
await this.server.connect(transport);
|
|
388
|
+
console.error('AI CLI MCP server running on stdio');
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
async cleanup(): Promise<void> {
|
|
392
|
+
if (this.sigintHandler) {
|
|
393
|
+
process.removeListener('SIGINT', this.sigintHandler);
|
|
394
|
+
}
|
|
395
|
+
await this.server.close();
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
export async function runMcpServer(): Promise<void> {
|
|
400
|
+
const server = new ClaudeCodeServer();
|
|
401
|
+
await server.run();
|
|
402
|
+
}
|
package/src/cli-builder.ts
CHANGED
|
@@ -1,17 +1,14 @@
|
|
|
1
1
|
import { existsSync, readFileSync } from 'node:fs';
|
|
2
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.4',
|
|
8
|
-
'gemini-ultra': 'gemini-3.1-pro-preview'
|
|
9
|
-
};
|
|
3
|
+
import { MODEL_ALIASES } from './model-catalog.js';
|
|
10
4
|
|
|
11
5
|
export const ALLOWED_REASONING_EFFORTS = new Set(['low', 'medium', 'high', 'xhigh']);
|
|
12
6
|
const CLAUDE_REASONING_EFFORTS = new Set(['low', 'medium', 'high']);
|
|
13
7
|
|
|
14
|
-
function getAgentForModel(model: string): 'codex' | 'claude' | 'gemini' {
|
|
8
|
+
function getAgentForModel(model: string): 'codex' | 'claude' | 'gemini' | 'forge' {
|
|
9
|
+
if (model === 'forge') {
|
|
10
|
+
return 'forge';
|
|
11
|
+
}
|
|
15
12
|
if (model.startsWith('gpt-')) {
|
|
16
13
|
return 'codex';
|
|
17
14
|
}
|
|
@@ -50,6 +47,9 @@ export function getReasoningEffort(model: string, rawValue: unknown): string {
|
|
|
50
47
|
);
|
|
51
48
|
}
|
|
52
49
|
const agent = getAgentForModel(model);
|
|
50
|
+
if (agent === 'forge') {
|
|
51
|
+
throw new Error('reasoning_effort is not supported for forge.');
|
|
52
|
+
}
|
|
53
53
|
if (agent === 'gemini') {
|
|
54
54
|
throw new Error(
|
|
55
55
|
'reasoning_effort is only supported for Claude and Codex models.'
|
|
@@ -67,7 +67,7 @@ export interface CliCommand {
|
|
|
67
67
|
cliPath: string;
|
|
68
68
|
args: string[];
|
|
69
69
|
cwd: string;
|
|
70
|
-
agent: 'claude' | 'codex' | 'gemini';
|
|
70
|
+
agent: 'claude' | 'codex' | 'gemini' | 'forge';
|
|
71
71
|
prompt: string;
|
|
72
72
|
resolvedModel: string;
|
|
73
73
|
}
|
|
@@ -79,7 +79,7 @@ export interface BuildCliCommandOptions {
|
|
|
79
79
|
model?: string;
|
|
80
80
|
session_id?: string;
|
|
81
81
|
reasoning_effort?: string;
|
|
82
|
-
cliPaths: { claude: string; codex: string; gemini: string };
|
|
82
|
+
cliPaths: { claude: string; codex: string; gemini: string; forge: string };
|
|
83
83
|
}
|
|
84
84
|
|
|
85
85
|
/**
|
|
@@ -184,6 +184,15 @@ export function buildCliCommand(options: BuildCliCommandOptions): CliCommand {
|
|
|
184
184
|
|
|
185
185
|
args.push(prompt);
|
|
186
186
|
|
|
187
|
+
} else if (agent === 'forge') {
|
|
188
|
+
cliPath = options.cliPaths.forge;
|
|
189
|
+
args = ['-C', cwd];
|
|
190
|
+
|
|
191
|
+
if (options.session_id && typeof options.session_id === 'string') {
|
|
192
|
+
args.push('--conversation-id', options.session_id);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
args.push('-p', prompt);
|
|
187
196
|
} else {
|
|
188
197
|
cliPath = options.cliPaths.claude;
|
|
189
198
|
args = ['--dangerously-skip-permissions', '--output-format', 'stream-json', '--verbose'];
|
package/src/cli-parse.ts
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { parseClaudeOutput, parseCodexOutput, parseGeminiOutput } from './parsers.js';
|
|
2
|
+
import { parseClaudeOutput, parseCodexOutput, parseForgeOutput, parseGeminiOutput } from './parsers.js';
|
|
3
3
|
|
|
4
|
-
const AGENTS = ['claude', 'codex', 'gemini'] as const;
|
|
4
|
+
const AGENTS = ['claude', 'codex', 'gemini', 'forge'] as const;
|
|
5
5
|
type Agent = typeof AGENTS[number];
|
|
6
6
|
|
|
7
|
-
const USAGE = `Usage: npm run -s cli.run.parse -- --agent <claude|codex|gemini>
|
|
7
|
+
const USAGE = `Usage: npm run -s cli.run.parse -- --agent <claude|codex|gemini|forge>
|
|
8
8
|
|
|
9
9
|
Reads raw CLI output from stdin and outputs parsed JSON to stdout.
|
|
10
10
|
|
|
11
11
|
Options:
|
|
12
|
-
--agent Agent type: claude, codex, or
|
|
12
|
+
--agent Agent type: claude, codex, gemini, or forge (required)
|
|
13
13
|
--help Show this help message
|
|
14
14
|
|
|
15
15
|
Examples:
|
|
@@ -62,7 +62,7 @@ async function main(): Promise<void> {
|
|
|
62
62
|
|
|
63
63
|
const agent = args.agent as Agent;
|
|
64
64
|
if (!agent || !AGENTS.includes(agent)) {
|
|
65
|
-
process.stderr.write(`Error: --agent is required (claude, codex, or
|
|
65
|
+
process.stderr.write(`Error: --agent is required (claude, codex, gemini, or forge)\n\n`);
|
|
66
66
|
process.stderr.write(USAGE);
|
|
67
67
|
process.exit(1);
|
|
68
68
|
}
|
|
@@ -85,6 +85,9 @@ async function main(): Promise<void> {
|
|
|
85
85
|
case 'gemini':
|
|
86
86
|
parsed = parseGeminiOutput(input);
|
|
87
87
|
break;
|
|
88
|
+
case 'forge':
|
|
89
|
+
parsed = parseForgeOutput(input);
|
|
90
|
+
break;
|
|
88
91
|
}
|
|
89
92
|
|
|
90
93
|
process.stdout.write(JSON.stringify(parsed, null, 2) + '\n');
|