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
package/dist/parsers.js
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
import { debugLog } from './cli-utils.js';
|
|
2
|
+
const PEEK_TOOL_SUMMARY_MAX_LENGTH = 200;
|
|
3
|
+
const FORGE_EXECUTE_PATTERN = /^● \[[^\]]+\] Execute \[([^\]]*)\]\s+(.+)$/;
|
|
4
|
+
const FORGE_FINISHED_PATTERN = /^● \[[^\]]+\] Finished(?:\s+\S+)?\s*$/;
|
|
2
5
|
function isGeminiAssistantMessageEvent(parsed) {
|
|
3
6
|
return parsed.type === 'message' && parsed.role === 'assistant' && typeof parsed.content === 'string';
|
|
4
7
|
}
|
|
@@ -14,34 +17,257 @@ const GEMINI_STREAM_EVENT_TYPES = new Set([
|
|
|
14
17
|
function isGeminiStreamJsonEvent(parsed) {
|
|
15
18
|
return parsed && typeof parsed === 'object' && !Array.isArray(parsed) && GEMINI_STREAM_EVENT_TYPES.has(parsed.type);
|
|
16
19
|
}
|
|
17
|
-
function
|
|
20
|
+
function oneLine(value) {
|
|
21
|
+
return String(value ?? '').replace(/\s+/g, ' ').trim();
|
|
22
|
+
}
|
|
23
|
+
function boundedSummary(value) {
|
|
24
|
+
const summary = oneLine(value);
|
|
25
|
+
if (summary.length <= PEEK_TOOL_SUMMARY_MAX_LENGTH) {
|
|
26
|
+
return { summary };
|
|
27
|
+
}
|
|
28
|
+
return {
|
|
29
|
+
summary: `${summary.slice(0, PEEK_TOOL_SUMMARY_MAX_LENGTH - 3)}...`,
|
|
30
|
+
summary_truncated: true,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
function normalizeMcpToolName(tool, explicitServer) {
|
|
34
|
+
if (explicitServer) {
|
|
35
|
+
return {
|
|
36
|
+
server: explicitServer,
|
|
37
|
+
...boundedSummary(`${explicitServer}.${tool}`),
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
const mcpDouble = tool.match(/^mcp__([^_]+)__(.+)$/);
|
|
41
|
+
if (mcpDouble) {
|
|
42
|
+
return {
|
|
43
|
+
server: mcpDouble[1],
|
|
44
|
+
...boundedSummary(`${mcpDouble[1]}.${mcpDouble[2]}`),
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
const mcpSingle = tool.match(/^mcp_([^_]+)_(.+)$/);
|
|
48
|
+
if (mcpSingle) {
|
|
49
|
+
return {
|
|
50
|
+
server: mcpSingle[1],
|
|
51
|
+
...boundedSummary(`${mcpSingle[1]}.${mcpSingle[2]}`),
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
const acmShort = tool.match(/^acm_(.+)$/);
|
|
55
|
+
if (acmShort) {
|
|
56
|
+
return {
|
|
57
|
+
server: 'acm',
|
|
58
|
+
...boundedSummary(`acm.${acmShort[1]}`),
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
function buildToolSummary(tool, options = {}) {
|
|
64
|
+
if (typeof options.command === 'string' && options.command.trim()) {
|
|
65
|
+
return boundedSummary(options.command);
|
|
66
|
+
}
|
|
67
|
+
const mcpSummary = normalizeMcpToolName(tool, options.server);
|
|
68
|
+
if (mcpSummary) {
|
|
69
|
+
return mcpSummary;
|
|
70
|
+
}
|
|
71
|
+
return boundedSummary(tool || 'tool_call');
|
|
72
|
+
}
|
|
73
|
+
function normalizeToolStatus(rawStatus, exitCode, defaultStatus = 'unknown') {
|
|
74
|
+
if (typeof exitCode === 'number') {
|
|
75
|
+
return exitCode === 0 ? 'success' : 'failed';
|
|
76
|
+
}
|
|
77
|
+
const status = typeof rawStatus === 'string' ? rawStatus.toLowerCase() : '';
|
|
78
|
+
if (['success', 'succeeded', 'ok', 'completed'].includes(status)) {
|
|
79
|
+
return 'success';
|
|
80
|
+
}
|
|
81
|
+
if (['failed', 'failure', 'error', 'errored'].includes(status)) {
|
|
82
|
+
return 'failed';
|
|
83
|
+
}
|
|
84
|
+
if (['cancelled', 'canceled'].includes(status)) {
|
|
85
|
+
return 'cancelled';
|
|
86
|
+
}
|
|
87
|
+
return defaultStatus;
|
|
88
|
+
}
|
|
89
|
+
function createToolCallEvent(params) {
|
|
90
|
+
const tool = params.tool || 'tool_call';
|
|
91
|
+
const summary = buildToolSummary(tool, { server: params.server, command: params.command });
|
|
92
|
+
const event = {
|
|
93
|
+
kind: 'tool_call',
|
|
94
|
+
ts: params.ts,
|
|
95
|
+
phase: params.phase,
|
|
96
|
+
tool,
|
|
97
|
+
summary: summary.summary,
|
|
98
|
+
};
|
|
99
|
+
if (params.id) {
|
|
100
|
+
event.id = params.id;
|
|
101
|
+
}
|
|
102
|
+
if (summary.server) {
|
|
103
|
+
event.server = summary.server;
|
|
104
|
+
}
|
|
105
|
+
else if (params.server) {
|
|
106
|
+
event.server = params.server;
|
|
107
|
+
}
|
|
108
|
+
if (summary.summary_truncated) {
|
|
109
|
+
event.summary_truncated = true;
|
|
110
|
+
}
|
|
111
|
+
if (params.phase === 'completed') {
|
|
112
|
+
event.status = normalizeToolStatus(params.status, params.exit_code, params.defaultStatus);
|
|
113
|
+
if (typeof params.exit_code === 'number') {
|
|
114
|
+
event.exit_code = params.exit_code;
|
|
115
|
+
}
|
|
116
|
+
if (typeof params.duration_ms === 'number' && Number.isFinite(params.duration_ms)) {
|
|
117
|
+
event.duration_ms = params.duration_ms;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return event;
|
|
121
|
+
}
|
|
122
|
+
function rememberToolCall(event, memory) {
|
|
123
|
+
if (event.kind !== 'tool_call' || !event.id) {
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
memory.set(event.id, {
|
|
127
|
+
tool: event.tool,
|
|
128
|
+
server: event.server,
|
|
129
|
+
summary: event.summary,
|
|
130
|
+
summary_truncated: event.summary_truncated,
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
function createRememberedCompletion(params) {
|
|
134
|
+
const remembered = params.id ? params.memory.get(params.id) : undefined;
|
|
135
|
+
const event = createToolCallEvent({
|
|
136
|
+
ts: params.ts,
|
|
137
|
+
phase: 'completed',
|
|
138
|
+
id: params.id,
|
|
139
|
+
tool: remembered?.tool || params.fallbackTool,
|
|
140
|
+
server: remembered?.server,
|
|
141
|
+
status: params.status,
|
|
142
|
+
defaultStatus: params.defaultStatus,
|
|
143
|
+
});
|
|
144
|
+
if (remembered) {
|
|
145
|
+
event.summary = remembered.summary;
|
|
146
|
+
if (remembered.summary_truncated) {
|
|
147
|
+
event.summary_truncated = true;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
return event;
|
|
151
|
+
}
|
|
152
|
+
function extractPeekEventsFromParsedEvent(agent, parsed, observedAt, includeToolCalls, memory) {
|
|
18
153
|
if (agent === 'codex') {
|
|
19
154
|
if (parsed.item?.type === 'agent_message' && typeof parsed.item.text === 'string' && parsed.item.text.trim()) {
|
|
20
|
-
return [{ ts: observedAt, text: parsed.item.text }];
|
|
155
|
+
return [{ kind: 'message', ts: observedAt, text: parsed.item.text }];
|
|
21
156
|
}
|
|
22
157
|
if (parsed.msg?.type === 'agent_message' && typeof parsed.msg.message === 'string' && parsed.msg.message.trim()) {
|
|
23
|
-
return [{ ts: observedAt, text: parsed.msg.message }];
|
|
158
|
+
return [{ kind: 'message', ts: observedAt, text: parsed.msg.message }];
|
|
159
|
+
}
|
|
160
|
+
if (includeToolCalls && (parsed.type === 'item.started' || parsed.type === 'item.completed')) {
|
|
161
|
+
const item = parsed.item;
|
|
162
|
+
if (item?.type === 'command_execution') {
|
|
163
|
+
const event = createToolCallEvent({
|
|
164
|
+
ts: observedAt,
|
|
165
|
+
phase: parsed.type === 'item.started' ? 'started' : 'completed',
|
|
166
|
+
id: item.id,
|
|
167
|
+
tool: 'command_execution',
|
|
168
|
+
command: item.command,
|
|
169
|
+
status: item.status || item.error,
|
|
170
|
+
exit_code: typeof item.exit_code === 'number' ? item.exit_code : undefined,
|
|
171
|
+
defaultStatus: parsed.type === 'item.completed' ? 'success' : 'unknown',
|
|
172
|
+
});
|
|
173
|
+
rememberToolCall(event, memory);
|
|
174
|
+
return [event];
|
|
175
|
+
}
|
|
176
|
+
if (item?.type === 'mcp_tool_call') {
|
|
177
|
+
const event = createToolCallEvent({
|
|
178
|
+
ts: observedAt,
|
|
179
|
+
phase: parsed.type === 'item.started' ? 'started' : 'completed',
|
|
180
|
+
id: item.id,
|
|
181
|
+
tool: item.tool || 'mcp_tool_call',
|
|
182
|
+
server: item.server,
|
|
183
|
+
status: item.status || item.error,
|
|
184
|
+
defaultStatus: parsed.type === 'item.completed' ? 'success' : 'unknown',
|
|
185
|
+
});
|
|
186
|
+
rememberToolCall(event, memory);
|
|
187
|
+
return [event];
|
|
188
|
+
}
|
|
24
189
|
}
|
|
25
190
|
return [];
|
|
26
191
|
}
|
|
27
|
-
if (agent === 'claude'
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
192
|
+
if (agent === 'claude') {
|
|
193
|
+
if (parsed.type === 'assistant' && Array.isArray(parsed.message?.content)) {
|
|
194
|
+
const events = [];
|
|
195
|
+
for (const content of parsed.message.content) {
|
|
196
|
+
if (content?.type === 'text' && typeof content.text === 'string' && content.text.trim()) {
|
|
197
|
+
events.push({ kind: 'message', ts: observedAt, text: content.text });
|
|
198
|
+
}
|
|
199
|
+
else if (includeToolCalls && content?.type === 'tool_use') {
|
|
200
|
+
const event = createToolCallEvent({
|
|
201
|
+
ts: observedAt,
|
|
202
|
+
phase: 'started',
|
|
203
|
+
id: content.id,
|
|
204
|
+
tool: content.name || 'tool_use',
|
|
205
|
+
command: content.input?.command,
|
|
206
|
+
});
|
|
207
|
+
rememberToolCall(event, memory);
|
|
208
|
+
events.push(event);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
return events;
|
|
212
|
+
}
|
|
213
|
+
if (includeToolCalls && parsed.type === 'user' && Array.isArray(parsed.message?.content)) {
|
|
214
|
+
const events = [];
|
|
215
|
+
for (const content of parsed.message.content) {
|
|
216
|
+
if (content?.type === 'tool_result') {
|
|
217
|
+
events.push(createRememberedCompletion({
|
|
218
|
+
ts: observedAt,
|
|
219
|
+
id: content.tool_use_id,
|
|
220
|
+
memory,
|
|
221
|
+
fallbackTool: 'tool_result',
|
|
222
|
+
status: content.is_error === true ? 'failed' : undefined,
|
|
223
|
+
defaultStatus: content.is_error === true ? 'failed' : 'success',
|
|
224
|
+
}));
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
return events;
|
|
228
|
+
}
|
|
229
|
+
return [];
|
|
31
230
|
}
|
|
32
231
|
if (agent === 'opencode' && parsed.type === 'text' && parsed.part?.type === 'text' && typeof parsed.part.text === 'string' && parsed.part.text.trim()) {
|
|
33
|
-
return [{ ts: observedAt, text: parsed.part.text }];
|
|
232
|
+
return [{ kind: 'message', ts: observedAt, text: parsed.part.text }];
|
|
233
|
+
}
|
|
234
|
+
if (agent === 'opencode' && includeToolCalls && parsed.type === 'tool_use' && parsed.part?.type === 'tool') {
|
|
235
|
+
const state = parsed.part.state || {};
|
|
236
|
+
const start = state.time?.start;
|
|
237
|
+
const end = state.time?.end;
|
|
238
|
+
const event = createToolCallEvent({
|
|
239
|
+
ts: observedAt,
|
|
240
|
+
phase: state.status === 'running' || state.status === 'pending' ? 'started' : 'completed',
|
|
241
|
+
id: parsed.part.callID,
|
|
242
|
+
tool: parsed.part.tool || 'tool_use',
|
|
243
|
+
command: state.input?.command,
|
|
244
|
+
status: state.status,
|
|
245
|
+
defaultStatus: state.status === 'completed' ? 'success' : 'unknown',
|
|
246
|
+
duration_ms: typeof start === 'number' && typeof end === 'number' ? end - start : undefined,
|
|
247
|
+
});
|
|
248
|
+
rememberToolCall(event, memory);
|
|
249
|
+
return [event];
|
|
34
250
|
}
|
|
35
251
|
return [];
|
|
36
252
|
}
|
|
37
|
-
export class
|
|
253
|
+
export class PeekEventExtractor {
|
|
38
254
|
agent;
|
|
39
255
|
pending = '';
|
|
40
256
|
geminiAssistantBuffer = '';
|
|
41
|
-
|
|
257
|
+
includeToolCalls;
|
|
258
|
+
source;
|
|
259
|
+
toolMemory = new Map();
|
|
260
|
+
forgePendingTool = null;
|
|
261
|
+
forgeToolSequence = 0;
|
|
262
|
+
constructor(agent, options = {}) {
|
|
42
263
|
this.agent = agent;
|
|
264
|
+
this.includeToolCalls = options.includeToolCalls === true;
|
|
265
|
+
this.source = options.source || 'stdout';
|
|
43
266
|
}
|
|
44
267
|
push(chunk, observedAt = new Date().toISOString()) {
|
|
268
|
+
if (this.agent === 'forge' && this.source === 'stderr') {
|
|
269
|
+
return [];
|
|
270
|
+
}
|
|
45
271
|
if (!chunk) {
|
|
46
272
|
return [];
|
|
47
273
|
}
|
|
@@ -49,41 +275,129 @@ export class PeekMessageExtractor {
|
|
|
49
275
|
this.pending = lines.pop() || '';
|
|
50
276
|
return this.extractLines(lines, observedAt);
|
|
51
277
|
}
|
|
52
|
-
flush(observedAt = new Date().toISOString()) {
|
|
53
|
-
|
|
54
|
-
if (this.pending) {
|
|
55
|
-
const line = this.pending;
|
|
278
|
+
flush(observedAt = new Date().toISOString(), options = {}) {
|
|
279
|
+
if (this.agent === 'forge' && this.source === 'stderr') {
|
|
56
280
|
this.pending = '';
|
|
57
|
-
|
|
281
|
+
return [];
|
|
58
282
|
}
|
|
59
|
-
|
|
60
|
-
|
|
283
|
+
const events = [];
|
|
284
|
+
if (this.pending) {
|
|
285
|
+
if (this.agent !== 'forge' || options.terminal === true) {
|
|
286
|
+
const line = this.pending;
|
|
287
|
+
this.pending = '';
|
|
288
|
+
events.push(...this.extractLines([line], observedAt));
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
events.push(...this.flushGeminiAssistantBuffer(observedAt));
|
|
292
|
+
events.push(...this.flushForgePendingTool(observedAt, options.terminal === true));
|
|
293
|
+
return events;
|
|
61
294
|
}
|
|
62
295
|
extractLines(lines, observedAt) {
|
|
63
|
-
|
|
296
|
+
if (this.agent === 'forge') {
|
|
297
|
+
return this.extractForgeLines(lines, observedAt);
|
|
298
|
+
}
|
|
299
|
+
const events = [];
|
|
64
300
|
for (const line of lines) {
|
|
65
301
|
if (!line.trim()) {
|
|
66
302
|
continue;
|
|
67
303
|
}
|
|
68
304
|
try {
|
|
69
|
-
|
|
305
|
+
events.push(...this.extractParsedEvent(JSON.parse(line), observedAt));
|
|
70
306
|
}
|
|
71
307
|
catch {
|
|
72
308
|
debugLog(`[Debug] Skipping invalid peek JSON line: ${line}`);
|
|
73
|
-
|
|
309
|
+
events.push(...this.flushGeminiAssistantBuffer(observedAt));
|
|
74
310
|
}
|
|
75
311
|
}
|
|
76
|
-
return
|
|
312
|
+
return events;
|
|
313
|
+
}
|
|
314
|
+
extractForgeLines(lines, observedAt) {
|
|
315
|
+
const events = [];
|
|
316
|
+
for (const line of lines) {
|
|
317
|
+
if (!line.trim()) {
|
|
318
|
+
continue;
|
|
319
|
+
}
|
|
320
|
+
const summary = this.extractForgeMessage(line, 'Summary:');
|
|
321
|
+
if (summary !== null) {
|
|
322
|
+
events.push({ kind: 'message', ts: observedAt, text: summary });
|
|
323
|
+
continue;
|
|
324
|
+
}
|
|
325
|
+
const completed = this.extractForgeMessage(line, 'Completed successfully:');
|
|
326
|
+
if (completed !== null) {
|
|
327
|
+
events.push({ kind: 'message', ts: observedAt, text: completed });
|
|
328
|
+
continue;
|
|
329
|
+
}
|
|
330
|
+
if (this.includeToolCalls) {
|
|
331
|
+
const executeMatch = line.match(FORGE_EXECUTE_PATTERN);
|
|
332
|
+
if (executeMatch) {
|
|
333
|
+
events.push(...this.completeForgePendingTool(observedAt));
|
|
334
|
+
const [, rawTool, rawSummary] = executeMatch;
|
|
335
|
+
const tool = rawTool.trim() && !/\s/.test(rawTool.trim()) ? rawTool.trim() : 'shell';
|
|
336
|
+
const event = createToolCallEvent({
|
|
337
|
+
ts: observedAt,
|
|
338
|
+
phase: 'started',
|
|
339
|
+
id: `forge_${this.forgeToolSequence++}`,
|
|
340
|
+
tool,
|
|
341
|
+
command: rawSummary,
|
|
342
|
+
});
|
|
343
|
+
this.forgePendingTool = {
|
|
344
|
+
id: event.id,
|
|
345
|
+
tool: event.tool,
|
|
346
|
+
summary: event.summary,
|
|
347
|
+
summary_truncated: event.summary_truncated,
|
|
348
|
+
};
|
|
349
|
+
events.push(event);
|
|
350
|
+
continue;
|
|
351
|
+
}
|
|
352
|
+
if (FORGE_FINISHED_PATTERN.test(line)) {
|
|
353
|
+
events.push(...this.completeForgePendingTool(observedAt));
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
return events;
|
|
358
|
+
}
|
|
359
|
+
extractForgeMessage(line, prefix) {
|
|
360
|
+
if (!line.startsWith(prefix)) {
|
|
361
|
+
return null;
|
|
362
|
+
}
|
|
363
|
+
const text = line.slice(prefix.length).trim();
|
|
364
|
+
return text || null;
|
|
77
365
|
}
|
|
78
366
|
extractParsedEvent(parsed, observedAt) {
|
|
79
|
-
if (this.agent
|
|
80
|
-
|
|
367
|
+
if (this.agent === 'gemini') {
|
|
368
|
+
const events = this.extractGeminiParsedEvent(parsed, observedAt);
|
|
369
|
+
return events;
|
|
81
370
|
}
|
|
371
|
+
return extractPeekEventsFromParsedEvent(this.agent, parsed, observedAt, this.includeToolCalls, this.toolMemory);
|
|
372
|
+
}
|
|
373
|
+
extractGeminiParsedEvent(parsed, observedAt) {
|
|
82
374
|
if (isGeminiAssistantMessageEvent(parsed)) {
|
|
83
375
|
this.geminiAssistantBuffer += parsed.content;
|
|
84
376
|
return [];
|
|
85
377
|
}
|
|
86
|
-
|
|
378
|
+
const events = this.flushGeminiAssistantBuffer(observedAt);
|
|
379
|
+
if (this.includeToolCalls && parsed.type === 'tool_use') {
|
|
380
|
+
const event = createToolCallEvent({
|
|
381
|
+
ts: observedAt,
|
|
382
|
+
phase: 'started',
|
|
383
|
+
id: parsed.tool_id,
|
|
384
|
+
tool: parsed.tool_name || parsed.name || 'tool_use',
|
|
385
|
+
command: parsed.parameters?.command,
|
|
386
|
+
});
|
|
387
|
+
rememberToolCall(event, this.toolMemory);
|
|
388
|
+
events.push(event);
|
|
389
|
+
}
|
|
390
|
+
else if (this.includeToolCalls && parsed.type === 'tool_result') {
|
|
391
|
+
events.push(createRememberedCompletion({
|
|
392
|
+
ts: observedAt,
|
|
393
|
+
id: parsed.tool_id,
|
|
394
|
+
memory: this.toolMemory,
|
|
395
|
+
fallbackTool: parsed.tool_name || parsed.name || 'tool_result',
|
|
396
|
+
status: parsed.status,
|
|
397
|
+
defaultStatus: 'unknown',
|
|
398
|
+
}));
|
|
399
|
+
}
|
|
400
|
+
return events;
|
|
87
401
|
}
|
|
88
402
|
flushGeminiAssistantBuffer(observedAt) {
|
|
89
403
|
if (this.agent !== 'gemini' || !this.geminiAssistantBuffer) {
|
|
@@ -94,7 +408,50 @@ export class PeekMessageExtractor {
|
|
|
94
408
|
if (!text.trim()) {
|
|
95
409
|
return [];
|
|
96
410
|
}
|
|
97
|
-
return [{ ts: observedAt, text }];
|
|
411
|
+
return [{ kind: 'message', ts: observedAt, text }];
|
|
412
|
+
}
|
|
413
|
+
completeForgePendingTool(observedAt) {
|
|
414
|
+
if (!this.forgePendingTool) {
|
|
415
|
+
return [];
|
|
416
|
+
}
|
|
417
|
+
const pending = this.forgePendingTool;
|
|
418
|
+
this.forgePendingTool = null;
|
|
419
|
+
const event = createToolCallEvent({
|
|
420
|
+
ts: observedAt,
|
|
421
|
+
phase: 'completed',
|
|
422
|
+
id: pending.id,
|
|
423
|
+
tool: pending.tool,
|
|
424
|
+
status: 'unknown',
|
|
425
|
+
defaultStatus: 'unknown',
|
|
426
|
+
});
|
|
427
|
+
event.summary = pending.summary;
|
|
428
|
+
if (pending.summary_truncated) {
|
|
429
|
+
event.summary_truncated = true;
|
|
430
|
+
}
|
|
431
|
+
return [event];
|
|
432
|
+
}
|
|
433
|
+
flushForgePendingTool(observedAt, terminal) {
|
|
434
|
+
if (this.agent !== 'forge' || !terminal) {
|
|
435
|
+
return [];
|
|
436
|
+
}
|
|
437
|
+
return this.completeForgePendingTool(observedAt);
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
export class PeekMessageExtractor {
|
|
441
|
+
extractor;
|
|
442
|
+
constructor(agent) {
|
|
443
|
+
this.extractor = new PeekEventExtractor(agent, { includeToolCalls: false });
|
|
444
|
+
}
|
|
445
|
+
push(chunk, observedAt = new Date().toISOString()) {
|
|
446
|
+
return this.toMessages(this.extractor.push(chunk, observedAt));
|
|
447
|
+
}
|
|
448
|
+
flush(observedAt = new Date().toISOString(), options = {}) {
|
|
449
|
+
return this.toMessages(this.extractor.flush(observedAt, options));
|
|
450
|
+
}
|
|
451
|
+
toMessages(events) {
|
|
452
|
+
return events
|
|
453
|
+
.filter((event) => event.kind === 'message')
|
|
454
|
+
.map((event) => ({ ts: event.ts, text: event.text }));
|
|
98
455
|
}
|
|
99
456
|
}
|
|
100
457
|
export function parseCodexOutput(stdout) {
|
package/dist/peek.js
CHANGED
|
@@ -36,21 +36,24 @@ export function buildNotFoundPeekProcess(pid) {
|
|
|
36
36
|
pid,
|
|
37
37
|
agent: null,
|
|
38
38
|
status: 'not_found',
|
|
39
|
-
|
|
39
|
+
events: [],
|
|
40
40
|
truncated: false,
|
|
41
41
|
error: 'process not found',
|
|
42
42
|
};
|
|
43
43
|
}
|
|
44
|
-
export function
|
|
45
|
-
for (const
|
|
46
|
-
if (target.
|
|
47
|
-
target.
|
|
44
|
+
export function appendPeekEvents(target, events) {
|
|
45
|
+
for (const event of events) {
|
|
46
|
+
if (target.events.length < PEEK_MESSAGE_CAP) {
|
|
47
|
+
target.events.push(event);
|
|
48
48
|
}
|
|
49
49
|
else {
|
|
50
50
|
target.truncated = true;
|
|
51
51
|
}
|
|
52
52
|
}
|
|
53
53
|
}
|
|
54
|
+
export function appendPeekMessages(target, messages) {
|
|
55
|
+
appendPeekEvents(target, messages.map((message) => ({ kind: 'message', ...message })));
|
|
56
|
+
}
|
|
54
57
|
export function observedDurationSec(startedAtMs, endedAtMs = Date.now()) {
|
|
55
58
|
return Number(((endedAtMs - startedAtMs) / 1000).toFixed(2));
|
|
56
59
|
}
|
package/dist/process-service.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { spawn } from 'node:child_process';
|
|
2
2
|
import { buildCliCommand } from './cli-builder.js';
|
|
3
|
-
import { parseClaudeOutput, parseCodexOutput, parseForgeOutput, parseGeminiOutput, parseOpenCodeOutput,
|
|
4
|
-
import {
|
|
3
|
+
import { parseClaudeOutput, parseCodexOutput, parseForgeOutput, parseGeminiOutput, parseOpenCodeOutput, PeekEventExtractor } from './parsers.js';
|
|
4
|
+
import { appendPeekEvents, buildNotFoundPeekProcess, observedDurationSec, validatePeekPids, validatePeekTimeSec, } from './peek.js';
|
|
5
5
|
import { buildProcessResult } from './process-result.js';
|
|
6
6
|
function parseAgentOutput(agent, stdout, stderr) {
|
|
7
7
|
if (agent === 'codex') {
|
|
@@ -156,7 +156,7 @@ export class ProcessService {
|
|
|
156
156
|
}
|
|
157
157
|
}
|
|
158
158
|
}
|
|
159
|
-
async peekProcesses(pids, peekTimeSec = 10) {
|
|
159
|
+
async peekProcesses(pids, peekTimeSec = 10, includeToolCalls = false) {
|
|
160
160
|
const targetPids = validatePeekPids(pids);
|
|
161
161
|
const targetPeekTimeSec = validatePeekTimeSec(peekTimeSec);
|
|
162
162
|
const processes = [];
|
|
@@ -171,18 +171,18 @@ export class ProcessService {
|
|
|
171
171
|
pid,
|
|
172
172
|
agent: entry.toolType,
|
|
173
173
|
status: entry.status,
|
|
174
|
-
|
|
174
|
+
events: [],
|
|
175
175
|
truncated: false,
|
|
176
176
|
error: null,
|
|
177
177
|
};
|
|
178
178
|
processes.push(result);
|
|
179
|
-
const stdoutExtractor = new
|
|
180
|
-
const stderrExtractor = new
|
|
179
|
+
const stdoutExtractor = new PeekEventExtractor(entry.toolType, { includeToolCalls, source: 'stdout' });
|
|
180
|
+
const stderrExtractor = new PeekEventExtractor(entry.toolType, { includeToolCalls, source: 'stderr' });
|
|
181
181
|
const onStdout = (data) => {
|
|
182
|
-
|
|
182
|
+
appendPeekEvents(result, stdoutExtractor.push(data.toString(), new Date().toISOString()));
|
|
183
183
|
};
|
|
184
184
|
const onStderr = (data) => {
|
|
185
|
-
|
|
185
|
+
appendPeekEvents(result, stderrExtractor.push(data.toString(), new Date().toISOString()));
|
|
186
186
|
};
|
|
187
187
|
if (entry.status === 'running') {
|
|
188
188
|
entry.process.stdout?.on('data', onStdout);
|
|
@@ -210,8 +210,9 @@ export class ProcessService {
|
|
|
210
210
|
for (const observer of observers) {
|
|
211
211
|
observer.entry.process.stdout?.off('data', observer.onStdout);
|
|
212
212
|
observer.entry.process.stderr?.off('data', observer.onStderr);
|
|
213
|
-
|
|
214
|
-
|
|
213
|
+
const terminal = observer.entry.status !== 'running';
|
|
214
|
+
appendPeekEvents(observer.result, observer.stdoutExtractor.flush(flushTs, { terminal }));
|
|
215
|
+
appendPeekEvents(observer.result, observer.stderrExtractor.flush(flushTs, { terminal }));
|
|
215
216
|
observer.result.status = observer.entry.status;
|
|
216
217
|
}
|
|
217
218
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ai-cli-mcp",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.19.0",
|
|
4
4
|
"mcpName": "io.github.mkXultra/ai-cli-mcp",
|
|
5
5
|
"description": "MCP server for AI CLI tools (Claude, Codex, Gemini, Forge, and OpenCode) with background process management",
|
|
6
6
|
"author": "mkXultra",
|
|
@@ -210,7 +210,7 @@ describe('ai-cli app', () => {
|
|
|
210
210
|
});
|
|
211
211
|
|
|
212
212
|
const exitCode = await runCli(
|
|
213
|
-
['peek', '123', '456', '123', '--time', '5'],
|
|
213
|
+
['peek', '123', '456', '123', '--time', '5', '--include-tool-calls'],
|
|
214
214
|
{
|
|
215
215
|
stdout,
|
|
216
216
|
stderr,
|
|
@@ -219,7 +219,7 @@ describe('ai-cli app', () => {
|
|
|
219
219
|
);
|
|
220
220
|
|
|
221
221
|
expect(exitCode).toBe(0);
|
|
222
|
-
expect(peekProcesses).toHaveBeenCalledWith([123, 456], 5);
|
|
222
|
+
expect(peekProcesses).toHaveBeenCalledWith([123, 456], 5, true);
|
|
223
223
|
expect(stdout).toHaveBeenCalledWith(expect.stringContaining('"peek_started_at"'));
|
|
224
224
|
expect(stderr).not.toHaveBeenCalled();
|
|
225
225
|
});
|
|
@@ -235,7 +235,7 @@ describe('ai-cli app', () => {
|
|
|
235
235
|
|
|
236
236
|
const defaultExitCode = await runCli(['peek', '123'], { stdout, stderr, peekProcesses });
|
|
237
237
|
expect(defaultExitCode).toBe(0);
|
|
238
|
-
expect(peekProcesses).toHaveBeenCalledWith([123], 10);
|
|
238
|
+
expect(peekProcesses).toHaveBeenCalledWith([123], 10, false);
|
|
239
239
|
|
|
240
240
|
const followExitCode = await runCli(['peek', '123', '--follow'], { stdout, stderr, peekProcesses });
|
|
241
241
|
expect(followExitCode).toBe(1);
|
|
@@ -171,8 +171,9 @@ printf '%s\n' '{"type":"user","message":{"content":[{"type":"tool_result","tool_
|
|
|
171
171
|
pid: runResult.pid,
|
|
172
172
|
agent: 'claude',
|
|
173
173
|
status: 'completed',
|
|
174
|
-
|
|
174
|
+
events: [
|
|
175
175
|
{
|
|
176
|
+
kind: 'message',
|
|
176
177
|
ts: expect.any(String),
|
|
177
178
|
text: 'new cli message',
|
|
178
179
|
},
|
|
@@ -184,7 +185,7 @@ printf '%s\n' '{"type":"user","message":{"content":[{"type":"tool_result","tool_
|
|
|
184
185
|
pid: 999999,
|
|
185
186
|
agent: null,
|
|
186
187
|
status: 'not_found',
|
|
187
|
-
|
|
188
|
+
events: [],
|
|
188
189
|
truncated: false,
|
|
189
190
|
error: 'process not found',
|
|
190
191
|
});
|
|
@@ -137,6 +137,7 @@ describe('MCP Contract Tests', () => {
|
|
|
137
137
|
const peekTool = tools.find((tool: any) => tool.name === 'peek');
|
|
138
138
|
expect(peekTool.inputSchema.required).toEqual(['pids']);
|
|
139
139
|
expect(Object.keys(peekTool.inputSchema.properties).sort()).toEqual([
|
|
140
|
+
'include_tool_calls',
|
|
140
141
|
'peek_time_sec',
|
|
141
142
|
'pids',
|
|
142
143
|
]);
|