ai-cli-mcp 2.19.0 → 2.20.1

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.
Files changed (100) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/README.ja.md +34 -8
  3. package/README.md +41 -8
  4. package/dist/app/cli.js +1 -0
  5. package/dist/app/mcp.js +64 -12
  6. package/dist/cli-builder.js +13 -6
  7. package/dist/cli-process-service.js +76 -91
  8. package/dist/cli-utils.js +6 -0
  9. package/dist/cli.js +1 -1
  10. package/dist/model-catalog.js +3 -2
  11. package/dist/parsers.js +8 -2
  12. package/package.json +27 -3
  13. package/server.json +3 -3
  14. package/.gemini/settings.json +0 -11
  15. package/.github/dependabot.yml +0 -28
  16. package/.github/pull_request_template.md +0 -28
  17. package/.github/workflows/ci.yml +0 -34
  18. package/.github/workflows/dependency-review.yml +0 -22
  19. package/.github/workflows/publish.yml +0 -89
  20. package/.github/workflows/test.yml +0 -20
  21. package/.github/workflows/watch-session-prs.yml +0 -276
  22. package/.husky/pre-commit +0 -1
  23. package/.mcp.json +0 -11
  24. package/.releaserc.json +0 -18
  25. package/.vscode/settings.json +0 -3
  26. package/CONTRIBUTING.md +0 -81
  27. package/dist/__tests__/app-cli.test.js +0 -392
  28. package/dist/__tests__/cli-bin-smoke.test.js +0 -101
  29. package/dist/__tests__/cli-builder.test.js +0 -442
  30. package/dist/__tests__/cli-process-service.test.js +0 -655
  31. package/dist/__tests__/cli-utils.test.js +0 -171
  32. package/dist/__tests__/e2e.test.js +0 -256
  33. package/dist/__tests__/edge-cases.test.js +0 -130
  34. package/dist/__tests__/error-cases.test.js +0 -292
  35. package/dist/__tests__/mcp-contract.test.js +0 -636
  36. package/dist/__tests__/mocks.js +0 -32
  37. package/dist/__tests__/model-alias.test.js +0 -36
  38. package/dist/__tests__/parsers.test.js +0 -646
  39. package/dist/__tests__/peek.test.js +0 -36
  40. package/dist/__tests__/process-management.test.js +0 -949
  41. package/dist/__tests__/server.test.js +0 -809
  42. package/dist/__tests__/setup.js +0 -11
  43. package/dist/__tests__/utils/claude-mock.js +0 -80
  44. package/dist/__tests__/utils/mcp-client.js +0 -121
  45. package/dist/__tests__/utils/opencode-mock.js +0 -91
  46. package/dist/__tests__/utils/persistent-mock.js +0 -28
  47. package/dist/__tests__/utils/test-helpers.js +0 -11
  48. package/dist/__tests__/validation.test.js +0 -308
  49. package/dist/__tests__/version-print.test.js +0 -65
  50. package/dist/__tests__/wait.test.js +0 -260
  51. package/docs/RELEASE_CHECKLIST.md +0 -65
  52. package/docs/cli-architecture.md +0 -275
  53. package/docs/concept.md +0 -154
  54. package/docs/development.md +0 -156
  55. package/docs/e2e-testing.md +0 -148
  56. package/docs/prd.md +0 -146
  57. package/docs/session-stacking.md +0 -67
  58. package/src/__tests__/app-cli.test.ts +0 -495
  59. package/src/__tests__/cli-bin-smoke.test.ts +0 -136
  60. package/src/__tests__/cli-builder.test.ts +0 -549
  61. package/src/__tests__/cli-process-service.test.ts +0 -759
  62. package/src/__tests__/cli-utils.test.ts +0 -200
  63. package/src/__tests__/e2e.test.ts +0 -311
  64. package/src/__tests__/edge-cases.test.ts +0 -176
  65. package/src/__tests__/error-cases.test.ts +0 -370
  66. package/src/__tests__/mcp-contract.test.ts +0 -755
  67. package/src/__tests__/mocks.ts +0 -35
  68. package/src/__tests__/model-alias.test.ts +0 -44
  69. package/src/__tests__/parsers.test.ts +0 -730
  70. package/src/__tests__/peek.test.ts +0 -44
  71. package/src/__tests__/process-management.test.ts +0 -1129
  72. package/src/__tests__/server.test.ts +0 -1020
  73. package/src/__tests__/setup.ts +0 -13
  74. package/src/__tests__/utils/claude-mock.ts +0 -87
  75. package/src/__tests__/utils/mcp-client.ts +0 -159
  76. package/src/__tests__/utils/opencode-mock.ts +0 -108
  77. package/src/__tests__/utils/persistent-mock.ts +0 -33
  78. package/src/__tests__/utils/test-helpers.ts +0 -13
  79. package/src/__tests__/validation.test.ts +0 -369
  80. package/src/__tests__/version-print.test.ts +0 -81
  81. package/src/__tests__/wait.test.ts +0 -302
  82. package/src/app/cli.ts +0 -424
  83. package/src/app/mcp.ts +0 -466
  84. package/src/bin/ai-cli-mcp.ts +0 -7
  85. package/src/bin/ai-cli.ts +0 -11
  86. package/src/cli-builder.ts +0 -274
  87. package/src/cli-parse.ts +0 -105
  88. package/src/cli-process-service.ts +0 -709
  89. package/src/cli-utils.ts +0 -258
  90. package/src/cli.ts +0 -124
  91. package/src/model-catalog.ts +0 -87
  92. package/src/parsers.ts +0 -965
  93. package/src/peek.ts +0 -95
  94. package/src/process-result.ts +0 -88
  95. package/src/process-service.ts +0 -368
  96. package/src/server.ts +0 -10
  97. package/tsconfig.json +0 -16
  98. package/vitest.config.e2e.ts +0 -27
  99. package/vitest.config.ts +0 -22
  100. package/vitest.config.unit.ts +0 -28
package/src/parsers.ts DELETED
@@ -1,965 +0,0 @@
1
- import { debugLog } from './cli-utils.js';
2
-
3
- export interface PeekMessage {
4
- ts: string;
5
- text: string;
6
- }
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
-
28
- type PeekAgent = 'claude' | 'codex' | string | null;
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
-
63
- function isGeminiAssistantMessageEvent(parsed: any): boolean {
64
- return parsed.type === 'message' && parsed.role === 'assistant' && typeof parsed.content === 'string';
65
- }
66
-
67
- const GEMINI_STREAM_EVENT_TYPES = new Set([
68
- 'init',
69
- 'message',
70
- 'tool_use',
71
- 'tool_result',
72
- 'result',
73
- 'error',
74
- 'stats',
75
- ]);
76
-
77
- function isGeminiStreamJsonEvent(parsed: any): boolean {
78
- return parsed && typeof parsed === 'object' && !Array.isArray(parsed) && GEMINI_STREAM_EVENT_TYPES.has(parsed.type);
79
- }
80
-
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[] {
252
- if (agent === 'codex') {
253
- if (parsed.item?.type === 'agent_message' && typeof parsed.item.text === 'string' && parsed.item.text.trim()) {
254
- return [{ kind: 'message', ts: observedAt, text: parsed.item.text }];
255
- }
256
- if (parsed.msg?.type === 'agent_message' && typeof parsed.msg.message === 'string' && parsed.msg.message.trim()) {
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
- }
288
- }
289
- return [];
290
- }
291
-
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 [];
329
- }
330
-
331
- if (agent === 'opencode' && parsed.type === 'text' && parsed.part?.type === 'text' && typeof parsed.part.text === 'string' && parsed.part.text.trim()) {
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];
351
- }
352
-
353
- return [];
354
- }
355
-
356
- export class PeekEventExtractor {
357
- private pending = '';
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
- }
369
-
370
- push(chunk: string, observedAt = new Date().toISOString()): PeekEvent[] {
371
- if (this.agent === 'forge' && this.source === 'stderr') {
372
- return [];
373
- }
374
-
375
- if (!chunk) {
376
- return [];
377
- }
378
-
379
- const lines = `${this.pending}${chunk}`.split(/\r?\n/);
380
- this.pending = lines.pop() || '';
381
- return this.extractLines(lines, observedAt);
382
- }
383
-
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[] = [];
391
-
392
- if (this.pending) {
393
- if (this.agent !== 'forge' || options.terminal === true) {
394
- const line = this.pending;
395
- this.pending = '';
396
- events.push(...this.extractLines([line], observedAt));
397
- }
398
- }
399
-
400
- events.push(...this.flushGeminiAssistantBuffer(observedAt));
401
- events.push(...this.flushForgePendingTool(observedAt, options.terminal === true));
402
- return events;
403
- }
404
-
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[] = [];
411
-
412
- for (const line of lines) {
413
- if (!line.trim()) {
414
- continue;
415
- }
416
-
417
- try {
418
- events.push(...this.extractParsedEvent(JSON.parse(line), observedAt));
419
- } catch {
420
- debugLog(`[Debug] Skipping invalid peek JSON line: ${line}`);
421
- events.push(...this.flushGeminiAssistantBuffer(observedAt));
422
- }
423
- }
424
-
425
- return events;
426
- }
427
-
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;
483
- }
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[] {
499
- if (isGeminiAssistantMessageEvent(parsed)) {
500
- this.geminiAssistantBuffer += parsed.content;
501
- return [];
502
- }
503
-
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;
528
- }
529
-
530
- private flushGeminiAssistantBuffer(observedAt: string): PeekEvent[] {
531
- if (this.agent !== 'gemini' || !this.geminiAssistantBuffer) {
532
- return [];
533
- }
534
-
535
- const text = this.geminiAssistantBuffer;
536
- this.geminiAssistantBuffer = '';
537
-
538
- if (!text.trim()) {
539
- return [];
540
- }
541
-
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 }));
595
- }
596
- }
597
-
598
- export function parseCodexOutput(stdout: string): any {
599
- if (!stdout) return null;
600
-
601
- try {
602
- const lines = stdout.trim().split('\n');
603
- let lastMessage = null;
604
- let tokenCount = null;
605
- let threadId = null;
606
- const tools: any[] = [];
607
-
608
- for (const line of lines) {
609
- if (line.trim()) {
610
- try {
611
- const parsed = JSON.parse(line);
612
- if (parsed.type === 'thread.started' && parsed.thread_id) {
613
- threadId = parsed.thread_id;
614
- } else if (parsed.item?.type === 'agent_message') {
615
- lastMessage = parsed.item.text;
616
- } else if (parsed.msg?.type === 'agent_message') {
617
- lastMessage = parsed.msg.message;
618
- } else if (parsed.item?.type === 'reasoning') {
619
- } else if (parsed.msg?.type === 'token_count') {
620
- tokenCount = parsed.msg;
621
- } else if (parsed.type === 'item.completed' && parsed.item?.type === 'mcp_tool_call') {
622
- tools.push({
623
- server: parsed.item.server,
624
- tool: parsed.item.tool,
625
- input: parsed.item.arguments,
626
- output: parsed.item.result
627
- });
628
- } else if (parsed.type === 'item.completed' && parsed.item?.type === 'command_execution') {
629
- tools.push({
630
- tool: 'command_execution',
631
- input: { command: parsed.item.command },
632
- output: parsed.item.aggregated_output,
633
- exit_code: parsed.item.exit_code
634
- });
635
- }
636
- } catch (e) {
637
- debugLog(`[Debug] Skipping invalid JSON line: ${line}`);
638
- }
639
- }
640
- }
641
-
642
- if (lastMessage || tokenCount || threadId || tools.length > 0) {
643
- return {
644
- message: lastMessage,
645
- token_count: tokenCount,
646
- session_id: threadId,
647
- tools: tools.length > 0 ? tools : undefined
648
- };
649
- }
650
- } catch (e) {
651
- debugLog(`[Debug] Failed to parse Codex NDJSON output: ${e}`);
652
- }
653
-
654
- return null;
655
- }
656
-
657
- export function parseClaudeOutput(stdout: string): any {
658
- if (!stdout) return null;
659
-
660
- try {
661
- return JSON.parse(stdout);
662
- } catch (e) {
663
- }
664
-
665
- try {
666
- const lines = stdout.trim().split('\n');
667
- let lastMessage = null;
668
- let sessionId = null;
669
- const toolsMap = new Map<string, any>();
670
-
671
- for (const line of lines) {
672
- if (!line.trim()) continue;
673
-
674
- try {
675
- const parsed = JSON.parse(line);
676
-
677
- if (parsed.session_id) {
678
- sessionId = parsed.session_id;
679
- }
680
-
681
- if (parsed.type === 'result' && parsed.result) {
682
- lastMessage = parsed.result;
683
- }
684
-
685
- if (parsed.type === 'assistant' && parsed.message?.content) {
686
- for (const content of parsed.message.content) {
687
- if (content.type === 'tool_use') {
688
- toolsMap.set(content.id, {
689
- tool: content.name,
690
- input: content.input,
691
- output: null
692
- });
693
- }
694
- }
695
- }
696
-
697
- if (parsed.type === 'user' && parsed.message?.content) {
698
- for (const content of parsed.message.content) {
699
- if (content.type === 'tool_result' && content.tool_use_id) {
700
- const tool = toolsMap.get(content.tool_use_id);
701
- if (tool) {
702
- if (Array.isArray(content.content)) {
703
- const textContent = content.content.find((c: any) => c.type === 'text');
704
- tool.output = textContent?.text || null;
705
- } else {
706
- tool.output = content.content;
707
- }
708
- }
709
- }
710
- }
711
- }
712
-
713
- } catch (e) {
714
- debugLog(`[Debug] Skipping invalid JSON line in Claude output: ${line}`);
715
- }
716
- }
717
-
718
- const tools = Array.from(toolsMap.values());
719
-
720
- if (lastMessage || sessionId || tools.length > 0) {
721
- return {
722
- message: lastMessage,
723
- session_id: sessionId,
724
- tools: tools.length > 0 ? tools : undefined
725
- };
726
- }
727
-
728
- } catch (e) {
729
- debugLog(`[Debug] Failed to parse Claude NDJSON output: ${e}`);
730
- return null;
731
- }
732
-
733
- return null;
734
- }
735
-
736
- export function parseGeminiOutput(stdout: string): any {
737
- if (!stdout) return null;
738
-
739
- try {
740
- const parsed = JSON.parse(stdout.trim());
741
- if (!isGeminiStreamJsonEvent(parsed)) {
742
- return parsed;
743
- }
744
- } catch (e) {
745
- debugLog(`[Debug] Failed to parse Gemini JSON output: ${e}`);
746
- }
747
-
748
- let sessionId: string | null = null;
749
- let assistantBuffer = '';
750
- let lastMessage: string | null = null;
751
- let stats: any = null;
752
- const toolsById = new Map<string, any>();
753
- const toolsWithoutId: any[] = [];
754
- const flushAssistantMessage = () => {
755
- if (assistantBuffer.trim()) {
756
- lastMessage = assistantBuffer;
757
- }
758
- assistantBuffer = '';
759
- };
760
-
761
- for (const line of stdout.split('\n')) {
762
- if (!line.trim()) {
763
- continue;
764
- }
765
-
766
- let parsed: any;
767
- try {
768
- parsed = JSON.parse(line);
769
- } catch (e) {
770
- debugLog(`[Debug] Skipping invalid Gemini stream-json line: ${line}`);
771
- flushAssistantMessage();
772
- continue;
773
- }
774
-
775
- if (parsed.type === 'init' && typeof parsed.session_id === 'string' && parsed.session_id) {
776
- sessionId = parsed.session_id;
777
- continue;
778
- }
779
-
780
- if (isGeminiAssistantMessageEvent(parsed)) {
781
- assistantBuffer += parsed.content;
782
- continue;
783
- }
784
-
785
- flushAssistantMessage();
786
-
787
- if (parsed.type === 'result') {
788
- if (parsed.stats) {
789
- stats = parsed.stats;
790
- }
791
- continue;
792
- }
793
-
794
- if (parsed.type === 'tool_use') {
795
- const tool = {
796
- tool: parsed.tool_name || parsed.name || 'tool_use',
797
- input: parsed.parameters ?? parsed.input ?? null,
798
- output: null,
799
- status: null,
800
- };
801
- if (typeof parsed.tool_id === 'string' && parsed.tool_id) {
802
- toolsById.set(parsed.tool_id, tool);
803
- } else {
804
- toolsWithoutId.push(tool);
805
- }
806
- continue;
807
- }
808
-
809
- if (parsed.type === 'tool_result') {
810
- const toolId = typeof parsed.tool_id === 'string' ? parsed.tool_id : '';
811
- const tool = toolId ? toolsById.get(toolId) : null;
812
- if (tool) {
813
- tool.output = parsed.output ?? parsed.result ?? null;
814
- tool.status = parsed.status ?? null;
815
- } else {
816
- toolsWithoutId.push({
817
- tool: 'tool_result',
818
- input: null,
819
- output: parsed.output ?? parsed.result ?? null,
820
- status: parsed.status ?? null,
821
- });
822
- }
823
- }
824
- }
825
-
826
- flushAssistantMessage();
827
- const tools = [...toolsById.values(), ...toolsWithoutId];
828
-
829
- if (lastMessage || sessionId || stats || tools.length > 0) {
830
- return {
831
- message: lastMessage,
832
- session_id: sessionId,
833
- stats: stats || undefined,
834
- tools: tools.length > 0 ? tools : undefined,
835
- };
836
- }
837
-
838
- return null;
839
- }
840
-
841
- export function parseForgeOutput(stdout: string): any {
842
- if (!stdout) return null;
843
-
844
- const lines = stdout.split('\n');
845
- const markerPattern = /^● \[[^\]]+\] (Initialize|Continue|Finished) (\S+)\s*$/;
846
- let collecting = false;
847
- let currentConversationId: string | null = null;
848
- let currentBody: string[] = [];
849
- let lastConversationId: string | null = null;
850
- let lastMessage: string | null = null;
851
-
852
- for (const line of lines) {
853
- const match = line.match(markerPattern);
854
- if (match) {
855
- const [, action, conversationId] = match;
856
- lastConversationId = conversationId;
857
-
858
- if (action === 'Initialize' || action === 'Continue') {
859
- collecting = true;
860
- currentConversationId = conversationId;
861
- currentBody = [];
862
- } else if (collecting && currentConversationId === conversationId) {
863
- const message = currentBody.join('\n').trim();
864
- if (message) {
865
- lastMessage = message;
866
- }
867
- collecting = false;
868
- currentConversationId = null;
869
- currentBody = [];
870
- }
871
- continue;
872
- }
873
-
874
- if (collecting) {
875
- currentBody.push(line);
876
- }
877
- }
878
-
879
- if (collecting) {
880
- const message = currentBody.join('\n').trim();
881
- if (message) {
882
- lastMessage = message;
883
- }
884
- if (currentConversationId) {
885
- lastConversationId = currentConversationId;
886
- }
887
- }
888
-
889
- if (!lastMessage && !lastConversationId) {
890
- return null;
891
- }
892
-
893
- return {
894
- message: lastMessage,
895
- session_id: lastConversationId,
896
- };
897
- }
898
-
899
- export function parseOpenCodeOutput(stdout: string): any {
900
- if (!stdout) {
901
- return null;
902
- }
903
-
904
- let sessionId: string | null = null;
905
- let currentStepBuffer = '';
906
- let latestCompletedStep: {
907
- message: string;
908
- session_id?: string;
909
- tokens?: any;
910
- cost?: number;
911
- } | null = null;
912
- let hasStepFinish = false;
913
- let hasParseableAssistantText = false;
914
-
915
- for (const line of stdout.split('\n')) {
916
- if (!line.trim()) {
917
- continue;
918
- }
919
-
920
- let parsed: any;
921
- try {
922
- parsed = JSON.parse(line);
923
- } catch {
924
- continue;
925
- }
926
-
927
- if (typeof parsed.sessionID === 'string' && parsed.sessionID) {
928
- sessionId = parsed.sessionID;
929
- }
930
-
931
- if (parsed.type === 'step_start') {
932
- currentStepBuffer = '';
933
- continue;
934
- }
935
-
936
- if (parsed.type === 'text' && parsed.part?.type === 'text' && typeof parsed.part.text === 'string') {
937
- currentStepBuffer += parsed.part.text;
938
- hasParseableAssistantText = true;
939
- continue;
940
- }
941
-
942
- if (parsed.type === 'step_finish') {
943
- hasStepFinish = true;
944
- latestCompletedStep = {
945
- message: currentStepBuffer,
946
- session_id: sessionId || undefined,
947
- tokens: parsed.part?.tokens,
948
- cost: parsed.part?.cost,
949
- };
950
- }
951
- }
952
-
953
- if (hasStepFinish && latestCompletedStep) {
954
- return latestCompletedStep;
955
- }
956
-
957
- if (hasParseableAssistantText) {
958
- return {
959
- message: currentStepBuffer,
960
- session_id: sessionId || undefined,
961
- };
962
- }
963
-
964
- return null;
965
- }