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
|
@@ -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]
|
|
125
|
-
|
|
126
|
-
|
|
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('
|
|
197
|
+
it('preserves successful prompt_file execution through the MCP process path', async () => {
|
|
140
198
|
const promptFile = join(testDir, 'prompt.txt');
|
|
141
|
-
writeFileSync(promptFile, '
|
|
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
|
+
});
|
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
|
|
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
|
|
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
|
|
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.
|
|
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
|
|
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.
|
|
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: [{
|
package/src/cli-builder.ts
CHANGED
|
@@ -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'];
|