ai-cli-mcp 2.17.0 → 2.18.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +7 -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 +144 -1
- package/dist/__tests__/peek.test.js +8 -7
- package/dist/__tests__/process-management.test.js +78 -4
- package/dist/app/cli.js +6 -5
- package/dist/app/mcp.js +11 -2
- package/dist/cli-process-service.js +10 -10
- package/dist/parsers.js +282 -22
- package/dist/peek.js +8 -5
- package/dist/process-service.js +10 -10
- package/package.json +1 -1
- package/src/__tests__/app-cli.test.ts +3 -3
- package/src/__tests__/cli-process-service.test.ts +3 -2
- package/src/__tests__/mcp-contract.test.ts +1 -0
- package/src/__tests__/parsers.test.ts +155 -1
- package/src/__tests__/peek.test.ts +8 -7
- package/src/__tests__/process-management.test.ts +86 -4
- package/src/app/cli.ts +7 -6
- package/src/app/mcp.ts +11 -2
- package/src/cli-process-service.ts +12 -12
- package/src/parsers.ts +371 -27
- package/src/peek.ts +14 -7
- package/src/process-service.ts +12 -12
package/src/parsers.ts
CHANGED
|
@@ -5,8 +5,47 @@ 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
|
+
}
|
|
33
|
+
|
|
34
|
+
interface ToolSummary {
|
|
35
|
+
summary: string;
|
|
36
|
+
server?: string;
|
|
37
|
+
summary_truncated?: boolean;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
interface ToolCallMemory {
|
|
41
|
+
tool: string;
|
|
42
|
+
server?: string;
|
|
43
|
+
summary: string;
|
|
44
|
+
summary_truncated?: boolean;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const PEEK_TOOL_SUMMARY_MAX_LENGTH = 200;
|
|
48
|
+
|
|
10
49
|
function isGeminiAssistantMessageEvent(parsed: any): boolean {
|
|
11
50
|
return parsed.type === 'message' && parsed.role === 'assistant' && typeof parsed.content === 'string';
|
|
12
51
|
}
|
|
@@ -25,37 +64,292 @@ function isGeminiStreamJsonEvent(parsed: any): boolean {
|
|
|
25
64
|
return parsed && typeof parsed === 'object' && !Array.isArray(parsed) && GEMINI_STREAM_EVENT_TYPES.has(parsed.type);
|
|
26
65
|
}
|
|
27
66
|
|
|
28
|
-
function
|
|
67
|
+
function oneLine(value: unknown): string {
|
|
68
|
+
return String(value ?? '').replace(/\s+/g, ' ').trim();
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function boundedSummary(value: string): { summary: string; summary_truncated?: boolean } {
|
|
72
|
+
const summary = oneLine(value);
|
|
73
|
+
if (summary.length <= PEEK_TOOL_SUMMARY_MAX_LENGTH) {
|
|
74
|
+
return { summary };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return {
|
|
78
|
+
summary: `${summary.slice(0, PEEK_TOOL_SUMMARY_MAX_LENGTH - 3)}...`,
|
|
79
|
+
summary_truncated: true,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function normalizeMcpToolName(tool: string, explicitServer?: string): ToolSummary | null {
|
|
84
|
+
if (explicitServer) {
|
|
85
|
+
return {
|
|
86
|
+
server: explicitServer,
|
|
87
|
+
...boundedSummary(`${explicitServer}.${tool}`),
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const mcpDouble = tool.match(/^mcp__([^_]+)__(.+)$/);
|
|
92
|
+
if (mcpDouble) {
|
|
93
|
+
return {
|
|
94
|
+
server: mcpDouble[1],
|
|
95
|
+
...boundedSummary(`${mcpDouble[1]}.${mcpDouble[2]}`),
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const mcpSingle = tool.match(/^mcp_([^_]+)_(.+)$/);
|
|
100
|
+
if (mcpSingle) {
|
|
101
|
+
return {
|
|
102
|
+
server: mcpSingle[1],
|
|
103
|
+
...boundedSummary(`${mcpSingle[1]}.${mcpSingle[2]}`),
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const acmShort = tool.match(/^acm_(.+)$/);
|
|
108
|
+
if (acmShort) {
|
|
109
|
+
return {
|
|
110
|
+
server: 'acm',
|
|
111
|
+
...boundedSummary(`acm.${acmShort[1]}`),
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return null;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function buildToolSummary(tool: string, options: { server?: string; command?: unknown } = {}): ToolSummary {
|
|
119
|
+
if (typeof options.command === 'string' && options.command.trim()) {
|
|
120
|
+
return boundedSummary(options.command);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const mcpSummary = normalizeMcpToolName(tool, options.server);
|
|
124
|
+
if (mcpSummary) {
|
|
125
|
+
return mcpSummary;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return boundedSummary(tool || 'tool_call');
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function normalizeToolStatus(rawStatus: unknown, exitCode?: number, defaultStatus: PeekToolCallStatus = 'unknown'): PeekToolCallStatus {
|
|
132
|
+
if (typeof exitCode === 'number') {
|
|
133
|
+
return exitCode === 0 ? 'success' : 'failed';
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const status = typeof rawStatus === 'string' ? rawStatus.toLowerCase() : '';
|
|
137
|
+
if (['success', 'succeeded', 'ok', 'completed'].includes(status)) {
|
|
138
|
+
return 'success';
|
|
139
|
+
}
|
|
140
|
+
if (['failed', 'failure', 'error', 'errored'].includes(status)) {
|
|
141
|
+
return 'failed';
|
|
142
|
+
}
|
|
143
|
+
if (['cancelled', 'canceled'].includes(status)) {
|
|
144
|
+
return 'cancelled';
|
|
145
|
+
}
|
|
146
|
+
return defaultStatus;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function createToolCallEvent(params: {
|
|
150
|
+
ts: string;
|
|
151
|
+
phase: 'started' | 'completed';
|
|
152
|
+
tool: string;
|
|
153
|
+
id?: string;
|
|
154
|
+
server?: string;
|
|
155
|
+
command?: unknown;
|
|
156
|
+
status?: unknown;
|
|
157
|
+
defaultStatus?: PeekToolCallStatus;
|
|
158
|
+
exit_code?: number;
|
|
159
|
+
duration_ms?: number;
|
|
160
|
+
}): PeekToolCallEvent {
|
|
161
|
+
const tool = params.tool || 'tool_call';
|
|
162
|
+
const summary = buildToolSummary(tool, { server: params.server, command: params.command });
|
|
163
|
+
const event: PeekToolCallEvent = {
|
|
164
|
+
kind: 'tool_call',
|
|
165
|
+
ts: params.ts,
|
|
166
|
+
phase: params.phase,
|
|
167
|
+
tool,
|
|
168
|
+
summary: summary.summary,
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
if (params.id) {
|
|
172
|
+
event.id = params.id;
|
|
173
|
+
}
|
|
174
|
+
if (summary.server) {
|
|
175
|
+
event.server = summary.server;
|
|
176
|
+
} else if (params.server) {
|
|
177
|
+
event.server = params.server;
|
|
178
|
+
}
|
|
179
|
+
if (summary.summary_truncated) {
|
|
180
|
+
event.summary_truncated = true;
|
|
181
|
+
}
|
|
182
|
+
if (params.phase === 'completed') {
|
|
183
|
+
event.status = normalizeToolStatus(params.status, params.exit_code, params.defaultStatus);
|
|
184
|
+
if (typeof params.exit_code === 'number') {
|
|
185
|
+
event.exit_code = params.exit_code;
|
|
186
|
+
}
|
|
187
|
+
if (typeof params.duration_ms === 'number' && Number.isFinite(params.duration_ms)) {
|
|
188
|
+
event.duration_ms = params.duration_ms;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
return event;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function rememberToolCall(event: PeekEvent, memory: Map<string, ToolCallMemory>): void {
|
|
196
|
+
if (event.kind !== 'tool_call' || !event.id) {
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
memory.set(event.id, {
|
|
201
|
+
tool: event.tool,
|
|
202
|
+
server: event.server,
|
|
203
|
+
summary: event.summary,
|
|
204
|
+
summary_truncated: event.summary_truncated,
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function createRememberedCompletion(params: {
|
|
209
|
+
ts: string;
|
|
210
|
+
id?: string;
|
|
211
|
+
memory: Map<string, ToolCallMemory>;
|
|
212
|
+
fallbackTool: string;
|
|
213
|
+
status?: unknown;
|
|
214
|
+
defaultStatus?: PeekToolCallStatus;
|
|
215
|
+
}): PeekEvent {
|
|
216
|
+
const remembered = params.id ? params.memory.get(params.id) : undefined;
|
|
217
|
+
const event = createToolCallEvent({
|
|
218
|
+
ts: params.ts,
|
|
219
|
+
phase: 'completed',
|
|
220
|
+
id: params.id,
|
|
221
|
+
tool: remembered?.tool || params.fallbackTool,
|
|
222
|
+
server: remembered?.server,
|
|
223
|
+
status: params.status,
|
|
224
|
+
defaultStatus: params.defaultStatus,
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
if (remembered) {
|
|
228
|
+
event.summary = remembered.summary;
|
|
229
|
+
if (remembered.summary_truncated) {
|
|
230
|
+
event.summary_truncated = true;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
return event;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function extractPeekEventsFromParsedEvent(agent: PeekAgent, parsed: any, observedAt: string, includeToolCalls: boolean, memory: Map<string, ToolCallMemory>): PeekEvent[] {
|
|
29
238
|
if (agent === 'codex') {
|
|
30
239
|
if (parsed.item?.type === 'agent_message' && typeof parsed.item.text === 'string' && parsed.item.text.trim()) {
|
|
31
|
-
return [{ ts: observedAt, text: parsed.item.text }];
|
|
240
|
+
return [{ kind: 'message', ts: observedAt, text: parsed.item.text }];
|
|
32
241
|
}
|
|
33
242
|
if (parsed.msg?.type === 'agent_message' && typeof parsed.msg.message === 'string' && parsed.msg.message.trim()) {
|
|
34
|
-
return [{ ts: observedAt, text: parsed.msg.message }];
|
|
243
|
+
return [{ kind: 'message', ts: observedAt, text: parsed.msg.message }];
|
|
244
|
+
}
|
|
245
|
+
if (includeToolCalls && (parsed.type === 'item.started' || parsed.type === 'item.completed')) {
|
|
246
|
+
const item = parsed.item;
|
|
247
|
+
if (item?.type === 'command_execution') {
|
|
248
|
+
const event = createToolCallEvent({
|
|
249
|
+
ts: observedAt,
|
|
250
|
+
phase: parsed.type === 'item.started' ? 'started' : 'completed',
|
|
251
|
+
id: item.id,
|
|
252
|
+
tool: 'command_execution',
|
|
253
|
+
command: item.command,
|
|
254
|
+
status: item.status || item.error,
|
|
255
|
+
exit_code: typeof item.exit_code === 'number' ? item.exit_code : undefined,
|
|
256
|
+
defaultStatus: parsed.type === 'item.completed' ? 'success' : 'unknown',
|
|
257
|
+
});
|
|
258
|
+
rememberToolCall(event, memory);
|
|
259
|
+
return [event];
|
|
260
|
+
}
|
|
261
|
+
if (item?.type === 'mcp_tool_call') {
|
|
262
|
+
const event = createToolCallEvent({
|
|
263
|
+
ts: observedAt,
|
|
264
|
+
phase: parsed.type === 'item.started' ? 'started' : 'completed',
|
|
265
|
+
id: item.id,
|
|
266
|
+
tool: item.tool || 'mcp_tool_call',
|
|
267
|
+
server: item.server,
|
|
268
|
+
status: item.status || item.error,
|
|
269
|
+
defaultStatus: parsed.type === 'item.completed' ? 'success' : 'unknown',
|
|
270
|
+
});
|
|
271
|
+
rememberToolCall(event, memory);
|
|
272
|
+
return [event];
|
|
273
|
+
}
|
|
35
274
|
}
|
|
36
275
|
return [];
|
|
37
276
|
}
|
|
38
277
|
|
|
39
|
-
if (agent === 'claude'
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
278
|
+
if (agent === 'claude') {
|
|
279
|
+
if (parsed.type === 'assistant' && Array.isArray(parsed.message?.content)) {
|
|
280
|
+
const events: PeekEvent[] = [];
|
|
281
|
+
for (const content of parsed.message.content) {
|
|
282
|
+
if (content?.type === 'text' && typeof content.text === 'string' && content.text.trim()) {
|
|
283
|
+
events.push({ kind: 'message', ts: observedAt, text: content.text });
|
|
284
|
+
} else if (includeToolCalls && content?.type === 'tool_use') {
|
|
285
|
+
const event = createToolCallEvent({
|
|
286
|
+
ts: observedAt,
|
|
287
|
+
phase: 'started',
|
|
288
|
+
id: content.id,
|
|
289
|
+
tool: content.name || 'tool_use',
|
|
290
|
+
command: content.input?.command,
|
|
291
|
+
});
|
|
292
|
+
rememberToolCall(event, memory);
|
|
293
|
+
events.push(event);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
return events;
|
|
297
|
+
}
|
|
298
|
+
if (includeToolCalls && parsed.type === 'user' && Array.isArray(parsed.message?.content)) {
|
|
299
|
+
const events: PeekEvent[] = [];
|
|
300
|
+
for (const content of parsed.message.content) {
|
|
301
|
+
if (content?.type === 'tool_result') {
|
|
302
|
+
events.push(createRememberedCompletion({
|
|
303
|
+
ts: observedAt,
|
|
304
|
+
id: content.tool_use_id,
|
|
305
|
+
memory,
|
|
306
|
+
fallbackTool: 'tool_result',
|
|
307
|
+
status: content.is_error === true ? 'failed' : undefined,
|
|
308
|
+
defaultStatus: content.is_error === true ? 'failed' : 'success',
|
|
309
|
+
}));
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
return events;
|
|
313
|
+
}
|
|
314
|
+
return [];
|
|
43
315
|
}
|
|
44
316
|
|
|
45
317
|
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 }];
|
|
318
|
+
return [{ kind: 'message', ts: observedAt, text: parsed.part.text }];
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
if (agent === 'opencode' && includeToolCalls && parsed.type === 'tool_use' && parsed.part?.type === 'tool') {
|
|
322
|
+
const state = parsed.part.state || {};
|
|
323
|
+
const start = state.time?.start;
|
|
324
|
+
const end = state.time?.end;
|
|
325
|
+
const event = createToolCallEvent({
|
|
326
|
+
ts: observedAt,
|
|
327
|
+
phase: state.status === 'running' || state.status === 'pending' ? 'started' : 'completed',
|
|
328
|
+
id: parsed.part.callID,
|
|
329
|
+
tool: parsed.part.tool || 'tool_use',
|
|
330
|
+
command: state.input?.command,
|
|
331
|
+
status: state.status,
|
|
332
|
+
defaultStatus: state.status === 'completed' ? 'success' : 'unknown',
|
|
333
|
+
duration_ms: typeof start === 'number' && typeof end === 'number' ? end - start : undefined,
|
|
334
|
+
});
|
|
335
|
+
rememberToolCall(event, memory);
|
|
336
|
+
return [event];
|
|
47
337
|
}
|
|
48
338
|
|
|
49
339
|
return [];
|
|
50
340
|
}
|
|
51
341
|
|
|
52
|
-
export class
|
|
342
|
+
export class PeekEventExtractor {
|
|
53
343
|
private pending = '';
|
|
54
344
|
private geminiAssistantBuffer = '';
|
|
345
|
+
private readonly includeToolCalls: boolean;
|
|
346
|
+
private readonly toolMemory = new Map<string, ToolCallMemory>();
|
|
55
347
|
|
|
56
|
-
constructor(private readonly agent: PeekAgent) {
|
|
348
|
+
constructor(private readonly agent: PeekAgent, options: PeekEventExtractorOptions = {}) {
|
|
349
|
+
this.includeToolCalls = options.includeToolCalls === true;
|
|
350
|
+
}
|
|
57
351
|
|
|
58
|
-
push(chunk: string, observedAt = new Date().toISOString()):
|
|
352
|
+
push(chunk: string, observedAt = new Date().toISOString()): PeekEvent[] {
|
|
59
353
|
if (!chunk) {
|
|
60
354
|
return [];
|
|
61
355
|
}
|
|
@@ -65,21 +359,21 @@ export class PeekMessageExtractor {
|
|
|
65
359
|
return this.extractLines(lines, observedAt);
|
|
66
360
|
}
|
|
67
361
|
|
|
68
|
-
flush(observedAt = new Date().toISOString()):
|
|
69
|
-
const
|
|
362
|
+
flush(observedAt = new Date().toISOString()): PeekEvent[] {
|
|
363
|
+
const events: PeekEvent[] = [];
|
|
70
364
|
|
|
71
365
|
if (this.pending) {
|
|
72
366
|
const line = this.pending;
|
|
73
367
|
this.pending = '';
|
|
74
|
-
|
|
368
|
+
events.push(...this.extractLines([line], observedAt));
|
|
75
369
|
}
|
|
76
370
|
|
|
77
|
-
|
|
78
|
-
return
|
|
371
|
+
events.push(...this.flushGeminiAssistantBuffer(observedAt));
|
|
372
|
+
return events;
|
|
79
373
|
}
|
|
80
374
|
|
|
81
|
-
private extractLines(lines: string[], observedAt: string):
|
|
82
|
-
const
|
|
375
|
+
private extractLines(lines: string[], observedAt: string): PeekEvent[] {
|
|
376
|
+
const events: PeekEvent[] = [];
|
|
83
377
|
|
|
84
378
|
for (const line of lines) {
|
|
85
379
|
if (!line.trim()) {
|
|
@@ -87,30 +381,58 @@ export class PeekMessageExtractor {
|
|
|
87
381
|
}
|
|
88
382
|
|
|
89
383
|
try {
|
|
90
|
-
|
|
384
|
+
events.push(...this.extractParsedEvent(JSON.parse(line), observedAt));
|
|
91
385
|
} catch {
|
|
92
386
|
debugLog(`[Debug] Skipping invalid peek JSON line: ${line}`);
|
|
93
|
-
|
|
387
|
+
events.push(...this.flushGeminiAssistantBuffer(observedAt));
|
|
94
388
|
}
|
|
95
389
|
}
|
|
96
390
|
|
|
97
|
-
return
|
|
391
|
+
return events;
|
|
98
392
|
}
|
|
99
393
|
|
|
100
|
-
private extractParsedEvent(parsed: any, observedAt: string):
|
|
101
|
-
if (this.agent
|
|
102
|
-
|
|
394
|
+
private extractParsedEvent(parsed: any, observedAt: string): PeekEvent[] {
|
|
395
|
+
if (this.agent === 'gemini') {
|
|
396
|
+
const events = this.extractGeminiParsedEvent(parsed, observedAt);
|
|
397
|
+
return events;
|
|
103
398
|
}
|
|
104
399
|
|
|
400
|
+
return extractPeekEventsFromParsedEvent(this.agent, parsed, observedAt, this.includeToolCalls, this.toolMemory);
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
private extractGeminiParsedEvent(parsed: any, observedAt: string): PeekEvent[] {
|
|
105
404
|
if (isGeminiAssistantMessageEvent(parsed)) {
|
|
106
405
|
this.geminiAssistantBuffer += parsed.content;
|
|
107
406
|
return [];
|
|
108
407
|
}
|
|
109
408
|
|
|
110
|
-
|
|
409
|
+
const events = this.flushGeminiAssistantBuffer(observedAt);
|
|
410
|
+
|
|
411
|
+
if (this.includeToolCalls && parsed.type === 'tool_use') {
|
|
412
|
+
const event = createToolCallEvent({
|
|
413
|
+
ts: observedAt,
|
|
414
|
+
phase: 'started',
|
|
415
|
+
id: parsed.tool_id,
|
|
416
|
+
tool: parsed.tool_name || parsed.name || 'tool_use',
|
|
417
|
+
command: parsed.parameters?.command,
|
|
418
|
+
});
|
|
419
|
+
rememberToolCall(event, this.toolMemory);
|
|
420
|
+
events.push(event);
|
|
421
|
+
} else if (this.includeToolCalls && parsed.type === 'tool_result') {
|
|
422
|
+
events.push(createRememberedCompletion({
|
|
423
|
+
ts: observedAt,
|
|
424
|
+
id: parsed.tool_id,
|
|
425
|
+
memory: this.toolMemory,
|
|
426
|
+
fallbackTool: parsed.tool_name || parsed.name || 'tool_result',
|
|
427
|
+
status: parsed.status,
|
|
428
|
+
defaultStatus: 'unknown',
|
|
429
|
+
}));
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
return events;
|
|
111
433
|
}
|
|
112
434
|
|
|
113
|
-
private flushGeminiAssistantBuffer(observedAt: string):
|
|
435
|
+
private flushGeminiAssistantBuffer(observedAt: string): PeekEvent[] {
|
|
114
436
|
if (this.agent !== 'gemini' || !this.geminiAssistantBuffer) {
|
|
115
437
|
return [];
|
|
116
438
|
}
|
|
@@ -122,7 +444,29 @@ export class PeekMessageExtractor {
|
|
|
122
444
|
return [];
|
|
123
445
|
}
|
|
124
446
|
|
|
125
|
-
return [{ ts: observedAt, text }];
|
|
447
|
+
return [{ kind: 'message', ts: observedAt, text }];
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
export class PeekMessageExtractor {
|
|
452
|
+
private readonly extractor: PeekEventExtractor;
|
|
453
|
+
|
|
454
|
+
constructor(agent: PeekAgent) {
|
|
455
|
+
this.extractor = new PeekEventExtractor(agent, { includeToolCalls: false });
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
push(chunk: string, observedAt = new Date().toISOString()): PeekMessage[] {
|
|
459
|
+
return this.toMessages(this.extractor.push(chunk, observedAt));
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
flush(observedAt = new Date().toISOString()): PeekMessage[] {
|
|
463
|
+
return this.toMessages(this.extractor.flush(observedAt));
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
private toMessages(events: PeekEvent[]): PeekMessage[] {
|
|
467
|
+
return events
|
|
468
|
+
.filter((event): event is Extract<PeekEvent, { kind: 'message' }> => event.kind === 'message')
|
|
469
|
+
.map((event) => ({ ts: event.ts, text: event.text }));
|
|
126
470
|
}
|
|
127
471
|
}
|
|
128
472
|
|
package/src/peek.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { PeekMessage } from './parsers.js';
|
|
1
|
+
import type { PeekEvent, PeekMessage } from './parsers.js';
|
|
2
2
|
import type { AgentType, ProcessStatus } from './process-service.js';
|
|
3
3
|
|
|
4
4
|
export const DEFAULT_PEEK_TIME_SEC = 10;
|
|
@@ -13,7 +13,7 @@ export interface PeekProcessResult {
|
|
|
13
13
|
pid: number;
|
|
14
14
|
agent: PeekAgent;
|
|
15
15
|
status: PeekStatus;
|
|
16
|
-
|
|
16
|
+
events: PeekEvent[];
|
|
17
17
|
truncated: boolean;
|
|
18
18
|
error: string | null;
|
|
19
19
|
}
|
|
@@ -67,22 +67,29 @@ export function buildNotFoundPeekProcess(pid: number): PeekProcessResult {
|
|
|
67
67
|
pid,
|
|
68
68
|
agent: null,
|
|
69
69
|
status: 'not_found',
|
|
70
|
-
|
|
70
|
+
events: [],
|
|
71
71
|
truncated: false,
|
|
72
72
|
error: 'process not found',
|
|
73
73
|
};
|
|
74
74
|
}
|
|
75
75
|
|
|
76
|
-
export function
|
|
77
|
-
for (const
|
|
78
|
-
if (target.
|
|
79
|
-
target.
|
|
76
|
+
export function appendPeekEvents(target: PeekProcessResult, events: PeekEvent[]): void {
|
|
77
|
+
for (const event of events) {
|
|
78
|
+
if (target.events.length < PEEK_MESSAGE_CAP) {
|
|
79
|
+
target.events.push(event);
|
|
80
80
|
} else {
|
|
81
81
|
target.truncated = true;
|
|
82
82
|
}
|
|
83
83
|
}
|
|
84
84
|
}
|
|
85
85
|
|
|
86
|
+
export function appendPeekMessages(target: PeekProcessResult, messages: PeekMessage[]): void {
|
|
87
|
+
appendPeekEvents(
|
|
88
|
+
target,
|
|
89
|
+
messages.map((message) => ({ kind: 'message' as const, ...message })),
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
|
|
86
93
|
export function observedDurationSec(startedAtMs: number, endedAtMs = Date.now()): number {
|
|
87
94
|
return Number(((endedAtMs - startedAtMs) / 1000).toFixed(2));
|
|
88
95
|
}
|
package/src/process-service.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { spawn, type ChildProcess } from 'node:child_process';
|
|
2
2
|
import { buildCliCommand, type BuildCliCommandOptions } from './cli-builder.js';
|
|
3
|
-
import { parseClaudeOutput, parseCodexOutput, parseForgeOutput, parseGeminiOutput, parseOpenCodeOutput,
|
|
3
|
+
import { parseClaudeOutput, parseCodexOutput, parseForgeOutput, parseGeminiOutput, parseOpenCodeOutput, PeekEventExtractor } from './parsers.js';
|
|
4
4
|
import {
|
|
5
|
-
|
|
5
|
+
appendPeekEvents,
|
|
6
6
|
buildNotFoundPeekProcess,
|
|
7
7
|
observedDurationSec,
|
|
8
8
|
validatePeekPids,
|
|
@@ -226,15 +226,15 @@ export class ProcessService {
|
|
|
226
226
|
}
|
|
227
227
|
}
|
|
228
228
|
|
|
229
|
-
async peekProcesses(pids: number[], peekTimeSec = 10): Promise<PeekResponse> {
|
|
229
|
+
async peekProcesses(pids: number[], peekTimeSec = 10, includeToolCalls = false): Promise<PeekResponse> {
|
|
230
230
|
const targetPids = validatePeekPids(pids);
|
|
231
231
|
const targetPeekTimeSec = validatePeekTimeSec(peekTimeSec);
|
|
232
232
|
const processes: PeekProcessResult[] = [];
|
|
233
233
|
const observers: Array<{
|
|
234
234
|
entry: TrackedProcess;
|
|
235
235
|
result: PeekProcessResult;
|
|
236
|
-
stdoutExtractor:
|
|
237
|
-
stderrExtractor:
|
|
236
|
+
stdoutExtractor: PeekEventExtractor;
|
|
237
|
+
stderrExtractor: PeekEventExtractor;
|
|
238
238
|
onStdout: (data: Buffer | string) => void;
|
|
239
239
|
onStderr: (data: Buffer | string) => void;
|
|
240
240
|
}> = [];
|
|
@@ -250,19 +250,19 @@ export class ProcessService {
|
|
|
250
250
|
pid,
|
|
251
251
|
agent: entry.toolType,
|
|
252
252
|
status: entry.status,
|
|
253
|
-
|
|
253
|
+
events: [],
|
|
254
254
|
truncated: false,
|
|
255
255
|
error: null,
|
|
256
256
|
};
|
|
257
257
|
processes.push(result);
|
|
258
258
|
|
|
259
|
-
const stdoutExtractor = new
|
|
260
|
-
const stderrExtractor = new
|
|
259
|
+
const stdoutExtractor = new PeekEventExtractor(entry.toolType, { includeToolCalls });
|
|
260
|
+
const stderrExtractor = new PeekEventExtractor(entry.toolType, { includeToolCalls });
|
|
261
261
|
const onStdout = (data: Buffer | string) => {
|
|
262
|
-
|
|
262
|
+
appendPeekEvents(result, stdoutExtractor.push(data.toString(), new Date().toISOString()));
|
|
263
263
|
};
|
|
264
264
|
const onStderr = (data: Buffer | string) => {
|
|
265
|
-
|
|
265
|
+
appendPeekEvents(result, stderrExtractor.push(data.toString(), new Date().toISOString()));
|
|
266
266
|
};
|
|
267
267
|
|
|
268
268
|
if (entry.status === 'running') {
|
|
@@ -294,8 +294,8 @@ export class ProcessService {
|
|
|
294
294
|
for (const observer of observers) {
|
|
295
295
|
observer.entry.process.stdout?.off('data', observer.onStdout);
|
|
296
296
|
observer.entry.process.stderr?.off('data', observer.onStderr);
|
|
297
|
-
|
|
298
|
-
|
|
297
|
+
appendPeekEvents(observer.result, observer.stdoutExtractor.flush(flushTs));
|
|
298
|
+
appendPeekEvents(observer.result, observer.stderrExtractor.flush(flushTs));
|
|
299
299
|
observer.result.status = observer.entry.status;
|
|
300
300
|
}
|
|
301
301
|
}
|