ai-cli-mcp 2.17.0 → 2.19.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -19,10 +19,10 @@ import { join, basename, dirname } from 'node:path';
19
19
  import { homedir } from 'node:os';
20
20
  import { buildCliCommand, type BuildCliCommandOptions } from './cli-builder.js';
21
21
  import { findClaudeCli, findCodexCli, findForgeCli, findGeminiCli, findOpencodeCli } from './cli-utils.js';
22
- import { parseClaudeOutput, parseCodexOutput, parseForgeOutput, parseGeminiOutput, parseOpenCodeOutput, PeekMessageExtractor } from './parsers.js';
22
+ import { parseClaudeOutput, parseCodexOutput, parseForgeOutput, parseGeminiOutput, parseOpenCodeOutput, PeekEventExtractor } from './parsers.js';
23
23
  import { buildProcessResult } from './process-result.js';
24
24
  import {
25
- appendPeekMessages,
25
+ appendPeekEvents,
26
26
  buildNotFoundPeekProcess,
27
27
  observedDurationSec,
28
28
  validatePeekPids,
@@ -255,15 +255,15 @@ export class CliProcessService {
255
255
  }
256
256
  }
257
257
 
258
- async peekProcesses(pids: number[], peekTimeSec = 10): Promise<PeekResponse> {
258
+ async peekProcesses(pids: number[], peekTimeSec = 10, includeToolCalls = false): Promise<PeekResponse> {
259
259
  const targetPids = validatePeekPids(pids);
260
260
  const targetPeekTimeSec = validatePeekTimeSec(peekTimeSec);
261
261
  const processes: PeekProcessResult[] = [];
262
262
  const observers: Array<{
263
263
  process: StoredProcess;
264
264
  result: PeekProcessResult;
265
- stdoutExtractor: PeekMessageExtractor;
266
- stderrExtractor: PeekMessageExtractor;
265
+ stdoutExtractor: PeekEventExtractor;
266
+ stderrExtractor: PeekEventExtractor;
267
267
  stdoutOffset: number;
268
268
  stderrOffset: number;
269
269
  }> = [];
@@ -281,7 +281,7 @@ export class CliProcessService {
281
281
  pid,
282
282
  agent: process.toolType,
283
283
  status: process.status,
284
- messages: [],
284
+ events: [],
285
285
  truncated: false,
286
286
  error: null,
287
287
  };
@@ -289,8 +289,8 @@ export class CliProcessService {
289
289
  observers.push({
290
290
  process,
291
291
  result,
292
- stdoutExtractor: new PeekMessageExtractor(process.toolType),
293
- stderrExtractor: new PeekMessageExtractor(process.toolType),
292
+ stdoutExtractor: new PeekEventExtractor(process.toolType, { includeToolCalls, source: 'stdout' }),
293
+ stderrExtractor: new PeekEventExtractor(process.toolType, { includeToolCalls, source: 'stderr' }),
294
294
  stdoutOffset: this.fileSizeSafe(process.stdoutPath),
295
295
  stderrOffset: this.fileSizeSafe(process.stderrPath),
296
296
  });
@@ -307,11 +307,11 @@ export class CliProcessService {
307
307
  for (const observer of observers) {
308
308
  const stdoutRead = this.readTextFromOffset(observer.process.stdoutPath, observer.stdoutOffset);
309
309
  observer.stdoutOffset = stdoutRead.offset;
310
- appendPeekMessages(observer.result, observer.stdoutExtractor.push(stdoutRead.text, observedAt));
310
+ appendPeekEvents(observer.result, observer.stdoutExtractor.push(stdoutRead.text, observedAt));
311
311
 
312
312
  const stderrRead = this.readTextFromOffset(observer.process.stderrPath, observer.stderrOffset);
313
313
  observer.stderrOffset = stderrRead.offset;
314
- appendPeekMessages(observer.result, observer.stderrExtractor.push(stderrRead.text, observedAt));
314
+ appendPeekEvents(observer.result, observer.stderrExtractor.push(stderrRead.text, observedAt));
315
315
 
316
316
  observer.process = this.refreshStatus(this.readProcess(observer.process.pid));
317
317
  observer.result.status = observer.process.status;
@@ -335,8 +335,9 @@ export class CliProcessService {
335
335
  for (const observer of observers) {
336
336
  observer.process = this.refreshStatus(this.readProcess(observer.process.pid));
337
337
  observer.result.status = observer.process.status;
338
- appendPeekMessages(observer.result, observer.stdoutExtractor.flush(flushTs));
339
- appendPeekMessages(observer.result, observer.stderrExtractor.flush(flushTs));
338
+ const terminal = observer.process.status !== 'running';
339
+ appendPeekEvents(observer.result, observer.stdoutExtractor.flush(flushTs, { terminal }));
340
+ appendPeekEvents(observer.result, observer.stderrExtractor.flush(flushTs, { terminal }));
340
341
  }
341
342
 
342
343
  return {
package/src/parsers.ts CHANGED
@@ -5,8 +5,61 @@ export interface PeekMessage {
5
5
  text: string;
6
6
  }
7
7
 
8
+ export type PeekToolCallStatus = 'success' | 'failed' | 'cancelled' | 'unknown';
9
+
10
+ export type PeekEvent =
11
+ | { kind: 'message'; ts: string; text: string }
12
+ | {
13
+ kind: 'tool_call';
14
+ ts: string;
15
+ phase: 'started' | 'completed';
16
+ tool: string;
17
+ summary: string;
18
+ id?: string;
19
+ status?: PeekToolCallStatus;
20
+ server?: string;
21
+ exit_code?: number;
22
+ duration_ms?: number;
23
+ summary_truncated?: boolean;
24
+ };
25
+
26
+ type PeekToolCallEvent = Extract<PeekEvent, { kind: 'tool_call' }>;
27
+
8
28
  type PeekAgent = 'claude' | 'codex' | string | null;
9
29
 
30
+ interface PeekEventExtractorOptions {
31
+ includeToolCalls?: boolean;
32
+ source?: 'stdout' | 'stderr';
33
+ }
34
+
35
+ interface PeekFlushOptions {
36
+ terminal?: boolean;
37
+ }
38
+
39
+ interface ToolSummary {
40
+ summary: string;
41
+ server?: string;
42
+ summary_truncated?: boolean;
43
+ }
44
+
45
+ interface ToolCallMemory {
46
+ tool: string;
47
+ server?: string;
48
+ summary: string;
49
+ summary_truncated?: boolean;
50
+ }
51
+
52
+ interface PendingForgeTool {
53
+ id: string;
54
+ tool: string;
55
+ summary: string;
56
+ summary_truncated?: boolean;
57
+ }
58
+
59
+ const PEEK_TOOL_SUMMARY_MAX_LENGTH = 200;
60
+ const FORGE_EXECUTE_PATTERN = /^● \[[^\]]+\] Execute \[([^\]]*)\]\s+(.+)$/;
61
+ const FORGE_FINISHED_PATTERN = /^● \[[^\]]+\] Finished(?:\s+\S+)?\s*$/;
62
+
10
63
  function isGeminiAssistantMessageEvent(parsed: any): boolean {
11
64
  return parsed.type === 'message' && parsed.role === 'assistant' && typeof parsed.content === 'string';
12
65
  }
@@ -25,37 +78,300 @@ function isGeminiStreamJsonEvent(parsed: any): boolean {
25
78
  return parsed && typeof parsed === 'object' && !Array.isArray(parsed) && GEMINI_STREAM_EVENT_TYPES.has(parsed.type);
26
79
  }
27
80
 
28
- function extractPeekMessagesFromParsedEvent(agent: PeekAgent, parsed: any, observedAt: string): PeekMessage[] {
81
+ function oneLine(value: unknown): string {
82
+ return String(value ?? '').replace(/\s+/g, ' ').trim();
83
+ }
84
+
85
+ function boundedSummary(value: string): { summary: string; summary_truncated?: boolean } {
86
+ const summary = oneLine(value);
87
+ if (summary.length <= PEEK_TOOL_SUMMARY_MAX_LENGTH) {
88
+ return { summary };
89
+ }
90
+
91
+ return {
92
+ summary: `${summary.slice(0, PEEK_TOOL_SUMMARY_MAX_LENGTH - 3)}...`,
93
+ summary_truncated: true,
94
+ };
95
+ }
96
+
97
+ function normalizeMcpToolName(tool: string, explicitServer?: string): ToolSummary | null {
98
+ if (explicitServer) {
99
+ return {
100
+ server: explicitServer,
101
+ ...boundedSummary(`${explicitServer}.${tool}`),
102
+ };
103
+ }
104
+
105
+ const mcpDouble = tool.match(/^mcp__([^_]+)__(.+)$/);
106
+ if (mcpDouble) {
107
+ return {
108
+ server: mcpDouble[1],
109
+ ...boundedSummary(`${mcpDouble[1]}.${mcpDouble[2]}`),
110
+ };
111
+ }
112
+
113
+ const mcpSingle = tool.match(/^mcp_([^_]+)_(.+)$/);
114
+ if (mcpSingle) {
115
+ return {
116
+ server: mcpSingle[1],
117
+ ...boundedSummary(`${mcpSingle[1]}.${mcpSingle[2]}`),
118
+ };
119
+ }
120
+
121
+ const acmShort = tool.match(/^acm_(.+)$/);
122
+ if (acmShort) {
123
+ return {
124
+ server: 'acm',
125
+ ...boundedSummary(`acm.${acmShort[1]}`),
126
+ };
127
+ }
128
+
129
+ return null;
130
+ }
131
+
132
+ function buildToolSummary(tool: string, options: { server?: string; command?: unknown } = {}): ToolSummary {
133
+ if (typeof options.command === 'string' && options.command.trim()) {
134
+ return boundedSummary(options.command);
135
+ }
136
+
137
+ const mcpSummary = normalizeMcpToolName(tool, options.server);
138
+ if (mcpSummary) {
139
+ return mcpSummary;
140
+ }
141
+
142
+ return boundedSummary(tool || 'tool_call');
143
+ }
144
+
145
+ function normalizeToolStatus(rawStatus: unknown, exitCode?: number, defaultStatus: PeekToolCallStatus = 'unknown'): PeekToolCallStatus {
146
+ if (typeof exitCode === 'number') {
147
+ return exitCode === 0 ? 'success' : 'failed';
148
+ }
149
+
150
+ const status = typeof rawStatus === 'string' ? rawStatus.toLowerCase() : '';
151
+ if (['success', 'succeeded', 'ok', 'completed'].includes(status)) {
152
+ return 'success';
153
+ }
154
+ if (['failed', 'failure', 'error', 'errored'].includes(status)) {
155
+ return 'failed';
156
+ }
157
+ if (['cancelled', 'canceled'].includes(status)) {
158
+ return 'cancelled';
159
+ }
160
+ return defaultStatus;
161
+ }
162
+
163
+ function createToolCallEvent(params: {
164
+ ts: string;
165
+ phase: 'started' | 'completed';
166
+ tool: string;
167
+ id?: string;
168
+ server?: string;
169
+ command?: unknown;
170
+ status?: unknown;
171
+ defaultStatus?: PeekToolCallStatus;
172
+ exit_code?: number;
173
+ duration_ms?: number;
174
+ }): PeekToolCallEvent {
175
+ const tool = params.tool || 'tool_call';
176
+ const summary = buildToolSummary(tool, { server: params.server, command: params.command });
177
+ const event: PeekToolCallEvent = {
178
+ kind: 'tool_call',
179
+ ts: params.ts,
180
+ phase: params.phase,
181
+ tool,
182
+ summary: summary.summary,
183
+ };
184
+
185
+ if (params.id) {
186
+ event.id = params.id;
187
+ }
188
+ if (summary.server) {
189
+ event.server = summary.server;
190
+ } else if (params.server) {
191
+ event.server = params.server;
192
+ }
193
+ if (summary.summary_truncated) {
194
+ event.summary_truncated = true;
195
+ }
196
+ if (params.phase === 'completed') {
197
+ event.status = normalizeToolStatus(params.status, params.exit_code, params.defaultStatus);
198
+ if (typeof params.exit_code === 'number') {
199
+ event.exit_code = params.exit_code;
200
+ }
201
+ if (typeof params.duration_ms === 'number' && Number.isFinite(params.duration_ms)) {
202
+ event.duration_ms = params.duration_ms;
203
+ }
204
+ }
205
+
206
+ return event;
207
+ }
208
+
209
+ function rememberToolCall(event: PeekEvent, memory: Map<string, ToolCallMemory>): void {
210
+ if (event.kind !== 'tool_call' || !event.id) {
211
+ return;
212
+ }
213
+
214
+ memory.set(event.id, {
215
+ tool: event.tool,
216
+ server: event.server,
217
+ summary: event.summary,
218
+ summary_truncated: event.summary_truncated,
219
+ });
220
+ }
221
+
222
+ function createRememberedCompletion(params: {
223
+ ts: string;
224
+ id?: string;
225
+ memory: Map<string, ToolCallMemory>;
226
+ fallbackTool: string;
227
+ status?: unknown;
228
+ defaultStatus?: PeekToolCallStatus;
229
+ }): PeekEvent {
230
+ const remembered = params.id ? params.memory.get(params.id) : undefined;
231
+ const event = createToolCallEvent({
232
+ ts: params.ts,
233
+ phase: 'completed',
234
+ id: params.id,
235
+ tool: remembered?.tool || params.fallbackTool,
236
+ server: remembered?.server,
237
+ status: params.status,
238
+ defaultStatus: params.defaultStatus,
239
+ });
240
+
241
+ if (remembered) {
242
+ event.summary = remembered.summary;
243
+ if (remembered.summary_truncated) {
244
+ event.summary_truncated = true;
245
+ }
246
+ }
247
+
248
+ return event;
249
+ }
250
+
251
+ function extractPeekEventsFromParsedEvent(agent: PeekAgent, parsed: any, observedAt: string, includeToolCalls: boolean, memory: Map<string, ToolCallMemory>): PeekEvent[] {
29
252
  if (agent === 'codex') {
30
253
  if (parsed.item?.type === 'agent_message' && typeof parsed.item.text === 'string' && parsed.item.text.trim()) {
31
- return [{ ts: observedAt, text: parsed.item.text }];
254
+ return [{ kind: 'message', ts: observedAt, text: parsed.item.text }];
32
255
  }
33
256
  if (parsed.msg?.type === 'agent_message' && typeof parsed.msg.message === 'string' && parsed.msg.message.trim()) {
34
- return [{ ts: observedAt, text: parsed.msg.message }];
257
+ return [{ kind: 'message', ts: observedAt, text: parsed.msg.message }];
258
+ }
259
+ if (includeToolCalls && (parsed.type === 'item.started' || parsed.type === 'item.completed')) {
260
+ const item = parsed.item;
261
+ if (item?.type === 'command_execution') {
262
+ const event = createToolCallEvent({
263
+ ts: observedAt,
264
+ phase: parsed.type === 'item.started' ? 'started' : 'completed',
265
+ id: item.id,
266
+ tool: 'command_execution',
267
+ command: item.command,
268
+ status: item.status || item.error,
269
+ exit_code: typeof item.exit_code === 'number' ? item.exit_code : undefined,
270
+ defaultStatus: parsed.type === 'item.completed' ? 'success' : 'unknown',
271
+ });
272
+ rememberToolCall(event, memory);
273
+ return [event];
274
+ }
275
+ if (item?.type === 'mcp_tool_call') {
276
+ const event = createToolCallEvent({
277
+ ts: observedAt,
278
+ phase: parsed.type === 'item.started' ? 'started' : 'completed',
279
+ id: item.id,
280
+ tool: item.tool || 'mcp_tool_call',
281
+ server: item.server,
282
+ status: item.status || item.error,
283
+ defaultStatus: parsed.type === 'item.completed' ? 'success' : 'unknown',
284
+ });
285
+ rememberToolCall(event, memory);
286
+ return [event];
287
+ }
35
288
  }
36
289
  return [];
37
290
  }
38
291
 
39
- if (agent === 'claude' && parsed.type === 'assistant' && Array.isArray(parsed.message?.content)) {
40
- return parsed.message.content
41
- .filter((content: any) => content?.type === 'text' && typeof content.text === 'string' && content.text.trim())
42
- .map((content: any) => ({ ts: observedAt, text: content.text }));
292
+ if (agent === 'claude') {
293
+ if (parsed.type === 'assistant' && Array.isArray(parsed.message?.content)) {
294
+ const events: PeekEvent[] = [];
295
+ for (const content of parsed.message.content) {
296
+ if (content?.type === 'text' && typeof content.text === 'string' && content.text.trim()) {
297
+ events.push({ kind: 'message', ts: observedAt, text: content.text });
298
+ } else if (includeToolCalls && content?.type === 'tool_use') {
299
+ const event = createToolCallEvent({
300
+ ts: observedAt,
301
+ phase: 'started',
302
+ id: content.id,
303
+ tool: content.name || 'tool_use',
304
+ command: content.input?.command,
305
+ });
306
+ rememberToolCall(event, memory);
307
+ events.push(event);
308
+ }
309
+ }
310
+ return events;
311
+ }
312
+ if (includeToolCalls && parsed.type === 'user' && Array.isArray(parsed.message?.content)) {
313
+ const events: PeekEvent[] = [];
314
+ for (const content of parsed.message.content) {
315
+ if (content?.type === 'tool_result') {
316
+ events.push(createRememberedCompletion({
317
+ ts: observedAt,
318
+ id: content.tool_use_id,
319
+ memory,
320
+ fallbackTool: 'tool_result',
321
+ status: content.is_error === true ? 'failed' : undefined,
322
+ defaultStatus: content.is_error === true ? 'failed' : 'success',
323
+ }));
324
+ }
325
+ }
326
+ return events;
327
+ }
328
+ return [];
43
329
  }
44
330
 
45
331
  if (agent === 'opencode' && parsed.type === 'text' && parsed.part?.type === 'text' && typeof parsed.part.text === 'string' && parsed.part.text.trim()) {
46
- return [{ ts: observedAt, text: parsed.part.text }];
332
+ return [{ kind: 'message', ts: observedAt, text: parsed.part.text }];
333
+ }
334
+
335
+ if (agent === 'opencode' && includeToolCalls && parsed.type === 'tool_use' && parsed.part?.type === 'tool') {
336
+ const state = parsed.part.state || {};
337
+ const start = state.time?.start;
338
+ const end = state.time?.end;
339
+ const event = createToolCallEvent({
340
+ ts: observedAt,
341
+ phase: state.status === 'running' || state.status === 'pending' ? 'started' : 'completed',
342
+ id: parsed.part.callID,
343
+ tool: parsed.part.tool || 'tool_use',
344
+ command: state.input?.command,
345
+ status: state.status,
346
+ defaultStatus: state.status === 'completed' ? 'success' : 'unknown',
347
+ duration_ms: typeof start === 'number' && typeof end === 'number' ? end - start : undefined,
348
+ });
349
+ rememberToolCall(event, memory);
350
+ return [event];
47
351
  }
48
352
 
49
353
  return [];
50
354
  }
51
355
 
52
- export class PeekMessageExtractor {
356
+ export class PeekEventExtractor {
53
357
  private pending = '';
54
358
  private geminiAssistantBuffer = '';
359
+ private readonly includeToolCalls: boolean;
360
+ private readonly source: 'stdout' | 'stderr';
361
+ private readonly toolMemory = new Map<string, ToolCallMemory>();
362
+ private forgePendingTool: PendingForgeTool | null = null;
363
+ private forgeToolSequence = 0;
364
+
365
+ constructor(private readonly agent: PeekAgent, options: PeekEventExtractorOptions = {}) {
366
+ this.includeToolCalls = options.includeToolCalls === true;
367
+ this.source = options.source || 'stdout';
368
+ }
55
369
 
56
- constructor(private readonly agent: PeekAgent) {}
370
+ push(chunk: string, observedAt = new Date().toISOString()): PeekEvent[] {
371
+ if (this.agent === 'forge' && this.source === 'stderr') {
372
+ return [];
373
+ }
57
374
 
58
- push(chunk: string, observedAt = new Date().toISOString()): PeekMessage[] {
59
375
  if (!chunk) {
60
376
  return [];
61
377
  }
@@ -65,21 +381,33 @@ export class PeekMessageExtractor {
65
381
  return this.extractLines(lines, observedAt);
66
382
  }
67
383
 
68
- flush(observedAt = new Date().toISOString()): PeekMessage[] {
69
- const messages: PeekMessage[] = [];
384
+ flush(observedAt = new Date().toISOString(), options: PeekFlushOptions = {}): PeekEvent[] {
385
+ if (this.agent === 'forge' && this.source === 'stderr') {
386
+ this.pending = '';
387
+ return [];
388
+ }
389
+
390
+ const events: PeekEvent[] = [];
70
391
 
71
392
  if (this.pending) {
72
- const line = this.pending;
73
- this.pending = '';
74
- messages.push(...this.extractLines([line], observedAt));
393
+ if (this.agent !== 'forge' || options.terminal === true) {
394
+ const line = this.pending;
395
+ this.pending = '';
396
+ events.push(...this.extractLines([line], observedAt));
397
+ }
75
398
  }
76
399
 
77
- messages.push(...this.flushGeminiAssistantBuffer(observedAt));
78
- return messages;
400
+ events.push(...this.flushGeminiAssistantBuffer(observedAt));
401
+ events.push(...this.flushForgePendingTool(observedAt, options.terminal === true));
402
+ return events;
79
403
  }
80
404
 
81
- private extractLines(lines: string[], observedAt: string): PeekMessage[] {
82
- const messages: PeekMessage[] = [];
405
+ private extractLines(lines: string[], observedAt: string): PeekEvent[] {
406
+ if (this.agent === 'forge') {
407
+ return this.extractForgeLines(lines, observedAt);
408
+ }
409
+
410
+ const events: PeekEvent[] = [];
83
411
 
84
412
  for (const line of lines) {
85
413
  if (!line.trim()) {
@@ -87,30 +415,119 @@ export class PeekMessageExtractor {
87
415
  }
88
416
 
89
417
  try {
90
- messages.push(...this.extractParsedEvent(JSON.parse(line), observedAt));
418
+ events.push(...this.extractParsedEvent(JSON.parse(line), observedAt));
91
419
  } catch {
92
420
  debugLog(`[Debug] Skipping invalid peek JSON line: ${line}`);
93
- messages.push(...this.flushGeminiAssistantBuffer(observedAt));
421
+ events.push(...this.flushGeminiAssistantBuffer(observedAt));
94
422
  }
95
423
  }
96
424
 
97
- return messages;
425
+ return events;
98
426
  }
99
427
 
100
- private extractParsedEvent(parsed: any, observedAt: string): PeekMessage[] {
101
- if (this.agent !== 'gemini') {
102
- return extractPeekMessagesFromParsedEvent(this.agent, parsed, observedAt);
428
+ private extractForgeLines(lines: string[], observedAt: string): PeekEvent[] {
429
+ const events: PeekEvent[] = [];
430
+
431
+ for (const line of lines) {
432
+ if (!line.trim()) {
433
+ continue;
434
+ }
435
+
436
+ const summary = this.extractForgeMessage(line, 'Summary:');
437
+ if (summary !== null) {
438
+ events.push({ kind: 'message', ts: observedAt, text: summary });
439
+ continue;
440
+ }
441
+
442
+ const completed = this.extractForgeMessage(line, 'Completed successfully:');
443
+ if (completed !== null) {
444
+ events.push({ kind: 'message', ts: observedAt, text: completed });
445
+ continue;
446
+ }
447
+
448
+ if (this.includeToolCalls) {
449
+ const executeMatch = line.match(FORGE_EXECUTE_PATTERN);
450
+ if (executeMatch) {
451
+ events.push(...this.completeForgePendingTool(observedAt));
452
+ const [, rawTool, rawSummary] = executeMatch;
453
+ const tool = rawTool.trim() && !/\s/.test(rawTool.trim()) ? rawTool.trim() : 'shell';
454
+ const event = createToolCallEvent({
455
+ ts: observedAt,
456
+ phase: 'started',
457
+ id: `forge_${this.forgeToolSequence++}`,
458
+ tool,
459
+ command: rawSummary,
460
+ });
461
+ this.forgePendingTool = {
462
+ id: event.id!,
463
+ tool: event.tool,
464
+ summary: event.summary,
465
+ summary_truncated: event.summary_truncated,
466
+ };
467
+ events.push(event);
468
+ continue;
469
+ }
470
+
471
+ if (FORGE_FINISHED_PATTERN.test(line)) {
472
+ events.push(...this.completeForgePendingTool(observedAt));
473
+ }
474
+ }
475
+ }
476
+
477
+ return events;
478
+ }
479
+
480
+ private extractForgeMessage(line: string, prefix: string): string | null {
481
+ if (!line.startsWith(prefix)) {
482
+ return null;
103
483
  }
104
484
 
485
+ const text = line.slice(prefix.length).trim();
486
+ return text || null;
487
+ }
488
+
489
+ private extractParsedEvent(parsed: any, observedAt: string): PeekEvent[] {
490
+ if (this.agent === 'gemini') {
491
+ const events = this.extractGeminiParsedEvent(parsed, observedAt);
492
+ return events;
493
+ }
494
+
495
+ return extractPeekEventsFromParsedEvent(this.agent, parsed, observedAt, this.includeToolCalls, this.toolMemory);
496
+ }
497
+
498
+ private extractGeminiParsedEvent(parsed: any, observedAt: string): PeekEvent[] {
105
499
  if (isGeminiAssistantMessageEvent(parsed)) {
106
500
  this.geminiAssistantBuffer += parsed.content;
107
501
  return [];
108
502
  }
109
503
 
110
- return this.flushGeminiAssistantBuffer(observedAt);
504
+ const events = this.flushGeminiAssistantBuffer(observedAt);
505
+
506
+ if (this.includeToolCalls && parsed.type === 'tool_use') {
507
+ const event = createToolCallEvent({
508
+ ts: observedAt,
509
+ phase: 'started',
510
+ id: parsed.tool_id,
511
+ tool: parsed.tool_name || parsed.name || 'tool_use',
512
+ command: parsed.parameters?.command,
513
+ });
514
+ rememberToolCall(event, this.toolMemory);
515
+ events.push(event);
516
+ } else if (this.includeToolCalls && parsed.type === 'tool_result') {
517
+ events.push(createRememberedCompletion({
518
+ ts: observedAt,
519
+ id: parsed.tool_id,
520
+ memory: this.toolMemory,
521
+ fallbackTool: parsed.tool_name || parsed.name || 'tool_result',
522
+ status: parsed.status,
523
+ defaultStatus: 'unknown',
524
+ }));
525
+ }
526
+
527
+ return events;
111
528
  }
112
529
 
113
- private flushGeminiAssistantBuffer(observedAt: string): PeekMessage[] {
530
+ private flushGeminiAssistantBuffer(observedAt: string): PeekEvent[] {
114
531
  if (this.agent !== 'gemini' || !this.geminiAssistantBuffer) {
115
532
  return [];
116
533
  }
@@ -122,7 +539,59 @@ export class PeekMessageExtractor {
122
539
  return [];
123
540
  }
124
541
 
125
- return [{ ts: observedAt, text }];
542
+ return [{ kind: 'message', ts: observedAt, text }];
543
+ }
544
+
545
+ private completeForgePendingTool(observedAt: string): PeekEvent[] {
546
+ if (!this.forgePendingTool) {
547
+ return [];
548
+ }
549
+
550
+ const pending = this.forgePendingTool;
551
+ this.forgePendingTool = null;
552
+ const event = createToolCallEvent({
553
+ ts: observedAt,
554
+ phase: 'completed',
555
+ id: pending.id,
556
+ tool: pending.tool,
557
+ status: 'unknown',
558
+ defaultStatus: 'unknown',
559
+ });
560
+ event.summary = pending.summary;
561
+ if (pending.summary_truncated) {
562
+ event.summary_truncated = true;
563
+ }
564
+ return [event];
565
+ }
566
+
567
+ private flushForgePendingTool(observedAt: string, terminal: boolean): PeekEvent[] {
568
+ if (this.agent !== 'forge' || !terminal) {
569
+ return [];
570
+ }
571
+
572
+ return this.completeForgePendingTool(observedAt);
573
+ }
574
+ }
575
+
576
+ export class PeekMessageExtractor {
577
+ private readonly extractor: PeekEventExtractor;
578
+
579
+ constructor(agent: PeekAgent) {
580
+ this.extractor = new PeekEventExtractor(agent, { includeToolCalls: false });
581
+ }
582
+
583
+ push(chunk: string, observedAt = new Date().toISOString()): PeekMessage[] {
584
+ return this.toMessages(this.extractor.push(chunk, observedAt));
585
+ }
586
+
587
+ flush(observedAt = new Date().toISOString(), options: PeekFlushOptions = {}): PeekMessage[] {
588
+ return this.toMessages(this.extractor.flush(observedAt, options));
589
+ }
590
+
591
+ private toMessages(events: PeekEvent[]): PeekMessage[] {
592
+ return events
593
+ .filter((event): event is Extract<PeekEvent, { kind: 'message' }> => event.kind === 'message')
594
+ .map((event) => ({ ts: event.ts, text: event.text }));
126
595
  }
127
596
  }
128
597