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.
@@ -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
 
4
4
  describe('parseCodexOutput', () => {
5
5
  it('should parse basic Codex output with message and session_id', () => {
@@ -65,6 +65,193 @@ INVALID_JSON
65
65
  });
66
66
  });
67
67
 
68
+ describe('PeekMessageExtractor', () => {
69
+ const ts = '2026-04-11T12:34:56.789Z';
70
+
71
+ it('extracts only Codex agent_message text', () => {
72
+ const extractor = new PeekMessageExtractor('codex');
73
+ const output = [
74
+ '{"type":"item.completed","item":{"type":"reasoning","text":"hidden"}}',
75
+ '{"type":"item.completed","item":{"type":"command_execution","aggregated_output":"secret command output"}}',
76
+ '{"type":"item.completed","item":{"type":"agent_message","text":"Visible Codex message"}}',
77
+ '{"msg":{"type":"token_count","total":123}}',
78
+ '{"msg":{"type":"agent_message","message":"Visible legacy Codex message"}}',
79
+ ].join('\n') + '\n';
80
+
81
+ expect(extractor.push(output, ts)).toEqual([
82
+ { ts, text: 'Visible Codex message' },
83
+ { ts, text: 'Visible legacy Codex message' },
84
+ ]);
85
+ });
86
+
87
+ it('extracts only Claude assistant text content', () => {
88
+ const extractor = new PeekMessageExtractor('claude');
89
+ const output = [
90
+ '{"type":"assistant","message":{"content":[{"type":"text","text":"Visible Claude text"},{"type":"tool_use","id":"tool-1","name":"Read","input":{"file_path":"/tmp/a"}}]}}',
91
+ '{"type":"user","message":{"content":[{"type":"tool_result","tool_use_id":"tool-1","content":"secret"}]}}',
92
+ '{"type":"result","result":"Final result is not peek assistant text"}',
93
+ ].join('\n') + '\n';
94
+
95
+ expect(extractor.push(output, ts)).toEqual([
96
+ { ts, text: 'Visible Claude text' },
97
+ ]);
98
+ });
99
+
100
+ it('extracts only Gemini assistant message content', () => {
101
+ const extractor = new PeekMessageExtractor('gemini');
102
+ const output = [
103
+ '{"type":"message","timestamp":"2026-04-11T14:44:42.294Z","role":"user","content":"hidden user text"}',
104
+ '{"type":"message","timestamp":"2026-04-11T14:44:53.820Z","role":"assistant","content":"Visible Gemini text","delta":true}',
105
+ '{"type":"tool_use","timestamp":"2026-04-11T14:44:53.821Z","tool_name":"run_shell_command","parameters":{"command":"echo secret"}}',
106
+ '{"type":"tool_result","timestamp":"2026-04-11T14:45:03.011Z","status":"success","output":"secret command output"}',
107
+ '{"type":"result","timestamp":"2026-04-11T14:45:10.380Z","status":"success","response":"Final result is not peek assistant text"}',
108
+ ].join('\n') + '\n';
109
+
110
+ expect(extractor.push(output, ts)).toEqual([
111
+ { ts, text: 'Visible Gemini text' },
112
+ ]);
113
+ });
114
+
115
+ it('joins split Gemini assistant chunks into one peek message on flush', () => {
116
+ const extractor = new PeekMessageExtractor('gemini');
117
+ const output = [
118
+ '{"type":"message","timestamp":"2026-04-11T14:44:53.820Z","role":"assistant","content":"Step 2 done. Starting step ","delta":true}',
119
+ '{"type":"message","timestamp":"2026-04-11T14:44:53.821Z","role":"assistant","content":"3.","delta":true}',
120
+ ].join('\n') + '\n';
121
+
122
+ expect(extractor.push(output, ts)).toEqual([]);
123
+ expect(extractor.flush('2026-04-11T12:34:59.000Z')).toEqual([
124
+ { ts: '2026-04-11T12:34:59.000Z', text: 'Step 2 done. Starting step 3.' },
125
+ ]);
126
+ });
127
+
128
+ it('emits separate Gemini peek messages when a boundary separates logical messages', () => {
129
+ const extractor = new PeekMessageExtractor('gemini');
130
+ const output = [
131
+ '{"type":"message","timestamp":"2026-04-11T14:44:53.820Z","role":"assistant","content":"Starting step ","delta":true}',
132
+ '{"type":"message","timestamp":"2026-04-11T14:44:53.821Z","role":"assistant","content":"1.","delta":true}',
133
+ '{"type":"tool_use","timestamp":"2026-04-11T14:44:53.822Z","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":"message","timestamp":"2026-04-11T14:45:10.315Z","role":"assistant","content":"Final ","delta":true}',
136
+ '{"type":"message","timestamp":"2026-04-11T14:45:10.316Z","role":"assistant","content":"answer.","delta":true}',
137
+ '{"type":"result","timestamp":"2026-04-11T14:45:10.380Z","status":"success","response":"Final result response is not peek text","stats":{"total_tokens":21999}}',
138
+ ].join('\n') + '\n';
139
+
140
+ expect(extractor.push(output, ts)).toEqual([
141
+ { ts, text: 'Starting step 1.' },
142
+ { ts, text: 'Final answer.' },
143
+ ]);
144
+ expect(extractor.flush(ts)).toEqual([]);
145
+ });
146
+
147
+ it('does not emit Gemini user, tool, tool result, stats, or result response text', () => {
148
+ const extractor = new PeekMessageExtractor('gemini');
149
+ const output = [
150
+ '{"type":"message","timestamp":"2026-04-11T14:44:42.294Z","role":"user","content":"hidden user text"}',
151
+ '{"type":"tool_use","timestamp":"2026-04-11T14:44:53.821Z","tool_name":"run_shell_command","parameters":{"command":"echo secret"}}',
152
+ '{"type":"tool_result","timestamp":"2026-04-11T14:45:03.011Z","status":"success","output":"secret command output"}',
153
+ '{"type":"result","timestamp":"2026-04-11T14:45:10.380Z","status":"success","response":"Final result response is not peek text","stats":{"total_tokens":21999}}',
154
+ ].join('\n') + '\n';
155
+
156
+ expect(extractor.push(output, ts)).toEqual([]);
157
+ expect(extractor.flush(ts)).toEqual([]);
158
+ });
159
+
160
+ it('denies unsupported agents and invalid shapes by default', () => {
161
+ const extractor = new PeekMessageExtractor('forge');
162
+ const output = [
163
+ '{"type":"assistant","message":{"content":[{"type":"text","text":"not supported here"}]}}',
164
+ '{"type":"item.completed","item":{"type":"agent_message","text":"not supported here"}}',
165
+ '{"type":"text","part":{"type":"text","text":"not supported here"}}',
166
+ '{"type":"message","role":"assistant","content":"not supported here"}',
167
+ 'plain stdout',
168
+ ].join('\n') + '\n';
169
+
170
+ expect(extractor.push(output, ts)).toEqual([]);
171
+ });
172
+
173
+ it('extracts only OpenCode natural-language text events', () => {
174
+ const extractor = new PeekMessageExtractor('opencode');
175
+ const output = [
176
+ '{"type":"text","timestamp":1775918783605,"sessionID":"ses-1","part":{"type":"text","text":"OpenCode visible text"}}',
177
+ '{"type":"tool_use","timestamp":1775918783606,"sessionID":"ses-1","part":{"type":"tool","state":{"output":"secret command output"},"metadata":{"output":"secret metadata output"}}}',
178
+ '{"type":"text","timestamp":1775918783607,"sessionID":"ses-1","part":{"type":"tool","text":"wrong part type"}}',
179
+ ].join('\n') + '\n';
180
+
181
+ expect(extractor.push(output, ts)).toEqual([
182
+ { ts, text: 'OpenCode visible text' },
183
+ ]);
184
+ });
185
+
186
+ it('can flush a complete JSON event without a trailing newline', () => {
187
+ const extractor = new PeekMessageExtractor('codex');
188
+ expect(extractor.push('{"type":"item.completed","item":{"type":"agent_message","text":"pending"}}', ts)).toEqual([]);
189
+ expect(extractor.flush(ts)).toEqual([{ ts, text: 'pending' }]);
190
+ });
191
+ });
192
+
193
+ describe('parseGeminiOutput', () => {
194
+ it('should parse legacy final JSON output', () => {
195
+ const output = JSON.stringify({
196
+ session_id: 'gemini-session-json',
197
+ response: 'Legacy Gemini final response',
198
+ stats: {
199
+ total_tokens: 123,
200
+ },
201
+ });
202
+
203
+ expect(parseGeminiOutput(output)).toEqual({
204
+ session_id: 'gemini-session-json',
205
+ response: 'Legacy Gemini final response',
206
+ stats: {
207
+ total_tokens: 123,
208
+ },
209
+ });
210
+ });
211
+
212
+ it('should normalize a single-line Gemini assistant stream event', () => {
213
+ const output = '{"type":"message","timestamp":"2026-04-11T14:44:53.820Z","role":"assistant","content":"Only answer","delta":true}';
214
+
215
+ const result = parseGeminiOutput(output);
216
+
217
+ expect(result).toMatchObject({
218
+ message: 'Only answer',
219
+ session_id: null,
220
+ });
221
+ expect(result).not.toHaveProperty('type');
222
+ expect(result).not.toHaveProperty('content');
223
+ });
224
+
225
+ it('should parse Gemini stream-json NDJSON output', () => {
226
+ const output = [
227
+ '{"type":"init","timestamp":"2026-04-11T14:44:42.293Z","session_id":"gemini-session-stream","model":"gemini-3.1-pro-preview"}',
228
+ '{"type":"message","timestamp":"2026-04-11T14:44:42.294Z","role":"user","content":"hidden user text"}',
229
+ '{"type":"message","timestamp":"2026-04-11T14:44:53.820Z","role":"assistant","content":"First logical assistant response.","delta":true}',
230
+ '{"type":"tool_use","timestamp":"2026-04-11T14:44:53.821Z","tool_name":"run_shell_command","tool_id":"tool-1","parameters":{"command":"echo hidden"}}',
231
+ '{"type":"tool_result","timestamp":"2026-04-11T14:45:03.011Z","tool_id":"tool-1","status":"success","output":"hidden command output"}',
232
+ '{"type":"message","timestamp":"2026-04-11T14:45:10.315Z","role":"assistant","content":"Final assistant ","delta":true}',
233
+ '{"type":"message","timestamp":"2026-04-11T14:45:10.316Z","role":"assistant","content":"response.","delta":true}',
234
+ '{"type":"result","timestamp":"2026-04-11T14:45:10.380Z","status":"success","response":"Result response is not the parsed message","stats":{"total_tokens":21999}}',
235
+ ].join('\n') + '\n';
236
+
237
+ expect(parseGeminiOutput(output)).toEqual({
238
+ message: 'Final assistant response.',
239
+ session_id: 'gemini-session-stream',
240
+ stats: {
241
+ total_tokens: 21999,
242
+ },
243
+ tools: [
244
+ {
245
+ tool: 'run_shell_command',
246
+ input: { command: 'echo hidden' },
247
+ output: 'hidden command output',
248
+ status: 'success',
249
+ },
250
+ ],
251
+ });
252
+ });
253
+ });
254
+
68
255
  describe('parseClaudeOutput', () => {
69
256
  it('should parse legacy JSON output', () => {
70
257
  const output = JSON.stringify({
@@ -0,0 +1,43 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { appendPeekMessages, validatePeekPids, validatePeekTimeSec, type PeekProcessResult } from '../peek.js';
3
+
4
+ describe('peek helpers', () => {
5
+ it('dedupes pids while preserving first occurrence order', () => {
6
+ expect(validatePeekPids([3, 1, 3, 2, 1])).toEqual([3, 1, 2]);
7
+ });
8
+
9
+ it('validates pid and time limits', () => {
10
+ expect(() => validatePeekPids([])).toThrow(/1..32/);
11
+ expect(() => validatePeekPids([1.5])).toThrow(/positive safe integers/);
12
+ expect(() => validatePeekPids([Number.MAX_SAFE_INTEGER + 1])).toThrow(/positive safe integers/);
13
+ expect(validatePeekTimeSec(undefined)).toBe(10);
14
+ expect(validatePeekTimeSec(60)).toBe(60);
15
+ expect(() => validatePeekTimeSec(0)).toThrow(/positive integer/);
16
+ expect(() => validatePeekTimeSec(1.5)).toThrow(/positive integer/);
17
+ expect(() => validatePeekTimeSec(61)).toThrow(/positive integer/);
18
+ });
19
+
20
+ it('keeps the first 50 messages and marks truncation when later messages are dropped', () => {
21
+ const process: PeekProcessResult = {
22
+ pid: 123,
23
+ agent: 'codex',
24
+ status: 'running',
25
+ messages: [],
26
+ truncated: false,
27
+ error: null,
28
+ };
29
+
30
+ appendPeekMessages(
31
+ process,
32
+ Array.from({ length: 55 }, (_, index) => ({
33
+ ts: '2026-04-11T12:34:56.789Z',
34
+ text: `message ${index}`,
35
+ })),
36
+ );
37
+
38
+ expect(process.messages).toHaveLength(50);
39
+ expect(process.messages[0].text).toBe('message 0');
40
+ expect(process.messages[49].text).toBe('message 49');
41
+ expect(process.truncated).toBe(true);
42
+ });
43
+ });
@@ -118,6 +118,190 @@ describe('Process Management Tests', () => {
118
118
  expect(response.message).toBe('claude process started successfully');
119
119
  });
120
120
 
121
+ it('should peek only natural-language messages observed after registration', async () => {
122
+ const { handlers } = await setupServer();
123
+
124
+ const mockProcess = new EventEmitter() as any;
125
+ mockProcess.pid = 12345;
126
+ mockProcess.stdout = new EventEmitter();
127
+ mockProcess.stderr = new EventEmitter();
128
+ mockProcess.kill = vi.fn();
129
+
130
+ mockSpawn.mockReturnValue(mockProcess);
131
+
132
+ const callToolHandler = handlers.get('callTool')!;
133
+ await callToolHandler!({
134
+ params: {
135
+ name: 'run',
136
+ arguments: {
137
+ prompt: 'test prompt',
138
+ workFolder: '/tmp'
139
+ }
140
+ }
141
+ });
142
+
143
+ mockProcess.stdout.emit('data', '{"type":"assistant","message":{"content":[{"type":"text","text":"old message"}]}}\n');
144
+
145
+ const peekPromise = callToolHandler!({
146
+ params: {
147
+ name: 'peek',
148
+ arguments: {
149
+ pids: [12345, 12345, 99999],
150
+ peek_time_sec: 1,
151
+ }
152
+ }
153
+ });
154
+
155
+ setTimeout(() => {
156
+ mockProcess.stdout.emit('data', '{"type":"assistant","message":{"content":[{"type":"text","text":"new message"},{"type":"tool_use","id":"tool-1","name":"Read","input":{"file_path":"/tmp/a"}}]}}\n');
157
+ mockProcess.stdout.emit('data', '{"type":"user","message":{"content":[{"type":"tool_result","tool_use_id":"tool-1","content":"secret"}]}}\n');
158
+ mockProcess.emit('close', 0);
159
+ }, 10);
160
+
161
+ const result = await peekPromise;
162
+ const response = JSON.parse(result.content[0].text);
163
+
164
+ expect(response.processes).toHaveLength(2);
165
+ expect(response.processes[0]).toMatchObject({
166
+ pid: 12345,
167
+ agent: 'claude',
168
+ status: 'completed',
169
+ messages: [
170
+ {
171
+ ts: expect.any(String),
172
+ text: 'new message',
173
+ },
174
+ ],
175
+ truncated: false,
176
+ error: null,
177
+ });
178
+ expect(response.processes[1]).toEqual({
179
+ pid: 99999,
180
+ agent: null,
181
+ status: 'not_found',
182
+ messages: [],
183
+ truncated: false,
184
+ error: 'process not found',
185
+ });
186
+ });
187
+
188
+ it('should peek OpenCode text events and exclude OpenCode tool output', async () => {
189
+ const { handlers } = await setupServer();
190
+
191
+ const mockProcess = new EventEmitter() as any;
192
+ mockProcess.pid = 12346;
193
+ mockProcess.stdout = new EventEmitter();
194
+ mockProcess.stderr = new EventEmitter();
195
+ mockProcess.kill = vi.fn();
196
+
197
+ mockSpawn.mockReturnValue(mockProcess);
198
+
199
+ const callToolHandler = handlers.get('callTool')!;
200
+ await callToolHandler!({
201
+ params: {
202
+ name: 'run',
203
+ arguments: {
204
+ prompt: 'opencode peek prompt',
205
+ workFolder: '/tmp',
206
+ model: 'opencode',
207
+ }
208
+ }
209
+ });
210
+
211
+ const peekPromise = callToolHandler!({
212
+ params: {
213
+ name: 'peek',
214
+ arguments: {
215
+ pids: [12346],
216
+ peek_time_sec: 1,
217
+ }
218
+ }
219
+ });
220
+
221
+ setTimeout(() => {
222
+ mockProcess.stdout.emit('data', '{"type":"text","timestamp":1775918783605,"sessionID":"ses-1","part":{"type":"text","text":"OpenCode visible text"}}\n');
223
+ mockProcess.stdout.emit('data', '{"type":"tool_use","timestamp":1775918783606,"sessionID":"ses-1","part":{"type":"tool","state":{"output":"secret command output"},"metadata":{"output":"secret metadata output"}}}\n');
224
+ mockProcess.emit('close', 0);
225
+ }, 10);
226
+
227
+ const result = await peekPromise;
228
+ const response = JSON.parse(result.content[0].text);
229
+
230
+ expect(response.processes).toHaveLength(1);
231
+ expect(response.processes[0]).toMatchObject({
232
+ pid: 12346,
233
+ agent: 'opencode',
234
+ status: 'completed',
235
+ messages: [
236
+ {
237
+ ts: expect.any(String),
238
+ text: 'OpenCode visible text',
239
+ },
240
+ ],
241
+ truncated: false,
242
+ error: null,
243
+ });
244
+ });
245
+
246
+ it('should peek Gemini assistant message events and exclude tool output', async () => {
247
+ const { handlers } = await setupServer();
248
+
249
+ const mockProcess = new EventEmitter() as any;
250
+ mockProcess.pid = 12347;
251
+ mockProcess.stdout = new EventEmitter();
252
+ mockProcess.stderr = new EventEmitter();
253
+ mockProcess.kill = vi.fn();
254
+
255
+ mockSpawn.mockReturnValue(mockProcess);
256
+
257
+ const callToolHandler = handlers.get('callTool')!;
258
+ await callToolHandler!({
259
+ params: {
260
+ name: 'run',
261
+ arguments: {
262
+ prompt: 'gemini peek prompt',
263
+ workFolder: '/tmp',
264
+ model: 'gemini-2.5-pro',
265
+ }
266
+ }
267
+ });
268
+
269
+ const peekPromise = callToolHandler!({
270
+ params: {
271
+ name: 'peek',
272
+ arguments: {
273
+ pids: [12347],
274
+ peek_time_sec: 1,
275
+ }
276
+ }
277
+ });
278
+
279
+ setTimeout(() => {
280
+ mockProcess.stdout.emit('data', '{"type":"message","timestamp":"2026-04-11T14:44:42.294Z","role":"user","content":"hidden user text"}\n');
281
+ mockProcess.stdout.emit('data', '{"type":"message","timestamp":"2026-04-11T14:44:53.820Z","role":"assistant","content":"Visible Gemini text","delta":true}\n');
282
+ mockProcess.stdout.emit('data', '{"type":"tool_result","timestamp":"2026-04-11T14:45:03.011Z","status":"success","output":"secret command output"}\n');
283
+ mockProcess.emit('close', 0);
284
+ }, 10);
285
+
286
+ const result = await peekPromise;
287
+ const response = JSON.parse(result.content[0].text);
288
+
289
+ expect(response.processes).toHaveLength(1);
290
+ expect(response.processes[0]).toMatchObject({
291
+ pid: 12347,
292
+ agent: 'gemini',
293
+ status: 'completed',
294
+ messages: [
295
+ {
296
+ ts: expect.any(String),
297
+ text: 'Visible Gemini text',
298
+ },
299
+ ],
300
+ truncated: false,
301
+ error: null,
302
+ });
303
+ });
304
+
121
305
  it('should handle process with model parameter', async () => {
122
306
  const { handlers } = await setupServer();
123
307
 
@@ -497,14 +497,15 @@ describe('ClaudeCodeServer Unit Tests', () => {
497
497
  const handler = listToolsCall[1];
498
498
  const result = await handler();
499
499
 
500
- expect(result.tools).toHaveLength(6);
500
+ expect(result.tools).toHaveLength(7);
501
501
  expect(result.tools[0].name).toBe('run');
502
502
  expect(result.tools[0].description).toContain('AI Agent Runner');
503
503
  expect(result.tools[1].name).toBe('list_processes');
504
504
  expect(result.tools[2].name).toBe('get_result');
505
505
  expect(result.tools[3].name).toBe('wait');
506
- expect(result.tools[4].name).toBe('kill_process');
507
- expect(result.tools[5].name).toBe('cleanup_processes');
506
+ expect(result.tools[4].name).toBe('peek');
507
+ expect(result.tools[5].name).toBe('kill_process');
508
+ expect(result.tools[6].name).toBe('cleanup_processes');
508
509
  });
509
510
 
510
511
  it('should handle CallToolRequest', async () => {
package/src/app/cli.ts CHANGED
@@ -2,12 +2,14 @@ import { runMcpServer } from './mcp.js';
2
2
  import { CliProcessService } from '../cli-process-service.js';
3
3
  import { getCliDoctorStatus } from '../cli-utils.js';
4
4
  import { getModelsPayload } from '../model-catalog.js';
5
+ import { validatePeekPids, validatePeekTimeSec } from '../peek.js';
5
6
 
6
7
  export const CLI_HELP_TEXT = `Usage: ai-cli <command> [options]
7
8
 
8
9
  Commands:
9
10
  run Start an AI CLI process in the background
10
11
  wait Wait for one or more pids
12
+ peek Observe new natural-language agent messages for a short window
11
13
  ps List tracked processes
12
14
  result Get the current result for a pid
13
15
  kill Terminate a tracked pid
@@ -58,6 +60,17 @@ Options:
58
60
  --help, -h Show this help message
59
61
  `;
60
62
 
63
+ export const PEEK_HELP_TEXT = `Usage: ai-cli peek <pid...> [options]
64
+
65
+ Observe new natural-language agent messages for a short one-shot window.
66
+ In v1, message extraction is supported for Codex, Claude, OpenCode, and Gemini; Forge returns status with messages: [].
67
+ This is not a history API, gapless streaming, or stdout/stderr tailing. No --follow mode is available in v1.
68
+
69
+ Options:
70
+ --time <seconds> Observation window in seconds. Defaults to 10, maximum 60
71
+ --help, -h Show this help message
72
+ `;
73
+
61
74
  export const KILL_HELP_TEXT = `Usage: ai-cli kill <pid>
62
75
 
63
76
  Terminate a tracked process.
@@ -118,6 +131,7 @@ interface CliDeps {
118
131
  listProcesses: () => Promise<any>;
119
132
  getProcessResult: (pid: number, verbose: boolean) => Promise<any>;
120
133
  waitForProcesses: (pids: number[], timeoutSeconds?: number, verbose?: boolean) => Promise<any>;
134
+ peekProcesses: (pids: number[], peekTimeSec?: number) => Promise<any>;
121
135
  killProcess: (pid: number) => Promise<any>;
122
136
  cleanupProcesses: () => Promise<any>;
123
137
  getDoctorStatus: () => any;
@@ -140,6 +154,7 @@ const defaultDeps: CliDeps = {
140
154
  listProcesses: () => getCliProcessService().listProcesses(),
141
155
  getProcessResult: (pid, verbose) => getCliProcessService().getProcessResult(pid, verbose),
142
156
  waitForProcesses: (pids, timeoutSeconds, verbose) => getCliProcessService().waitForProcesses(pids, timeoutSeconds, verbose),
157
+ peekProcesses: (pids, peekTimeSec) => getCliProcessService().peekProcesses(pids, peekTimeSec),
143
158
  killProcess: (pid) => getCliProcessService().killProcess(pid),
144
159
  cleanupProcesses: () => getCliProcessService().cleanupProcesses(),
145
160
  getDoctorStatus: () => getCliDoctorStatus(),
@@ -204,6 +219,10 @@ function hasHelpFlag(flags: Record<string, string>): boolean {
204
219
  return 'help' in flags || 'h' in flags;
205
220
  }
206
221
 
222
+ function parsePeekCliPids(values: string[]): number[] {
223
+ return validatePeekPids(values.map((value) => Number(value)));
224
+ }
225
+
207
226
  export async function runCli(argv: string[], deps: Partial<CliDeps> = {}): Promise<number> {
208
227
  const {
209
228
  stdout,
@@ -213,6 +232,7 @@ export async function runCli(argv: string[], deps: Partial<CliDeps> = {}): Promi
213
232
  listProcesses,
214
233
  getProcessResult,
215
234
  waitForProcesses,
235
+ peekProcesses,
216
236
  killProcess,
217
237
  cleanupProcesses,
218
238
  getDoctorStatus,
@@ -323,6 +343,34 @@ export async function runCli(argv: string[], deps: Partial<CliDeps> = {}): Promi
323
343
  return 0;
324
344
  }
325
345
 
346
+ if (command === 'peek') {
347
+ const { positionals, flags } = parseArgs(argv.slice(1));
348
+ if (hasHelpFlag(flags)) {
349
+ stdout(PEEK_HELP_TEXT);
350
+ return 0;
351
+ }
352
+ if ('follow' in flags) {
353
+ stderr('peek does not support --follow in v1\n');
354
+ stdout(CLI_HELP_TEXT);
355
+ return 1;
356
+ }
357
+
358
+ let pids: number[];
359
+ let peekTimeSec: number;
360
+ try {
361
+ pids = parsePeekCliPids(positionals);
362
+ const timeRaw = getFirstFlag(flags, ['time']);
363
+ peekTimeSec = validatePeekTimeSec(timeRaw === undefined ? undefined : Number(timeRaw));
364
+ } catch (error: any) {
365
+ stderr(`${error.message}\n`);
366
+ stdout(CLI_HELP_TEXT);
367
+ return 1;
368
+ }
369
+
370
+ writeJson(stdout, await peekProcesses(pids, peekTimeSec));
371
+ return 0;
372
+ }
373
+
326
374
  if (command === 'kill') {
327
375
  const { positionals, flags } = parseArgs(argv.slice(1));
328
376
  if (hasHelpFlag(flags)) {
package/src/app/mcp.ts CHANGED
@@ -10,6 +10,7 @@ import {
10
10
  import { spawn } from 'node:child_process';
11
11
  import { debugLog, findClaudeCli, findCodexCli, findForgeCli, findGeminiCli, findOpencodeCli } from '../cli-utils.js';
12
12
  import { getModelParameterDescription, getSupportedModelsDescription } from '../model-catalog.js';
13
+ import { validatePeekPids, validatePeekTimeSec } from '../peek.js';
13
14
  import { ProcessService } from '../process-service.js';
14
15
 
15
16
  // Server version - update this when releasing new versions
@@ -230,6 +231,25 @@ ${getSupportedModelsDescription()}
230
231
  required: ['pids'],
231
232
  },
232
233
  },
234
+ {
235
+ name: 'peek',
236
+ description: 'One-shot short observation window for running child agents. Returns only natural-language agent messages observed during this call; not a history API, not gapless streaming, and not stdout/stderr tailing. In v1, message extraction is supported for Codex, Claude, OpenCode, and Gemini; Forge returns status with messages: [].',
237
+ inputSchema: {
238
+ type: 'object',
239
+ properties: {
240
+ pids: {
241
+ type: 'array',
242
+ items: { type: 'number' },
243
+ description: 'Process IDs returned by run. Duplicates are deduplicated server-side, preserving first occurrence order. Unknown PIDs are returned per process as not_found.',
244
+ },
245
+ peek_time_sec: {
246
+ type: 'number',
247
+ description: 'Optional positive integer observation window in seconds. Defaults to 10; maximum is 60.',
248
+ },
249
+ },
250
+ required: ['pids'],
251
+ },
252
+ },
233
253
  {
234
254
  name: 'kill_process',
235
255
  description: 'Terminate a running AI agent process by PID.',
@@ -270,6 +290,8 @@ ${getSupportedModelsDescription()}
270
290
  return this.handleGetResult(toolArguments);
271
291
  case 'wait':
272
292
  return this.handleWait(toolArguments);
293
+ case 'peek':
294
+ return this.handlePeek(toolArguments);
273
295
  case 'kill_process':
274
296
  return this.handleKillProcess(toolArguments);
275
297
  case 'cleanup_processes':
@@ -359,6 +381,30 @@ ${getSupportedModelsDescription()}
359
381
  }
360
382
  }
361
383
 
384
+ private async handlePeek(toolArguments: any): Promise<ServerResult> {
385
+ let pids: number[];
386
+ let peekTimeSec: number;
387
+
388
+ try {
389
+ pids = validatePeekPids(toolArguments.pids);
390
+ peekTimeSec = validatePeekTimeSec(toolArguments.peek_time_sec);
391
+ } catch (error: any) {
392
+ throw new McpError(ErrorCode.InvalidParams, error.message);
393
+ }
394
+
395
+ try {
396
+ const response = await this.processService.peekProcesses(pids, peekTimeSec);
397
+ return {
398
+ content: [{
399
+ type: 'text',
400
+ text: JSON.stringify(response, null, 2)
401
+ }]
402
+ };
403
+ } catch (error: any) {
404
+ throw new McpError(ErrorCode.InternalError, `Failed to peek processes: ${error.message}`);
405
+ }
406
+ }
407
+
362
408
  private async handleKillProcess(toolArguments: any): Promise<ServerResult> {
363
409
  if (!toolArguments.pid || typeof toolArguments.pid !== 'number') {
364
410
  throw new McpError(ErrorCode.InvalidParams, 'Missing or invalid required parameter: pid');
@@ -216,10 +216,10 @@ export function buildCliCommand(options: BuildCliCommandOptions): CliCommand {
216
216
  args.push('--model', resolvedModel);
217
217
  }
218
218
 
219
- args.push('--skip-git-repo-check', '--full-auto', '--json', prompt);
219
+ args.push('--skip-git-repo-check', '--dangerously-bypass-approvals-and-sandbox', '--json', prompt);
220
220
  } else if (agent === 'gemini') {
221
221
  cliPath = options.cliPaths.gemini;
222
- args = ['-y', '--output-format', 'json'];
222
+ args = ['-y', '--output-format', 'stream-json'];
223
223
 
224
224
  if (options.session_id && typeof options.session_id === 'string') {
225
225
  args.push('-r', options.session_id);