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
@@ -1,5 +1,5 @@
1
1
  import { afterAll, afterEach, beforeEach, describe, expect, it } from 'vitest';
2
- import { chmodSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs';
2
+ import { chmodSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
3
3
  import { join } from 'node:path';
4
4
  import { tmpdir } from 'node:os';
5
5
  import { cleanupSharedMock, getSharedMock } from './utils/persistent-mock.js';
@@ -19,6 +19,53 @@ function expectProcessSummaryShape(processInfo: any): void {
19
19
  });
20
20
  }
21
21
 
22
+ function createForgeMockScript(dir: string, argsLogPath: string): string {
23
+ const scriptPath = join(dir, 'mock-forge');
24
+ writeFileSync(
25
+ scriptPath,
26
+ `#!/bin/bash
27
+ set -euo pipefail
28
+
29
+ log_file="${argsLogPath}"
30
+ prompt=""
31
+ conversation_id=""
32
+
33
+ printf '%s\\n' "$*" >> "$log_file"
34
+
35
+ while [[ $# -gt 0 ]]; do
36
+ case "$1" in
37
+ -C)
38
+ shift 2
39
+ ;;
40
+ -p)
41
+ prompt="$2"
42
+ shift 2
43
+ ;;
44
+ --conversation-id)
45
+ conversation_id="$2"
46
+ shift 2
47
+ ;;
48
+ *)
49
+ shift
50
+ ;;
51
+ esac
52
+ done
53
+
54
+ if [[ -n "$conversation_id" ]]; then
55
+ printf '● [21:09:33] Continue %s\\n' "$conversation_id"
56
+ printf 'Resumed: %s\\n' "$prompt"
57
+ printf '● [21:09:37] Finished %s\\n' "$conversation_id"
58
+ else
59
+ printf '● [21:09:01] Initialize forge-session-1\\n'
60
+ printf 'Initial: %s\\n' "$prompt"
61
+ printf '● [21:09:08] Finished forge-session-1\\n'
62
+ fi
63
+ `
64
+ );
65
+ chmodSync(scriptPath, 0o755);
66
+ return scriptPath;
67
+ }
68
+
22
69
  describe('MCP Contract Tests', () => {
23
70
  let client: MCPTestClient;
24
71
  let testDir: string;
@@ -75,6 +122,7 @@ describe('MCP Contract Tests', () => {
75
122
  expect(Object.keys(waitTool.inputSchema.properties).sort()).toEqual([
76
123
  'pids',
77
124
  'timeout',
125
+ 'verbose',
78
126
  ]);
79
127
  });
80
128
 
@@ -108,22 +156,32 @@ describe('MCP Contract Tests', () => {
108
156
  pid: runData.pid,
109
157
  agent: 'claude',
110
158
  status: expect.any(String),
111
- startTime: expect.any(String),
112
- workFolder: testDir,
113
- prompt: 'create a file called contract.txt with content "hello"',
114
159
  model: 'haiku',
115
160
  stdout: expect.any(String),
116
161
  stderr: expect.any(String),
117
162
  });
163
+ expect(getResultData).toHaveProperty('exitCode');
164
+ expect(getResultData).not.toHaveProperty('startTime');
165
+ expect(getResultData).not.toHaveProperty('workFolder');
166
+ expect(getResultData).not.toHaveProperty('prompt');
118
167
 
119
168
  const waitResponse = await client.callTool('wait', { pids: [runData.pid], timeout: 5 });
120
169
  const waitData = parseToolJson(waitResponse);
121
170
 
122
171
  expect(Array.isArray(waitData)).toBe(true);
123
172
  expect(waitData).toHaveLength(1);
124
- expect(waitData[0].pid).toBe(runData.pid);
125
- expect(waitData[0].agent).toBe('claude');
126
- expect(waitData[0].status).toBe('completed');
173
+ expect(waitData[0]).toMatchObject({
174
+ pid: runData.pid,
175
+ agent: 'claude',
176
+ status: 'completed',
177
+ exitCode: 0,
178
+ model: 'haiku',
179
+ stdout: expect.any(String),
180
+ stderr: expect.any(String),
181
+ });
182
+ expect(waitData[0]).not.toHaveProperty('startTime');
183
+ expect(waitData[0]).not.toHaveProperty('workFolder');
184
+ expect(waitData[0]).not.toHaveProperty('prompt');
127
185
 
128
186
  const cleanupResponse = await client.callTool('cleanup_processes', {});
129
187
  const cleanupData = parseToolJson(cleanupResponse);
@@ -136,13 +194,14 @@ describe('MCP Contract Tests', () => {
136
194
  expect(cleanupData.removedPids).toContain(runData.pid);
137
195
  });
138
196
 
139
- it('accepts prompt_file and keeps the run response shape stable', async () => {
197
+ it('preserves successful prompt_file execution through the MCP process path', async () => {
140
198
  const promptFile = join(testDir, 'prompt.txt');
141
- writeFileSync(promptFile, 'create a file called from-file.txt');
199
+ writeFileSync(promptFile, 'Create a file from prompt_file');
142
200
 
143
201
  const runResponse = await client.callTool('run', {
144
202
  prompt_file: promptFile,
145
203
  workFolder: testDir,
204
+ model: 'haiku',
146
205
  });
147
206
  const runData = parseToolJson(runResponse);
148
207
 
@@ -152,6 +211,263 @@ describe('MCP Contract Tests', () => {
152
211
  agent: 'claude',
153
212
  message: expect.any(String),
154
213
  });
214
+
215
+ const waitResponse = await client.callTool('wait', { pids: [runData.pid], timeout: 5 });
216
+ const waitData = parseToolJson(waitResponse);
217
+
218
+ expect(waitData).toHaveLength(1);
219
+ expect(waitData[0]).toMatchObject({
220
+ pid: runData.pid,
221
+ agent: 'claude',
222
+ status: 'completed',
223
+ exitCode: 0,
224
+ model: 'haiku',
225
+ stdout: expect.stringContaining('Created file successfully'),
226
+ stderr: '',
227
+ });
228
+ expect(waitData[0]).not.toHaveProperty('prompt');
229
+ expect(waitData[0]).not.toHaveProperty('workFolder');
230
+ expect(waitData[0]).not.toHaveProperty('startTime');
231
+ });
232
+
233
+ it('returns compact results by default and full results when verbose is true for parsed output', async () => {
234
+ await client.disconnect();
235
+
236
+ const verboseMockPath = join(testDir, 'verbose-claude');
237
+ writeFileSync(
238
+ verboseMockPath,
239
+ `#!/bin/bash
240
+ printf '%s\n' '{"type":"assistant","message":{"content":[{"type":"tool_use","id":"tool-1","name":"Read","input":{"file_path":"/tmp/demo.txt"}}]}}'
241
+ printf '%s\n' '{"type":"user","message":{"content":[{"type":"tool_result","tool_use_id":"tool-1","content":[{"type":"text","text":"demo output"}]}]}}'
242
+ printf '%s\n' '{"type":"result","result":"Completed contract verbose test"}'
243
+ printf '%s\n' '{"type":"system","session_id":"session-verbose-1"}'
244
+ `
245
+ );
246
+ chmodSync(verboseMockPath, 0o755);
247
+
248
+ client = createTestClient({ claudeCliName: verboseMockPath, debug: false });
249
+ await client.connect();
250
+
251
+ const runResponse = await client.callTool('run', {
252
+ prompt: 'verbose-shape-test',
253
+ workFolder: testDir,
254
+ });
255
+ const runData = parseToolJson(runResponse);
256
+
257
+ const completedWait = parseToolJson(await client.callTool('wait', { pids: [runData.pid], timeout: 5 }));
258
+ expect(completedWait).toHaveLength(1);
259
+ expect(completedWait[0].status).toBe('completed');
260
+
261
+ const compactResult = parseToolJson(await client.callTool('get_result', { pid: runData.pid }));
262
+ expect(compactResult).toMatchObject({
263
+ pid: runData.pid,
264
+ agent: 'claude',
265
+ status: 'completed',
266
+ exitCode: 0,
267
+ model: null,
268
+ session_id: 'session-verbose-1',
269
+ agentOutput: {
270
+ message: 'Completed contract verbose test',
271
+ session_id: 'session-verbose-1',
272
+ },
273
+ });
274
+ expect(compactResult).not.toHaveProperty('startTime');
275
+ expect(compactResult).not.toHaveProperty('workFolder');
276
+ expect(compactResult).not.toHaveProperty('prompt');
277
+ expect(compactResult.agentOutput).not.toHaveProperty('tools');
278
+
279
+ const verboseResult = parseToolJson(await client.callTool('get_result', { pid: runData.pid, verbose: true }));
280
+ expect(verboseResult).toMatchObject({
281
+ pid: runData.pid,
282
+ agent: 'claude',
283
+ status: 'completed',
284
+ exitCode: 0,
285
+ model: null,
286
+ startTime: expect.any(String),
287
+ workFolder: testDir,
288
+ prompt: 'verbose-shape-test',
289
+ session_id: 'session-verbose-1',
290
+ agentOutput: {
291
+ message: 'Completed contract verbose test',
292
+ session_id: 'session-verbose-1',
293
+ tools: [
294
+ {
295
+ tool: 'Read',
296
+ input: { file_path: '/tmp/demo.txt' },
297
+ output: 'demo output',
298
+ },
299
+ ],
300
+ },
301
+ });
302
+
303
+ const compactWait = parseToolJson(await client.callTool('wait', { pids: [runData.pid], timeout: 5 }));
304
+ expect(compactWait).toHaveLength(1);
305
+ expect(compactWait[0]).toMatchObject({
306
+ pid: runData.pid,
307
+ agent: 'claude',
308
+ status: 'completed',
309
+ exitCode: 0,
310
+ model: null,
311
+ session_id: 'session-verbose-1',
312
+ agentOutput: {
313
+ message: 'Completed contract verbose test',
314
+ session_id: 'session-verbose-1',
315
+ },
316
+ });
317
+ expect(compactWait[0]).not.toHaveProperty('startTime');
318
+ expect(compactWait[0]).not.toHaveProperty('workFolder');
319
+ expect(compactWait[0]).not.toHaveProperty('prompt');
320
+ expect(compactWait[0].agentOutput).not.toHaveProperty('tools');
321
+
322
+ const verboseWait = parseToolJson(await client.callTool('wait', { pids: [runData.pid], timeout: 5, verbose: true }));
323
+ expect(verboseWait).toHaveLength(1);
324
+ expect(verboseWait[0]).toMatchObject({
325
+ pid: runData.pid,
326
+ agent: 'claude',
327
+ status: 'completed',
328
+ exitCode: 0,
329
+ model: null,
330
+ startTime: expect.any(String),
331
+ workFolder: testDir,
332
+ prompt: 'verbose-shape-test',
333
+ session_id: 'session-verbose-1',
334
+ agentOutput: {
335
+ message: 'Completed contract verbose test',
336
+ session_id: 'session-verbose-1',
337
+ tools: [
338
+ {
339
+ tool: 'Read',
340
+ input: { file_path: '/tmp/demo.txt' },
341
+ output: 'demo output',
342
+ },
343
+ ],
344
+ },
345
+ });
346
+ });
347
+
348
+ it('covers forge end-to-end through the MCP process path', async () => {
349
+ await client.disconnect();
350
+
351
+ const forgeArgsLogPath = join(testDir, 'forge-args.log');
352
+ const forgeMockPath = createForgeMockScript(testDir, forgeArgsLogPath);
353
+
354
+ client = createTestClient({
355
+ debug: false,
356
+ env: {
357
+ FORGE_CLI_NAME: forgeMockPath,
358
+ },
359
+ });
360
+ await client.connect();
361
+
362
+ const initialRunResponse = await client.callTool('run', {
363
+ prompt: 'forge-initial-prompt',
364
+ workFolder: testDir,
365
+ model: 'forge',
366
+ });
367
+ const initialRunData = parseToolJson(initialRunResponse);
368
+
369
+ expect(initialRunData).toEqual({
370
+ pid: expect.any(Number),
371
+ status: 'started',
372
+ agent: 'forge',
373
+ message: expect.any(String),
374
+ });
375
+
376
+ const initialWaitResponse = await client.callTool('wait', { pids: [initialRunData.pid], timeout: 5 });
377
+ const initialWaitData = parseToolJson(initialWaitResponse);
378
+
379
+ expect(initialWaitData).toHaveLength(1);
380
+ expect(initialWaitData[0]).toMatchObject({
381
+ pid: initialRunData.pid,
382
+ agent: 'forge',
383
+ status: 'completed',
384
+ session_id: 'forge-session-1',
385
+ agentOutput: {
386
+ message: 'Initial: forge-initial-prompt',
387
+ session_id: 'forge-session-1',
388
+ },
389
+ });
390
+
391
+ const initialResultResponse = await client.callTool('get_result', { pid: initialRunData.pid });
392
+ const initialResultData = parseToolJson(initialResultResponse);
393
+
394
+ expect(initialResultData).toMatchObject({
395
+ pid: initialRunData.pid,
396
+ agent: 'forge',
397
+ status: 'completed',
398
+ session_id: 'forge-session-1',
399
+ agentOutput: {
400
+ message: 'Initial: forge-initial-prompt',
401
+ session_id: 'forge-session-1',
402
+ },
403
+ });
404
+
405
+ const resumedRunResponse = await client.callTool('run', {
406
+ prompt: 'forge-resume-prompt',
407
+ workFolder: testDir,
408
+ model: 'forge',
409
+ session_id: 'forge-session-1',
410
+ });
411
+ const resumedRunData = parseToolJson(resumedRunResponse);
412
+
413
+ expect(resumedRunData).toEqual({
414
+ pid: expect.any(Number),
415
+ status: 'started',
416
+ agent: 'forge',
417
+ message: expect.any(String),
418
+ });
419
+
420
+ const resumedWaitResponse = await client.callTool('wait', { pids: [resumedRunData.pid], timeout: 5 });
421
+ const resumedWaitData = parseToolJson(resumedWaitResponse);
422
+
423
+ expect(resumedWaitData).toHaveLength(1);
424
+ expect(resumedWaitData[0]).toMatchObject({
425
+ pid: resumedRunData.pid,
426
+ agent: 'forge',
427
+ status: 'completed',
428
+ session_id: 'forge-session-1',
429
+ agentOutput: {
430
+ message: 'Resumed: forge-resume-prompt',
431
+ session_id: 'forge-session-1',
432
+ },
433
+ });
434
+
435
+ const resumedResultResponse = await client.callTool('get_result', { pid: resumedRunData.pid });
436
+ const resumedResultData = parseToolJson(resumedResultResponse);
437
+
438
+ expect(resumedResultData).toMatchObject({
439
+ pid: resumedRunData.pid,
440
+ agent: 'forge',
441
+ status: 'completed',
442
+ session_id: 'forge-session-1',
443
+ agentOutput: {
444
+ message: 'Resumed: forge-resume-prompt',
445
+ session_id: 'forge-session-1',
446
+ },
447
+ });
448
+
449
+ const forgeInvocations = readFileSync(forgeArgsLogPath, 'utf-8').trim().split('\n');
450
+ expect(forgeInvocations).toHaveLength(2);
451
+ expect(forgeInvocations[0]).toContain(`-C ${testDir}`);
452
+ expect(forgeInvocations[0]).toContain('-p forge-initial-prompt');
453
+ expect(forgeInvocations[0]).not.toContain('--model');
454
+ expect(forgeInvocations[0]).not.toContain('--agent');
455
+ expect(forgeInvocations[0]).not.toContain('--conversation-id');
456
+
457
+ expect(forgeInvocations[1]).toContain(`-C ${testDir}`);
458
+ expect(forgeInvocations[1]).toContain('--conversation-id forge-session-1');
459
+ expect(forgeInvocations[1]).toContain('-p forge-resume-prompt');
460
+ expect(forgeInvocations[1]).not.toContain('--model');
461
+ expect(forgeInvocations[1]).not.toContain('--agent');
462
+
463
+ await expect(
464
+ client.callTool('run', {
465
+ prompt: 'forge-invalid-reasoning',
466
+ workFolder: testDir,
467
+ model: 'forge',
468
+ reasoning_effort: 'high',
469
+ })
470
+ ).rejects.toThrow(/reasoning_effort is not supported for forge/i);
155
471
  });
156
472
 
157
473
  it('keeps key invalid-input errors stable', async () => {
@@ -1,5 +1,5 @@
1
1
  import { describe, it, expect } from 'vitest';
2
- import { parseCodexOutput, parseClaudeOutput } from '../parsers.js';
2
+ import { parseCodexOutput, parseClaudeOutput, parseForgeOutput } from '../parsers.js';
3
3
 
4
4
  describe('parseCodexOutput', () => {
5
5
  it('should parse basic Codex output with message and session_id', () => {
@@ -106,3 +106,46 @@ INVALID_LINE
106
106
  expect(result.message).toBe("Success");
107
107
  });
108
108
  });
109
+
110
+ describe('parseForgeOutput', () => {
111
+ it('should parse initialized forge output with a conversation id', () => {
112
+ const output = `● [21:09:01] Initialize 123e4567-e89b-12d3-a456-426614174000
113
+ Hello from Forge
114
+ ● [21:09:08] Finished 123e4567-e89b-12d3-a456-426614174000
115
+ `;
116
+
117
+ expect(parseForgeOutput(output)).toEqual({
118
+ message: 'Hello from Forge',
119
+ session_id: '123e4567-e89b-12d3-a456-426614174000',
120
+ });
121
+ });
122
+
123
+ it('should parse resumed forge output with multiline assistant content', () => {
124
+ const output = `● [21:09:33] Continue conv-123
125
+ Line one
126
+
127
+ Line three
128
+ ● [21:09:37] Finished conv-123
129
+ `;
130
+
131
+ expect(parseForgeOutput(output)).toEqual({
132
+ message: 'Line one\n\nLine three',
133
+ session_id: 'conv-123',
134
+ });
135
+ });
136
+
137
+ it('should return the current message while forge output is still in progress', () => {
138
+ const output = `● [21:09:33] Continue conv-456
139
+ Partial answer
140
+ still streaming`;
141
+
142
+ expect(parseForgeOutput(output)).toEqual({
143
+ message: 'Partial answer\nstill streaming',
144
+ session_id: 'conv-456',
145
+ });
146
+ });
147
+
148
+ it('should return null for unrelated forge output', () => {
149
+ expect(parseForgeOutput('plain text')).toBeNull();
150
+ });
151
+ });
@@ -191,7 +191,8 @@ describe('Process Management Tests', () => {
191
191
  params: {
192
192
  name: 'get_result',
193
193
  arguments: {
194
- pid: 12360
194
+ pid: 12360,
195
+ verbose: true
195
196
  }
196
197
  }
197
198
  });
package/src/app/cli.ts CHANGED
@@ -26,9 +26,9 @@ Options:
26
26
  --cwd <path> Working directory
27
27
  --prompt <text> Prompt text
28
28
  --prompt-file <path> Path to a prompt file
29
- --model <model> Model name or alias (e.g. sonnet, claude-ultra, gpt-5.2-codex, codex-ultra, gemini-2.5-pro, gemini-ultra)
29
+ --model <model> Model name or alias (e.g. sonnet, claude-ultra, gpt-5.2-codex, codex-ultra, gemini-2.5-pro, gemini-ultra, forge)
30
30
  --session-id <id> Resume a previous session
31
- --reasoning-effort <level> Reasoning level for Claude/Codex
31
+ --reasoning-effort <level> Reasoning level for Claude/Codex only
32
32
  --help, -h Show this help message
33
33
 
34
34
  Compatibility aliases:
@@ -41,18 +41,20 @@ Compatibility aliases:
41
41
  export const WAIT_HELP_TEXT = `Usage: ai-cli wait <pid...> [options]
42
42
 
43
43
  Wait for one or more tracked processes to finish.
44
+ By default each result uses the compact shape; set --verbose to include full metadata and detailed parsed output.
44
45
 
45
46
  Options:
46
47
  --timeout <seconds> Maximum wait time in seconds
48
+ --verbose Return full metadata and detailed parsed output
47
49
  --help, -h Show this help message
48
50
  `;
49
51
 
50
52
  export const RESULT_HELP_TEXT = `Usage: ai-cli result <pid> [options]
51
53
 
52
- Get the current result for a tracked process.
54
+ 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.
53
55
 
54
56
  Options:
55
- --verbose Include verbose parsed output
57
+ --verbose Return full metadata and detailed parsed output
56
58
  --help, -h Show this help message
57
59
  `;
58
60
 
@@ -115,7 +117,7 @@ interface CliDeps {
115
117
  }) => Promise<any>;
116
118
  listProcesses: () => Promise<any>;
117
119
  getProcessResult: (pid: number, verbose: boolean) => Promise<any>;
118
- waitForProcesses: (pids: number[], timeoutSeconds?: number) => Promise<any>;
120
+ waitForProcesses: (pids: number[], timeoutSeconds?: number, verbose?: boolean) => Promise<any>;
119
121
  killProcess: (pid: number) => Promise<any>;
120
122
  cleanupProcesses: () => Promise<any>;
121
123
  getDoctorStatus: () => any;
@@ -137,7 +139,7 @@ const defaultDeps: CliDeps = {
137
139
  runProcess: (options) => getCliProcessService().startProcess(options),
138
140
  listProcesses: () => getCliProcessService().listProcesses(),
139
141
  getProcessResult: (pid, verbose) => getCliProcessService().getProcessResult(pid, verbose),
140
- waitForProcesses: (pids, timeoutSeconds) => getCliProcessService().waitForProcesses(pids, timeoutSeconds),
142
+ waitForProcesses: (pids, timeoutSeconds, verbose) => getCliProcessService().waitForProcesses(pids, timeoutSeconds, verbose),
141
143
  killProcess: (pid) => getCliProcessService().killProcess(pid),
142
144
  cleanupProcesses: () => getCliProcessService().cleanupProcesses(),
143
145
  getDoctorStatus: () => getCliDoctorStatus(),
@@ -317,7 +319,7 @@ export async function runCli(argv: string[], deps: Partial<CliDeps> = {}): Promi
317
319
  return 1;
318
320
  }
319
321
 
320
- writeJson(stdout, await waitForProcesses(pids as number[], timeout));
322
+ writeJson(stdout, await waitForProcesses(pids as number[], timeout, 'verbose' in flags));
321
323
  return 0;
322
324
  }
323
325
 
package/src/app/mcp.ts CHANGED
@@ -8,7 +8,7 @@ import {
8
8
  type ServerResult,
9
9
  } from '@modelcontextprotocol/sdk/types.js';
10
10
  import { spawn } from 'node:child_process';
11
- import { debugLog, findClaudeCli, findCodexCli, findGeminiCli } from '../cli-utils.js';
11
+ import { debugLog, findClaudeCli, findCodexCli, findForgeCli, findGeminiCli } from '../cli-utils.js';
12
12
  import { getModelParameterDescription, getSupportedModelsDescription } from '../model-catalog.js';
13
13
  import { ProcessService } from '../process-service.js';
14
14
 
@@ -72,6 +72,7 @@ export class ClaudeCodeServer {
72
72
  private claudeCliPath: string;
73
73
  private codexCliPath: string;
74
74
  private geminiCliPath: string;
75
+ private forgeCliPath: string;
75
76
  private processService: ProcessService;
76
77
  private sigintHandler?: () => Promise<void>;
77
78
  private packageVersion: string;
@@ -80,15 +81,18 @@ export class ClaudeCodeServer {
80
81
  this.claudeCliPath = findClaudeCli();
81
82
  this.codexCliPath = findCodexCli();
82
83
  this.geminiCliPath = findGeminiCli();
84
+ this.forgeCliPath = findForgeCli();
83
85
  console.error(`[Setup] Using Claude CLI command/path: ${this.claudeCliPath}`);
84
86
  console.error(`[Setup] Using Codex CLI command/path: ${this.codexCliPath}`);
85
87
  console.error(`[Setup] Using Gemini CLI command/path: ${this.geminiCliPath}`);
88
+ console.error(`[Setup] Using Forge CLI command/path: ${this.forgeCliPath}`);
86
89
  this.packageVersion = SERVER_VERSION;
87
90
  this.processService = new ProcessService({
88
91
  cliPaths: {
89
92
  claude: this.claudeCliPath,
90
93
  codex: this.codexCliPath,
91
94
  gemini: this.geminiCliPath,
95
+ forge: this.forgeCliPath,
92
96
  },
93
97
  });
94
98
 
@@ -119,7 +123,7 @@ export class ClaudeCodeServer {
119
123
  tools: [
120
124
  {
121
125
  name: 'run',
122
- 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.
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.
123
127
 
124
128
  • File ops: Create, read, (fuzzy) edit, move, copy, delete, list files, analyze/ocr images, file content analysis
125
129
  • Code: Generate / analyse / refactor / fix
@@ -163,11 +167,11 @@ ${getSupportedModelsDescription()}
163
167
  },
164
168
  reasoning_effort: {
165
169
  type: 'string',
166
- description: 'Reasoning control for Claude and Codex. Claude uses --effort with "low", "medium", "high". Codex uses model_reasoning_effort with "low", "medium", "high", "xhigh".',
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.',
167
171
  },
168
172
  session_id: {
169
173
  type: 'string',
170
- 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.',
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.',
171
175
  },
172
176
  },
173
177
  required: ['workFolder'],
@@ -183,7 +187,7 @@ ${getSupportedModelsDescription()}
183
187
  },
184
188
  {
185
189
  name: 'get_result',
186
- 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.',
190
+ 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.',
187
191
  inputSchema: {
188
192
  type: 'object',
189
193
  properties: {
@@ -193,7 +197,7 @@ ${getSupportedModelsDescription()}
193
197
  },
194
198
  verbose: {
195
199
  type: 'boolean',
196
- description: 'Optional: If true, returns detailed execution information including tool usage history. Defaults to false.',
200
+ description: 'Optional: If true, returns the full result shape including metadata fields and detailed parsed output such as tool usage history. Defaults to false.',
197
201
  }
198
202
  },
199
203
  required: ['pid'],
@@ -201,7 +205,7 @@ ${getSupportedModelsDescription()}
201
205
  },
202
206
  {
203
207
  name: 'wait',
204
- description: 'Wait for multiple AI agent processes to complete and return their results. Blocks until all specified PIDs finish or timeout occurs.',
208
+ 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.',
205
209
  inputSchema: {
206
210
  type: 'object',
207
211
  properties: {
@@ -214,6 +218,10 @@ ${getSupportedModelsDescription()}
214
218
  type: 'number',
215
219
  description: 'Optional: Maximum time to wait in seconds. Defaults to 180 (3 minutes).',
216
220
  },
221
+ verbose: {
222
+ type: 'boolean',
223
+ description: 'Optional: If true, each result item uses the full result shape including metadata fields and detailed parsed output. Defaults to false.',
224
+ },
217
225
  },
218
226
  required: ['pids'],
219
227
  },
@@ -332,7 +340,8 @@ ${getSupportedModelsDescription()}
332
340
  try {
333
341
  const results = await this.processService.waitForProcesses(
334
342
  toolArguments.pids,
335
- typeof toolArguments.timeout === 'number' ? toolArguments.timeout : 180
343
+ typeof toolArguments.timeout === 'number' ? toolArguments.timeout : 180,
344
+ !!toolArguments.verbose
336
345
  );
337
346
  return {
338
347
  content: [{
@@ -5,7 +5,10 @@ import { MODEL_ALIASES } from './model-catalog.js';
5
5
  export const ALLOWED_REASONING_EFFORTS = new Set(['low', 'medium', 'high', 'xhigh']);
6
6
  const CLAUDE_REASONING_EFFORTS = new Set(['low', 'medium', 'high']);
7
7
 
8
- function getAgentForModel(model: string): 'codex' | 'claude' | 'gemini' {
8
+ function getAgentForModel(model: string): 'codex' | 'claude' | 'gemini' | 'forge' {
9
+ if (model === 'forge') {
10
+ return 'forge';
11
+ }
9
12
  if (model.startsWith('gpt-')) {
10
13
  return 'codex';
11
14
  }
@@ -44,6 +47,9 @@ export function getReasoningEffort(model: string, rawValue: unknown): string {
44
47
  );
45
48
  }
46
49
  const agent = getAgentForModel(model);
50
+ if (agent === 'forge') {
51
+ throw new Error('reasoning_effort is not supported for forge.');
52
+ }
47
53
  if (agent === 'gemini') {
48
54
  throw new Error(
49
55
  'reasoning_effort is only supported for Claude and Codex models.'
@@ -61,7 +67,7 @@ export interface CliCommand {
61
67
  cliPath: string;
62
68
  args: string[];
63
69
  cwd: string;
64
- agent: 'claude' | 'codex' | 'gemini';
70
+ agent: 'claude' | 'codex' | 'gemini' | 'forge';
65
71
  prompt: string;
66
72
  resolvedModel: string;
67
73
  }
@@ -73,7 +79,7 @@ export interface BuildCliCommandOptions {
73
79
  model?: string;
74
80
  session_id?: string;
75
81
  reasoning_effort?: string;
76
- cliPaths: { claude: string; codex: string; gemini: string };
82
+ cliPaths: { claude: string; codex: string; gemini: string; forge: string };
77
83
  }
78
84
 
79
85
  /**
@@ -178,6 +184,15 @@ export function buildCliCommand(options: BuildCliCommandOptions): CliCommand {
178
184
 
179
185
  args.push(prompt);
180
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);
181
196
  } else {
182
197
  cliPath = options.cliPaths.claude;
183
198
  args = ['--dangerously-skip-permissions', '--output-format', 'stream-json', '--verbose'];