ai-cli-mcp 2.12.0 → 2.14.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 +20 -0
- package/README.ja.md +20 -5
- package/README.md +20 -6
- package/dist/__tests__/app-cli.test.js +34 -2
- package/dist/__tests__/cli-bin-smoke.test.js +4 -0
- package/dist/__tests__/cli-builder.test.js +37 -0
- package/dist/__tests__/cli-process-service.test.js +180 -5
- package/dist/__tests__/cli-utils.test.js +31 -0
- package/dist/__tests__/mcp-contract.test.js +287 -9
- package/dist/__tests__/parsers.test.js +37 -1
- package/dist/__tests__/process-management.test.js +2 -1
- package/dist/app/cli.js +8 -6
- package/dist/app/mcp.js +16 -8
- package/dist/cli-builder.js +14 -0
- package/dist/cli-parse.js +8 -5
- package/dist/cli-process-service.js +13 -23
- package/dist/cli-utils.js +17 -0
- package/dist/cli.js +4 -3
- package/dist/model-catalog.js +4 -1
- package/dist/parsers.js +55 -0
- package/dist/process-result.js +51 -0
- package/dist/process-service.js +11 -22
- package/dist/server.js +1 -1
- package/package.json +2 -2
- package/server.json +1 -1
- package/src/__tests__/app-cli.test.ts +43 -1
- package/src/__tests__/cli-bin-smoke.test.ts +4 -0
- package/src/__tests__/cli-builder.test.ts +47 -0
- package/src/__tests__/cli-process-service.test.ts +200 -5
- package/src/__tests__/cli-utils.test.ts +34 -0
- package/src/__tests__/mcp-contract.test.ts +325 -9
- package/src/__tests__/parsers.test.ts +44 -1
- package/src/__tests__/process-management.test.ts +2 -1
- package/src/app/cli.ts +9 -7
- package/src/app/mcp.ts +17 -8
- package/src/cli-builder.ts +18 -3
- package/src/cli-parse.ts +8 -5
- package/src/cli-process-service.ts +12 -23
- package/src/cli-utils.ts +21 -1
- package/src/cli.ts +4 -3
- package/src/model-catalog.ts +5 -1
- package/src/parsers.ts +61 -0
- package/src/process-result.ts +79 -0
- package/src/process-service.ts +11 -24
- package/src/server.ts +1 -1
package/dist/app/cli.js
CHANGED
|
@@ -24,9 +24,9 @@ Options:
|
|
|
24
24
|
--cwd <path> Working directory
|
|
25
25
|
--prompt <text> Prompt text
|
|
26
26
|
--prompt-file <path> Path to a prompt file
|
|
27
|
-
--model <model> Model name or alias (e.g. sonnet, claude-ultra, gpt-5.2-codex, codex-ultra, gemini-2.5-pro, gemini-ultra)
|
|
27
|
+
--model <model> Model name or alias (e.g. sonnet, claude-ultra, gpt-5.2-codex, codex-ultra, gemini-2.5-pro, gemini-ultra, forge)
|
|
28
28
|
--session-id <id> Resume a previous session
|
|
29
|
-
--reasoning-effort <level> Reasoning level for Claude/Codex
|
|
29
|
+
--reasoning-effort <level> Reasoning level for Claude/Codex only
|
|
30
30
|
--help, -h Show this help message
|
|
31
31
|
|
|
32
32
|
Compatibility aliases:
|
|
@@ -38,17 +38,19 @@ Compatibility aliases:
|
|
|
38
38
|
export const WAIT_HELP_TEXT = `Usage: ai-cli wait <pid...> [options]
|
|
39
39
|
|
|
40
40
|
Wait for one or more tracked processes to finish.
|
|
41
|
+
By default each result uses the compact shape; set --verbose to include full metadata and detailed parsed output.
|
|
41
42
|
|
|
42
43
|
Options:
|
|
43
44
|
--timeout <seconds> Maximum wait time in seconds
|
|
45
|
+
--verbose Return full metadata and detailed parsed output
|
|
44
46
|
--help, -h Show this help message
|
|
45
47
|
`;
|
|
46
48
|
export const RESULT_HELP_TEXT = `Usage: ai-cli result <pid> [options]
|
|
47
49
|
|
|
48
|
-
Get the current
|
|
50
|
+
Get the current output and status of a tracked process. By default this returns a compact result shape; set --verbose to include full metadata and detailed parsed output.
|
|
49
51
|
|
|
50
52
|
Options:
|
|
51
|
-
--verbose
|
|
53
|
+
--verbose Return full metadata and detailed parsed output
|
|
52
54
|
--help, -h Show this help message
|
|
53
55
|
`;
|
|
54
56
|
export const KILL_HELP_TEXT = `Usage: ai-cli kill <pid>
|
|
@@ -104,7 +106,7 @@ const defaultDeps = {
|
|
|
104
106
|
runProcess: (options) => getCliProcessService().startProcess(options),
|
|
105
107
|
listProcesses: () => getCliProcessService().listProcesses(),
|
|
106
108
|
getProcessResult: (pid, verbose) => getCliProcessService().getProcessResult(pid, verbose),
|
|
107
|
-
waitForProcesses: (pids, timeoutSeconds) => getCliProcessService().waitForProcesses(pids, timeoutSeconds),
|
|
109
|
+
waitForProcesses: (pids, timeoutSeconds, verbose) => getCliProcessService().waitForProcesses(pids, timeoutSeconds, verbose),
|
|
108
110
|
killProcess: (pid) => getCliProcessService().killProcess(pid),
|
|
109
111
|
cleanupProcesses: () => getCliProcessService().cleanupProcesses(),
|
|
110
112
|
getDoctorStatus: () => getCliDoctorStatus(),
|
|
@@ -253,7 +255,7 @@ export async function runCli(argv, deps = {}) {
|
|
|
253
255
|
stdout(CLI_HELP_TEXT);
|
|
254
256
|
return 1;
|
|
255
257
|
}
|
|
256
|
-
writeJson(stdout, await waitForProcesses(pids, timeout));
|
|
258
|
+
writeJson(stdout, await waitForProcesses(pids, timeout, 'verbose' in flags));
|
|
257
259
|
return 0;
|
|
258
260
|
}
|
|
259
261
|
if (command === 'kill') {
|
package/dist/app/mcp.js
CHANGED
|
@@ -2,7 +2,7 @@ import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
|
2
2
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
3
3
|
import { CallToolRequestSchema, ErrorCode, ListToolsRequestSchema, McpError, } from '@modelcontextprotocol/sdk/types.js';
|
|
4
4
|
import { spawn } from 'node:child_process';
|
|
5
|
-
import { debugLog, findClaudeCli, findCodexCli, findGeminiCli } from '../cli-utils.js';
|
|
5
|
+
import { debugLog, findClaudeCli, findCodexCli, findForgeCli, findGeminiCli } from '../cli-utils.js';
|
|
6
6
|
import { getModelParameterDescription, getSupportedModelsDescription } from '../model-catalog.js';
|
|
7
7
|
import { ProcessService } from '../process-service.js';
|
|
8
8
|
// Server version - update this when releasing new versions
|
|
@@ -58,6 +58,7 @@ export class ClaudeCodeServer {
|
|
|
58
58
|
claudeCliPath;
|
|
59
59
|
codexCliPath;
|
|
60
60
|
geminiCliPath;
|
|
61
|
+
forgeCliPath;
|
|
61
62
|
processService;
|
|
62
63
|
sigintHandler;
|
|
63
64
|
packageVersion;
|
|
@@ -65,15 +66,18 @@ export class ClaudeCodeServer {
|
|
|
65
66
|
this.claudeCliPath = findClaudeCli();
|
|
66
67
|
this.codexCliPath = findCodexCli();
|
|
67
68
|
this.geminiCliPath = findGeminiCli();
|
|
69
|
+
this.forgeCliPath = findForgeCli();
|
|
68
70
|
console.error(`[Setup] Using Claude CLI command/path: ${this.claudeCliPath}`);
|
|
69
71
|
console.error(`[Setup] Using Codex CLI command/path: ${this.codexCliPath}`);
|
|
70
72
|
console.error(`[Setup] Using Gemini CLI command/path: ${this.geminiCliPath}`);
|
|
73
|
+
console.error(`[Setup] Using Forge CLI command/path: ${this.forgeCliPath}`);
|
|
71
74
|
this.packageVersion = SERVER_VERSION;
|
|
72
75
|
this.processService = new ProcessService({
|
|
73
76
|
cliPaths: {
|
|
74
77
|
claude: this.claudeCliPath,
|
|
75
78
|
codex: this.codexCliPath,
|
|
76
79
|
gemini: this.geminiCliPath,
|
|
80
|
+
forge: this.forgeCliPath,
|
|
77
81
|
},
|
|
78
82
|
});
|
|
79
83
|
this.server = new Server({
|
|
@@ -97,7 +101,7 @@ export class ClaudeCodeServer {
|
|
|
97
101
|
tools: [
|
|
98
102
|
{
|
|
99
103
|
name: 'run',
|
|
100
|
-
description: `AI Agent Runner: Starts a Claude, Codex, or
|
|
104
|
+
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.
|
|
101
105
|
|
|
102
106
|
• File ops: Create, read, (fuzzy) edit, move, copy, delete, list files, analyze/ocr images, file content analysis
|
|
103
107
|
• Code: Generate / analyse / refactor / fix
|
|
@@ -141,11 +145,11 @@ ${getSupportedModelsDescription()}
|
|
|
141
145
|
},
|
|
142
146
|
reasoning_effort: {
|
|
143
147
|
type: 'string',
|
|
144
|
-
description: 'Reasoning control for Claude and Codex. Claude uses --effort with "low", "medium", "high". Codex uses model_reasoning_effort with "low", "medium", "high", "xhigh".',
|
|
148
|
+
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.',
|
|
145
149
|
},
|
|
146
150
|
session_id: {
|
|
147
151
|
type: 'string',
|
|
148
|
-
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.',
|
|
152
|
+
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.',
|
|
149
153
|
},
|
|
150
154
|
},
|
|
151
155
|
required: ['workFolder'],
|
|
@@ -161,7 +165,7 @@ ${getSupportedModelsDescription()}
|
|
|
161
165
|
},
|
|
162
166
|
{
|
|
163
167
|
name: 'get_result',
|
|
164
|
-
description: 'Get the current output and status of an AI agent process by PID.
|
|
168
|
+
description: 'Get the current output and status of an AI agent process by PID. Defaults to a compact result shape; set verbose to true for full metadata and detailed parsed output.',
|
|
165
169
|
inputSchema: {
|
|
166
170
|
type: 'object',
|
|
167
171
|
properties: {
|
|
@@ -171,7 +175,7 @@ ${getSupportedModelsDescription()}
|
|
|
171
175
|
},
|
|
172
176
|
verbose: {
|
|
173
177
|
type: 'boolean',
|
|
174
|
-
description: 'Optional: If true, returns
|
|
178
|
+
description: 'Optional: If true, returns the full result shape including metadata fields and detailed parsed output such as tool usage history. Defaults to false.',
|
|
175
179
|
}
|
|
176
180
|
},
|
|
177
181
|
required: ['pid'],
|
|
@@ -179,7 +183,7 @@ ${getSupportedModelsDescription()}
|
|
|
179
183
|
},
|
|
180
184
|
{
|
|
181
185
|
name: 'wait',
|
|
182
|
-
description: 'Wait for multiple AI agent processes to complete and return their results.
|
|
186
|
+
description: 'Wait for multiple AI agent processes to complete and return their results. Defaults to compact result items; set verbose to true for full metadata and detailed parsed output.',
|
|
183
187
|
inputSchema: {
|
|
184
188
|
type: 'object',
|
|
185
189
|
properties: {
|
|
@@ -192,6 +196,10 @@ ${getSupportedModelsDescription()}
|
|
|
192
196
|
type: 'number',
|
|
193
197
|
description: 'Optional: Maximum time to wait in seconds. Defaults to 180 (3 minutes).',
|
|
194
198
|
},
|
|
199
|
+
verbose: {
|
|
200
|
+
type: 'boolean',
|
|
201
|
+
description: 'Optional: If true, each result item uses the full result shape including metadata fields and detailed parsed output. Defaults to false.',
|
|
202
|
+
},
|
|
195
203
|
},
|
|
196
204
|
required: ['pids'],
|
|
197
205
|
},
|
|
@@ -301,7 +309,7 @@ ${getSupportedModelsDescription()}
|
|
|
301
309
|
throw new McpError(ErrorCode.InvalidParams, 'Missing or invalid required parameter: pids (must be a non-empty array of numbers)');
|
|
302
310
|
}
|
|
303
311
|
try {
|
|
304
|
-
const results = await this.processService.waitForProcesses(toolArguments.pids, typeof toolArguments.timeout === 'number' ? toolArguments.timeout : 180);
|
|
312
|
+
const results = await this.processService.waitForProcesses(toolArguments.pids, typeof toolArguments.timeout === 'number' ? toolArguments.timeout : 180, !!toolArguments.verbose);
|
|
305
313
|
return {
|
|
306
314
|
content: [{
|
|
307
315
|
type: 'text',
|
package/dist/cli-builder.js
CHANGED
|
@@ -4,6 +4,9 @@ import { MODEL_ALIASES } from './model-catalog.js';
|
|
|
4
4
|
export const ALLOWED_REASONING_EFFORTS = new Set(['low', 'medium', 'high', 'xhigh']);
|
|
5
5
|
const CLAUDE_REASONING_EFFORTS = new Set(['low', 'medium', 'high']);
|
|
6
6
|
function getAgentForModel(model) {
|
|
7
|
+
if (model === 'forge') {
|
|
8
|
+
return 'forge';
|
|
9
|
+
}
|
|
7
10
|
if (model.startsWith('gpt-')) {
|
|
8
11
|
return 'codex';
|
|
9
12
|
}
|
|
@@ -38,6 +41,9 @@ export function getReasoningEffort(model, rawValue) {
|
|
|
38
41
|
throw new Error(`Invalid reasoning_effort: ${rawValue}. Allowed values: low, medium, high, xhigh.`);
|
|
39
42
|
}
|
|
40
43
|
const agent = getAgentForModel(model);
|
|
44
|
+
if (agent === 'forge') {
|
|
45
|
+
throw new Error('reasoning_effort is not supported for forge.');
|
|
46
|
+
}
|
|
41
47
|
if (agent === 'gemini') {
|
|
42
48
|
throw new Error('reasoning_effort is only supported for Claude and Codex models.');
|
|
43
49
|
}
|
|
@@ -134,6 +140,14 @@ export function buildCliCommand(options) {
|
|
|
134
140
|
}
|
|
135
141
|
args.push(prompt);
|
|
136
142
|
}
|
|
143
|
+
else if (agent === 'forge') {
|
|
144
|
+
cliPath = options.cliPaths.forge;
|
|
145
|
+
args = ['-C', cwd];
|
|
146
|
+
if (options.session_id && typeof options.session_id === 'string') {
|
|
147
|
+
args.push('--conversation-id', options.session_id);
|
|
148
|
+
}
|
|
149
|
+
args.push('-p', prompt);
|
|
150
|
+
}
|
|
137
151
|
else {
|
|
138
152
|
cliPath = options.cliPaths.claude;
|
|
139
153
|
args = ['--dangerously-skip-permissions', '--output-format', 'stream-json', '--verbose'];
|
package/dist/cli-parse.js
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { parseClaudeOutput, parseCodexOutput, parseGeminiOutput } from './parsers.js';
|
|
3
|
-
const AGENTS = ['claude', 'codex', 'gemini'];
|
|
4
|
-
const USAGE = `Usage: npm run -s cli.run.parse -- --agent <claude|codex|gemini>
|
|
2
|
+
import { parseClaudeOutput, parseCodexOutput, parseForgeOutput, parseGeminiOutput } from './parsers.js';
|
|
3
|
+
const AGENTS = ['claude', 'codex', 'gemini', 'forge'];
|
|
4
|
+
const USAGE = `Usage: npm run -s cli.run.parse -- --agent <claude|codex|gemini|forge>
|
|
5
5
|
|
|
6
6
|
Reads raw CLI output from stdin and outputs parsed JSON to stdout.
|
|
7
7
|
|
|
8
8
|
Options:
|
|
9
|
-
--agent Agent type: claude, codex, or
|
|
9
|
+
--agent Agent type: claude, codex, gemini, or forge (required)
|
|
10
10
|
--help Show this help message
|
|
11
11
|
|
|
12
12
|
Examples:
|
|
@@ -56,7 +56,7 @@ async function main() {
|
|
|
56
56
|
}
|
|
57
57
|
const agent = args.agent;
|
|
58
58
|
if (!agent || !AGENTS.includes(agent)) {
|
|
59
|
-
process.stderr.write(`Error: --agent is required (claude, codex, or
|
|
59
|
+
process.stderr.write(`Error: --agent is required (claude, codex, gemini, or forge)\n\n`);
|
|
60
60
|
process.stderr.write(USAGE);
|
|
61
61
|
process.exit(1);
|
|
62
62
|
}
|
|
@@ -76,6 +76,9 @@ async function main() {
|
|
|
76
76
|
case 'gemini':
|
|
77
77
|
parsed = parseGeminiOutput(input);
|
|
78
78
|
break;
|
|
79
|
+
case 'forge':
|
|
80
|
+
parsed = parseForgeOutput(input);
|
|
81
|
+
break;
|
|
79
82
|
}
|
|
80
83
|
process.stdout.write(JSON.stringify(parsed, null, 2) + '\n');
|
|
81
84
|
}
|
|
@@ -3,8 +3,9 @@ import { closeSync, existsSync, mkdirSync, openSync, readFileSync, readdirSync,
|
|
|
3
3
|
import { join } from 'node:path';
|
|
4
4
|
import { homedir } from 'node:os';
|
|
5
5
|
import { buildCliCommand } from './cli-builder.js';
|
|
6
|
-
import { findClaudeCli, findCodexCli, findGeminiCli } from './cli-utils.js';
|
|
7
|
-
import { parseClaudeOutput, parseCodexOutput, parseGeminiOutput } from './parsers.js';
|
|
6
|
+
import { findClaudeCli, findCodexCli, findForgeCli, findGeminiCli } from './cli-utils.js';
|
|
7
|
+
import { parseClaudeOutput, parseCodexOutput, parseForgeOutput, parseGeminiOutput } from './parsers.js';
|
|
8
|
+
import { buildProcessResult } from './process-result.js';
|
|
8
9
|
function resolveDefaultStateDir() {
|
|
9
10
|
return process.env.AI_CLI_STATE_DIR || join(homedir(), '.local', 'state', 'ai-cli');
|
|
10
11
|
}
|
|
@@ -35,6 +36,7 @@ export class CliProcessService {
|
|
|
35
36
|
claude: findClaudeCli(),
|
|
36
37
|
codex: findCodexCli(),
|
|
37
38
|
gemini: findGeminiCli(),
|
|
39
|
+
forge: findForgeCli(),
|
|
38
40
|
};
|
|
39
41
|
mkdirSync(this.stateDir, { recursive: true });
|
|
40
42
|
}
|
|
@@ -127,8 +129,11 @@ export class CliProcessService {
|
|
|
127
129
|
else if (refreshed.toolType === 'gemini') {
|
|
128
130
|
agentOutput = parseGeminiOutput(stdout);
|
|
129
131
|
}
|
|
132
|
+
else if (refreshed.toolType === 'forge') {
|
|
133
|
+
agentOutput = parseForgeOutput(stdout);
|
|
134
|
+
}
|
|
130
135
|
}
|
|
131
|
-
|
|
136
|
+
return buildProcessResult({
|
|
132
137
|
pid,
|
|
133
138
|
agent: refreshed.toolType,
|
|
134
139
|
status: refreshed.status,
|
|
@@ -137,26 +142,11 @@ export class CliProcessService {
|
|
|
137
142
|
workFolder: refreshed.workFolder,
|
|
138
143
|
prompt: refreshed.prompt,
|
|
139
144
|
model: refreshed.model,
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
const { tools, ...rest } = agentOutput;
|
|
144
|
-
response.agentOutput = rest;
|
|
145
|
-
}
|
|
146
|
-
else {
|
|
147
|
-
response.agentOutput = agentOutput;
|
|
148
|
-
}
|
|
149
|
-
if (agentOutput.session_id) {
|
|
150
|
-
response.session_id = agentOutput.session_id;
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
else {
|
|
154
|
-
response.stdout = stdout;
|
|
155
|
-
response.stderr = stderr;
|
|
156
|
-
}
|
|
157
|
-
return response;
|
|
145
|
+
stdout,
|
|
146
|
+
stderr,
|
|
147
|
+
}, agentOutput, verbose);
|
|
158
148
|
}
|
|
159
|
-
async waitForProcesses(pids, timeoutSeconds = 180) {
|
|
149
|
+
async waitForProcesses(pids, timeoutSeconds = 180, verbose = false) {
|
|
160
150
|
const start = Date.now();
|
|
161
151
|
for (const pid of pids) {
|
|
162
152
|
this.readProcess(pid);
|
|
@@ -164,7 +154,7 @@ export class CliProcessService {
|
|
|
164
154
|
while (true) {
|
|
165
155
|
const statuses = pids.map((pid) => this.refreshStatus(this.readProcess(pid)).status);
|
|
166
156
|
if (statuses.every((status) => status !== 'running')) {
|
|
167
|
-
return Promise.all(pids.map((pid) => this.getProcessResult(pid,
|
|
157
|
+
return Promise.all(pids.map((pid) => this.getProcessResult(pid, verbose)));
|
|
168
158
|
}
|
|
169
159
|
if (Date.now() - start >= timeoutSeconds * 1000) {
|
|
170
160
|
throw new Error(`Timed out after ${timeoutSeconds} seconds waiting for processes`);
|
package/dist/cli-utils.js
CHANGED
|
@@ -128,6 +128,14 @@ function getCliBinaryConfig(name) {
|
|
|
128
128
|
localInstallPath: join(homedir(), '.codex', 'local', 'codex'),
|
|
129
129
|
};
|
|
130
130
|
}
|
|
131
|
+
if (name === 'forge') {
|
|
132
|
+
return {
|
|
133
|
+
envVarName: 'FORGE_CLI_NAME',
|
|
134
|
+
customCliName: process.env.FORGE_CLI_NAME,
|
|
135
|
+
defaultCliName: 'forge',
|
|
136
|
+
localInstallPath: join(homedir(), '.forge', 'local', 'forge'),
|
|
137
|
+
};
|
|
138
|
+
}
|
|
131
139
|
return {
|
|
132
140
|
envVarName: 'GEMINI_CLI_NAME',
|
|
133
141
|
customCliName: process.env.GEMINI_CLI_NAME,
|
|
@@ -143,6 +151,7 @@ export function getCliDoctorStatus() {
|
|
|
143
151
|
claude: getCliBinaryStatus('claude'),
|
|
144
152
|
codex: getCliBinaryStatus('codex'),
|
|
145
153
|
gemini: getCliBinaryStatus('gemini'),
|
|
154
|
+
forge: getCliBinaryStatus('forge'),
|
|
146
155
|
};
|
|
147
156
|
}
|
|
148
157
|
/**
|
|
@@ -163,6 +172,14 @@ export function findCodexCli() {
|
|
|
163
172
|
const status = getCliBinaryStatus('codex');
|
|
164
173
|
return getCliCommandOrThrow(status);
|
|
165
174
|
}
|
|
175
|
+
/**
|
|
176
|
+
* Determine the Forge CLI command/path.
|
|
177
|
+
*/
|
|
178
|
+
export function findForgeCli() {
|
|
179
|
+
debugLog('[Debug] Attempting to find Forge CLI...');
|
|
180
|
+
const status = getCliBinaryStatus('forge');
|
|
181
|
+
return getCliCommandOrThrow(status);
|
|
182
|
+
}
|
|
166
183
|
/**
|
|
167
184
|
* Determine the Claude CLI command/path.
|
|
168
185
|
* 1. Checks for CLAUDE_CLI_NAME environment variable:
|
package/dist/cli.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { spawn } from 'node:child_process';
|
|
3
3
|
import { buildCliCommand } from './cli-builder.js';
|
|
4
|
-
import { findClaudeCli, findCodexCli, findGeminiCli } from './cli-utils.js';
|
|
4
|
+
import { findClaudeCli, findCodexCli, findForgeCli, findGeminiCli } from './cli-utils.js';
|
|
5
5
|
/**
|
|
6
6
|
* Minimal argv parser. No external dependencies.
|
|
7
7
|
* Supports: --key value, --key=value
|
|
@@ -35,12 +35,12 @@ function parseArgs(argv) {
|
|
|
35
35
|
const USAGE = `Usage: npm run -s cli.run -- --model <model> --workFolder <path> --prompt "..." [options]
|
|
36
36
|
|
|
37
37
|
Options:
|
|
38
|
-
--model Model name or alias (e.g. sonnet, opus, gpt-5.2-codex, gemini-2.5-pro)
|
|
38
|
+
--model Model name or alias (e.g. sonnet, opus, gpt-5.2-codex, gemini-2.5-pro, forge)
|
|
39
39
|
--workFolder Working directory (absolute path)
|
|
40
40
|
--prompt Prompt string (mutually exclusive with --prompt_file)
|
|
41
41
|
--prompt_file Path to a file containing the prompt
|
|
42
42
|
--session_id Session ID to resume
|
|
43
|
-
--reasoning_effort Claude/Codex: Claude=low|medium|high, Codex=low|medium|high|xhigh
|
|
43
|
+
--reasoning_effort Claude/Codex only: Claude=low|medium|high, Codex=low|medium|high|xhigh
|
|
44
44
|
--help Show this help message
|
|
45
45
|
|
|
46
46
|
Raw CLI output goes to stdout. Use cli.run.parse to parse the output:
|
|
@@ -68,6 +68,7 @@ async function main() {
|
|
|
68
68
|
claude: findClaudeCli(),
|
|
69
69
|
codex: findCodexCli(),
|
|
70
70
|
gemini: findGeminiCli(),
|
|
71
|
+
forge: findForgeCli(),
|
|
71
72
|
};
|
|
72
73
|
// Build command
|
|
73
74
|
let cmd;
|
package/dist/model-catalog.js
CHANGED
|
@@ -19,6 +19,7 @@ export const GEMINI_MODELS = [
|
|
|
19
19
|
'gemini-3-pro-preview',
|
|
20
20
|
'gemini-3-flash-preview',
|
|
21
21
|
];
|
|
22
|
+
export const FORGE_MODELS = ['forge'];
|
|
22
23
|
export const MODEL_ALIASES = {
|
|
23
24
|
'claude-ultra': 'opus',
|
|
24
25
|
'codex-ultra': 'gpt-5.4',
|
|
@@ -35,10 +36,11 @@ export function getSupportedModelsDescription() {
|
|
|
35
36
|
...CLAUDE_MODELS.map((model) => `"${model}"`),
|
|
36
37
|
...CODEX_MODELS.map((model) => `"${model}"`),
|
|
37
38
|
...GEMINI_MODELS.map((model) => `"${model}"`),
|
|
39
|
+
...FORGE_MODELS.map((model) => `"${model}"`),
|
|
38
40
|
].join(', ');
|
|
39
41
|
}
|
|
40
42
|
export function getModelParameterDescription() {
|
|
41
|
-
return `The model to use. Aliases: "claude-ultra" (auto high effort), "codex-ultra" (auto xhigh reasoning), "gemini-ultra". Standard: ${[...CLAUDE_MODELS, ...CODEX_MODELS, ...GEMINI_MODELS].map((model) => `"${model}"`).join(', ')}.`;
|
|
43
|
+
return `The model to use. Aliases: "claude-ultra" (auto high effort), "codex-ultra" (auto xhigh reasoning), "gemini-ultra". Standard: ${[...CLAUDE_MODELS, ...CODEX_MODELS, ...GEMINI_MODELS, ...FORGE_MODELS].map((model) => `"${model}"`).join(', ')}. "forge" is a provider key, not a Forge model family selector.`;
|
|
42
44
|
}
|
|
43
45
|
export function getModelsPayload() {
|
|
44
46
|
return {
|
|
@@ -46,5 +48,6 @@ export function getModelsPayload() {
|
|
|
46
48
|
claude: CLAUDE_MODELS,
|
|
47
49
|
codex: CODEX_MODELS,
|
|
48
50
|
gemini: GEMINI_MODELS,
|
|
51
|
+
forge: FORGE_MODELS,
|
|
49
52
|
};
|
|
50
53
|
}
|
package/dist/parsers.js
CHANGED
|
@@ -163,3 +163,58 @@ export function parseGeminiOutput(stdout) {
|
|
|
163
163
|
return null;
|
|
164
164
|
}
|
|
165
165
|
}
|
|
166
|
+
/**
|
|
167
|
+
* Parse Forge output framed by Initialize/Continue/Finished markers.
|
|
168
|
+
*/
|
|
169
|
+
export function parseForgeOutput(stdout) {
|
|
170
|
+
if (!stdout)
|
|
171
|
+
return null;
|
|
172
|
+
const lines = stdout.split('\n');
|
|
173
|
+
const markerPattern = /^● \[[^\]]+\] (Initialize|Continue|Finished) (\S+)\s*$/;
|
|
174
|
+
let collecting = false;
|
|
175
|
+
let currentConversationId = null;
|
|
176
|
+
let currentBody = [];
|
|
177
|
+
let lastConversationId = null;
|
|
178
|
+
let lastMessage = null;
|
|
179
|
+
for (const line of lines) {
|
|
180
|
+
const match = line.match(markerPattern);
|
|
181
|
+
if (match) {
|
|
182
|
+
const [, action, conversationId] = match;
|
|
183
|
+
lastConversationId = conversationId;
|
|
184
|
+
if (action === 'Initialize' || action === 'Continue') {
|
|
185
|
+
collecting = true;
|
|
186
|
+
currentConversationId = conversationId;
|
|
187
|
+
currentBody = [];
|
|
188
|
+
}
|
|
189
|
+
else if (collecting && currentConversationId === conversationId) {
|
|
190
|
+
const message = currentBody.join('\n').trim();
|
|
191
|
+
if (message) {
|
|
192
|
+
lastMessage = message;
|
|
193
|
+
}
|
|
194
|
+
collecting = false;
|
|
195
|
+
currentConversationId = null;
|
|
196
|
+
currentBody = [];
|
|
197
|
+
}
|
|
198
|
+
continue;
|
|
199
|
+
}
|
|
200
|
+
if (collecting) {
|
|
201
|
+
currentBody.push(line);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
if (collecting) {
|
|
205
|
+
const message = currentBody.join('\n').trim();
|
|
206
|
+
if (message) {
|
|
207
|
+
lastMessage = message;
|
|
208
|
+
}
|
|
209
|
+
if (currentConversationId) {
|
|
210
|
+
lastConversationId = currentConversationId;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
if (!lastMessage && !lastConversationId) {
|
|
214
|
+
return null;
|
|
215
|
+
}
|
|
216
|
+
return {
|
|
217
|
+
message: lastMessage,
|
|
218
|
+
session_id: lastConversationId,
|
|
219
|
+
};
|
|
220
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
function compactAgentOutput(agentOutput) {
|
|
2
|
+
if (!agentOutput || typeof agentOutput !== 'object') {
|
|
3
|
+
return null;
|
|
4
|
+
}
|
|
5
|
+
const { tools: _tools, ...rest } = agentOutput;
|
|
6
|
+
const compact = Object.fromEntries(Object.entries(rest).filter(([, value]) => value !== undefined && value !== null));
|
|
7
|
+
return Object.keys(compact).length > 0 ? compact : null;
|
|
8
|
+
}
|
|
9
|
+
function hasMeaningfulParsedOutput(agentOutput) {
|
|
10
|
+
if (!agentOutput || typeof agentOutput !== 'object') {
|
|
11
|
+
return false;
|
|
12
|
+
}
|
|
13
|
+
return Object.entries(agentOutput).some(([key, value]) => {
|
|
14
|
+
if (value === undefined || value === null) {
|
|
15
|
+
return false;
|
|
16
|
+
}
|
|
17
|
+
if (key === 'session_id') {
|
|
18
|
+
return false;
|
|
19
|
+
}
|
|
20
|
+
if (key === 'tools') {
|
|
21
|
+
return Array.isArray(value) ? value.length > 0 : true;
|
|
22
|
+
}
|
|
23
|
+
return true;
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
export function buildProcessResult(context, agentOutput, verbose = false) {
|
|
27
|
+
const response = {
|
|
28
|
+
pid: context.pid,
|
|
29
|
+
agent: context.agent,
|
|
30
|
+
status: context.status,
|
|
31
|
+
exitCode: context.exitCode ?? null,
|
|
32
|
+
model: context.model ?? null,
|
|
33
|
+
};
|
|
34
|
+
if (verbose) {
|
|
35
|
+
response.startTime = context.startTime;
|
|
36
|
+
response.workFolder = context.workFolder;
|
|
37
|
+
response.prompt = context.prompt;
|
|
38
|
+
}
|
|
39
|
+
if (agentOutput?.session_id) {
|
|
40
|
+
response.session_id = agentOutput.session_id;
|
|
41
|
+
}
|
|
42
|
+
const shapedAgentOutput = verbose ? agentOutput : compactAgentOutput(agentOutput);
|
|
43
|
+
if (hasMeaningfulParsedOutput(shapedAgentOutput)) {
|
|
44
|
+
response.agentOutput = shapedAgentOutput;
|
|
45
|
+
}
|
|
46
|
+
if (!response.agentOutput) {
|
|
47
|
+
response.stdout = context.stdout;
|
|
48
|
+
response.stderr = context.stderr;
|
|
49
|
+
}
|
|
50
|
+
return response;
|
|
51
|
+
}
|
package/dist/process-service.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { spawn } from 'node:child_process';
|
|
2
2
|
import { buildCliCommand } from './cli-builder.js';
|
|
3
|
-
import { parseClaudeOutput, parseCodexOutput, parseGeminiOutput } from './parsers.js';
|
|
3
|
+
import { parseClaudeOutput, parseCodexOutput, parseForgeOutput, parseGeminiOutput } from './parsers.js';
|
|
4
|
+
import { buildProcessResult } from './process-result.js';
|
|
4
5
|
export class ProcessService {
|
|
5
6
|
processManager = new Map();
|
|
6
7
|
cliPaths;
|
|
@@ -96,8 +97,11 @@ export class ProcessService {
|
|
|
96
97
|
else if (process.toolType === 'gemini') {
|
|
97
98
|
agentOutput = parseGeminiOutput(process.stdout);
|
|
98
99
|
}
|
|
100
|
+
else if (process.toolType === 'forge') {
|
|
101
|
+
agentOutput = parseForgeOutput(process.stdout);
|
|
102
|
+
}
|
|
99
103
|
}
|
|
100
|
-
|
|
104
|
+
return buildProcessResult({
|
|
101
105
|
pid,
|
|
102
106
|
agent: process.toolType,
|
|
103
107
|
status: process.status,
|
|
@@ -106,26 +110,11 @@ export class ProcessService {
|
|
|
106
110
|
workFolder: process.workFolder,
|
|
107
111
|
prompt: process.prompt,
|
|
108
112
|
model: process.model,
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
const { tools, ...rest } = agentOutput;
|
|
113
|
-
response.agentOutput = rest;
|
|
114
|
-
}
|
|
115
|
-
else {
|
|
116
|
-
response.agentOutput = agentOutput;
|
|
117
|
-
}
|
|
118
|
-
if (agentOutput.session_id) {
|
|
119
|
-
response.session_id = agentOutput.session_id;
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
else {
|
|
123
|
-
response.stdout = process.stdout;
|
|
124
|
-
response.stderr = process.stderr;
|
|
125
|
-
}
|
|
126
|
-
return response;
|
|
113
|
+
stdout: process.stdout,
|
|
114
|
+
stderr: process.stderr,
|
|
115
|
+
}, agentOutput, verbose);
|
|
127
116
|
}
|
|
128
|
-
async waitForProcesses(pids, timeoutSeconds = 180) {
|
|
117
|
+
async waitForProcesses(pids, timeoutSeconds = 180, verbose = false) {
|
|
129
118
|
for (const pid of pids) {
|
|
130
119
|
if (!this.processManager.has(pid)) {
|
|
131
120
|
throw new Error(`Process with PID ${pid} not found`);
|
|
@@ -152,7 +141,7 @@ export class ProcessService {
|
|
|
152
141
|
});
|
|
153
142
|
try {
|
|
154
143
|
await Promise.race([Promise.all(waitPromises), timeoutPromise]);
|
|
155
|
-
return pids.map((pid) => this.getProcessResult(pid,
|
|
144
|
+
return pids.map((pid) => this.getProcessResult(pid, verbose));
|
|
156
145
|
}
|
|
157
146
|
finally {
|
|
158
147
|
if (timeoutHandle) {
|
package/dist/server.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
export { debugLog, findClaudeCli, findCodexCli, findGeminiCli } from './cli-utils.js';
|
|
2
|
+
export { debugLog, findClaudeCli, findCodexCli, findForgeCli, findGeminiCli } from './cli-utils.js';
|
|
3
3
|
export { resolveModelAlias } from './cli-builder.js';
|
|
4
4
|
export { ClaudeCodeServer, runMcpServer, spawnAsync } from './app/mcp.js';
|
|
5
5
|
import { runMcpServer } from './app/mcp.js';
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ai-cli-mcp",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.14.0",
|
|
4
4
|
"mcpName": "io.github.mkXultra/ai-cli-mcp",
|
|
5
|
-
"description": "MCP server for AI CLI tools (Claude, Codex, and
|
|
5
|
+
"description": "MCP server for AI CLI tools (Claude, Codex, Gemini, and Forge) with background process management",
|
|
6
6
|
"author": "mkXultra",
|
|
7
7
|
"license": "MIT",
|
|
8
8
|
"main": "dist/server.js",
|
package/server.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json",
|
|
3
3
|
"name": "io.github.mkXultra/ai-cli-mcp",
|
|
4
|
-
"description": "MCP server for AI CLI tools (Claude, Codex, and
|
|
4
|
+
"description": "MCP server for AI CLI tools (Claude, Codex, Gemini, and Forge) with background process management",
|
|
5
5
|
"repository": {
|
|
6
6
|
"url": "https://github.com/mkXultra/ai-cli-mcp",
|
|
7
7
|
"source": "github"
|