ai-cli-mcp 2.17.0 → 2.18.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 +7 -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 +144 -1
- package/dist/__tests__/peek.test.js +8 -7
- package/dist/__tests__/process-management.test.js +78 -4
- package/dist/app/cli.js +6 -5
- package/dist/app/mcp.js +11 -2
- package/dist/cli-process-service.js +10 -10
- package/dist/parsers.js +282 -22
- package/dist/peek.js +8 -5
- package/dist/process-service.js +10 -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 +155 -1
- package/src/__tests__/peek.test.ts +8 -7
- package/src/__tests__/process-management.test.ts +86 -4
- package/src/app/cli.ts +7 -6
- package/src/app/mcp.ts +11 -2
- package/src/cli-process-service.ts +12 -12
- package/src/parsers.ts +371 -27
- package/src/peek.ts +14 -7
- package/src/process-service.ts +12 -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,160 @@ 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
|
+
|
|
193
347
|
describe('parseGeminiOutput', () => {
|
|
194
348
|
it('should parse legacy final JSON output', () => {
|
|
195
349
|
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,85 @@ 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
|
+
|
|
305
387
|
it('should handle process with model parameter', async () => {
|
|
306
388
|
const { handlers } = await setupServer();
|
|
307
389
|
|
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 Gemini; Forge returns status with
|
|
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, and Gemini; Forge returns status with events: [].
|
|
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, and Gemini; Forge returns status with events: []. 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',
|
|
@@ -19,10 +19,10 @@ import { join, basename, dirname } from 'node:path';
|
|
|
19
19
|
import { homedir } from 'node:os';
|
|
20
20
|
import { buildCliCommand, type BuildCliCommandOptions } from './cli-builder.js';
|
|
21
21
|
import { findClaudeCli, findCodexCli, findForgeCli, findGeminiCli, findOpencodeCli } from './cli-utils.js';
|
|
22
|
-
import { parseClaudeOutput, parseCodexOutput, parseForgeOutput, parseGeminiOutput, parseOpenCodeOutput,
|
|
22
|
+
import { parseClaudeOutput, parseCodexOutput, parseForgeOutput, parseGeminiOutput, parseOpenCodeOutput, PeekEventExtractor } from './parsers.js';
|
|
23
23
|
import { buildProcessResult } from './process-result.js';
|
|
24
24
|
import {
|
|
25
|
-
|
|
25
|
+
appendPeekEvents,
|
|
26
26
|
buildNotFoundPeekProcess,
|
|
27
27
|
observedDurationSec,
|
|
28
28
|
validatePeekPids,
|
|
@@ -255,15 +255,15 @@ export class CliProcessService {
|
|
|
255
255
|
}
|
|
256
256
|
}
|
|
257
257
|
|
|
258
|
-
async peekProcesses(pids: number[], peekTimeSec = 10): Promise<PeekResponse> {
|
|
258
|
+
async peekProcesses(pids: number[], peekTimeSec = 10, includeToolCalls = false): Promise<PeekResponse> {
|
|
259
259
|
const targetPids = validatePeekPids(pids);
|
|
260
260
|
const targetPeekTimeSec = validatePeekTimeSec(peekTimeSec);
|
|
261
261
|
const processes: PeekProcessResult[] = [];
|
|
262
262
|
const observers: Array<{
|
|
263
263
|
process: StoredProcess;
|
|
264
264
|
result: PeekProcessResult;
|
|
265
|
-
stdoutExtractor:
|
|
266
|
-
stderrExtractor:
|
|
265
|
+
stdoutExtractor: PeekEventExtractor;
|
|
266
|
+
stderrExtractor: PeekEventExtractor;
|
|
267
267
|
stdoutOffset: number;
|
|
268
268
|
stderrOffset: number;
|
|
269
269
|
}> = [];
|
|
@@ -281,7 +281,7 @@ export class CliProcessService {
|
|
|
281
281
|
pid,
|
|
282
282
|
agent: process.toolType,
|
|
283
283
|
status: process.status,
|
|
284
|
-
|
|
284
|
+
events: [],
|
|
285
285
|
truncated: false,
|
|
286
286
|
error: null,
|
|
287
287
|
};
|
|
@@ -289,8 +289,8 @@ export class CliProcessService {
|
|
|
289
289
|
observers.push({
|
|
290
290
|
process,
|
|
291
291
|
result,
|
|
292
|
-
stdoutExtractor: new
|
|
293
|
-
stderrExtractor: new
|
|
292
|
+
stdoutExtractor: new PeekEventExtractor(process.toolType, { includeToolCalls }),
|
|
293
|
+
stderrExtractor: new PeekEventExtractor(process.toolType, { includeToolCalls }),
|
|
294
294
|
stdoutOffset: this.fileSizeSafe(process.stdoutPath),
|
|
295
295
|
stderrOffset: this.fileSizeSafe(process.stderrPath),
|
|
296
296
|
});
|
|
@@ -307,11 +307,11 @@ export class CliProcessService {
|
|
|
307
307
|
for (const observer of observers) {
|
|
308
308
|
const stdoutRead = this.readTextFromOffset(observer.process.stdoutPath, observer.stdoutOffset);
|
|
309
309
|
observer.stdoutOffset = stdoutRead.offset;
|
|
310
|
-
|
|
310
|
+
appendPeekEvents(observer.result, observer.stdoutExtractor.push(stdoutRead.text, observedAt));
|
|
311
311
|
|
|
312
312
|
const stderrRead = this.readTextFromOffset(observer.process.stderrPath, observer.stderrOffset);
|
|
313
313
|
observer.stderrOffset = stderrRead.offset;
|
|
314
|
-
|
|
314
|
+
appendPeekEvents(observer.result, observer.stderrExtractor.push(stderrRead.text, observedAt));
|
|
315
315
|
|
|
316
316
|
observer.process = this.refreshStatus(this.readProcess(observer.process.pid));
|
|
317
317
|
observer.result.status = observer.process.status;
|
|
@@ -335,8 +335,8 @@ export class CliProcessService {
|
|
|
335
335
|
for (const observer of observers) {
|
|
336
336
|
observer.process = this.refreshStatus(this.readProcess(observer.process.pid));
|
|
337
337
|
observer.result.status = observer.process.status;
|
|
338
|
-
|
|
339
|
-
|
|
338
|
+
appendPeekEvents(observer.result, observer.stdoutExtractor.flush(flushTs));
|
|
339
|
+
appendPeekEvents(observer.result, observer.stderrExtractor.flush(flushTs));
|
|
340
340
|
}
|
|
341
341
|
|
|
342
342
|
return {
|