ai-cli-mcp 2.17.0 → 2.19.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/CHANGELOG.md +14 -0
- package/README.ja.md +13 -9
- package/README.md +13 -9
- package/dist/__tests__/app-cli.test.js +3 -3
- package/dist/__tests__/cli-process-service.test.js +3 -2
- package/dist/__tests__/mcp-contract.test.js +1 -0
- package/dist/__tests__/parsers.test.js +290 -1
- package/dist/__tests__/peek.test.js +8 -7
- package/dist/__tests__/process-management.test.js +156 -4
- package/dist/app/cli.js +6 -5
- package/dist/app/mcp.js +11 -2
- package/dist/cli-process-service.js +11 -10
- package/dist/parsers.js +382 -25
- package/dist/peek.js +8 -5
- package/dist/process-service.js +11 -10
- package/package.json +1 -1
- package/src/__tests__/app-cli.test.ts +3 -3
- package/src/__tests__/cli-process-service.test.ts +3 -2
- package/src/__tests__/mcp-contract.test.ts +1 -0
- package/src/__tests__/parsers.test.ts +321 -1
- package/src/__tests__/peek.test.ts +8 -7
- package/src/__tests__/process-management.test.ts +172 -4
- package/src/app/cli.ts +7 -6
- package/src/app/mcp.ts +11 -2
- package/src/cli-process-service.ts +13 -12
- package/src/parsers.ts +498 -29
- package/src/peek.ts +14 -7
- package/src/process-service.ts +13 -12
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { describe, it, expect } from 'vitest';
|
|
2
|
-
import { parseCodexOutput, parseClaudeOutput, parseForgeOutput, parseGeminiOutput, parseOpenCodeOutput, PeekMessageExtractor } from '../parsers.js';
|
|
2
|
+
import { parseCodexOutput, parseClaudeOutput, parseForgeOutput, parseGeminiOutput, parseOpenCodeOutput, PeekEventExtractor, PeekMessageExtractor } from '../parsers.js';
|
|
3
3
|
|
|
4
4
|
describe('parseCodexOutput', () => {
|
|
5
5
|
it('should parse basic Codex output with message and session_id', () => {
|
|
@@ -190,6 +190,326 @@ describe('PeekMessageExtractor', () => {
|
|
|
190
190
|
});
|
|
191
191
|
});
|
|
192
192
|
|
|
193
|
+
describe('PeekEventExtractor', () => {
|
|
194
|
+
const ts = '2026-04-12T02:10:00.000Z';
|
|
195
|
+
|
|
196
|
+
it('emits only message events when include_tool_calls is false', () => {
|
|
197
|
+
const extractor = new PeekEventExtractor('codex', { includeToolCalls: false });
|
|
198
|
+
const output = [
|
|
199
|
+
'{"type":"item.started","item":{"id":"item_0","type":"command_execution","command":"echo secret","status":"in_progress"}}',
|
|
200
|
+
'{"type":"item.completed","item":{"id":"item_0","type":"command_execution","command":"echo secret","aggregated_output":"secret output\\n","exit_code":0,"status":"completed"}}',
|
|
201
|
+
'{"type":"item.completed","item":{"id":"item_1","type":"agent_message","text":"Visible Codex message"}}',
|
|
202
|
+
].join('\n') + '\n';
|
|
203
|
+
|
|
204
|
+
expect(extractor.push(output, ts)).toEqual([
|
|
205
|
+
{ kind: 'message', ts, text: 'Visible Codex message' },
|
|
206
|
+
]);
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
it('emits Codex command and MCP tool_call events without raw output when include_tool_calls is true', () => {
|
|
210
|
+
const extractor = new PeekEventExtractor('codex', { includeToolCalls: true });
|
|
211
|
+
const output = [
|
|
212
|
+
'{"type":"item.started","item":{"id":"cmd_0","type":"command_execution","command":"/bin/sh -c \\"echo secret\\"","status":"in_progress"}}',
|
|
213
|
+
'{"type":"item.completed","item":{"id":"cmd_0","type":"command_execution","command":"/bin/sh -c \\"echo secret\\"","aggregated_output":"secret output\\n","exit_code":0,"status":"completed"}}',
|
|
214
|
+
'{"type":"item.started","item":{"id":"mcp_0","type":"mcp_tool_call","server":"acm","tool":"list_processes","arguments":{},"status":"in_progress"}}',
|
|
215
|
+
'{"type":"item.completed","item":{"id":"mcp_0","type":"mcp_tool_call","server":"acm","tool":"list_processes","arguments":{},"result":{"content":[{"type":"text","text":"secret result"}]},"status":"completed"}}',
|
|
216
|
+
].join('\n') + '\n';
|
|
217
|
+
|
|
218
|
+
expect(extractor.push(output, ts)).toEqual([
|
|
219
|
+
{
|
|
220
|
+
kind: 'tool_call',
|
|
221
|
+
ts,
|
|
222
|
+
phase: 'started',
|
|
223
|
+
id: 'cmd_0',
|
|
224
|
+
tool: 'command_execution',
|
|
225
|
+
summary: '/bin/sh -c "echo secret"',
|
|
226
|
+
},
|
|
227
|
+
{
|
|
228
|
+
kind: 'tool_call',
|
|
229
|
+
ts,
|
|
230
|
+
phase: 'completed',
|
|
231
|
+
id: 'cmd_0',
|
|
232
|
+
tool: 'command_execution',
|
|
233
|
+
summary: '/bin/sh -c "echo secret"',
|
|
234
|
+
status: 'success',
|
|
235
|
+
exit_code: 0,
|
|
236
|
+
},
|
|
237
|
+
{
|
|
238
|
+
kind: 'tool_call',
|
|
239
|
+
ts,
|
|
240
|
+
phase: 'started',
|
|
241
|
+
id: 'mcp_0',
|
|
242
|
+
tool: 'list_processes',
|
|
243
|
+
server: 'acm',
|
|
244
|
+
summary: 'acm.list_processes',
|
|
245
|
+
},
|
|
246
|
+
{
|
|
247
|
+
kind: 'tool_call',
|
|
248
|
+
ts,
|
|
249
|
+
phase: 'completed',
|
|
250
|
+
id: 'mcp_0',
|
|
251
|
+
tool: 'list_processes',
|
|
252
|
+
server: 'acm',
|
|
253
|
+
summary: 'acm.list_processes',
|
|
254
|
+
status: 'success',
|
|
255
|
+
},
|
|
256
|
+
]);
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
it('emits Claude MCP tool_call events paired by id', () => {
|
|
260
|
+
const extractor = new PeekEventExtractor('claude', { includeToolCalls: true });
|
|
261
|
+
const output = [
|
|
262
|
+
'{"type":"assistant","message":{"content":[{"type":"tool_use","id":"toolu_1","name":"mcp__acm__list_processes","input":{}}]}}',
|
|
263
|
+
'{"type":"user","message":{"content":[{"tool_use_id":"toolu_1","type":"tool_result","content":[{"type":"text","text":"secret result"}]}]}}',
|
|
264
|
+
'{"type":"assistant","message":{"content":[{"type":"text","text":"Done."}]}}',
|
|
265
|
+
].join('\n') + '\n';
|
|
266
|
+
|
|
267
|
+
expect(extractor.push(output, ts)).toEqual([
|
|
268
|
+
{
|
|
269
|
+
kind: 'tool_call',
|
|
270
|
+
ts,
|
|
271
|
+
phase: 'started',
|
|
272
|
+
id: 'toolu_1',
|
|
273
|
+
tool: 'mcp__acm__list_processes',
|
|
274
|
+
server: 'acm',
|
|
275
|
+
summary: 'acm.list_processes',
|
|
276
|
+
},
|
|
277
|
+
{
|
|
278
|
+
kind: 'tool_call',
|
|
279
|
+
ts,
|
|
280
|
+
phase: 'completed',
|
|
281
|
+
id: 'toolu_1',
|
|
282
|
+
tool: 'mcp__acm__list_processes',
|
|
283
|
+
server: 'acm',
|
|
284
|
+
summary: 'acm.list_processes',
|
|
285
|
+
status: 'success',
|
|
286
|
+
},
|
|
287
|
+
{ kind: 'message', ts, text: 'Done.' },
|
|
288
|
+
]);
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
it('emits Gemini MCP tool_call events and joined assistant message events', () => {
|
|
292
|
+
const extractor = new PeekEventExtractor('gemini', { includeToolCalls: true });
|
|
293
|
+
const output = [
|
|
294
|
+
'{"type":"tool_use","timestamp":"2026-04-12T02:56:29.992Z","tool_name":"mcp_acm_list_processes","tool_id":"mcp_1","parameters":{}}',
|
|
295
|
+
'{"type":"tool_result","timestamp":"2026-04-12T02:56:30.059Z","tool_id":"mcp_1","status":"success","output":"secret result"}',
|
|
296
|
+
'{"type":"message","timestamp":"2026-04-12T02:56:32.855Z","role":"assistant","content":"The tool ","delta":true}',
|
|
297
|
+
'{"type":"message","timestamp":"2026-04-12T02:56:32.902Z","role":"assistant","content":"succeeded.","delta":true}',
|
|
298
|
+
'{"type":"result","timestamp":"2026-04-12T02:56:32.954Z","status":"success","stats":{"tool_calls":1}}',
|
|
299
|
+
].join('\n') + '\n';
|
|
300
|
+
|
|
301
|
+
expect(extractor.push(output, ts)).toEqual([
|
|
302
|
+
{
|
|
303
|
+
kind: 'tool_call',
|
|
304
|
+
ts,
|
|
305
|
+
phase: 'started',
|
|
306
|
+
id: 'mcp_1',
|
|
307
|
+
tool: 'mcp_acm_list_processes',
|
|
308
|
+
server: 'acm',
|
|
309
|
+
summary: 'acm.list_processes',
|
|
310
|
+
},
|
|
311
|
+
{
|
|
312
|
+
kind: 'tool_call',
|
|
313
|
+
ts,
|
|
314
|
+
phase: 'completed',
|
|
315
|
+
id: 'mcp_1',
|
|
316
|
+
tool: 'mcp_acm_list_processes',
|
|
317
|
+
server: 'acm',
|
|
318
|
+
summary: 'acm.list_processes',
|
|
319
|
+
status: 'success',
|
|
320
|
+
},
|
|
321
|
+
{ kind: 'message', ts, text: 'The tool succeeded.' },
|
|
322
|
+
]);
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
it('emits OpenCode completed MCP tool_call events from tool_use state', () => {
|
|
326
|
+
const extractor = new PeekEventExtractor('opencode', { includeToolCalls: true });
|
|
327
|
+
const output = [
|
|
328
|
+
'{"type":"tool_use","timestamp":1775962663837,"sessionID":"ses-1","part":{"id":"part-1","type":"tool","tool":"acm_list_processes","callID":"call_1","state":{"status":"completed","input":{},"output":"secret result","metadata":{"truncated":false},"time":{"start":1775962663834,"end":1775962663837}}}}',
|
|
329
|
+
].join('\n') + '\n';
|
|
330
|
+
|
|
331
|
+
expect(extractor.push(output, ts)).toEqual([
|
|
332
|
+
{
|
|
333
|
+
kind: 'tool_call',
|
|
334
|
+
ts,
|
|
335
|
+
phase: 'completed',
|
|
336
|
+
id: 'call_1',
|
|
337
|
+
tool: 'acm_list_processes',
|
|
338
|
+
server: 'acm',
|
|
339
|
+
summary: 'acm.list_processes',
|
|
340
|
+
status: 'success',
|
|
341
|
+
duration_ms: 3,
|
|
342
|
+
},
|
|
343
|
+
]);
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
it('emits Forge message events from Summary and Completed successfully prefixes', () => {
|
|
347
|
+
const extractor = new PeekEventExtractor('forge');
|
|
348
|
+
const output = [
|
|
349
|
+
'Summary: Forge finished the task',
|
|
350
|
+
'Completed successfully: Built the project',
|
|
351
|
+
'Summary: ',
|
|
352
|
+
].join('\n') + '\n';
|
|
353
|
+
|
|
354
|
+
expect(extractor.push(output, ts)).toEqual([
|
|
355
|
+
{ kind: 'message', ts, text: 'Forge finished the task' },
|
|
356
|
+
{ kind: 'message', ts, text: 'Built the project' },
|
|
357
|
+
]);
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
it('preserves long Forge Summary message text without truncation', () => {
|
|
361
|
+
const extractor = new PeekEventExtractor('forge');
|
|
362
|
+
const longText = 'x'.repeat(260);
|
|
363
|
+
|
|
364
|
+
expect(extractor.push(`Summary: ${longText}\n`, ts)).toEqual([
|
|
365
|
+
{ kind: 'message', ts, text: longText },
|
|
366
|
+
]);
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
it('emits Forge Execute tool_call starts when include_tool_calls is true', () => {
|
|
370
|
+
const extractor = new PeekEventExtractor('forge', { includeToolCalls: true });
|
|
371
|
+
|
|
372
|
+
expect(extractor.push("● [11:28:40] Execute [/bin/zsh] /bin/sh -c 'echo hi'\n", ts)).toEqual([
|
|
373
|
+
{
|
|
374
|
+
kind: 'tool_call',
|
|
375
|
+
ts,
|
|
376
|
+
phase: 'started',
|
|
377
|
+
id: 'forge_0',
|
|
378
|
+
tool: '/bin/zsh',
|
|
379
|
+
summary: "/bin/sh -c 'echo hi'",
|
|
380
|
+
},
|
|
381
|
+
]);
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
it('falls back to shell for Forge Execute labels with spaces', () => {
|
|
385
|
+
const extractor = new PeekEventExtractor('forge', { includeToolCalls: true });
|
|
386
|
+
|
|
387
|
+
expect(extractor.push("● [11:28:40] Execute [local shell] /bin/sh -c 'echo hi'\n", ts)).toEqual([
|
|
388
|
+
{
|
|
389
|
+
kind: 'tool_call',
|
|
390
|
+
ts,
|
|
391
|
+
phase: 'started',
|
|
392
|
+
id: 'forge_0',
|
|
393
|
+
tool: 'shell',
|
|
394
|
+
summary: "/bin/sh -c 'echo hi'",
|
|
395
|
+
},
|
|
396
|
+
]);
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
it('suppresses Forge tool_call events when include_tool_calls is false but keeps messages', () => {
|
|
400
|
+
const extractor = new PeekEventExtractor('forge', { includeToolCalls: false });
|
|
401
|
+
const output = [
|
|
402
|
+
"● [11:28:40] Execute [/bin/zsh] /bin/sh -c 'echo hi'",
|
|
403
|
+
'Summary: done',
|
|
404
|
+
].join('\n') + '\n';
|
|
405
|
+
|
|
406
|
+
expect(extractor.push(output, ts)).toEqual([
|
|
407
|
+
{ kind: 'message', ts, text: 'done' },
|
|
408
|
+
]);
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
it('completes a pending Forge tool_call only on anchored Finished markers', () => {
|
|
412
|
+
const extractor = new PeekEventExtractor('forge', { includeToolCalls: true });
|
|
413
|
+
const output = [
|
|
414
|
+
"● [11:28:40] Execute [/bin/zsh] /bin/sh -c 'echo hi'",
|
|
415
|
+
'This line says Finished but is not a Forge marker',
|
|
416
|
+
'● [11:28:41] Finished abc123',
|
|
417
|
+
].join('\n') + '\n';
|
|
418
|
+
|
|
419
|
+
expect(extractor.push(output, ts)).toEqual([
|
|
420
|
+
{
|
|
421
|
+
kind: 'tool_call',
|
|
422
|
+
ts,
|
|
423
|
+
phase: 'started',
|
|
424
|
+
id: 'forge_0',
|
|
425
|
+
tool: '/bin/zsh',
|
|
426
|
+
summary: "/bin/sh -c 'echo hi'",
|
|
427
|
+
},
|
|
428
|
+
{
|
|
429
|
+
kind: 'tool_call',
|
|
430
|
+
ts,
|
|
431
|
+
phase: 'completed',
|
|
432
|
+
id: 'forge_0',
|
|
433
|
+
tool: '/bin/zsh',
|
|
434
|
+
summary: "/bin/sh -c 'echo hi'",
|
|
435
|
+
status: 'unknown',
|
|
436
|
+
},
|
|
437
|
+
]);
|
|
438
|
+
});
|
|
439
|
+
|
|
440
|
+
it('completes a pending Forge tool_call before starting a consecutive Execute marker', () => {
|
|
441
|
+
const extractor = new PeekEventExtractor('forge', { includeToolCalls: true });
|
|
442
|
+
const output = [
|
|
443
|
+
"● [11:28:40] Execute [/bin/zsh] /bin/sh -c 'echo one'",
|
|
444
|
+
"● [11:28:41] Execute [/bin/zsh] /bin/sh -c 'echo two'",
|
|
445
|
+
].join('\n') + '\n';
|
|
446
|
+
|
|
447
|
+
expect(extractor.push(output, ts)).toEqual([
|
|
448
|
+
{
|
|
449
|
+
kind: 'tool_call',
|
|
450
|
+
ts,
|
|
451
|
+
phase: 'started',
|
|
452
|
+
id: 'forge_0',
|
|
453
|
+
tool: '/bin/zsh',
|
|
454
|
+
summary: "/bin/sh -c 'echo one'",
|
|
455
|
+
},
|
|
456
|
+
{
|
|
457
|
+
kind: 'tool_call',
|
|
458
|
+
ts,
|
|
459
|
+
phase: 'completed',
|
|
460
|
+
id: 'forge_0',
|
|
461
|
+
tool: '/bin/zsh',
|
|
462
|
+
summary: "/bin/sh -c 'echo one'",
|
|
463
|
+
status: 'unknown',
|
|
464
|
+
},
|
|
465
|
+
{
|
|
466
|
+
kind: 'tool_call',
|
|
467
|
+
ts,
|
|
468
|
+
phase: 'started',
|
|
469
|
+
id: 'forge_1',
|
|
470
|
+
tool: '/bin/zsh',
|
|
471
|
+
summary: "/bin/sh -c 'echo two'",
|
|
472
|
+
},
|
|
473
|
+
]);
|
|
474
|
+
});
|
|
475
|
+
|
|
476
|
+
it('does not synthesize Forge completion on non-terminal flush', () => {
|
|
477
|
+
const extractor = new PeekEventExtractor('forge', { includeToolCalls: true });
|
|
478
|
+
|
|
479
|
+
expect(extractor.push("● [11:28:40] Execute [/bin/zsh] /bin/sh -c 'echo hi'\n", ts)).toHaveLength(1);
|
|
480
|
+
expect(extractor.flush(ts, { terminal: false })).toEqual([]);
|
|
481
|
+
});
|
|
482
|
+
|
|
483
|
+
it('synthesizes Forge completion with unknown status on terminal flush', () => {
|
|
484
|
+
const extractor = new PeekEventExtractor('forge', { includeToolCalls: true });
|
|
485
|
+
|
|
486
|
+
expect(extractor.push("● [11:28:40] Execute [/bin/zsh] /bin/sh -c 'echo hi'\n", ts)).toHaveLength(1);
|
|
487
|
+
expect(extractor.flush('2026-04-12T02:10:05.000Z', { terminal: true })).toEqual([
|
|
488
|
+
{
|
|
489
|
+
kind: 'tool_call',
|
|
490
|
+
ts: '2026-04-12T02:10:05.000Z',
|
|
491
|
+
phase: 'completed',
|
|
492
|
+
id: 'forge_0',
|
|
493
|
+
tool: '/bin/zsh',
|
|
494
|
+
summary: "/bin/sh -c 'echo hi'",
|
|
495
|
+
status: 'unknown',
|
|
496
|
+
},
|
|
497
|
+
]);
|
|
498
|
+
});
|
|
499
|
+
|
|
500
|
+
it('treats Forge stderr as a no-op source', () => {
|
|
501
|
+
const extractor = new PeekEventExtractor('forge', { includeToolCalls: true, source: 'stderr' });
|
|
502
|
+
const output = [
|
|
503
|
+
'Summary: hidden',
|
|
504
|
+
"● [11:28:40] Execute [/bin/zsh] /bin/sh -c 'echo hidden'",
|
|
505
|
+
'● [11:28:41] Finished',
|
|
506
|
+
].join('\n') + '\n';
|
|
507
|
+
|
|
508
|
+
expect(extractor.push(output, ts)).toEqual([]);
|
|
509
|
+
expect(extractor.flush(ts, { terminal: true })).toEqual([]);
|
|
510
|
+
});
|
|
511
|
+
});
|
|
512
|
+
|
|
193
513
|
describe('parseGeminiOutput', () => {
|
|
194
514
|
it('should parse legacy final JSON output', () => {
|
|
195
515
|
const output = JSON.stringify({
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { describe, expect, it } from 'vitest';
|
|
2
|
-
import {
|
|
2
|
+
import { appendPeekEvents, validatePeekPids, validatePeekTimeSec, type PeekProcessResult } from '../peek.js';
|
|
3
3
|
|
|
4
4
|
describe('peek helpers', () => {
|
|
5
5
|
it('dedupes pids while preserving first occurrence order', () => {
|
|
@@ -17,27 +17,28 @@ describe('peek helpers', () => {
|
|
|
17
17
|
expect(() => validatePeekTimeSec(61)).toThrow(/positive integer/);
|
|
18
18
|
});
|
|
19
19
|
|
|
20
|
-
it('keeps the first 50
|
|
20
|
+
it('keeps the first 50 events and marks truncation when later events are dropped', () => {
|
|
21
21
|
const process: PeekProcessResult = {
|
|
22
22
|
pid: 123,
|
|
23
23
|
agent: 'codex',
|
|
24
24
|
status: 'running',
|
|
25
|
-
|
|
25
|
+
events: [],
|
|
26
26
|
truncated: false,
|
|
27
27
|
error: null,
|
|
28
28
|
};
|
|
29
29
|
|
|
30
|
-
|
|
30
|
+
appendPeekEvents(
|
|
31
31
|
process,
|
|
32
32
|
Array.from({ length: 55 }, (_, index) => ({
|
|
33
|
+
kind: 'message' as const,
|
|
33
34
|
ts: '2026-04-11T12:34:56.789Z',
|
|
34
35
|
text: `message ${index}`,
|
|
35
36
|
})),
|
|
36
37
|
);
|
|
37
38
|
|
|
38
|
-
expect(process.
|
|
39
|
-
expect(process.
|
|
40
|
-
expect(process.
|
|
39
|
+
expect(process.events).toHaveLength(50);
|
|
40
|
+
expect(process.events[0]).toMatchObject({ kind: 'message', text: 'message 0' });
|
|
41
|
+
expect(process.events[49]).toMatchObject({ kind: 'message', text: 'message 49' });
|
|
41
42
|
expect(process.truncated).toBe(true);
|
|
42
43
|
});
|
|
43
44
|
});
|
|
@@ -166,8 +166,9 @@ describe('Process Management Tests', () => {
|
|
|
166
166
|
pid: 12345,
|
|
167
167
|
agent: 'claude',
|
|
168
168
|
status: 'completed',
|
|
169
|
-
|
|
169
|
+
events: [
|
|
170
170
|
{
|
|
171
|
+
kind: 'message',
|
|
171
172
|
ts: expect.any(String),
|
|
172
173
|
text: 'new message',
|
|
173
174
|
},
|
|
@@ -179,7 +180,7 @@ describe('Process Management Tests', () => {
|
|
|
179
180
|
pid: 99999,
|
|
180
181
|
agent: null,
|
|
181
182
|
status: 'not_found',
|
|
182
|
-
|
|
183
|
+
events: [],
|
|
183
184
|
truncated: false,
|
|
184
185
|
error: 'process not found',
|
|
185
186
|
});
|
|
@@ -232,8 +233,9 @@ describe('Process Management Tests', () => {
|
|
|
232
233
|
pid: 12346,
|
|
233
234
|
agent: 'opencode',
|
|
234
235
|
status: 'completed',
|
|
235
|
-
|
|
236
|
+
events: [
|
|
236
237
|
{
|
|
238
|
+
kind: 'message',
|
|
237
239
|
ts: expect.any(String),
|
|
238
240
|
text: 'OpenCode visible text',
|
|
239
241
|
},
|
|
@@ -291,8 +293,9 @@ describe('Process Management Tests', () => {
|
|
|
291
293
|
pid: 12347,
|
|
292
294
|
agent: 'gemini',
|
|
293
295
|
status: 'completed',
|
|
294
|
-
|
|
296
|
+
events: [
|
|
295
297
|
{
|
|
298
|
+
kind: 'message',
|
|
296
299
|
ts: expect.any(String),
|
|
297
300
|
text: 'Visible Gemini text',
|
|
298
301
|
},
|
|
@@ -302,6 +305,171 @@ describe('Process Management Tests', () => {
|
|
|
302
305
|
});
|
|
303
306
|
});
|
|
304
307
|
|
|
308
|
+
it('should include normalized tool_call events when requested', async () => {
|
|
309
|
+
const { handlers } = await setupServer();
|
|
310
|
+
|
|
311
|
+
const mockProcess = new EventEmitter() as any;
|
|
312
|
+
mockProcess.pid = 12348;
|
|
313
|
+
mockProcess.stdout = new EventEmitter();
|
|
314
|
+
mockProcess.stderr = new EventEmitter();
|
|
315
|
+
mockProcess.kill = vi.fn();
|
|
316
|
+
|
|
317
|
+
mockSpawn.mockReturnValue(mockProcess);
|
|
318
|
+
|
|
319
|
+
const callToolHandler = handlers.get('callTool')!;
|
|
320
|
+
await callToolHandler!({
|
|
321
|
+
params: {
|
|
322
|
+
name: 'run',
|
|
323
|
+
arguments: {
|
|
324
|
+
prompt: 'claude mcp peek prompt',
|
|
325
|
+
workFolder: '/tmp',
|
|
326
|
+
model: 'haiku',
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
const peekPromise = callToolHandler!({
|
|
332
|
+
params: {
|
|
333
|
+
name: 'peek',
|
|
334
|
+
arguments: {
|
|
335
|
+
pids: [12348],
|
|
336
|
+
peek_time_sec: 1,
|
|
337
|
+
include_tool_calls: true,
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
setTimeout(() => {
|
|
343
|
+
mockProcess.stdout.emit('data', '{"type":"assistant","message":{"content":[{"type":"tool_use","id":"toolu_1","name":"mcp__acm__list_processes","input":{}}]}}\n');
|
|
344
|
+
mockProcess.stdout.emit('data', '{"type":"user","message":{"content":[{"tool_use_id":"toolu_1","type":"tool_result","content":[{"type":"text","text":"secret result"}]}]}}\n');
|
|
345
|
+
mockProcess.stdout.emit('data', '{"type":"assistant","message":{"content":[{"type":"text","text":"MCP succeeded."}]}}\n');
|
|
346
|
+
mockProcess.emit('close', 0);
|
|
347
|
+
}, 10);
|
|
348
|
+
|
|
349
|
+
const result = await peekPromise;
|
|
350
|
+
const response = JSON.parse(result.content[0].text);
|
|
351
|
+
|
|
352
|
+
expect(response.processes).toHaveLength(1);
|
|
353
|
+
expect(response.processes[0]).toMatchObject({
|
|
354
|
+
pid: 12348,
|
|
355
|
+
agent: 'claude',
|
|
356
|
+
status: 'completed',
|
|
357
|
+
events: [
|
|
358
|
+
{
|
|
359
|
+
kind: 'tool_call',
|
|
360
|
+
phase: 'started',
|
|
361
|
+
id: 'toolu_1',
|
|
362
|
+
tool: 'mcp__acm__list_processes',
|
|
363
|
+
server: 'acm',
|
|
364
|
+
summary: 'acm.list_processes',
|
|
365
|
+
},
|
|
366
|
+
{
|
|
367
|
+
kind: 'tool_call',
|
|
368
|
+
phase: 'completed',
|
|
369
|
+
id: 'toolu_1',
|
|
370
|
+
tool: 'mcp__acm__list_processes',
|
|
371
|
+
server: 'acm',
|
|
372
|
+
summary: 'acm.list_processes',
|
|
373
|
+
status: 'success',
|
|
374
|
+
},
|
|
375
|
+
{
|
|
376
|
+
kind: 'message',
|
|
377
|
+
ts: expect.any(String),
|
|
378
|
+
text: 'MCP succeeded.',
|
|
379
|
+
},
|
|
380
|
+
],
|
|
381
|
+
truncated: false,
|
|
382
|
+
error: null,
|
|
383
|
+
});
|
|
384
|
+
expect(JSON.stringify(response)).not.toContain('secret result');
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
it('should peek Forge plain-text messages and low-precision tool calls without raw command output', async () => {
|
|
388
|
+
const { handlers } = await setupServer();
|
|
389
|
+
|
|
390
|
+
const mockProcess = new EventEmitter() as any;
|
|
391
|
+
mockProcess.pid = 12349;
|
|
392
|
+
mockProcess.stdout = new EventEmitter();
|
|
393
|
+
mockProcess.stderr = new EventEmitter();
|
|
394
|
+
mockProcess.kill = vi.fn();
|
|
395
|
+
|
|
396
|
+
mockSpawn.mockReturnValue(mockProcess);
|
|
397
|
+
|
|
398
|
+
const callToolHandler = handlers.get('callTool')!;
|
|
399
|
+
await callToolHandler!({
|
|
400
|
+
params: {
|
|
401
|
+
name: 'run',
|
|
402
|
+
arguments: {
|
|
403
|
+
prompt: 'forge peek prompt',
|
|
404
|
+
workFolder: '/tmp',
|
|
405
|
+
model: 'forge',
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
const peekPromise = callToolHandler!({
|
|
411
|
+
params: {
|
|
412
|
+
name: 'peek',
|
|
413
|
+
arguments: {
|
|
414
|
+
pids: [12349],
|
|
415
|
+
peek_time_sec: 1,
|
|
416
|
+
include_tool_calls: true,
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
});
|
|
420
|
+
|
|
421
|
+
setTimeout(() => {
|
|
422
|
+
mockProcess.stdout.emit('data', 'Summary: Forge started\n');
|
|
423
|
+
mockProcess.stdout.emit('data', "● [11:28:40] Execute [/bin/zsh] /bin/sh -c 'echo hi'\n");
|
|
424
|
+
mockProcess.stdout.emit('data', 'secret child output\n');
|
|
425
|
+
mockProcess.stderr.emit('data', 'Summary: stderr should be ignored\n');
|
|
426
|
+
mockProcess.stdout.emit('data', '● [11:28:41] Finished abc123\n');
|
|
427
|
+
mockProcess.stdout.emit('data', 'Completed successfully: Forge done\n');
|
|
428
|
+
mockProcess.emit('close', 0);
|
|
429
|
+
}, 10);
|
|
430
|
+
|
|
431
|
+
const result = await peekPromise;
|
|
432
|
+
const response = JSON.parse(result.content[0].text);
|
|
433
|
+
|
|
434
|
+
expect(response.processes).toHaveLength(1);
|
|
435
|
+
expect(response.processes[0]).toMatchObject({
|
|
436
|
+
pid: 12349,
|
|
437
|
+
agent: 'forge',
|
|
438
|
+
status: 'completed',
|
|
439
|
+
events: [
|
|
440
|
+
{
|
|
441
|
+
kind: 'message',
|
|
442
|
+
ts: expect.any(String),
|
|
443
|
+
text: 'Forge started',
|
|
444
|
+
},
|
|
445
|
+
{
|
|
446
|
+
kind: 'tool_call',
|
|
447
|
+
phase: 'started',
|
|
448
|
+
id: 'forge_0',
|
|
449
|
+
tool: '/bin/zsh',
|
|
450
|
+
summary: "/bin/sh -c 'echo hi'",
|
|
451
|
+
},
|
|
452
|
+
{
|
|
453
|
+
kind: 'tool_call',
|
|
454
|
+
phase: 'completed',
|
|
455
|
+
id: 'forge_0',
|
|
456
|
+
tool: '/bin/zsh',
|
|
457
|
+
summary: "/bin/sh -c 'echo hi'",
|
|
458
|
+
status: 'unknown',
|
|
459
|
+
},
|
|
460
|
+
{
|
|
461
|
+
kind: 'message',
|
|
462
|
+
ts: expect.any(String),
|
|
463
|
+
text: 'Forge done',
|
|
464
|
+
},
|
|
465
|
+
],
|
|
466
|
+
truncated: false,
|
|
467
|
+
error: null,
|
|
468
|
+
});
|
|
469
|
+
expect(JSON.stringify(response)).not.toContain('secret child output');
|
|
470
|
+
expect(JSON.stringify(response)).not.toContain('stderr should be ignored');
|
|
471
|
+
});
|
|
472
|
+
|
|
305
473
|
it('should handle process with model parameter', async () => {
|
|
306
474
|
const { handlers } = await setupServer();
|
|
307
475
|
|
package/src/app/cli.ts
CHANGED
|
@@ -9,7 +9,7 @@ export const CLI_HELP_TEXT = `Usage: ai-cli <command> [options]
|
|
|
9
9
|
Commands:
|
|
10
10
|
run Start an AI CLI process in the background
|
|
11
11
|
wait Wait for one or more pids
|
|
12
|
-
peek Observe new
|
|
12
|
+
peek Observe new agent events for a short window
|
|
13
13
|
ps List tracked processes
|
|
14
14
|
result Get the current result for a pid
|
|
15
15
|
kill Terminate a tracked pid
|
|
@@ -62,12 +62,13 @@ Options:
|
|
|
62
62
|
|
|
63
63
|
export const PEEK_HELP_TEXT = `Usage: ai-cli peek <pid...> [options]
|
|
64
64
|
|
|
65
|
-
Observe new natural-language agent messages for a short one-shot window.
|
|
66
|
-
In v1, message extraction is supported for Codex, Claude, OpenCode, and
|
|
65
|
+
Observe new natural-language agent messages, and optionally tool calls, for a short one-shot window.
|
|
66
|
+
In v1, message extraction is supported for Codex, Claude, OpenCode, Gemini, and best-effort Forge Summary/Completed successfully lines. Forge tool calls are low-precision Execute/Finished markers and never include command output.
|
|
67
67
|
This is not a history API, gapless streaming, or stdout/stderr tailing. No --follow mode is available in v1.
|
|
68
68
|
|
|
69
69
|
Options:
|
|
70
70
|
--time <seconds> Observation window in seconds. Defaults to 10, maximum 60
|
|
71
|
+
--include-tool-calls Include normalized tool_call events without raw tool output
|
|
71
72
|
--help, -h Show this help message
|
|
72
73
|
`;
|
|
73
74
|
|
|
@@ -131,7 +132,7 @@ interface CliDeps {
|
|
|
131
132
|
listProcesses: () => Promise<any>;
|
|
132
133
|
getProcessResult: (pid: number, verbose: boolean) => Promise<any>;
|
|
133
134
|
waitForProcesses: (pids: number[], timeoutSeconds?: number, verbose?: boolean) => Promise<any>;
|
|
134
|
-
peekProcesses: (pids: number[], peekTimeSec?: number) => Promise<any>;
|
|
135
|
+
peekProcesses: (pids: number[], peekTimeSec?: number, includeToolCalls?: boolean) => Promise<any>;
|
|
135
136
|
killProcess: (pid: number) => Promise<any>;
|
|
136
137
|
cleanupProcesses: () => Promise<any>;
|
|
137
138
|
getDoctorStatus: () => any;
|
|
@@ -154,7 +155,7 @@ const defaultDeps: CliDeps = {
|
|
|
154
155
|
listProcesses: () => getCliProcessService().listProcesses(),
|
|
155
156
|
getProcessResult: (pid, verbose) => getCliProcessService().getProcessResult(pid, verbose),
|
|
156
157
|
waitForProcesses: (pids, timeoutSeconds, verbose) => getCliProcessService().waitForProcesses(pids, timeoutSeconds, verbose),
|
|
157
|
-
peekProcesses: (pids, peekTimeSec) => getCliProcessService().peekProcesses(pids, peekTimeSec),
|
|
158
|
+
peekProcesses: (pids, peekTimeSec, includeToolCalls) => getCliProcessService().peekProcesses(pids, peekTimeSec, includeToolCalls),
|
|
158
159
|
killProcess: (pid) => getCliProcessService().killProcess(pid),
|
|
159
160
|
cleanupProcesses: () => getCliProcessService().cleanupProcesses(),
|
|
160
161
|
getDoctorStatus: () => getCliDoctorStatus(),
|
|
@@ -367,7 +368,7 @@ export async function runCli(argv: string[], deps: Partial<CliDeps> = {}): Promi
|
|
|
367
368
|
return 1;
|
|
368
369
|
}
|
|
369
370
|
|
|
370
|
-
writeJson(stdout, await peekProcesses(pids, peekTimeSec));
|
|
371
|
+
writeJson(stdout, await peekProcesses(pids, peekTimeSec, 'include-tool-calls' in flags || 'include_tool_calls' in flags));
|
|
371
372
|
return 0;
|
|
372
373
|
}
|
|
373
374
|
|
package/src/app/mcp.ts
CHANGED
|
@@ -233,7 +233,7 @@ ${getSupportedModelsDescription()}
|
|
|
233
233
|
},
|
|
234
234
|
{
|
|
235
235
|
name: 'peek',
|
|
236
|
-
description: 'One-shot short observation window for running child agents. Returns only natural-language
|
|
236
|
+
description: 'One-shot short observation window for running child agents. Returns only natural-language message events, and optionally normalized tool_call events, observed during this call; not a history API, not gapless streaming, and not stdout/stderr tailing. In v1, message extraction is supported for Codex, Claude, OpenCode, Gemini, and best-effort Forge Summary/Completed successfully lines. Forge tool calls are low-precision Execute/Finished markers and never include command output. Tool calls exclude raw tool output.',
|
|
237
237
|
inputSchema: {
|
|
238
238
|
type: 'object',
|
|
239
239
|
properties: {
|
|
@@ -246,6 +246,10 @@ ${getSupportedModelsDescription()}
|
|
|
246
246
|
type: 'number',
|
|
247
247
|
description: 'Optional positive integer observation window in seconds. Defaults to 10; maximum is 60.',
|
|
248
248
|
},
|
|
249
|
+
include_tool_calls: {
|
|
250
|
+
type: 'boolean',
|
|
251
|
+
description: 'Optional: include normalized tool_call events without raw tool output. Defaults to false.',
|
|
252
|
+
},
|
|
249
253
|
},
|
|
250
254
|
required: ['pids'],
|
|
251
255
|
},
|
|
@@ -384,16 +388,21 @@ ${getSupportedModelsDescription()}
|
|
|
384
388
|
private async handlePeek(toolArguments: any): Promise<ServerResult> {
|
|
385
389
|
let pids: number[];
|
|
386
390
|
let peekTimeSec: number;
|
|
391
|
+
let includeToolCalls: boolean;
|
|
387
392
|
|
|
388
393
|
try {
|
|
389
394
|
pids = validatePeekPids(toolArguments.pids);
|
|
390
395
|
peekTimeSec = validatePeekTimeSec(toolArguments.peek_time_sec);
|
|
396
|
+
if (toolArguments.include_tool_calls !== undefined && typeof toolArguments.include_tool_calls !== 'boolean') {
|
|
397
|
+
throw new Error('include_tool_calls must be a boolean when provided');
|
|
398
|
+
}
|
|
399
|
+
includeToolCalls = toolArguments.include_tool_calls === true;
|
|
391
400
|
} catch (error: any) {
|
|
392
401
|
throw new McpError(ErrorCode.InvalidParams, error.message);
|
|
393
402
|
}
|
|
394
403
|
|
|
395
404
|
try {
|
|
396
|
-
const response = await this.processService.peekProcesses(pids, peekTimeSec);
|
|
405
|
+
const response = await this.processService.peekProcesses(pids, peekTimeSec, includeToolCalls);
|
|
397
406
|
return {
|
|
398
407
|
content: [{
|
|
399
408
|
type: 'text',
|