ai-cli-mcp 2.16.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 +14 -0
- package/README.ja.md +14 -10
- package/README.md +14 -10
- package/dist/__tests__/app-cli.test.js +3 -3
- package/dist/__tests__/cli-builder.test.js +2 -1
- 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 +144 -1
- package/dist/__tests__/peek.test.js +8 -7
- package/dist/__tests__/process-management.test.js +78 -4
- package/dist/app/cli.js +6 -5
- package/dist/app/mcp.js +11 -2
- package/dist/cli-builder.js +1 -1
- package/dist/cli-process-service.js +10 -10
- package/dist/parsers.js +282 -22
- package/dist/peek.js +8 -5
- package/dist/process-service.js +10 -10
- package/package.json +1 -1
- package/src/__tests__/app-cli.test.ts +3 -3
- package/src/__tests__/cli-builder.test.ts +2 -1
- 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 +155 -1
- package/src/__tests__/peek.test.ts +8 -7
- package/src/__tests__/process-management.test.ts +86 -4
- package/src/app/cli.ts +7 -6
- package/src/app/mcp.ts +11 -2
- package/src/cli-builder.ts +1 -1
- package/src/cli-process-service.ts +12 -12
- package/src/parsers.ts +371 -27
- package/src/peek.ts +14 -7
- package/src/process-service.ts +12 -12
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,17 @@
|
|
|
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
|
+
|
|
8
|
+
# [2.17.0](https://github.com/mkXultra/ai-cli-mcp/compare/v2.16.0...v2.17.0) (2026-04-11)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Features
|
|
12
|
+
|
|
13
|
+
* CodexのCLIフラグを--dangerously-bypass-approvals-and-sandboxに更新 ([5eb15f0](https://github.com/mkXultra/ai-cli-mcp/commit/5eb15f0138b54572c64ed11a1c7d44718afa65e0))
|
|
14
|
+
|
|
1
15
|
# [2.16.0](https://github.com/mkXultra/ai-cli-mcp/compare/v2.15.0...v2.16.0) (2026-04-11)
|
|
2
16
|
|
|
3
17
|
|
package/README.ja.md
CHANGED
|
@@ -18,7 +18,7 @@ Cursorなどのエディタが、複雑な手順を伴う編集や操作に苦
|
|
|
18
18
|
このMCPサーバーは、LLMがAI CLIツールと対話するためのツールを提供します。MCPクライアントと統合することで、LLMは以下のことが可能になります:
|
|
19
19
|
|
|
20
20
|
- すべての権限確認をスキップしてClaude CLIを実行(`--dangerously-skip-permissions` を使用)
|
|
21
|
-
-
|
|
21
|
+
- 承認とサンドボックスをバイパスしてCodex CLIを実行(`--dangerously-bypass-approvals-and-sandbox` を使用)
|
|
22
22
|
- 自動承認モードでGemini CLIを実行(`-y` を使用)
|
|
23
23
|
- Forge CLI を非対話モードで実行(`forge -C <workFolder> -p <prompt>` を使用)
|
|
24
24
|
- OpenCode を非対話 JSON モードで実行(`opencode run --format json --dir <workFolder> <prompt>` を使用)
|
|
@@ -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` イベントから認識します。
|
|
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
|
-
"
|
|
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
|
@@ -20,7 +20,7 @@ Did you notice that Cursor sometimes struggles with complex, multi-step edits or
|
|
|
20
20
|
This MCP server provides tools that can be used by LLMs to interact with AI CLI tools. When integrated with MCP clients, it allows LLMs to:
|
|
21
21
|
|
|
22
22
|
- Run Claude CLI with all permissions bypassed (using `--dangerously-skip-permissions`)
|
|
23
|
-
- Execute Codex CLI with
|
|
23
|
+
- Execute Codex CLI with approvals and sandbox bypassed (using `--dangerously-bypass-approvals-and-sandbox`)
|
|
24
24
|
- Execute Gemini CLI with automatic approval mode (using `-y`)
|
|
25
25
|
- Execute Forge CLI in non-interactive mode (using `forge -C <workFolder> -p <prompt>`)
|
|
26
26
|
- Execute OpenCode in non-interactive JSON mode (using `opencode run --format json --dir <workFolder> <prompt>`)
|
|
@@ -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"`, 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
|
-
"
|
|
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');
|
|
@@ -241,7 +241,8 @@ describe('cli-builder', () => {
|
|
|
241
241
|
expect(cmd.agent).toBe('codex');
|
|
242
242
|
expect(cmd.cliPath).toBe('/usr/bin/codex');
|
|
243
243
|
expect(cmd.args).toContain('exec');
|
|
244
|
-
expect(cmd.args).toContain('--
|
|
244
|
+
expect(cmd.args).toContain('--dangerously-bypass-approvals-and-sandbox');
|
|
245
|
+
expect(cmd.args).not.toContain('--full-auto');
|
|
245
246
|
expect(cmd.args).toContain('--json');
|
|
246
247
|
expect(cmd.args).toContain('--model');
|
|
247
248
|
expect(cmd.args).toContain('gpt-5.2-codex');
|
|
@@ -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,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 {
|
|
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
|
});
|
|
@@ -141,8 +141,9 @@ describe('Process Management Tests', () => {
|
|
|
141
141
|
pid: 12345,
|
|
142
142
|
agent: 'claude',
|
|
143
143
|
status: 'completed',
|
|
144
|
-
|
|
144
|
+
events: [
|
|
145
145
|
{
|
|
146
|
+
kind: 'message',
|
|
146
147
|
ts: expect.any(String),
|
|
147
148
|
text: 'new message',
|
|
148
149
|
},
|
|
@@ -154,7 +155,7 @@ describe('Process Management Tests', () => {
|
|
|
154
155
|
pid: 99999,
|
|
155
156
|
agent: null,
|
|
156
157
|
status: 'not_found',
|
|
157
|
-
|
|
158
|
+
events: [],
|
|
158
159
|
truncated: false,
|
|
159
160
|
error: 'process not found',
|
|
160
161
|
});
|
|
@@ -199,8 +200,9 @@ describe('Process Management Tests', () => {
|
|
|
199
200
|
pid: 12346,
|
|
200
201
|
agent: 'opencode',
|
|
201
202
|
status: 'completed',
|
|
202
|
-
|
|
203
|
+
events: [
|
|
203
204
|
{
|
|
205
|
+
kind: 'message',
|
|
204
206
|
ts: expect.any(String),
|
|
205
207
|
text: 'OpenCode visible text',
|
|
206
208
|
},
|
|
@@ -250,8 +252,9 @@ describe('Process Management Tests', () => {
|
|
|
250
252
|
pid: 12347,
|
|
251
253
|
agent: 'gemini',
|
|
252
254
|
status: 'completed',
|
|
253
|
-
|
|
255
|
+
events: [
|
|
254
256
|
{
|
|
257
|
+
kind: 'message',
|
|
255
258
|
ts: expect.any(String),
|
|
256
259
|
text: 'Visible Gemini text',
|
|
257
260
|
},
|
|
@@ -260,6 +263,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
|
|
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
|
|
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
|
|
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',
|
package/dist/cli-builder.js
CHANGED
|
@@ -155,7 +155,7 @@ export function buildCliCommand(options) {
|
|
|
155
155
|
if (resolvedModel) {
|
|
156
156
|
args.push('--model', resolvedModel);
|
|
157
157
|
}
|
|
158
|
-
args.push('--skip-git-repo-check', '--
|
|
158
|
+
args.push('--skip-git-repo-check', '--dangerously-bypass-approvals-and-sandbox', '--json', prompt);
|
|
159
159
|
}
|
|
160
160
|
else if (agent === 'gemini') {
|
|
161
161
|
cliPath = options.cliPaths.gemini;
|