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
|
@@ -141,8 +141,9 @@ describe('Process Management Tests', () => {
|
|
|
141
141
|
pid: 12345,
|
|
142
142
|
agent: 'claude',
|
|
143
143
|
status: 'completed',
|
|
144
|
-
|
|
144
|
+
events: [
|
|
145
145
|
{
|
|
146
|
+
kind: 'message',
|
|
146
147
|
ts: expect.any(String),
|
|
147
148
|
text: 'new message',
|
|
148
149
|
},
|
|
@@ -154,7 +155,7 @@ describe('Process Management Tests', () => {
|
|
|
154
155
|
pid: 99999,
|
|
155
156
|
agent: null,
|
|
156
157
|
status: 'not_found',
|
|
157
|
-
|
|
158
|
+
events: [],
|
|
158
159
|
truncated: false,
|
|
159
160
|
error: 'process not found',
|
|
160
161
|
});
|
|
@@ -199,8 +200,9 @@ describe('Process Management Tests', () => {
|
|
|
199
200
|
pid: 12346,
|
|
200
201
|
agent: 'opencode',
|
|
201
202
|
status: 'completed',
|
|
202
|
-
|
|
203
|
+
events: [
|
|
203
204
|
{
|
|
205
|
+
kind: 'message',
|
|
204
206
|
ts: expect.any(String),
|
|
205
207
|
text: 'OpenCode visible text',
|
|
206
208
|
},
|
|
@@ -250,8 +252,9 @@ describe('Process Management Tests', () => {
|
|
|
250
252
|
pid: 12347,
|
|
251
253
|
agent: 'gemini',
|
|
252
254
|
status: 'completed',
|
|
253
|
-
|
|
255
|
+
events: [
|
|
254
256
|
{
|
|
257
|
+
kind: 'message',
|
|
255
258
|
ts: expect.any(String),
|
|
256
259
|
text: 'Visible Gemini text',
|
|
257
260
|
},
|
|
@@ -260,6 +263,155 @@ describe('Process Management Tests', () => {
|
|
|
260
263
|
error: null,
|
|
261
264
|
});
|
|
262
265
|
});
|
|
266
|
+
it('should include normalized tool_call events when requested', async () => {
|
|
267
|
+
const { handlers } = await setupServer();
|
|
268
|
+
const mockProcess = new EventEmitter();
|
|
269
|
+
mockProcess.pid = 12348;
|
|
270
|
+
mockProcess.stdout = new EventEmitter();
|
|
271
|
+
mockProcess.stderr = new EventEmitter();
|
|
272
|
+
mockProcess.kill = vi.fn();
|
|
273
|
+
mockSpawn.mockReturnValue(mockProcess);
|
|
274
|
+
const callToolHandler = handlers.get('callTool');
|
|
275
|
+
await callToolHandler({
|
|
276
|
+
params: {
|
|
277
|
+
name: 'run',
|
|
278
|
+
arguments: {
|
|
279
|
+
prompt: 'claude mcp peek prompt',
|
|
280
|
+
workFolder: '/tmp',
|
|
281
|
+
model: 'haiku',
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
});
|
|
285
|
+
const peekPromise = callToolHandler({
|
|
286
|
+
params: {
|
|
287
|
+
name: 'peek',
|
|
288
|
+
arguments: {
|
|
289
|
+
pids: [12348],
|
|
290
|
+
peek_time_sec: 1,
|
|
291
|
+
include_tool_calls: true,
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
});
|
|
295
|
+
setTimeout(() => {
|
|
296
|
+
mockProcess.stdout.emit('data', '{"type":"assistant","message":{"content":[{"type":"tool_use","id":"toolu_1","name":"mcp__acm__list_processes","input":{}}]}}\n');
|
|
297
|
+
mockProcess.stdout.emit('data', '{"type":"user","message":{"content":[{"tool_use_id":"toolu_1","type":"tool_result","content":[{"type":"text","text":"secret result"}]}]}}\n');
|
|
298
|
+
mockProcess.stdout.emit('data', '{"type":"assistant","message":{"content":[{"type":"text","text":"MCP succeeded."}]}}\n');
|
|
299
|
+
mockProcess.emit('close', 0);
|
|
300
|
+
}, 10);
|
|
301
|
+
const result = await peekPromise;
|
|
302
|
+
const response = JSON.parse(result.content[0].text);
|
|
303
|
+
expect(response.processes).toHaveLength(1);
|
|
304
|
+
expect(response.processes[0]).toMatchObject({
|
|
305
|
+
pid: 12348,
|
|
306
|
+
agent: 'claude',
|
|
307
|
+
status: 'completed',
|
|
308
|
+
events: [
|
|
309
|
+
{
|
|
310
|
+
kind: 'tool_call',
|
|
311
|
+
phase: 'started',
|
|
312
|
+
id: 'toolu_1',
|
|
313
|
+
tool: 'mcp__acm__list_processes',
|
|
314
|
+
server: 'acm',
|
|
315
|
+
summary: 'acm.list_processes',
|
|
316
|
+
},
|
|
317
|
+
{
|
|
318
|
+
kind: 'tool_call',
|
|
319
|
+
phase: 'completed',
|
|
320
|
+
id: 'toolu_1',
|
|
321
|
+
tool: 'mcp__acm__list_processes',
|
|
322
|
+
server: 'acm',
|
|
323
|
+
summary: 'acm.list_processes',
|
|
324
|
+
status: 'success',
|
|
325
|
+
},
|
|
326
|
+
{
|
|
327
|
+
kind: 'message',
|
|
328
|
+
ts: expect.any(String),
|
|
329
|
+
text: 'MCP succeeded.',
|
|
330
|
+
},
|
|
331
|
+
],
|
|
332
|
+
truncated: false,
|
|
333
|
+
error: null,
|
|
334
|
+
});
|
|
335
|
+
expect(JSON.stringify(response)).not.toContain('secret result');
|
|
336
|
+
});
|
|
337
|
+
it('should peek Forge plain-text messages and low-precision tool calls without raw command output', async () => {
|
|
338
|
+
const { handlers } = await setupServer();
|
|
339
|
+
const mockProcess = new EventEmitter();
|
|
340
|
+
mockProcess.pid = 12349;
|
|
341
|
+
mockProcess.stdout = new EventEmitter();
|
|
342
|
+
mockProcess.stderr = new EventEmitter();
|
|
343
|
+
mockProcess.kill = vi.fn();
|
|
344
|
+
mockSpawn.mockReturnValue(mockProcess);
|
|
345
|
+
const callToolHandler = handlers.get('callTool');
|
|
346
|
+
await callToolHandler({
|
|
347
|
+
params: {
|
|
348
|
+
name: 'run',
|
|
349
|
+
arguments: {
|
|
350
|
+
prompt: 'forge peek prompt',
|
|
351
|
+
workFolder: '/tmp',
|
|
352
|
+
model: 'forge',
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
});
|
|
356
|
+
const peekPromise = callToolHandler({
|
|
357
|
+
params: {
|
|
358
|
+
name: 'peek',
|
|
359
|
+
arguments: {
|
|
360
|
+
pids: [12349],
|
|
361
|
+
peek_time_sec: 1,
|
|
362
|
+
include_tool_calls: true,
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
});
|
|
366
|
+
setTimeout(() => {
|
|
367
|
+
mockProcess.stdout.emit('data', 'Summary: Forge started\n');
|
|
368
|
+
mockProcess.stdout.emit('data', "● [11:28:40] Execute [/bin/zsh] /bin/sh -c 'echo hi'\n");
|
|
369
|
+
mockProcess.stdout.emit('data', 'secret child output\n');
|
|
370
|
+
mockProcess.stderr.emit('data', 'Summary: stderr should be ignored\n');
|
|
371
|
+
mockProcess.stdout.emit('data', '● [11:28:41] Finished abc123\n');
|
|
372
|
+
mockProcess.stdout.emit('data', 'Completed successfully: Forge done\n');
|
|
373
|
+
mockProcess.emit('close', 0);
|
|
374
|
+
}, 10);
|
|
375
|
+
const result = await peekPromise;
|
|
376
|
+
const response = JSON.parse(result.content[0].text);
|
|
377
|
+
expect(response.processes).toHaveLength(1);
|
|
378
|
+
expect(response.processes[0]).toMatchObject({
|
|
379
|
+
pid: 12349,
|
|
380
|
+
agent: 'forge',
|
|
381
|
+
status: 'completed',
|
|
382
|
+
events: [
|
|
383
|
+
{
|
|
384
|
+
kind: 'message',
|
|
385
|
+
ts: expect.any(String),
|
|
386
|
+
text: 'Forge started',
|
|
387
|
+
},
|
|
388
|
+
{
|
|
389
|
+
kind: 'tool_call',
|
|
390
|
+
phase: 'started',
|
|
391
|
+
id: 'forge_0',
|
|
392
|
+
tool: '/bin/zsh',
|
|
393
|
+
summary: "/bin/sh -c 'echo hi'",
|
|
394
|
+
},
|
|
395
|
+
{
|
|
396
|
+
kind: 'tool_call',
|
|
397
|
+
phase: 'completed',
|
|
398
|
+
id: 'forge_0',
|
|
399
|
+
tool: '/bin/zsh',
|
|
400
|
+
summary: "/bin/sh -c 'echo hi'",
|
|
401
|
+
status: 'unknown',
|
|
402
|
+
},
|
|
403
|
+
{
|
|
404
|
+
kind: 'message',
|
|
405
|
+
ts: expect.any(String),
|
|
406
|
+
text: 'Forge done',
|
|
407
|
+
},
|
|
408
|
+
],
|
|
409
|
+
truncated: false,
|
|
410
|
+
error: null,
|
|
411
|
+
});
|
|
412
|
+
expect(JSON.stringify(response)).not.toContain('secret child output');
|
|
413
|
+
expect(JSON.stringify(response)).not.toContain('stderr should be ignored');
|
|
414
|
+
});
|
|
263
415
|
it('should handle process with model parameter', async () => {
|
|
264
416
|
const { handlers } = await setupServer();
|
|
265
417
|
const mockProcess = new EventEmitter();
|
package/dist/app/cli.js
CHANGED
|
@@ -8,7 +8,7 @@ export const CLI_HELP_TEXT = `Usage: ai-cli <command> [options]
|
|
|
8
8
|
Commands:
|
|
9
9
|
run Start an AI CLI process in the background
|
|
10
10
|
wait Wait for one or more pids
|
|
11
|
-
peek Observe new
|
|
11
|
+
peek Observe new agent events for a short window
|
|
12
12
|
ps List tracked processes
|
|
13
13
|
result Get the current result for a pid
|
|
14
14
|
kill Terminate a tracked pid
|
|
@@ -57,12 +57,13 @@ Options:
|
|
|
57
57
|
`;
|
|
58
58
|
export const PEEK_HELP_TEXT = `Usage: ai-cli peek <pid...> [options]
|
|
59
59
|
|
|
60
|
-
Observe new natural-language agent messages for a short one-shot window.
|
|
61
|
-
In v1, message extraction is supported for Codex, Claude, OpenCode, and
|
|
60
|
+
Observe new natural-language agent messages, and optionally tool calls, for a short one-shot window.
|
|
61
|
+
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.
|
|
62
62
|
This is not a history API, gapless streaming, or stdout/stderr tailing. No --follow mode is available in v1.
|
|
63
63
|
|
|
64
64
|
Options:
|
|
65
65
|
--time <seconds> Observation window in seconds. Defaults to 10, maximum 60
|
|
66
|
+
--include-tool-calls Include normalized tool_call events without raw tool output
|
|
66
67
|
--help, -h Show this help message
|
|
67
68
|
`;
|
|
68
69
|
export const KILL_HELP_TEXT = `Usage: ai-cli kill <pid>
|
|
@@ -119,7 +120,7 @@ const defaultDeps = {
|
|
|
119
120
|
listProcesses: () => getCliProcessService().listProcesses(),
|
|
120
121
|
getProcessResult: (pid, verbose) => getCliProcessService().getProcessResult(pid, verbose),
|
|
121
122
|
waitForProcesses: (pids, timeoutSeconds, verbose) => getCliProcessService().waitForProcesses(pids, timeoutSeconds, verbose),
|
|
122
|
-
peekProcesses: (pids, peekTimeSec) => getCliProcessService().peekProcesses(pids, peekTimeSec),
|
|
123
|
+
peekProcesses: (pids, peekTimeSec, includeToolCalls) => getCliProcessService().peekProcesses(pids, peekTimeSec, includeToolCalls),
|
|
123
124
|
killProcess: (pid) => getCliProcessService().killProcess(pid),
|
|
124
125
|
cleanupProcesses: () => getCliProcessService().cleanupProcesses(),
|
|
125
126
|
getDoctorStatus: () => getCliDoctorStatus(),
|
|
@@ -297,7 +298,7 @@ export async function runCli(argv, deps = {}) {
|
|
|
297
298
|
stdout(CLI_HELP_TEXT);
|
|
298
299
|
return 1;
|
|
299
300
|
}
|
|
300
|
-
writeJson(stdout, await peekProcesses(pids, peekTimeSec));
|
|
301
|
+
writeJson(stdout, await peekProcesses(pids, peekTimeSec, 'include-tool-calls' in flags || 'include_tool_calls' in flags));
|
|
301
302
|
return 0;
|
|
302
303
|
}
|
|
303
304
|
if (command === 'kill') {
|
package/dist/app/mcp.js
CHANGED
|
@@ -211,7 +211,7 @@ ${getSupportedModelsDescription()}
|
|
|
211
211
|
},
|
|
212
212
|
{
|
|
213
213
|
name: 'peek',
|
|
214
|
-
description: 'One-shot short observation window for running child agents. Returns only natural-language
|
|
214
|
+
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.',
|
|
215
215
|
inputSchema: {
|
|
216
216
|
type: 'object',
|
|
217
217
|
properties: {
|
|
@@ -224,6 +224,10 @@ ${getSupportedModelsDescription()}
|
|
|
224
224
|
type: 'number',
|
|
225
225
|
description: 'Optional positive integer observation window in seconds. Defaults to 10; maximum is 60.',
|
|
226
226
|
},
|
|
227
|
+
include_tool_calls: {
|
|
228
|
+
type: 'boolean',
|
|
229
|
+
description: 'Optional: include normalized tool_call events without raw tool output. Defaults to false.',
|
|
230
|
+
},
|
|
227
231
|
},
|
|
228
232
|
required: ['pids'],
|
|
229
233
|
},
|
|
@@ -351,15 +355,20 @@ ${getSupportedModelsDescription()}
|
|
|
351
355
|
async handlePeek(toolArguments) {
|
|
352
356
|
let pids;
|
|
353
357
|
let peekTimeSec;
|
|
358
|
+
let includeToolCalls;
|
|
354
359
|
try {
|
|
355
360
|
pids = validatePeekPids(toolArguments.pids);
|
|
356
361
|
peekTimeSec = validatePeekTimeSec(toolArguments.peek_time_sec);
|
|
362
|
+
if (toolArguments.include_tool_calls !== undefined && typeof toolArguments.include_tool_calls !== 'boolean') {
|
|
363
|
+
throw new Error('include_tool_calls must be a boolean when provided');
|
|
364
|
+
}
|
|
365
|
+
includeToolCalls = toolArguments.include_tool_calls === true;
|
|
357
366
|
}
|
|
358
367
|
catch (error) {
|
|
359
368
|
throw new McpError(ErrorCode.InvalidParams, error.message);
|
|
360
369
|
}
|
|
361
370
|
try {
|
|
362
|
-
const response = await this.processService.peekProcesses(pids, peekTimeSec);
|
|
371
|
+
const response = await this.processService.peekProcesses(pids, peekTimeSec, includeToolCalls);
|
|
363
372
|
return {
|
|
364
373
|
content: [{
|
|
365
374
|
type: 'text',
|
|
@@ -4,9 +4,9 @@ import { join, basename, dirname } from 'node:path';
|
|
|
4
4
|
import { homedir } from 'node:os';
|
|
5
5
|
import { buildCliCommand } from './cli-builder.js';
|
|
6
6
|
import { findClaudeCli, findCodexCli, findForgeCli, findGeminiCli, findOpencodeCli } from './cli-utils.js';
|
|
7
|
-
import { parseClaudeOutput, parseCodexOutput, parseForgeOutput, parseGeminiOutput, parseOpenCodeOutput,
|
|
7
|
+
import { parseClaudeOutput, parseCodexOutput, parseForgeOutput, parseGeminiOutput, parseOpenCodeOutput, PeekEventExtractor } from './parsers.js';
|
|
8
8
|
import { buildProcessResult } from './process-result.js';
|
|
9
|
-
import {
|
|
9
|
+
import { appendPeekEvents, buildNotFoundPeekProcess, observedDurationSec, validatePeekPids, validatePeekTimeSec, } from './peek.js';
|
|
10
10
|
function resolveDefaultStateDir() {
|
|
11
11
|
return process.env.AI_CLI_STATE_DIR || join(homedir(), '.local', 'state', 'ai-cli');
|
|
12
12
|
}
|
|
@@ -175,7 +175,7 @@ export class CliProcessService {
|
|
|
175
175
|
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
176
176
|
}
|
|
177
177
|
}
|
|
178
|
-
async peekProcesses(pids, peekTimeSec = 10) {
|
|
178
|
+
async peekProcesses(pids, peekTimeSec = 10, includeToolCalls = false) {
|
|
179
179
|
const targetPids = validatePeekPids(pids);
|
|
180
180
|
const targetPeekTimeSec = validatePeekTimeSec(peekTimeSec);
|
|
181
181
|
const processes = [];
|
|
@@ -193,7 +193,7 @@ export class CliProcessService {
|
|
|
193
193
|
pid,
|
|
194
194
|
agent: process.toolType,
|
|
195
195
|
status: process.status,
|
|
196
|
-
|
|
196
|
+
events: [],
|
|
197
197
|
truncated: false,
|
|
198
198
|
error: null,
|
|
199
199
|
};
|
|
@@ -201,8 +201,8 @@ export class CliProcessService {
|
|
|
201
201
|
observers.push({
|
|
202
202
|
process,
|
|
203
203
|
result,
|
|
204
|
-
stdoutExtractor: new
|
|
205
|
-
stderrExtractor: new
|
|
204
|
+
stdoutExtractor: new PeekEventExtractor(process.toolType, { includeToolCalls, source: 'stdout' }),
|
|
205
|
+
stderrExtractor: new PeekEventExtractor(process.toolType, { includeToolCalls, source: 'stderr' }),
|
|
206
206
|
stdoutOffset: this.fileSizeSafe(process.stdoutPath),
|
|
207
207
|
stderrOffset: this.fileSizeSafe(process.stderrPath),
|
|
208
208
|
});
|
|
@@ -216,10 +216,10 @@ export class CliProcessService {
|
|
|
216
216
|
for (const observer of observers) {
|
|
217
217
|
const stdoutRead = this.readTextFromOffset(observer.process.stdoutPath, observer.stdoutOffset);
|
|
218
218
|
observer.stdoutOffset = stdoutRead.offset;
|
|
219
|
-
|
|
219
|
+
appendPeekEvents(observer.result, observer.stdoutExtractor.push(stdoutRead.text, observedAt));
|
|
220
220
|
const stderrRead = this.readTextFromOffset(observer.process.stderrPath, observer.stderrOffset);
|
|
221
221
|
observer.stderrOffset = stderrRead.offset;
|
|
222
|
-
|
|
222
|
+
appendPeekEvents(observer.result, observer.stderrExtractor.push(stderrRead.text, observedAt));
|
|
223
223
|
observer.process = this.refreshStatus(this.readProcess(observer.process.pid));
|
|
224
224
|
observer.result.status = observer.process.status;
|
|
225
225
|
if (observer.process.status === 'running') {
|
|
@@ -239,8 +239,9 @@ export class CliProcessService {
|
|
|
239
239
|
for (const observer of observers) {
|
|
240
240
|
observer.process = this.refreshStatus(this.readProcess(observer.process.pid));
|
|
241
241
|
observer.result.status = observer.process.status;
|
|
242
|
-
|
|
243
|
-
|
|
242
|
+
const terminal = observer.process.status !== 'running';
|
|
243
|
+
appendPeekEvents(observer.result, observer.stdoutExtractor.flush(flushTs, { terminal }));
|
|
244
|
+
appendPeekEvents(observer.result, observer.stderrExtractor.flush(flushTs, { terminal }));
|
|
244
245
|
}
|
|
245
246
|
return {
|
|
246
247
|
peek_started_at: startedAt.toISOString(),
|