ai-cli-mcp 2.17.0 → 2.19.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +14 -0
- package/README.ja.md +13 -9
- package/README.md +13 -9
- package/dist/__tests__/app-cli.test.js +3 -3
- package/dist/__tests__/cli-process-service.test.js +3 -2
- package/dist/__tests__/mcp-contract.test.js +1 -0
- package/dist/__tests__/parsers.test.js +290 -1
- package/dist/__tests__/peek.test.js +8 -7
- package/dist/__tests__/process-management.test.js +156 -4
- package/dist/app/cli.js +6 -5
- package/dist/app/mcp.js +11 -2
- package/dist/cli-process-service.js +11 -10
- package/dist/parsers.js +382 -25
- package/dist/peek.js +8 -5
- package/dist/process-service.js +11 -10
- package/package.json +1 -1
- package/src/__tests__/app-cli.test.ts +3 -3
- package/src/__tests__/cli-process-service.test.ts +3 -2
- package/src/__tests__/mcp-contract.test.ts +1 -0
- package/src/__tests__/parsers.test.ts +321 -1
- package/src/__tests__/peek.test.ts +8 -7
- package/src/__tests__/process-management.test.ts +172 -4
- package/src/app/cli.ts +7 -6
- package/src/app/mcp.ts +11 -2
- package/src/cli-process-service.ts +13 -12
- package/src/parsers.ts +498 -29
- package/src/peek.ts +14 -7
- package/src/process-service.ts +13 -12
|
@@ -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,
|
|
22
|
+
import { parseClaudeOutput, parseCodexOutput, parseForgeOutput, parseGeminiOutput, parseOpenCodeOutput, PeekEventExtractor } from './parsers.js';
|
|
23
23
|
import { buildProcessResult } from './process-result.js';
|
|
24
24
|
import {
|
|
25
|
-
|
|
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:
|
|
266
|
-
stderrExtractor:
|
|
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
|
-
|
|
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
|
|
293
|
-
stderrExtractor: new
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
339
|
-
|
|
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
|
|
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'
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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
|
|
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
|
-
|
|
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()):
|
|
69
|
-
|
|
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
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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
|
-
|
|
78
|
-
|
|
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):
|
|
82
|
-
|
|
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
|
-
|
|
418
|
+
events.push(...this.extractParsedEvent(JSON.parse(line), observedAt));
|
|
91
419
|
} catch {
|
|
92
420
|
debugLog(`[Debug] Skipping invalid peek JSON line: ${line}`);
|
|
93
|
-
|
|
421
|
+
events.push(...this.flushGeminiAssistantBuffer(observedAt));
|
|
94
422
|
}
|
|
95
423
|
}
|
|
96
424
|
|
|
97
|
-
return
|
|
425
|
+
return events;
|
|
98
426
|
}
|
|
99
427
|
|
|
100
|
-
private
|
|
101
|
-
|
|
102
|
-
|
|
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
|
-
|
|
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):
|
|
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
|
|