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 CHANGED
@@ -1,3 +1,10 @@
1
+ # [2.18.0](https://github.com/mkXultra/ai-cli-mcp/compare/v2.17.0...v2.18.0) (2026-04-12)
2
+
3
+
4
+ ### Features
5
+
6
+ * peekコマンドにtool_callイベントサポートを追加 ([c05b916](https://github.com/mkXultra/ai-cli-mcp/commit/c05b91677019714077d0803c4169a0b5205ff25f))
7
+
1
8
  # [2.17.0](https://github.com/mkXultra/ai-cli-mcp/compare/v2.16.0...v2.17.0) (2026-04-11)
2
9
 
3
10
 
package/README.ja.md CHANGED
@@ -278,26 +278,29 @@ Claude CLI、Codex CLI、Gemini CLI、Forge CLI、または OpenCode を使用
278
278
 
279
279
  ### `peek`
280
280
 
281
- 実行中の子エージェントを短時間だけ観測し、その `peek` 呼び出しの観測ウィンドウ内で ai-cli-mcp が受理した自然言語メッセージだけを返します。履歴APIではなく、欠落のないストリーミングでもなく、シェルの `stdout` / `stderr` tail でもありません。別々の `peek` 呼び出しの間に出たメッセージは取得できない場合があります。v1 では `--follow` はありません。
281
+ 実行中の子エージェントを短時間だけ観測し、その `peek` 呼び出しの観測ウィンドウ内で ai-cli-mcp が受理した構造化イベントを返します。デフォルトでは自然言語メッセージイベントだけを返し、`include_tool_calls` または `--include-tool-calls` を指定すると正規化された tool-call イベントも含めます。履歴APIではなく、欠落のないストリーミングでもなく、シェルの `stdout` / `stderr` tail でもありません。別々の `peek` 呼び出しの間に出たイベントは取得できない場合があります。v1 では `--follow` はありません。
282
282
 
283
283
  CLI v1:
284
284
 
285
285
  ```bash
286
286
  ai-cli peek 123 --time 10
287
287
  ai-cli peek 123 456 --time 10
288
+ ai-cli peek 123 --time 10 --include-tool-calls
288
289
  ```
289
290
 
290
291
  **引数:**
291
292
  - `pids` (array of numbers, 必須): `run` が返したプロセスIDを 1..32 件指定します。重複したPIDはサーバー側で重複排除され、最初に出た順序が維持されます。未知または管理外のPIDは、呼び出し全体の失敗ではなく、プロセスごとに `not_found` として返されます。
292
293
  - `peek_time_sec` (number, 任意): 観測時間(秒)の正の整数です。デフォルトは10秒、最大60秒です。`0`、負数、小数は無効です。
294
+ - `include_tool_calls` (boolean, 任意): `true` の場合、各プロセスの `events` 配列にメッセージイベントに加えて正規化された `tool_call` イベントを含めます。デフォルトは `false` です。
293
295
 
294
296
  **観測とフィルタリング:**
295
- - `peek_started_at` と `messages[].ts` は、ai-cli-mcp サーバー側の UTC RFC3339 タイムスタンプです。`peek_started_at` は検証とリスナー登録後に観測ウィンドウが始まった時刻、`messages[].ts` は ai-cli-mcp がメッセージを観測して受理した時刻です。
297
+ - `peek_started_at` と `events[].ts` は、ai-cli-mcp サーバー側の UTC RFC3339 タイムスタンプです。`peek_started_at` は検証とリスナー登録後に観測ウィンドウが始まった時刻、`events[].ts` は ai-cli-mcp がイベントを観測して受理した時刻です。
296
298
  - 観測ウィンドウは `peek_time_sec` が経過するか、対象プロセスがすべて終端状態になった時点で終了します。
297
- - 観測開始前のメッセージは返しません。同じPIDへの同時 `peek` は可能で、それぞれ独立した観測ウィンドウを持つため、メッセージが重複して返ることがあります。
298
- - 返すのは認識済みの自然言語メッセージだけです。Codex の `agent_message` text、Claude assistant の text content、OpenCode の `type: "text"` かつ `part.type` が `"text"` のイベント、Gemini stream-json の `role` が `"assistant"` の `message` イベントを含めます。raw `stdout` / `stderr`、raw JSONL、reasoning、`tool_use`、`tool_result`、コマンドの `stdout` / `stderr`、command execution メタデータ、token usage、verbose メタデータは除外します。
299
- - 未知のイベント形状はデフォルトで拒否します。Forge など、自然言語抽出がまだ明示対応されていない管理対象エージェントは、実際のプロセス状態を返しつつ、`messages: []`、`truncated: false`、`error: null` にします。
300
- - 各PIDごとに、観測ウィンドウ内で最初に観測された50件までを保持します。それ以降のメッセージを捨てた場合は `truncated` `true` になります。
299
+ - 観測開始前のイベントは返しません。同じPIDへの同時 `peek` は可能で、それぞれ独立した観測ウィンドウを持つため、イベントが重複して返ることがあります。
300
+ - メッセージイベントは、Codex の `agent_message` text、Claude assistant の text content、OpenCode の `type: "text"` かつ `part.type` が `"text"` のイベント、Gemini stream-json の `role` が `"assistant"` の `message` イベントから認識します。
301
+ - tool call を含める場合、Codex command/MCP call、Claude の tool use/result、Gemini の tool use/result、OpenCode の完了済み tool use event を正規化した `tool_call` イベントとして返します。tool summary は tool 名と入力メタデータだけから作る短い1行文字列です。raw `stdout` / `stderr`、raw JSONL、tool result output、コマンド出力、`result.response`、stats、token usage、verbose メタデータは除外します。
302
+ - 未知のイベント形状はデフォルトで拒否します。Forge など、まだ明示対応されていない管理対象エージェントは、実際のプロセス状態を返しつつ、`events: []`、`truncated: false`、`error: null` にします。
303
+ - 各PIDごとに、観測ウィンドウ内で最初に観測された50件までを保持します。それ以降のイベントを捨てた場合は `truncated` が `true` になります。
301
304
  - `status` は `running`、`completed`、`failed`、`not_found` のいずれかで、観測ウィンドウ終了時点の状態を表します。
302
305
  - `agent` は `claude`、`codex`、`gemini`、`forge`、`opencode`、将来追加される追跡済みエージェント文字列、または `null` です。`null` はプロセスが見つからない、またはエージェント種別を判断できない場合を表します。
303
306
 
@@ -312,8 +315,9 @@ ai-cli peek 123 456 --time 10
312
315
  "pid": 123,
313
316
  "agent": "codex",
314
317
  "status": "running",
315
- "messages": [
316
- { "ts": "2026-04-11T12:34:59.120Z", "text": "I'm checking the implementation." }
318
+ "events": [
319
+ { "kind": "message", "ts": "2026-04-11T12:34:59.120Z", "text": "I'm checking the implementation." },
320
+ { "kind": "tool_call", "ts": "2026-04-11T12:35:00.000Z", "phase": "started", "id": "item_0", "tool": "command_execution", "summary": "/bin/sh -c 'echo hi'" }
317
321
  ],
318
322
  "truncated": false,
319
323
  "error": null
@@ -322,7 +326,7 @@ ai-cli peek 123 456 --time 10
322
326
  "pid": 999,
323
327
  "agent": null,
324
328
  "status": "not_found",
325
- "messages": [],
329
+ "events": [],
326
330
  "truncated": false,
327
331
  "error": "process not found"
328
332
  }
package/README.md CHANGED
@@ -275,26 +275,29 @@ By default, each returned result item uses the compact shape shared with `get_re
275
275
 
276
276
  ### `peek`
277
277
 
278
- Starts a one-shot short observation window for running child agents and returns only natural-language agent messages observed during that specific call. It is not a history API, not gapless streaming, and not shell stdout/stderr tailing. Separate `peek` calls may miss messages emitted between calls; `--follow` is intentionally not part of v1.
278
+ Starts a one-shot short observation window for running child agents and returns structured events observed during that specific call. By default this includes only natural-language message events; pass `include_tool_calls` or `--include-tool-calls` to also include normalized tool-call events. It is not a history API, not gapless streaming, and not shell stdout/stderr tailing. Separate `peek` calls may miss events emitted between calls; `--follow` is intentionally not part of v1.
279
279
 
280
280
  CLI v1:
281
281
 
282
282
  ```bash
283
283
  ai-cli peek 123 --time 10
284
284
  ai-cli peek 123 456 --time 10
285
+ ai-cli peek 123 --time 10 --include-tool-calls
285
286
  ```
286
287
 
287
288
  **Arguments:**
288
289
  - `pids` (array of numbers, required): 1..32 process IDs returned by `run`. Duplicate PIDs are deduplicated server-side, preserving first occurrence order. Unknown or unmanaged PIDs are returned per process as `not_found`, not as a whole-call failure.
289
290
  - `peek_time_sec` (number, optional): Positive integer observation length in seconds. Defaults to 10 and is capped at 60. `0`, negative values, and fractional values are invalid.
291
+ - `include_tool_calls` (boolean, optional): When `true`, each process `events` array includes normalized `tool_call` events in addition to message events. Defaults to `false`.
290
292
 
291
293
  **Observation and filtering:**
292
- - `peek_started_at` and `messages[].ts` are ai-cli-mcp server-side UTC RFC3339 timestamps. `peek_started_at` is when the observation window starts after validation and listener registration; `messages[].ts` is when ai-cli-mcp observed and accepted the message.
294
+ - `peek_started_at` and `events[].ts` are ai-cli-mcp server-side UTC RFC3339 timestamps. `peek_started_at` is when the observation window starts after validation and listener registration; `events[].ts` is when ai-cli-mcp observed and accepted the event.
293
295
  - The window ends when `peek_time_sec` elapses or all target processes reach a terminal state, whichever comes first.
294
- - Messages emitted before the window starts are not returned. Concurrent `peek` calls for the same PID are allowed; each has an independent window and may return overlapping messages.
295
- - Only recognized natural-language agent messages are returned: Codex `agent_message` text, Claude assistant text content, OpenCode `type: "text"` events where `part.type` is `"text"`, and Gemini stream-json `message` events where `role` is `"assistant"`. Raw stdout/stderr, raw JSONL, reasoning, `tool_use`, `tool_result`, command stdout/stderr, command execution metadata, token usage, and verbose metadata are excluded.
296
- - Unknown event shapes are denied by default. Managed agents without supported natural-language extraction, such as Forge until explicitly supported, return their real process status with `messages: []`, `truncated: false`, and `error: null`.
297
- - Each PID keeps the first 50 messages observed in the window. If later messages are dropped, `truncated` is `true`.
296
+ - Events emitted before the window starts are not returned. Concurrent `peek` calls for the same PID are allowed; each has an independent window and may return overlapping events.
297
+ - Message events are recognized from Codex `agent_message` text, Claude assistant text content, OpenCode `type: "text"` events where `part.type` is `"text"`, and Gemini stream-json `message` events where `role` is `"assistant"`.
298
+ - When tool calls are included, `tool_call` events are normalized for Codex command/MCP calls, Claude tool use/results, Gemini tool use/results, and OpenCode completed tool use events. Tool summaries are bounded one-line strings derived from tool names and input metadata only. Raw stdout/stderr, raw JSONL, tool result output, command output, `result.response`, stats, token usage, and verbose metadata are excluded.
299
+ - Unknown event shapes are denied by default. Managed agents without supported extraction, such as Forge until explicitly supported, return their real process status with `events: []`, `truncated: false`, and `error: null`.
300
+ - Each PID keeps the first 50 events observed in the window. If later events are dropped, `truncated` is `true`.
298
301
  - `status` is one of `running`, `completed`, `failed`, or `not_found`, and reflects state when the observation window closes.
299
302
  - `agent` is `claude`, `codex`, `gemini`, `forge`, `opencode`, a future tracked string value, or `null` when the process is not found or the agent cannot be determined.
300
303
 
@@ -309,8 +312,9 @@ Example response:
309
312
  "pid": 123,
310
313
  "agent": "codex",
311
314
  "status": "running",
312
- "messages": [
313
- { "ts": "2026-04-11T12:34:59.120Z", "text": "I'm checking the implementation." }
315
+ "events": [
316
+ { "kind": "message", "ts": "2026-04-11T12:34:59.120Z", "text": "I'm checking the implementation." },
317
+ { "kind": "tool_call", "ts": "2026-04-11T12:35:00.000Z", "phase": "started", "id": "item_0", "tool": "command_execution", "summary": "/bin/sh -c 'echo hi'" }
314
318
  ],
315
319
  "truncated": false,
316
320
  "error": null
@@ -319,7 +323,7 @@ Example response:
319
323
  "pid": 999,
320
324
  "agent": null,
321
325
  "status": "not_found",
322
- "messages": [],
326
+ "events": [],
323
327
  "truncated": false,
324
328
  "error": "process not found"
325
329
  }
@@ -159,13 +159,13 @@ describe('ai-cli app', () => {
159
159
  observed_duration_sec: 0.01,
160
160
  processes: [],
161
161
  });
162
- const exitCode = await runCli(['peek', '123', '456', '123', '--time', '5'], {
162
+ const exitCode = await runCli(['peek', '123', '456', '123', '--time', '5', '--include-tool-calls'], {
163
163
  stdout,
164
164
  stderr,
165
165
  peekProcesses,
166
166
  });
167
167
  expect(exitCode).toBe(0);
168
- expect(peekProcesses).toHaveBeenCalledWith([123, 456], 5);
168
+ expect(peekProcesses).toHaveBeenCalledWith([123, 456], 5, true);
169
169
  expect(stdout).toHaveBeenCalledWith(expect.stringContaining('"peek_started_at"'));
170
170
  expect(stderr).not.toHaveBeenCalled();
171
171
  });
@@ -179,7 +179,7 @@ describe('ai-cli app', () => {
179
179
  });
180
180
  const defaultExitCode = await runCli(['peek', '123'], { stdout, stderr, peekProcesses });
181
181
  expect(defaultExitCode).toBe(0);
182
- expect(peekProcesses).toHaveBeenCalledWith([123], 10);
182
+ expect(peekProcesses).toHaveBeenCalledWith([123], 10, false);
183
183
  const followExitCode = await runCli(['peek', '123', '--follow'], { stdout, stderr, peekProcesses });
184
184
  expect(followExitCode).toBe(1);
185
185
  expect(stderr).toHaveBeenCalledWith('peek does not support --follow in v1\n');
@@ -148,8 +148,9 @@ printf '%s\n' '{"type":"user","message":{"content":[{"type":"tool_result","tool_
148
148
  pid: runResult.pid,
149
149
  agent: 'claude',
150
150
  status: 'completed',
151
- messages: [
151
+ events: [
152
152
  {
153
+ kind: 'message',
153
154
  ts: expect.any(String),
154
155
  text: 'new cli message',
155
156
  },
@@ -161,7 +162,7 @@ printf '%s\n' '{"type":"user","message":{"content":[{"type":"tool_result","tool_
161
162
  pid: 999999,
162
163
  agent: null,
163
164
  status: 'not_found',
164
- messages: [],
165
+ events: [],
165
166
  truncated: false,
166
167
  error: 'process not found',
167
168
  });
@@ -119,6 +119,7 @@ describe('MCP Contract Tests', () => {
119
119
  const peekTool = tools.find((tool) => tool.name === 'peek');
120
120
  expect(peekTool.inputSchema.required).toEqual(['pids']);
121
121
  expect(Object.keys(peekTool.inputSchema.properties).sort()).toEqual([
122
+ 'include_tool_calls',
122
123
  'peek_time_sec',
123
124
  'pids',
124
125
  ]);
@@ -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
  describe('parseCodexOutput', () => {
4
4
  it('should parse basic Codex output with message and session_id', () => {
5
5
  const output = `
@@ -165,6 +165,149 @@ describe('PeekMessageExtractor', () => {
165
165
  expect(extractor.flush(ts)).toEqual([{ ts, text: 'pending' }]);
166
166
  });
167
167
  });
168
+ describe('PeekEventExtractor', () => {
169
+ const ts = '2026-04-12T02:10:00.000Z';
170
+ it('emits only message events when include_tool_calls is false', () => {
171
+ const extractor = new PeekEventExtractor('codex', { includeToolCalls: false });
172
+ const output = [
173
+ '{"type":"item.started","item":{"id":"item_0","type":"command_execution","command":"echo secret","status":"in_progress"}}',
174
+ '{"type":"item.completed","item":{"id":"item_0","type":"command_execution","command":"echo secret","aggregated_output":"secret output\\n","exit_code":0,"status":"completed"}}',
175
+ '{"type":"item.completed","item":{"id":"item_1","type":"agent_message","text":"Visible Codex message"}}',
176
+ ].join('\n') + '\n';
177
+ expect(extractor.push(output, ts)).toEqual([
178
+ { kind: 'message', ts, text: 'Visible Codex message' },
179
+ ]);
180
+ });
181
+ it('emits Codex command and MCP tool_call events without raw output when include_tool_calls is true', () => {
182
+ const extractor = new PeekEventExtractor('codex', { includeToolCalls: true });
183
+ const output = [
184
+ '{"type":"item.started","item":{"id":"cmd_0","type":"command_execution","command":"/bin/sh -c \\"echo secret\\"","status":"in_progress"}}',
185
+ '{"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"}}',
186
+ '{"type":"item.started","item":{"id":"mcp_0","type":"mcp_tool_call","server":"acm","tool":"list_processes","arguments":{},"status":"in_progress"}}',
187
+ '{"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"}}',
188
+ ].join('\n') + '\n';
189
+ expect(extractor.push(output, ts)).toEqual([
190
+ {
191
+ kind: 'tool_call',
192
+ ts,
193
+ phase: 'started',
194
+ id: 'cmd_0',
195
+ tool: 'command_execution',
196
+ summary: '/bin/sh -c "echo secret"',
197
+ },
198
+ {
199
+ kind: 'tool_call',
200
+ ts,
201
+ phase: 'completed',
202
+ id: 'cmd_0',
203
+ tool: 'command_execution',
204
+ summary: '/bin/sh -c "echo secret"',
205
+ status: 'success',
206
+ exit_code: 0,
207
+ },
208
+ {
209
+ kind: 'tool_call',
210
+ ts,
211
+ phase: 'started',
212
+ id: 'mcp_0',
213
+ tool: 'list_processes',
214
+ server: 'acm',
215
+ summary: 'acm.list_processes',
216
+ },
217
+ {
218
+ kind: 'tool_call',
219
+ ts,
220
+ phase: 'completed',
221
+ id: 'mcp_0',
222
+ tool: 'list_processes',
223
+ server: 'acm',
224
+ summary: 'acm.list_processes',
225
+ status: 'success',
226
+ },
227
+ ]);
228
+ });
229
+ it('emits Claude MCP tool_call events paired by id', () => {
230
+ const extractor = new PeekEventExtractor('claude', { includeToolCalls: true });
231
+ const output = [
232
+ '{"type":"assistant","message":{"content":[{"type":"tool_use","id":"toolu_1","name":"mcp__acm__list_processes","input":{}}]}}',
233
+ '{"type":"user","message":{"content":[{"tool_use_id":"toolu_1","type":"tool_result","content":[{"type":"text","text":"secret result"}]}]}}',
234
+ '{"type":"assistant","message":{"content":[{"type":"text","text":"Done."}]}}',
235
+ ].join('\n') + '\n';
236
+ expect(extractor.push(output, ts)).toEqual([
237
+ {
238
+ kind: 'tool_call',
239
+ ts,
240
+ phase: 'started',
241
+ id: 'toolu_1',
242
+ tool: 'mcp__acm__list_processes',
243
+ server: 'acm',
244
+ summary: 'acm.list_processes',
245
+ },
246
+ {
247
+ kind: 'tool_call',
248
+ ts,
249
+ phase: 'completed',
250
+ id: 'toolu_1',
251
+ tool: 'mcp__acm__list_processes',
252
+ server: 'acm',
253
+ summary: 'acm.list_processes',
254
+ status: 'success',
255
+ },
256
+ { kind: 'message', ts, text: 'Done.' },
257
+ ]);
258
+ });
259
+ it('emits Gemini MCP tool_call events and joined assistant message events', () => {
260
+ const extractor = new PeekEventExtractor('gemini', { includeToolCalls: true });
261
+ const output = [
262
+ '{"type":"tool_use","timestamp":"2026-04-12T02:56:29.992Z","tool_name":"mcp_acm_list_processes","tool_id":"mcp_1","parameters":{}}',
263
+ '{"type":"tool_result","timestamp":"2026-04-12T02:56:30.059Z","tool_id":"mcp_1","status":"success","output":"secret result"}',
264
+ '{"type":"message","timestamp":"2026-04-12T02:56:32.855Z","role":"assistant","content":"The tool ","delta":true}',
265
+ '{"type":"message","timestamp":"2026-04-12T02:56:32.902Z","role":"assistant","content":"succeeded.","delta":true}',
266
+ '{"type":"result","timestamp":"2026-04-12T02:56:32.954Z","status":"success","stats":{"tool_calls":1}}',
267
+ ].join('\n') + '\n';
268
+ expect(extractor.push(output, ts)).toEqual([
269
+ {
270
+ kind: 'tool_call',
271
+ ts,
272
+ phase: 'started',
273
+ id: 'mcp_1',
274
+ tool: 'mcp_acm_list_processes',
275
+ server: 'acm',
276
+ summary: 'acm.list_processes',
277
+ },
278
+ {
279
+ kind: 'tool_call',
280
+ ts,
281
+ phase: 'completed',
282
+ id: 'mcp_1',
283
+ tool: 'mcp_acm_list_processes',
284
+ server: 'acm',
285
+ summary: 'acm.list_processes',
286
+ status: 'success',
287
+ },
288
+ { kind: 'message', ts, text: 'The tool succeeded.' },
289
+ ]);
290
+ });
291
+ it('emits OpenCode completed MCP tool_call events from tool_use state', () => {
292
+ const extractor = new PeekEventExtractor('opencode', { includeToolCalls: true });
293
+ const output = [
294
+ '{"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}}}}',
295
+ ].join('\n') + '\n';
296
+ expect(extractor.push(output, ts)).toEqual([
297
+ {
298
+ kind: 'tool_call',
299
+ ts,
300
+ phase: 'completed',
301
+ id: 'call_1',
302
+ tool: 'acm_list_processes',
303
+ server: 'acm',
304
+ summary: 'acm.list_processes',
305
+ status: 'success',
306
+ duration_ms: 3,
307
+ },
308
+ ]);
309
+ });
310
+ });
168
311
  describe('parseGeminiOutput', () => {
169
312
  it('should parse legacy final JSON output', () => {
170
313
  const output = JSON.stringify({
@@ -1,5 +1,5 @@
1
1
  import { describe, expect, it } from 'vitest';
2
- import { appendPeekMessages, validatePeekPids, validatePeekTimeSec } from '../peek.js';
2
+ import { appendPeekEvents, validatePeekPids, validatePeekTimeSec } from '../peek.js';
3
3
  describe('peek helpers', () => {
4
4
  it('dedupes pids while preserving first occurrence order', () => {
5
5
  expect(validatePeekPids([3, 1, 3, 2, 1])).toEqual([3, 1, 2]);
@@ -14,22 +14,23 @@ describe('peek helpers', () => {
14
14
  expect(() => validatePeekTimeSec(1.5)).toThrow(/positive integer/);
15
15
  expect(() => validatePeekTimeSec(61)).toThrow(/positive integer/);
16
16
  });
17
- it('keeps the first 50 messages and marks truncation when later messages are dropped', () => {
17
+ it('keeps the first 50 events and marks truncation when later events are dropped', () => {
18
18
  const process = {
19
19
  pid: 123,
20
20
  agent: 'codex',
21
21
  status: 'running',
22
- messages: [],
22
+ events: [],
23
23
  truncated: false,
24
24
  error: null,
25
25
  };
26
- appendPeekMessages(process, Array.from({ length: 55 }, (_, index) => ({
26
+ appendPeekEvents(process, Array.from({ length: 55 }, (_, index) => ({
27
+ kind: 'message',
27
28
  ts: '2026-04-11T12:34:56.789Z',
28
29
  text: `message ${index}`,
29
30
  })));
30
- expect(process.messages).toHaveLength(50);
31
- expect(process.messages[0].text).toBe('message 0');
32
- expect(process.messages[49].text).toBe('message 49');
31
+ expect(process.events).toHaveLength(50);
32
+ expect(process.events[0]).toMatchObject({ kind: 'message', text: 'message 0' });
33
+ expect(process.events[49]).toMatchObject({ kind: 'message', text: 'message 49' });
33
34
  expect(process.truncated).toBe(true);
34
35
  });
35
36
  });
@@ -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,77 @@ 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
+ });
263
337
  it('should handle process with model parameter', async () => {
264
338
  const { handlers } = await setupServer();
265
339
  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, and Gemini; Forge returns status with events: [].
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, and Gemini; Forge returns status with events: []. 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',