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
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
|
|
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` と `
|
|
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
|
-
-
|
|
298
|
-
-
|
|
299
|
-
-
|
|
300
|
-
-
|
|
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
|
-
"
|
|
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
|
-
"
|
|
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
|
|
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 `
|
|
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
|
-
-
|
|
295
|
-
-
|
|
296
|
-
-
|
|
297
|
-
-
|
|
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
|
-
"
|
|
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
|
-
"
|
|
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
|
-
|
|
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
|
-
|
|
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 {
|
|
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
|
|
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
|
-
|
|
22
|
+
events: [],
|
|
23
23
|
truncated: false,
|
|
24
24
|
error: null,
|
|
25
25
|
};
|
|
26
|
-
|
|
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.
|
|
31
|
-
expect(process.
|
|
32
|
-
expect(process.
|
|
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
|
});
|