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.
@@ -141,8 +141,9 @@ describe('Process Management Tests', () => {
141
141
  pid: 12345,
142
142
  agent: 'claude',
143
143
  status: 'completed',
144
- messages: [
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
- messages: [],
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
- messages: [
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
- messages: [
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 natural-language agent messages for a short window
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 Gemini; Forge returns status with messages: [].
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 agent messages 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 messages: [].',
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, PeekMessageExtractor } from './parsers.js';
7
+ import { parseClaudeOutput, parseCodexOutput, parseForgeOutput, parseGeminiOutput, parseOpenCodeOutput, PeekEventExtractor } from './parsers.js';
8
8
  import { buildProcessResult } from './process-result.js';
9
- import { appendPeekMessages, buildNotFoundPeekProcess, observedDurationSec, validatePeekPids, validatePeekTimeSec, } from './peek.js';
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
- messages: [],
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 PeekMessageExtractor(process.toolType),
205
- stderrExtractor: new PeekMessageExtractor(process.toolType),
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
- appendPeekMessages(observer.result, observer.stdoutExtractor.push(stdoutRead.text, observedAt));
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
- appendPeekMessages(observer.result, observer.stderrExtractor.push(stderrRead.text, observedAt));
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
- appendPeekMessages(observer.result, observer.stdoutExtractor.flush(flushTs));
243
- appendPeekMessages(observer.result, observer.stderrExtractor.flush(flushTs));
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(),