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 CHANGED
@@ -1,3 +1,17 @@
1
+ # [2.19.0](https://github.com/mkXultra/ai-cli-mcp/compare/v2.18.0...v2.19.0) (2026-04-15)
2
+
3
+
4
+ ### Features
5
+
6
+ * peekコマンドにForgeエージェントのベストエフォートサポートを追加 ([7c01958](https://github.com/mkXultra/ai-cli-mcp/commit/7c01958b0c9a8133da07c556b303481abd511b6b))
7
+
8
+ # [2.18.0](https://github.com/mkXultra/ai-cli-mcp/compare/v2.17.0...v2.18.0) (2026-04-12)
9
+
10
+
11
+ ### Features
12
+
13
+ * peekコマンドにtool_callイベントサポートを追加 ([c05b916](https://github.com/mkXultra/ai-cli-mcp/commit/c05b91677019714077d0803c4169a0b5205ff25f))
14
+
1
15
  # [2.17.0](https://github.com/mkXultra/ai-cli-mcp/compare/v2.16.0...v2.17.0) (2026-04-11)
2
16
 
3
17
 
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` イベント、Forge `Summary:` または `Completed successfully:` で始まる plain-text 行から best-effort に認識します。
301
+ - tool call を含める場合、Codex の command/MCP call、Claude の tool use/result、Gemini の tool use/result、OpenCode の完了済み tool use event、Forge の低精度な `Execute` / `Finished` marker を正規化した `tool_call` イベントとして返します。tool summary は tool 名と入力メタデータだけから作る短い1行文字列です。Forge のコマンド出力自体は tail せず、公開しません。raw `stdout` / `stderr`、raw JSONL、tool result output、コマンド出力、`result.response`、stats、token usage、verbose メタデータは除外します。
302
+ - 未知のイベント形状はデフォルトで拒否します。まだ明示対応されていない管理対象エージェントは、実際のプロセス状態を返しつつ、`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"`, Gemini stream-json `message` events where `role` is `"assistant"`, and best-effort Forge plain-text lines beginning with `Summary:` or `Completed successfully:`.
298
+ - When tool calls are included, `tool_call` events are normalized for Codex command/MCP calls, Claude tool use/results, Gemini tool use/results, OpenCode completed tool use events, and low-precision Forge `Execute`/`Finished` markers. Tool summaries are bounded one-line strings derived from tool names and input metadata only. Forge command output itself is not tailed or exposed. 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 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,295 @@ 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
+ it('emits Forge message events from Summary and Completed successfully prefixes', () => {
311
+ const extractor = new PeekEventExtractor('forge');
312
+ const output = [
313
+ 'Summary: Forge finished the task',
314
+ 'Completed successfully: Built the project',
315
+ 'Summary: ',
316
+ ].join('\n') + '\n';
317
+ expect(extractor.push(output, ts)).toEqual([
318
+ { kind: 'message', ts, text: 'Forge finished the task' },
319
+ { kind: 'message', ts, text: 'Built the project' },
320
+ ]);
321
+ });
322
+ it('preserves long Forge Summary message text without truncation', () => {
323
+ const extractor = new PeekEventExtractor('forge');
324
+ const longText = 'x'.repeat(260);
325
+ expect(extractor.push(`Summary: ${longText}\n`, ts)).toEqual([
326
+ { kind: 'message', ts, text: longText },
327
+ ]);
328
+ });
329
+ it('emits Forge Execute tool_call starts when include_tool_calls is true', () => {
330
+ const extractor = new PeekEventExtractor('forge', { includeToolCalls: true });
331
+ expect(extractor.push("● [11:28:40] Execute [/bin/zsh] /bin/sh -c 'echo hi'\n", ts)).toEqual([
332
+ {
333
+ kind: 'tool_call',
334
+ ts,
335
+ phase: 'started',
336
+ id: 'forge_0',
337
+ tool: '/bin/zsh',
338
+ summary: "/bin/sh -c 'echo hi'",
339
+ },
340
+ ]);
341
+ });
342
+ it('falls back to shell for Forge Execute labels with spaces', () => {
343
+ const extractor = new PeekEventExtractor('forge', { includeToolCalls: true });
344
+ expect(extractor.push("● [11:28:40] Execute [local shell] /bin/sh -c 'echo hi'\n", ts)).toEqual([
345
+ {
346
+ kind: 'tool_call',
347
+ ts,
348
+ phase: 'started',
349
+ id: 'forge_0',
350
+ tool: 'shell',
351
+ summary: "/bin/sh -c 'echo hi'",
352
+ },
353
+ ]);
354
+ });
355
+ it('suppresses Forge tool_call events when include_tool_calls is false but keeps messages', () => {
356
+ const extractor = new PeekEventExtractor('forge', { includeToolCalls: false });
357
+ const output = [
358
+ "● [11:28:40] Execute [/bin/zsh] /bin/sh -c 'echo hi'",
359
+ 'Summary: done',
360
+ ].join('\n') + '\n';
361
+ expect(extractor.push(output, ts)).toEqual([
362
+ { kind: 'message', ts, text: 'done' },
363
+ ]);
364
+ });
365
+ it('completes a pending Forge tool_call only on anchored Finished markers', () => {
366
+ const extractor = new PeekEventExtractor('forge', { includeToolCalls: true });
367
+ const output = [
368
+ "● [11:28:40] Execute [/bin/zsh] /bin/sh -c 'echo hi'",
369
+ 'This line says Finished but is not a Forge marker',
370
+ '● [11:28:41] Finished abc123',
371
+ ].join('\n') + '\n';
372
+ expect(extractor.push(output, 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
+ kind: 'tool_call',
383
+ ts,
384
+ phase: 'completed',
385
+ id: 'forge_0',
386
+ tool: '/bin/zsh',
387
+ summary: "/bin/sh -c 'echo hi'",
388
+ status: 'unknown',
389
+ },
390
+ ]);
391
+ });
392
+ it('completes a pending Forge tool_call before starting a consecutive Execute marker', () => {
393
+ const extractor = new PeekEventExtractor('forge', { includeToolCalls: true });
394
+ const output = [
395
+ "● [11:28:40] Execute [/bin/zsh] /bin/sh -c 'echo one'",
396
+ "● [11:28:41] Execute [/bin/zsh] /bin/sh -c 'echo two'",
397
+ ].join('\n') + '\n';
398
+ expect(extractor.push(output, ts)).toEqual([
399
+ {
400
+ kind: 'tool_call',
401
+ ts,
402
+ phase: 'started',
403
+ id: 'forge_0',
404
+ tool: '/bin/zsh',
405
+ summary: "/bin/sh -c 'echo one'",
406
+ },
407
+ {
408
+ kind: 'tool_call',
409
+ ts,
410
+ phase: 'completed',
411
+ id: 'forge_0',
412
+ tool: '/bin/zsh',
413
+ summary: "/bin/sh -c 'echo one'",
414
+ status: 'unknown',
415
+ },
416
+ {
417
+ kind: 'tool_call',
418
+ ts,
419
+ phase: 'started',
420
+ id: 'forge_1',
421
+ tool: '/bin/zsh',
422
+ summary: "/bin/sh -c 'echo two'",
423
+ },
424
+ ]);
425
+ });
426
+ it('does not synthesize Forge completion on non-terminal flush', () => {
427
+ const extractor = new PeekEventExtractor('forge', { includeToolCalls: true });
428
+ expect(extractor.push("● [11:28:40] Execute [/bin/zsh] /bin/sh -c 'echo hi'\n", ts)).toHaveLength(1);
429
+ expect(extractor.flush(ts, { terminal: false })).toEqual([]);
430
+ });
431
+ it('synthesizes Forge completion with unknown status on terminal flush', () => {
432
+ const extractor = new PeekEventExtractor('forge', { includeToolCalls: true });
433
+ expect(extractor.push("● [11:28:40] Execute [/bin/zsh] /bin/sh -c 'echo hi'\n", ts)).toHaveLength(1);
434
+ expect(extractor.flush('2026-04-12T02:10:05.000Z', { terminal: true })).toEqual([
435
+ {
436
+ kind: 'tool_call',
437
+ ts: '2026-04-12T02:10:05.000Z',
438
+ phase: 'completed',
439
+ id: 'forge_0',
440
+ tool: '/bin/zsh',
441
+ summary: "/bin/sh -c 'echo hi'",
442
+ status: 'unknown',
443
+ },
444
+ ]);
445
+ });
446
+ it('treats Forge stderr as a no-op source', () => {
447
+ const extractor = new PeekEventExtractor('forge', { includeToolCalls: true, source: 'stderr' });
448
+ const output = [
449
+ 'Summary: hidden',
450
+ "● [11:28:40] Execute [/bin/zsh] /bin/sh -c 'echo hidden'",
451
+ '● [11:28:41] Finished',
452
+ ].join('\n') + '\n';
453
+ expect(extractor.push(output, ts)).toEqual([]);
454
+ expect(extractor.flush(ts, { terminal: true })).toEqual([]);
455
+ });
456
+ });
168
457
  describe('parseGeminiOutput', () => {
169
458
  it('should parse legacy final JSON output', () => {
170
459
  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
  });