ai-cli-mcp 2.15.0 → 2.17.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 +59 -1
- package/README.md +59 -1
- package/dist/__tests__/app-cli.test.js +56 -1
- package/dist/__tests__/cli-builder.test.js +3 -2
- package/dist/__tests__/cli-process-service.test.js +59 -0
- package/dist/__tests__/e2e.test.js +2 -1
- package/dist/__tests__/mcp-contract.test.js +8 -0
- package/dist/__tests__/parsers.test.js +163 -1
- package/dist/__tests__/peek.test.js +35 -0
- package/dist/__tests__/process-management.test.js +159 -0
- package/dist/__tests__/server.test.js +4 -3
- package/dist/app/cli.js +43 -1
- package/dist/app/mcp.js +45 -0
- package/dist/cli-builder.js +2 -2
- package/dist/cli-process-service.js +104 -2
- package/dist/parsers.js +185 -2
- package/dist/peek.js +56 -0
- package/dist/process-service.js +81 -1
- package/package.json +1 -1
- package/src/__tests__/app-cli.test.ts +71 -0
- package/src/__tests__/cli-builder.test.ts +3 -2
- package/src/__tests__/cli-process-service.test.ts +68 -0
- package/src/__tests__/e2e.test.ts +2 -1
- package/src/__tests__/mcp-contract.test.ts +9 -0
- package/src/__tests__/parsers.test.ts +188 -1
- package/src/__tests__/peek.test.ts +43 -0
- package/src/__tests__/process-management.test.ts +184 -0
- package/src/__tests__/server.test.ts +4 -3
- package/src/app/cli.ts +48 -0
- package/src/app/mcp.ts +46 -0
- package/src/cli-builder.ts +2 -2
- package/src/cli-process-service.ts +134 -1
- package/src/parsers.ts +222 -2
- package/src/peek.ts +88 -0
- package/src/process-service.ts +107 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,17 @@
|
|
|
1
|
+
# [2.17.0](https://github.com/mkXultra/ai-cli-mcp/compare/v2.16.0...v2.17.0) (2026-04-11)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
### Features
|
|
5
|
+
|
|
6
|
+
* CodexのCLIフラグを--dangerously-bypass-approvals-and-sandboxに更新 ([5eb15f0](https://github.com/mkXultra/ai-cli-mcp/commit/5eb15f0138b54572c64ed11a1c7d44718afa65e0))
|
|
7
|
+
|
|
8
|
+
# [2.16.0](https://github.com/mkXultra/ai-cli-mcp/compare/v2.15.0...v2.16.0) (2026-04-11)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Features
|
|
12
|
+
|
|
13
|
+
* peekコマンドを追加 — 実行中エージェントの自然言語メッセージをワンショット観測 ([c12fd4c](https://github.com/mkXultra/ai-cli-mcp/commit/c12fd4cbe374a05b5223191e10fb2144b5d86bd0))
|
|
14
|
+
|
|
1
15
|
# [2.15.0](https://github.com/mkXultra/ai-cli-mcp/compare/v2.14.1...v2.15.0) (2026-04-09)
|
|
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>` を使用)
|
|
@@ -124,6 +124,7 @@ ai-cli run --cwd "$PWD" --model oc-openai/gpt-5.4 --session-id ses_123 --prompt
|
|
|
124
124
|
ai-cli ps
|
|
125
125
|
ai-cli result 12345
|
|
126
126
|
ai-cli result 12345 --verbose
|
|
127
|
+
ai-cli peek 12345 --time 10
|
|
127
128
|
ai-cli wait 12345 --timeout 300
|
|
128
129
|
ai-cli wait 12345 --verbose
|
|
129
130
|
ai-cli kill 12345
|
|
@@ -178,6 +179,7 @@ macOSでは、これらのツールを初めて実行する際にフォルダへ
|
|
|
178
179
|
- `run`
|
|
179
180
|
- `ps`
|
|
180
181
|
- `result`
|
|
182
|
+
- `peek`
|
|
181
183
|
- `wait`
|
|
182
184
|
- `kill`
|
|
183
185
|
- `cleanup`
|
|
@@ -194,6 +196,8 @@ ai-cli run --cwd "$PWD" --model codex-ultra --prompt "fix failing tests"
|
|
|
194
196
|
ai-cli run --cwd "$PWD" --model opencode --session-id ses_existing --prompt "この OpenCode セッションを継続して"
|
|
195
197
|
ai-cli run --cwd "$PWD" --model oc-openai/gpt-5.4 --prompt "明示的な OpenCode モデルで実行"
|
|
196
198
|
ai-cli ps
|
|
199
|
+
ai-cli peek 12345 --time 10
|
|
200
|
+
ai-cli peek 12345 12346 --time 10
|
|
197
201
|
ai-cli wait 12345
|
|
198
202
|
ai-cli wait 12345 --verbose
|
|
199
203
|
ai-cli result 12345
|
|
@@ -272,6 +276,60 @@ Claude CLI、Codex CLI、Gemini CLI、Forge CLI、または OpenCode を使用
|
|
|
272
276
|
- `timeout` (number, 任意): 最大待機時間(秒)。デフォルトは180秒(3分)です。
|
|
273
277
|
- `verbose` (boolean, 任意): `true` の場合、各結果項目を full 形で返します。デフォルトは `false` です。
|
|
274
278
|
|
|
279
|
+
### `peek`
|
|
280
|
+
|
|
281
|
+
実行中の子エージェントを短時間だけ観測し、その `peek` 呼び出しの観測ウィンドウ内で ai-cli-mcp が受理した自然言語メッセージだけを返します。履歴APIではなく、欠落のないストリーミングでもなく、シェルの `stdout` / `stderr` tail でもありません。別々の `peek` 呼び出しの間に出たメッセージは取得できない場合があります。v1 では `--follow` はありません。
|
|
282
|
+
|
|
283
|
+
CLI v1:
|
|
284
|
+
|
|
285
|
+
```bash
|
|
286
|
+
ai-cli peek 123 --time 10
|
|
287
|
+
ai-cli peek 123 456 --time 10
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
**引数:**
|
|
291
|
+
- `pids` (array of numbers, 必須): `run` が返したプロセスIDを 1..32 件指定します。重複したPIDはサーバー側で重複排除され、最初に出た順序が維持されます。未知または管理外のPIDは、呼び出し全体の失敗ではなく、プロセスごとに `not_found` として返されます。
|
|
292
|
+
- `peek_time_sec` (number, 任意): 観測時間(秒)の正の整数です。デフォルトは10秒、最大60秒です。`0`、負数、小数は無効です。
|
|
293
|
+
|
|
294
|
+
**観測とフィルタリング:**
|
|
295
|
+
- `peek_started_at` と `messages[].ts` は、ai-cli-mcp サーバー側の UTC RFC3339 タイムスタンプです。`peek_started_at` は検証とリスナー登録後に観測ウィンドウが始まった時刻、`messages[].ts` は ai-cli-mcp がメッセージを観測して受理した時刻です。
|
|
296
|
+
- 観測ウィンドウは `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` になります。
|
|
301
|
+
- `status` は `running`、`completed`、`failed`、`not_found` のいずれかで、観測ウィンドウ終了時点の状態を表します。
|
|
302
|
+
- `agent` は `claude`、`codex`、`gemini`、`forge`、`opencode`、将来追加される追跡済みエージェント文字列、または `null` です。`null` はプロセスが見つからない、またはエージェント種別を判断できない場合を表します。
|
|
303
|
+
|
|
304
|
+
レスポンス例:
|
|
305
|
+
|
|
306
|
+
```json
|
|
307
|
+
{
|
|
308
|
+
"peek_started_at": "2026-04-11T12:34:56.789Z",
|
|
309
|
+
"observed_duration_sec": 10.01,
|
|
310
|
+
"processes": [
|
|
311
|
+
{
|
|
312
|
+
"pid": 123,
|
|
313
|
+
"agent": "codex",
|
|
314
|
+
"status": "running",
|
|
315
|
+
"messages": [
|
|
316
|
+
{ "ts": "2026-04-11T12:34:59.120Z", "text": "I'm checking the implementation." }
|
|
317
|
+
],
|
|
318
|
+
"truncated": false,
|
|
319
|
+
"error": null
|
|
320
|
+
},
|
|
321
|
+
{
|
|
322
|
+
"pid": 999,
|
|
323
|
+
"agent": null,
|
|
324
|
+
"status": "not_found",
|
|
325
|
+
"messages": [],
|
|
326
|
+
"truncated": false,
|
|
327
|
+
"error": "process not found"
|
|
328
|
+
}
|
|
329
|
+
]
|
|
330
|
+
}
|
|
331
|
+
```
|
|
332
|
+
|
|
275
333
|
### `list_processes`
|
|
276
334
|
|
|
277
335
|
実行中および完了したすべてのAIエージェントプロセスを、ステータス、PID、基本情報とともにリストアップします。
|
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>`)
|
|
@@ -121,6 +121,7 @@ ai-cli run --cwd "$PWD" --model oc-openai/gpt-5.4 --session-id ses_123 --prompt
|
|
|
121
121
|
ai-cli ps
|
|
122
122
|
ai-cli result 12345
|
|
123
123
|
ai-cli result 12345 --verbose
|
|
124
|
+
ai-cli peek 12345 --time 10
|
|
124
125
|
ai-cli wait 12345 --timeout 300
|
|
125
126
|
ai-cli wait 12345 --verbose
|
|
126
127
|
ai-cli kill 12345
|
|
@@ -175,6 +176,7 @@ macOS might ask for folder permissions the first time any of these tools run. If
|
|
|
175
176
|
- `run`
|
|
176
177
|
- `ps`
|
|
177
178
|
- `result`
|
|
179
|
+
- `peek`
|
|
178
180
|
- `wait`
|
|
179
181
|
- `kill`
|
|
180
182
|
- `cleanup`
|
|
@@ -191,6 +193,8 @@ ai-cli run --cwd "$PWD" --model codex-ultra --prompt "fix failing tests"
|
|
|
191
193
|
ai-cli run --cwd "$PWD" --model opencode --session-id ses_existing --prompt "continue this OpenCode session"
|
|
192
194
|
ai-cli run --cwd "$PWD" --model oc-openai/gpt-5.4 --prompt "run with an explicit OpenCode backend model"
|
|
193
195
|
ai-cli ps
|
|
196
|
+
ai-cli peek 12345 --time 10
|
|
197
|
+
ai-cli peek 12345 12346 --time 10
|
|
194
198
|
ai-cli wait 12345
|
|
195
199
|
ai-cli wait 12345 --verbose
|
|
196
200
|
ai-cli result 12345
|
|
@@ -269,6 +273,60 @@ By default, each returned result item uses the compact shape shared with `get_re
|
|
|
269
273
|
- `timeout` (number, optional): Maximum wait time in seconds. Defaults to 180 (3 minutes).
|
|
270
274
|
- `verbose` (boolean, optional): If `true`, each result item uses the full result shape. Defaults to `false`.
|
|
271
275
|
|
|
276
|
+
### `peek`
|
|
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.
|
|
279
|
+
|
|
280
|
+
CLI v1:
|
|
281
|
+
|
|
282
|
+
```bash
|
|
283
|
+
ai-cli peek 123 --time 10
|
|
284
|
+
ai-cli peek 123 456 --time 10
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
**Arguments:**
|
|
288
|
+
- `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
|
+
- `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.
|
|
290
|
+
|
|
291
|
+
**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.
|
|
293
|
+
- 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`.
|
|
298
|
+
- `status` is one of `running`, `completed`, `failed`, or `not_found`, and reflects state when the observation window closes.
|
|
299
|
+
- `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
|
+
|
|
301
|
+
Example response:
|
|
302
|
+
|
|
303
|
+
```json
|
|
304
|
+
{
|
|
305
|
+
"peek_started_at": "2026-04-11T12:34:56.789Z",
|
|
306
|
+
"observed_duration_sec": 10.01,
|
|
307
|
+
"processes": [
|
|
308
|
+
{
|
|
309
|
+
"pid": 123,
|
|
310
|
+
"agent": "codex",
|
|
311
|
+
"status": "running",
|
|
312
|
+
"messages": [
|
|
313
|
+
{ "ts": "2026-04-11T12:34:59.120Z", "text": "I'm checking the implementation." }
|
|
314
|
+
],
|
|
315
|
+
"truncated": false,
|
|
316
|
+
"error": null
|
|
317
|
+
},
|
|
318
|
+
{
|
|
319
|
+
"pid": 999,
|
|
320
|
+
"agent": null,
|
|
321
|
+
"status": "not_found",
|
|
322
|
+
"messages": [],
|
|
323
|
+
"truncated": false,
|
|
324
|
+
"error": "process not found"
|
|
325
|
+
}
|
|
326
|
+
]
|
|
327
|
+
}
|
|
328
|
+
```
|
|
329
|
+
|
|
272
330
|
### `list_processes`
|
|
273
331
|
|
|
274
332
|
Lists all running and completed AI agent processes with their status, PID, and basic info.
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { describe, expect, it, vi } from 'vitest';
|
|
2
|
-
import { CLI_HELP_TEXT, DOCTOR_HELP_TEXT, MODELS_HELP_TEXT, RESULT_HELP_TEXT, RUN_HELP_TEXT, WAIT_HELP_TEXT, runCli, } from '../app/cli.js';
|
|
2
|
+
import { CLI_HELP_TEXT, DOCTOR_HELP_TEXT, MODELS_HELP_TEXT, PEEK_HELP_TEXT, RESULT_HELP_TEXT, RUN_HELP_TEXT, WAIT_HELP_TEXT, runCli, } from '../app/cli.js';
|
|
3
3
|
describe('ai-cli app', () => {
|
|
4
4
|
it('prints help and exits successfully when no subcommand is provided', async () => {
|
|
5
5
|
const stdout = vi.fn();
|
|
@@ -151,6 +151,52 @@ describe('ai-cli app', () => {
|
|
|
151
151
|
expect(stdout).toHaveBeenCalledWith(CLI_HELP_TEXT);
|
|
152
152
|
expect(waitForProcesses).not.toHaveBeenCalled();
|
|
153
153
|
});
|
|
154
|
+
it('dispatches peek with deduped pid arguments and time', async () => {
|
|
155
|
+
const stdout = vi.fn();
|
|
156
|
+
const stderr = vi.fn();
|
|
157
|
+
const peekProcesses = vi.fn().mockResolvedValue({
|
|
158
|
+
peek_started_at: '2026-04-11T12:34:56.789Z',
|
|
159
|
+
observed_duration_sec: 0.01,
|
|
160
|
+
processes: [],
|
|
161
|
+
});
|
|
162
|
+
const exitCode = await runCli(['peek', '123', '456', '123', '--time', '5'], {
|
|
163
|
+
stdout,
|
|
164
|
+
stderr,
|
|
165
|
+
peekProcesses,
|
|
166
|
+
});
|
|
167
|
+
expect(exitCode).toBe(0);
|
|
168
|
+
expect(peekProcesses).toHaveBeenCalledWith([123, 456], 5);
|
|
169
|
+
expect(stdout).toHaveBeenCalledWith(expect.stringContaining('"peek_started_at"'));
|
|
170
|
+
expect(stderr).not.toHaveBeenCalled();
|
|
171
|
+
});
|
|
172
|
+
it('defaults peek time and rejects --follow', async () => {
|
|
173
|
+
const stdout = vi.fn();
|
|
174
|
+
const stderr = vi.fn();
|
|
175
|
+
const peekProcesses = vi.fn().mockResolvedValue({
|
|
176
|
+
peek_started_at: '2026-04-11T12:34:56.789Z',
|
|
177
|
+
observed_duration_sec: 0.01,
|
|
178
|
+
processes: [],
|
|
179
|
+
});
|
|
180
|
+
const defaultExitCode = await runCli(['peek', '123'], { stdout, stderr, peekProcesses });
|
|
181
|
+
expect(defaultExitCode).toBe(0);
|
|
182
|
+
expect(peekProcesses).toHaveBeenCalledWith([123], 10);
|
|
183
|
+
const followExitCode = await runCli(['peek', '123', '--follow'], { stdout, stderr, peekProcesses });
|
|
184
|
+
expect(followExitCode).toBe(1);
|
|
185
|
+
expect(stderr).toHaveBeenCalledWith('peek does not support --follow in v1\n');
|
|
186
|
+
});
|
|
187
|
+
it('rejects invalid peek time values', async () => {
|
|
188
|
+
const stdout = vi.fn();
|
|
189
|
+
const stderr = vi.fn();
|
|
190
|
+
const peekProcesses = vi.fn();
|
|
191
|
+
const exitCode = await runCli(['peek', '123', '--time', '1.5'], {
|
|
192
|
+
stdout,
|
|
193
|
+
stderr,
|
|
194
|
+
peekProcesses,
|
|
195
|
+
});
|
|
196
|
+
expect(exitCode).toBe(1);
|
|
197
|
+
expect(stderr).toHaveBeenCalledWith(expect.stringContaining('peek_time_sec must be a positive integer'));
|
|
198
|
+
expect(peekProcesses).not.toHaveBeenCalled();
|
|
199
|
+
});
|
|
154
200
|
it('dispatches ps, result, and kill', async () => {
|
|
155
201
|
const stdout = vi.fn();
|
|
156
202
|
const stderr = vi.fn();
|
|
@@ -281,6 +327,15 @@ describe('ai-cli app', () => {
|
|
|
281
327
|
expect(stdout).toHaveBeenCalledWith(expect.stringContaining('--verbose'));
|
|
282
328
|
expect(stderr).not.toHaveBeenCalled();
|
|
283
329
|
});
|
|
330
|
+
it('prints detailed help for peek --help', async () => {
|
|
331
|
+
const stdout = vi.fn();
|
|
332
|
+
const stderr = vi.fn();
|
|
333
|
+
const exitCode = await runCli(['peek', '--help'], { stdout, stderr });
|
|
334
|
+
expect(exitCode).toBe(0);
|
|
335
|
+
expect(stdout).toHaveBeenCalledWith(PEEK_HELP_TEXT);
|
|
336
|
+
expect(stdout).toHaveBeenCalledWith(expect.stringContaining('No --follow mode'));
|
|
337
|
+
expect(stderr).not.toHaveBeenCalled();
|
|
338
|
+
});
|
|
284
339
|
it('prints detailed help for models --help', async () => {
|
|
285
340
|
const stdout = vi.fn();
|
|
286
341
|
const stderr = vi.fn();
|
|
@@ -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');
|
|
@@ -305,7 +306,7 @@ describe('cli-builder', () => {
|
|
|
305
306
|
expect(cmd.cliPath).toBe('/usr/bin/gemini');
|
|
306
307
|
expect(cmd.args).toContain('-y');
|
|
307
308
|
expect(cmd.args).toContain('--output-format');
|
|
308
|
-
expect(cmd.args).toContain('json');
|
|
309
|
+
expect(cmd.args).toContain('stream-json');
|
|
309
310
|
expect(cmd.args).toContain('--model');
|
|
310
311
|
expect(cmd.args).toContain('gemini-2.5-pro');
|
|
311
312
|
});
|
|
@@ -107,6 +107,65 @@ describe('CliProcessService', () => {
|
|
|
107
107
|
expect(result).not.toHaveProperty('prompt');
|
|
108
108
|
expect(readFileSync(join(processDir, 'meta.json'), 'utf-8')).toContain('"status": "completed"');
|
|
109
109
|
});
|
|
110
|
+
it('peeks only appended natural-language messages from detached logs', async () => {
|
|
111
|
+
const root = mkdtempSync(join(tmpdir(), 'ai-cli-cli-service-'));
|
|
112
|
+
tempDirs.push(root);
|
|
113
|
+
const scriptPath = join(root, 'mock-claude-peek');
|
|
114
|
+
writeFileSync(scriptPath, `#!/bin/bash
|
|
115
|
+
printf '%s\n' '{"type":"assistant","message":{"content":[{"type":"text","text":"old cli message"}]}}'
|
|
116
|
+
sleep 2
|
|
117
|
+
printf '%s\n' '{"type":"assistant","message":{"content":[{"type":"text","text":"new cli message"},{"type":"tool_use","id":"tool-1","name":"Read","input":{"file_path":"/tmp/a"}}]}}'
|
|
118
|
+
printf '%s\n' '{"type":"user","message":{"content":[{"type":"tool_result","tool_use_id":"tool-1","content":"secret"}]}}'
|
|
119
|
+
`);
|
|
120
|
+
chmodSync(scriptPath, 0o755);
|
|
121
|
+
const stateDir = join(root, 'state');
|
|
122
|
+
const workFolder = join(root, 'work');
|
|
123
|
+
mkdirSync(workFolder, { recursive: true });
|
|
124
|
+
const service = new CliProcessService({
|
|
125
|
+
stateDir,
|
|
126
|
+
cliPaths: {
|
|
127
|
+
claude: scriptPath,
|
|
128
|
+
codex: scriptPath,
|
|
129
|
+
gemini: scriptPath,
|
|
130
|
+
forge: scriptPath,
|
|
131
|
+
opencode: scriptPath,
|
|
132
|
+
},
|
|
133
|
+
});
|
|
134
|
+
const runResult = await service.startProcess({
|
|
135
|
+
prompt: 'hello peek',
|
|
136
|
+
cwd: workFolder,
|
|
137
|
+
});
|
|
138
|
+
const processDir = join(stateDir, 'cwds', encodeCwd(realpathSync(workFolder)), String(runResult.pid));
|
|
139
|
+
const stdoutPath = join(processDir, 'stdout.log');
|
|
140
|
+
const startedAt = Date.now();
|
|
141
|
+
while (Date.now() - startedAt < 5000 && !readFileSync(stdoutPath, 'utf-8').includes('old cli message')) {
|
|
142
|
+
await new Promise((resolve) => setTimeout(resolve, 25));
|
|
143
|
+
}
|
|
144
|
+
expect(readFileSync(stdoutPath, 'utf-8')).toContain('old cli message');
|
|
145
|
+
const peekResult = await service.peekProcesses([runResult.pid, runResult.pid, 999999], 3);
|
|
146
|
+
expect(peekResult.processes).toHaveLength(2);
|
|
147
|
+
expect(peekResult.processes[0]).toMatchObject({
|
|
148
|
+
pid: runResult.pid,
|
|
149
|
+
agent: 'claude',
|
|
150
|
+
status: 'completed',
|
|
151
|
+
messages: [
|
|
152
|
+
{
|
|
153
|
+
ts: expect.any(String),
|
|
154
|
+
text: 'new cli message',
|
|
155
|
+
},
|
|
156
|
+
],
|
|
157
|
+
truncated: false,
|
|
158
|
+
error: null,
|
|
159
|
+
});
|
|
160
|
+
expect(peekResult.processes[1]).toEqual({
|
|
161
|
+
pid: 999999,
|
|
162
|
+
agent: null,
|
|
163
|
+
status: 'not_found',
|
|
164
|
+
messages: [],
|
|
165
|
+
truncated: false,
|
|
166
|
+
error: 'process not found',
|
|
167
|
+
});
|
|
168
|
+
});
|
|
110
169
|
it('returns compact results by default and full results when verbose is true', async () => {
|
|
111
170
|
const root = mkdtempSync(join(tmpdir(), 'ai-cli-cli-service-'));
|
|
112
171
|
tempDirs.push(root);
|
|
@@ -29,7 +29,7 @@ describe('Claude Code MCP E2E Tests', () => {
|
|
|
29
29
|
describe('Tool Registration', () => {
|
|
30
30
|
it('should register run tool', async () => {
|
|
31
31
|
const tools = await client.listTools();
|
|
32
|
-
expect(tools).toHaveLength(
|
|
32
|
+
expect(tools).toHaveLength(7);
|
|
33
33
|
const claudeCodeTool = tools.find((t) => t.name === 'run');
|
|
34
34
|
expect(claudeCodeTool.inputSchema.properties.model.description).toContain('sonnet');
|
|
35
35
|
expect(claudeCodeTool.inputSchema.properties.model.description).toContain('opencode');
|
|
@@ -38,6 +38,7 @@ describe('Claude Code MCP E2E Tests', () => {
|
|
|
38
38
|
// Verify other tools exist
|
|
39
39
|
expect(tools.some((t) => t.name === 'list_processes')).toBe(true);
|
|
40
40
|
expect(tools.some((t) => t.name === 'get_result')).toBe(true);
|
|
41
|
+
expect(tools.some((t) => t.name === 'peek')).toBe(true);
|
|
41
42
|
expect(tools.some((t) => t.name === 'kill_process')).toBe(true);
|
|
42
43
|
});
|
|
43
44
|
});
|
|
@@ -84,6 +84,7 @@ describe('MCP Contract Tests', () => {
|
|
|
84
84
|
'get_result',
|
|
85
85
|
'kill_process',
|
|
86
86
|
'list_processes',
|
|
87
|
+
'peek',
|
|
87
88
|
'run',
|
|
88
89
|
'wait',
|
|
89
90
|
]);
|
|
@@ -115,6 +116,13 @@ describe('MCP Contract Tests', () => {
|
|
|
115
116
|
'timeout',
|
|
116
117
|
'verbose',
|
|
117
118
|
]);
|
|
119
|
+
const peekTool = tools.find((tool) => tool.name === 'peek');
|
|
120
|
+
expect(peekTool.inputSchema.required).toEqual(['pids']);
|
|
121
|
+
expect(Object.keys(peekTool.inputSchema.properties).sort()).toEqual([
|
|
122
|
+
'peek_time_sec',
|
|
123
|
+
'pids',
|
|
124
|
+
]);
|
|
125
|
+
expect(peekTool.description).toContain('One-shot');
|
|
118
126
|
});
|
|
119
127
|
it('preserves the stdio MCP smoke flow and response shapes', async () => {
|
|
120
128
|
const runResponse = await client.callTool('run', {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { describe, it, expect } from 'vitest';
|
|
2
|
-
import { parseCodexOutput, parseClaudeOutput, parseForgeOutput, parseOpenCodeOutput } from '../parsers.js';
|
|
2
|
+
import { parseCodexOutput, parseClaudeOutput, parseForgeOutput, parseGeminiOutput, parseOpenCodeOutput, 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 = `
|
|
@@ -58,6 +58,168 @@ INVALID_JSON
|
|
|
58
58
|
expect(result.message).toBe("Still parses valid lines");
|
|
59
59
|
});
|
|
60
60
|
});
|
|
61
|
+
describe('PeekMessageExtractor', () => {
|
|
62
|
+
const ts = '2026-04-11T12:34:56.789Z';
|
|
63
|
+
it('extracts only Codex agent_message text', () => {
|
|
64
|
+
const extractor = new PeekMessageExtractor('codex');
|
|
65
|
+
const output = [
|
|
66
|
+
'{"type":"item.completed","item":{"type":"reasoning","text":"hidden"}}',
|
|
67
|
+
'{"type":"item.completed","item":{"type":"command_execution","aggregated_output":"secret command output"}}',
|
|
68
|
+
'{"type":"item.completed","item":{"type":"agent_message","text":"Visible Codex message"}}',
|
|
69
|
+
'{"msg":{"type":"token_count","total":123}}',
|
|
70
|
+
'{"msg":{"type":"agent_message","message":"Visible legacy Codex message"}}',
|
|
71
|
+
].join('\n') + '\n';
|
|
72
|
+
expect(extractor.push(output, ts)).toEqual([
|
|
73
|
+
{ ts, text: 'Visible Codex message' },
|
|
74
|
+
{ ts, text: 'Visible legacy Codex message' },
|
|
75
|
+
]);
|
|
76
|
+
});
|
|
77
|
+
it('extracts only Claude assistant text content', () => {
|
|
78
|
+
const extractor = new PeekMessageExtractor('claude');
|
|
79
|
+
const output = [
|
|
80
|
+
'{"type":"assistant","message":{"content":[{"type":"text","text":"Visible Claude text"},{"type":"tool_use","id":"tool-1","name":"Read","input":{"file_path":"/tmp/a"}}]}}',
|
|
81
|
+
'{"type":"user","message":{"content":[{"type":"tool_result","tool_use_id":"tool-1","content":"secret"}]}}',
|
|
82
|
+
'{"type":"result","result":"Final result is not peek assistant text"}',
|
|
83
|
+
].join('\n') + '\n';
|
|
84
|
+
expect(extractor.push(output, ts)).toEqual([
|
|
85
|
+
{ ts, text: 'Visible Claude text' },
|
|
86
|
+
]);
|
|
87
|
+
});
|
|
88
|
+
it('extracts only Gemini assistant message content', () => {
|
|
89
|
+
const extractor = new PeekMessageExtractor('gemini');
|
|
90
|
+
const output = [
|
|
91
|
+
'{"type":"message","timestamp":"2026-04-11T14:44:42.294Z","role":"user","content":"hidden user text"}',
|
|
92
|
+
'{"type":"message","timestamp":"2026-04-11T14:44:53.820Z","role":"assistant","content":"Visible Gemini text","delta":true}',
|
|
93
|
+
'{"type":"tool_use","timestamp":"2026-04-11T14:44:53.821Z","tool_name":"run_shell_command","parameters":{"command":"echo secret"}}',
|
|
94
|
+
'{"type":"tool_result","timestamp":"2026-04-11T14:45:03.011Z","status":"success","output":"secret command output"}',
|
|
95
|
+
'{"type":"result","timestamp":"2026-04-11T14:45:10.380Z","status":"success","response":"Final result is not peek assistant text"}',
|
|
96
|
+
].join('\n') + '\n';
|
|
97
|
+
expect(extractor.push(output, ts)).toEqual([
|
|
98
|
+
{ ts, text: 'Visible Gemini text' },
|
|
99
|
+
]);
|
|
100
|
+
});
|
|
101
|
+
it('joins split Gemini assistant chunks into one peek message on flush', () => {
|
|
102
|
+
const extractor = new PeekMessageExtractor('gemini');
|
|
103
|
+
const output = [
|
|
104
|
+
'{"type":"message","timestamp":"2026-04-11T14:44:53.820Z","role":"assistant","content":"Step 2 done. Starting step ","delta":true}',
|
|
105
|
+
'{"type":"message","timestamp":"2026-04-11T14:44:53.821Z","role":"assistant","content":"3.","delta":true}',
|
|
106
|
+
].join('\n') + '\n';
|
|
107
|
+
expect(extractor.push(output, ts)).toEqual([]);
|
|
108
|
+
expect(extractor.flush('2026-04-11T12:34:59.000Z')).toEqual([
|
|
109
|
+
{ ts: '2026-04-11T12:34:59.000Z', text: 'Step 2 done. Starting step 3.' },
|
|
110
|
+
]);
|
|
111
|
+
});
|
|
112
|
+
it('emits separate Gemini peek messages when a boundary separates logical messages', () => {
|
|
113
|
+
const extractor = new PeekMessageExtractor('gemini');
|
|
114
|
+
const output = [
|
|
115
|
+
'{"type":"message","timestamp":"2026-04-11T14:44:53.820Z","role":"assistant","content":"Starting step ","delta":true}',
|
|
116
|
+
'{"type":"message","timestamp":"2026-04-11T14:44:53.821Z","role":"assistant","content":"1.","delta":true}',
|
|
117
|
+
'{"type":"tool_use","timestamp":"2026-04-11T14:44:53.822Z","tool_name":"run_shell_command","parameters":{"command":"echo secret"}}',
|
|
118
|
+
'{"type":"tool_result","timestamp":"2026-04-11T14:45:03.011Z","status":"success","output":"secret command output"}',
|
|
119
|
+
'{"type":"message","timestamp":"2026-04-11T14:45:10.315Z","role":"assistant","content":"Final ","delta":true}',
|
|
120
|
+
'{"type":"message","timestamp":"2026-04-11T14:45:10.316Z","role":"assistant","content":"answer.","delta":true}',
|
|
121
|
+
'{"type":"result","timestamp":"2026-04-11T14:45:10.380Z","status":"success","response":"Final result response is not peek text","stats":{"total_tokens":21999}}',
|
|
122
|
+
].join('\n') + '\n';
|
|
123
|
+
expect(extractor.push(output, ts)).toEqual([
|
|
124
|
+
{ ts, text: 'Starting step 1.' },
|
|
125
|
+
{ ts, text: 'Final answer.' },
|
|
126
|
+
]);
|
|
127
|
+
expect(extractor.flush(ts)).toEqual([]);
|
|
128
|
+
});
|
|
129
|
+
it('does not emit Gemini user, tool, tool result, stats, or result response text', () => {
|
|
130
|
+
const extractor = new PeekMessageExtractor('gemini');
|
|
131
|
+
const output = [
|
|
132
|
+
'{"type":"message","timestamp":"2026-04-11T14:44:42.294Z","role":"user","content":"hidden user text"}',
|
|
133
|
+
'{"type":"tool_use","timestamp":"2026-04-11T14:44:53.821Z","tool_name":"run_shell_command","parameters":{"command":"echo secret"}}',
|
|
134
|
+
'{"type":"tool_result","timestamp":"2026-04-11T14:45:03.011Z","status":"success","output":"secret command output"}',
|
|
135
|
+
'{"type":"result","timestamp":"2026-04-11T14:45:10.380Z","status":"success","response":"Final result response is not peek text","stats":{"total_tokens":21999}}',
|
|
136
|
+
].join('\n') + '\n';
|
|
137
|
+
expect(extractor.push(output, ts)).toEqual([]);
|
|
138
|
+
expect(extractor.flush(ts)).toEqual([]);
|
|
139
|
+
});
|
|
140
|
+
it('denies unsupported agents and invalid shapes by default', () => {
|
|
141
|
+
const extractor = new PeekMessageExtractor('forge');
|
|
142
|
+
const output = [
|
|
143
|
+
'{"type":"assistant","message":{"content":[{"type":"text","text":"not supported here"}]}}',
|
|
144
|
+
'{"type":"item.completed","item":{"type":"agent_message","text":"not supported here"}}',
|
|
145
|
+
'{"type":"text","part":{"type":"text","text":"not supported here"}}',
|
|
146
|
+
'{"type":"message","role":"assistant","content":"not supported here"}',
|
|
147
|
+
'plain stdout',
|
|
148
|
+
].join('\n') + '\n';
|
|
149
|
+
expect(extractor.push(output, ts)).toEqual([]);
|
|
150
|
+
});
|
|
151
|
+
it('extracts only OpenCode natural-language text events', () => {
|
|
152
|
+
const extractor = new PeekMessageExtractor('opencode');
|
|
153
|
+
const output = [
|
|
154
|
+
'{"type":"text","timestamp":1775918783605,"sessionID":"ses-1","part":{"type":"text","text":"OpenCode visible text"}}',
|
|
155
|
+
'{"type":"tool_use","timestamp":1775918783606,"sessionID":"ses-1","part":{"type":"tool","state":{"output":"secret command output"},"metadata":{"output":"secret metadata output"}}}',
|
|
156
|
+
'{"type":"text","timestamp":1775918783607,"sessionID":"ses-1","part":{"type":"tool","text":"wrong part type"}}',
|
|
157
|
+
].join('\n') + '\n';
|
|
158
|
+
expect(extractor.push(output, ts)).toEqual([
|
|
159
|
+
{ ts, text: 'OpenCode visible text' },
|
|
160
|
+
]);
|
|
161
|
+
});
|
|
162
|
+
it('can flush a complete JSON event without a trailing newline', () => {
|
|
163
|
+
const extractor = new PeekMessageExtractor('codex');
|
|
164
|
+
expect(extractor.push('{"type":"item.completed","item":{"type":"agent_message","text":"pending"}}', ts)).toEqual([]);
|
|
165
|
+
expect(extractor.flush(ts)).toEqual([{ ts, text: 'pending' }]);
|
|
166
|
+
});
|
|
167
|
+
});
|
|
168
|
+
describe('parseGeminiOutput', () => {
|
|
169
|
+
it('should parse legacy final JSON output', () => {
|
|
170
|
+
const output = JSON.stringify({
|
|
171
|
+
session_id: 'gemini-session-json',
|
|
172
|
+
response: 'Legacy Gemini final response',
|
|
173
|
+
stats: {
|
|
174
|
+
total_tokens: 123,
|
|
175
|
+
},
|
|
176
|
+
});
|
|
177
|
+
expect(parseGeminiOutput(output)).toEqual({
|
|
178
|
+
session_id: 'gemini-session-json',
|
|
179
|
+
response: 'Legacy Gemini final response',
|
|
180
|
+
stats: {
|
|
181
|
+
total_tokens: 123,
|
|
182
|
+
},
|
|
183
|
+
});
|
|
184
|
+
});
|
|
185
|
+
it('should normalize a single-line Gemini assistant stream event', () => {
|
|
186
|
+
const output = '{"type":"message","timestamp":"2026-04-11T14:44:53.820Z","role":"assistant","content":"Only answer","delta":true}';
|
|
187
|
+
const result = parseGeminiOutput(output);
|
|
188
|
+
expect(result).toMatchObject({
|
|
189
|
+
message: 'Only answer',
|
|
190
|
+
session_id: null,
|
|
191
|
+
});
|
|
192
|
+
expect(result).not.toHaveProperty('type');
|
|
193
|
+
expect(result).not.toHaveProperty('content');
|
|
194
|
+
});
|
|
195
|
+
it('should parse Gemini stream-json NDJSON output', () => {
|
|
196
|
+
const output = [
|
|
197
|
+
'{"type":"init","timestamp":"2026-04-11T14:44:42.293Z","session_id":"gemini-session-stream","model":"gemini-3.1-pro-preview"}',
|
|
198
|
+
'{"type":"message","timestamp":"2026-04-11T14:44:42.294Z","role":"user","content":"hidden user text"}',
|
|
199
|
+
'{"type":"message","timestamp":"2026-04-11T14:44:53.820Z","role":"assistant","content":"First logical assistant response.","delta":true}',
|
|
200
|
+
'{"type":"tool_use","timestamp":"2026-04-11T14:44:53.821Z","tool_name":"run_shell_command","tool_id":"tool-1","parameters":{"command":"echo hidden"}}',
|
|
201
|
+
'{"type":"tool_result","timestamp":"2026-04-11T14:45:03.011Z","tool_id":"tool-1","status":"success","output":"hidden command output"}',
|
|
202
|
+
'{"type":"message","timestamp":"2026-04-11T14:45:10.315Z","role":"assistant","content":"Final assistant ","delta":true}',
|
|
203
|
+
'{"type":"message","timestamp":"2026-04-11T14:45:10.316Z","role":"assistant","content":"response.","delta":true}',
|
|
204
|
+
'{"type":"result","timestamp":"2026-04-11T14:45:10.380Z","status":"success","response":"Result response is not the parsed message","stats":{"total_tokens":21999}}',
|
|
205
|
+
].join('\n') + '\n';
|
|
206
|
+
expect(parseGeminiOutput(output)).toEqual({
|
|
207
|
+
message: 'Final assistant response.',
|
|
208
|
+
session_id: 'gemini-session-stream',
|
|
209
|
+
stats: {
|
|
210
|
+
total_tokens: 21999,
|
|
211
|
+
},
|
|
212
|
+
tools: [
|
|
213
|
+
{
|
|
214
|
+
tool: 'run_shell_command',
|
|
215
|
+
input: { command: 'echo hidden' },
|
|
216
|
+
output: 'hidden command output',
|
|
217
|
+
status: 'success',
|
|
218
|
+
},
|
|
219
|
+
],
|
|
220
|
+
});
|
|
221
|
+
});
|
|
222
|
+
});
|
|
61
223
|
describe('parseClaudeOutput', () => {
|
|
62
224
|
it('should parse legacy JSON output', () => {
|
|
63
225
|
const output = JSON.stringify({
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { appendPeekMessages, validatePeekPids, validatePeekTimeSec } from '../peek.js';
|
|
3
|
+
describe('peek helpers', () => {
|
|
4
|
+
it('dedupes pids while preserving first occurrence order', () => {
|
|
5
|
+
expect(validatePeekPids([3, 1, 3, 2, 1])).toEqual([3, 1, 2]);
|
|
6
|
+
});
|
|
7
|
+
it('validates pid and time limits', () => {
|
|
8
|
+
expect(() => validatePeekPids([])).toThrow(/1..32/);
|
|
9
|
+
expect(() => validatePeekPids([1.5])).toThrow(/positive safe integers/);
|
|
10
|
+
expect(() => validatePeekPids([Number.MAX_SAFE_INTEGER + 1])).toThrow(/positive safe integers/);
|
|
11
|
+
expect(validatePeekTimeSec(undefined)).toBe(10);
|
|
12
|
+
expect(validatePeekTimeSec(60)).toBe(60);
|
|
13
|
+
expect(() => validatePeekTimeSec(0)).toThrow(/positive integer/);
|
|
14
|
+
expect(() => validatePeekTimeSec(1.5)).toThrow(/positive integer/);
|
|
15
|
+
expect(() => validatePeekTimeSec(61)).toThrow(/positive integer/);
|
|
16
|
+
});
|
|
17
|
+
it('keeps the first 50 messages and marks truncation when later messages are dropped', () => {
|
|
18
|
+
const process = {
|
|
19
|
+
pid: 123,
|
|
20
|
+
agent: 'codex',
|
|
21
|
+
status: 'running',
|
|
22
|
+
messages: [],
|
|
23
|
+
truncated: false,
|
|
24
|
+
error: null,
|
|
25
|
+
};
|
|
26
|
+
appendPeekMessages(process, Array.from({ length: 55 }, (_, index) => ({
|
|
27
|
+
ts: '2026-04-11T12:34:56.789Z',
|
|
28
|
+
text: `message ${index}`,
|
|
29
|
+
})));
|
|
30
|
+
expect(process.messages).toHaveLength(50);
|
|
31
|
+
expect(process.messages[0].text).toBe('message 0');
|
|
32
|
+
expect(process.messages[49].text).toBe('message 49');
|
|
33
|
+
expect(process.truncated).toBe(true);
|
|
34
|
+
});
|
|
35
|
+
});
|