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.
Files changed (46) hide show
  1. package/.github/workflows/publish.yml +25 -0
  2. package/CHANGELOG.md +20 -0
  3. package/README.ja.md +20 -5
  4. package/README.md +20 -6
  5. package/dist/__tests__/app-cli.test.js +34 -2
  6. package/dist/__tests__/cli-bin-smoke.test.js +4 -0
  7. package/dist/__tests__/cli-builder.test.js +37 -0
  8. package/dist/__tests__/cli-process-service.test.js +180 -5
  9. package/dist/__tests__/cli-utils.test.js +31 -0
  10. package/dist/__tests__/mcp-contract.test.js +287 -9
  11. package/dist/__tests__/parsers.test.js +37 -1
  12. package/dist/__tests__/process-management.test.js +2 -1
  13. package/dist/app/cli.js +8 -6
  14. package/dist/app/mcp.js +16 -8
  15. package/dist/cli-builder.js +14 -0
  16. package/dist/cli-parse.js +8 -5
  17. package/dist/cli-process-service.js +13 -23
  18. package/dist/cli-utils.js +17 -0
  19. package/dist/cli.js +4 -3
  20. package/dist/model-catalog.js +4 -1
  21. package/dist/parsers.js +55 -0
  22. package/dist/process-result.js +51 -0
  23. package/dist/process-service.js +11 -22
  24. package/dist/server.js +1 -1
  25. package/package.json +2 -2
  26. package/server.json +1 -1
  27. package/src/__tests__/app-cli.test.ts +43 -1
  28. package/src/__tests__/cli-bin-smoke.test.ts +4 -0
  29. package/src/__tests__/cli-builder.test.ts +47 -0
  30. package/src/__tests__/cli-process-service.test.ts +200 -5
  31. package/src/__tests__/cli-utils.test.ts +34 -0
  32. package/src/__tests__/mcp-contract.test.ts +325 -9
  33. package/src/__tests__/parsers.test.ts +44 -1
  34. package/src/__tests__/process-management.test.ts +2 -1
  35. package/src/app/cli.ts +9 -7
  36. package/src/app/mcp.ts +17 -8
  37. package/src/cli-builder.ts +18 -3
  38. package/src/cli-parse.ts +8 -5
  39. package/src/cli-process-service.ts +12 -23
  40. package/src/cli-utils.ts +21 -1
  41. package/src/cli.ts +4 -3
  42. package/src/model-catalog.ts +5 -1
  43. package/src/parsers.ts +61 -0
  44. package/src/process-result.ts +79 -0
  45. package/src/process-service.ts +11 -24
  46. package/src/server.ts +1 -1
@@ -154,7 +154,8 @@ describe('Process Management Tests', () => {
154
154
  params: {
155
155
  name: 'get_result',
156
156
  arguments: {
157
- pid: 12360
157
+ pid: 12360,
158
+ verbose: true
158
159
  }
159
160
  }
160
161
  });
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 result for a tracked process.
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 Include verbose parsed output
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 Gemini CLI process in the background and returns a PID immediately. Use list_processes and get_result to monitor progress.
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. Returns the output from the agent including session_id (if applicable), along with process metadata.',
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 detailed execution information including tool usage history. Defaults to false.',
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. Blocks until all specified PIDs finish or timeout occurs.',
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',
@@ -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 gemini (required)
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 gemini)\n\n`);
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
- const response = {
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
- if (agentOutput) {
142
- if (!verbose && agentOutput.tools) {
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, false)));
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;
@@ -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
+ }
@@ -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
- const response = {
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
- if (agentOutput) {
111
- if (!verbose && agentOutput.tools) {
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, false));
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.12.0",
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 Gemini) with background process management",
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 Gemini) with background process management",
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"