ai-cli-mcp 2.18.0 → 2.20.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.
Files changed (101) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/README.ja.md +37 -11
  3. package/README.md +44 -11
  4. package/dist/app/cli.js +2 -1
  5. package/dist/app/mcp.js +65 -13
  6. package/dist/cli-builder.js +13 -6
  7. package/dist/cli-process-service.js +81 -95
  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 +111 -8
  12. package/dist/process-service.js +5 -4
  13. package/package.json +26 -2
  14. package/server.json +3 -3
  15. package/.gemini/settings.json +0 -11
  16. package/.github/dependabot.yml +0 -28
  17. package/.github/pull_request_template.md +0 -28
  18. package/.github/workflows/ci.yml +0 -34
  19. package/.github/workflows/dependency-review.yml +0 -22
  20. package/.github/workflows/publish.yml +0 -89
  21. package/.github/workflows/test.yml +0 -20
  22. package/.github/workflows/watch-session-prs.yml +0 -276
  23. package/.husky/pre-commit +0 -1
  24. package/.mcp.json +0 -11
  25. package/.releaserc.json +0 -18
  26. package/.vscode/settings.json +0 -3
  27. package/CONTRIBUTING.md +0 -81
  28. package/dist/__tests__/app-cli.test.js +0 -392
  29. package/dist/__tests__/cli-bin-smoke.test.js +0 -101
  30. package/dist/__tests__/cli-builder.test.js +0 -442
  31. package/dist/__tests__/cli-process-service.test.js +0 -655
  32. package/dist/__tests__/cli-utils.test.js +0 -171
  33. package/dist/__tests__/e2e.test.js +0 -256
  34. package/dist/__tests__/edge-cases.test.js +0 -130
  35. package/dist/__tests__/error-cases.test.js +0 -292
  36. package/dist/__tests__/mcp-contract.test.js +0 -636
  37. package/dist/__tests__/mocks.js +0 -32
  38. package/dist/__tests__/model-alias.test.js +0 -36
  39. package/dist/__tests__/parsers.test.js +0 -500
  40. package/dist/__tests__/peek.test.js +0 -36
  41. package/dist/__tests__/process-management.test.js +0 -871
  42. package/dist/__tests__/server.test.js +0 -809
  43. package/dist/__tests__/setup.js +0 -11
  44. package/dist/__tests__/utils/claude-mock.js +0 -80
  45. package/dist/__tests__/utils/mcp-client.js +0 -121
  46. package/dist/__tests__/utils/opencode-mock.js +0 -91
  47. package/dist/__tests__/utils/persistent-mock.js +0 -28
  48. package/dist/__tests__/utils/test-helpers.js +0 -11
  49. package/dist/__tests__/validation.test.js +0 -308
  50. package/dist/__tests__/version-print.test.js +0 -65
  51. package/dist/__tests__/wait.test.js +0 -260
  52. package/docs/RELEASE_CHECKLIST.md +0 -65
  53. package/docs/cli-architecture.md +0 -275
  54. package/docs/concept.md +0 -154
  55. package/docs/development.md +0 -156
  56. package/docs/e2e-testing.md +0 -148
  57. package/docs/prd.md +0 -146
  58. package/docs/session-stacking.md +0 -67
  59. package/src/__tests__/app-cli.test.ts +0 -495
  60. package/src/__tests__/cli-bin-smoke.test.ts +0 -136
  61. package/src/__tests__/cli-builder.test.ts +0 -549
  62. package/src/__tests__/cli-process-service.test.ts +0 -759
  63. package/src/__tests__/cli-utils.test.ts +0 -200
  64. package/src/__tests__/e2e.test.ts +0 -311
  65. package/src/__tests__/edge-cases.test.ts +0 -176
  66. package/src/__tests__/error-cases.test.ts +0 -370
  67. package/src/__tests__/mcp-contract.test.ts +0 -755
  68. package/src/__tests__/mocks.ts +0 -35
  69. package/src/__tests__/model-alias.test.ts +0 -44
  70. package/src/__tests__/parsers.test.ts +0 -564
  71. package/src/__tests__/peek.test.ts +0 -44
  72. package/src/__tests__/process-management.test.ts +0 -1043
  73. package/src/__tests__/server.test.ts +0 -1020
  74. package/src/__tests__/setup.ts +0 -13
  75. package/src/__tests__/utils/claude-mock.ts +0 -87
  76. package/src/__tests__/utils/mcp-client.ts +0 -159
  77. package/src/__tests__/utils/opencode-mock.ts +0 -108
  78. package/src/__tests__/utils/persistent-mock.ts +0 -33
  79. package/src/__tests__/utils/test-helpers.ts +0 -13
  80. package/src/__tests__/validation.test.ts +0 -369
  81. package/src/__tests__/version-print.test.ts +0 -81
  82. package/src/__tests__/wait.test.ts +0 -302
  83. package/src/app/cli.ts +0 -424
  84. package/src/app/mcp.ts +0 -466
  85. package/src/bin/ai-cli-mcp.ts +0 -7
  86. package/src/bin/ai-cli.ts +0 -11
  87. package/src/cli-builder.ts +0 -274
  88. package/src/cli-parse.ts +0 -105
  89. package/src/cli-process-service.ts +0 -708
  90. package/src/cli-utils.ts +0 -258
  91. package/src/cli.ts +0 -124
  92. package/src/model-catalog.ts +0 -87
  93. package/src/parsers.ts +0 -840
  94. package/src/peek.ts +0 -95
  95. package/src/process-result.ts +0 -88
  96. package/src/process-service.ts +0 -367
  97. package/src/server.ts +0 -10
  98. package/tsconfig.json +0 -16
  99. package/vitest.config.e2e.ts +0 -27
  100. package/vitest.config.ts +0 -22
  101. package/vitest.config.unit.ts +0 -28
package/src/parsers.ts DELETED
@@ -1,840 +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
- }
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
-
49
- function isGeminiAssistantMessageEvent(parsed: any): boolean {
50
- return parsed.type === 'message' && parsed.role === 'assistant' && typeof parsed.content === 'string';
51
- }
52
-
53
- const GEMINI_STREAM_EVENT_TYPES = new Set([
54
- 'init',
55
- 'message',
56
- 'tool_use',
57
- 'tool_result',
58
- 'result',
59
- 'error',
60
- 'stats',
61
- ]);
62
-
63
- function isGeminiStreamJsonEvent(parsed: any): boolean {
64
- return parsed && typeof parsed === 'object' && !Array.isArray(parsed) && GEMINI_STREAM_EVENT_TYPES.has(parsed.type);
65
- }
66
-
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[] {
238
- if (agent === 'codex') {
239
- if (parsed.item?.type === 'agent_message' && typeof parsed.item.text === 'string' && parsed.item.text.trim()) {
240
- return [{ kind: 'message', ts: observedAt, text: parsed.item.text }];
241
- }
242
- if (parsed.msg?.type === 'agent_message' && typeof parsed.msg.message === 'string' && parsed.msg.message.trim()) {
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
- }
274
- }
275
- return [];
276
- }
277
-
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 [];
315
- }
316
-
317
- if (agent === 'opencode' && parsed.type === 'text' && parsed.part?.type === 'text' && typeof parsed.part.text === 'string' && parsed.part.text.trim()) {
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];
337
- }
338
-
339
- return [];
340
- }
341
-
342
- export class PeekEventExtractor {
343
- private pending = '';
344
- private geminiAssistantBuffer = '';
345
- private readonly includeToolCalls: boolean;
346
- private readonly toolMemory = new Map<string, ToolCallMemory>();
347
-
348
- constructor(private readonly agent: PeekAgent, options: PeekEventExtractorOptions = {}) {
349
- this.includeToolCalls = options.includeToolCalls === true;
350
- }
351
-
352
- push(chunk: string, observedAt = new Date().toISOString()): PeekEvent[] {
353
- if (!chunk) {
354
- return [];
355
- }
356
-
357
- const lines = `${this.pending}${chunk}`.split(/\r?\n/);
358
- this.pending = lines.pop() || '';
359
- return this.extractLines(lines, observedAt);
360
- }
361
-
362
- flush(observedAt = new Date().toISOString()): PeekEvent[] {
363
- const events: PeekEvent[] = [];
364
-
365
- if (this.pending) {
366
- const line = this.pending;
367
- this.pending = '';
368
- events.push(...this.extractLines([line], observedAt));
369
- }
370
-
371
- events.push(...this.flushGeminiAssistantBuffer(observedAt));
372
- return events;
373
- }
374
-
375
- private extractLines(lines: string[], observedAt: string): PeekEvent[] {
376
- const events: PeekEvent[] = [];
377
-
378
- for (const line of lines) {
379
- if (!line.trim()) {
380
- continue;
381
- }
382
-
383
- try {
384
- events.push(...this.extractParsedEvent(JSON.parse(line), observedAt));
385
- } catch {
386
- debugLog(`[Debug] Skipping invalid peek JSON line: ${line}`);
387
- events.push(...this.flushGeminiAssistantBuffer(observedAt));
388
- }
389
- }
390
-
391
- return events;
392
- }
393
-
394
- private extractParsedEvent(parsed: any, observedAt: string): PeekEvent[] {
395
- if (this.agent === 'gemini') {
396
- const events = this.extractGeminiParsedEvent(parsed, observedAt);
397
- return events;
398
- }
399
-
400
- return extractPeekEventsFromParsedEvent(this.agent, parsed, observedAt, this.includeToolCalls, this.toolMemory);
401
- }
402
-
403
- private extractGeminiParsedEvent(parsed: any, observedAt: string): PeekEvent[] {
404
- if (isGeminiAssistantMessageEvent(parsed)) {
405
- this.geminiAssistantBuffer += parsed.content;
406
- return [];
407
- }
408
-
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;
433
- }
434
-
435
- private flushGeminiAssistantBuffer(observedAt: string): PeekEvent[] {
436
- if (this.agent !== 'gemini' || !this.geminiAssistantBuffer) {
437
- return [];
438
- }
439
-
440
- const text = this.geminiAssistantBuffer;
441
- this.geminiAssistantBuffer = '';
442
-
443
- if (!text.trim()) {
444
- return [];
445
- }
446
-
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 }));
470
- }
471
- }
472
-
473
- export function parseCodexOutput(stdout: string): any {
474
- if (!stdout) return null;
475
-
476
- try {
477
- const lines = stdout.trim().split('\n');
478
- let lastMessage = null;
479
- let tokenCount = null;
480
- let threadId = null;
481
- const tools: any[] = [];
482
-
483
- for (const line of lines) {
484
- if (line.trim()) {
485
- try {
486
- const parsed = JSON.parse(line);
487
- if (parsed.type === 'thread.started' && parsed.thread_id) {
488
- threadId = parsed.thread_id;
489
- } else if (parsed.item?.type === 'agent_message') {
490
- lastMessage = parsed.item.text;
491
- } else if (parsed.msg?.type === 'agent_message') {
492
- lastMessage = parsed.msg.message;
493
- } else if (parsed.item?.type === 'reasoning') {
494
- } else if (parsed.msg?.type === 'token_count') {
495
- tokenCount = parsed.msg;
496
- } else if (parsed.type === 'item.completed' && parsed.item?.type === 'mcp_tool_call') {
497
- tools.push({
498
- server: parsed.item.server,
499
- tool: parsed.item.tool,
500
- input: parsed.item.arguments,
501
- output: parsed.item.result
502
- });
503
- } else if (parsed.type === 'item.completed' && parsed.item?.type === 'command_execution') {
504
- tools.push({
505
- tool: 'command_execution',
506
- input: { command: parsed.item.command },
507
- output: parsed.item.aggregated_output,
508
- exit_code: parsed.item.exit_code
509
- });
510
- }
511
- } catch (e) {
512
- debugLog(`[Debug] Skipping invalid JSON line: ${line}`);
513
- }
514
- }
515
- }
516
-
517
- if (lastMessage || tokenCount || threadId || tools.length > 0) {
518
- return {
519
- message: lastMessage,
520
- token_count: tokenCount,
521
- session_id: threadId,
522
- tools: tools.length > 0 ? tools : undefined
523
- };
524
- }
525
- } catch (e) {
526
- debugLog(`[Debug] Failed to parse Codex NDJSON output: ${e}`);
527
- }
528
-
529
- return null;
530
- }
531
-
532
- export function parseClaudeOutput(stdout: string): any {
533
- if (!stdout) return null;
534
-
535
- try {
536
- return JSON.parse(stdout);
537
- } catch (e) {
538
- }
539
-
540
- try {
541
- const lines = stdout.trim().split('\n');
542
- let lastMessage = null;
543
- let sessionId = null;
544
- const toolsMap = new Map<string, any>();
545
-
546
- for (const line of lines) {
547
- if (!line.trim()) continue;
548
-
549
- try {
550
- const parsed = JSON.parse(line);
551
-
552
- if (parsed.session_id) {
553
- sessionId = parsed.session_id;
554
- }
555
-
556
- if (parsed.type === 'result' && parsed.result) {
557
- lastMessage = parsed.result;
558
- }
559
-
560
- if (parsed.type === 'assistant' && parsed.message?.content) {
561
- for (const content of parsed.message.content) {
562
- if (content.type === 'tool_use') {
563
- toolsMap.set(content.id, {
564
- tool: content.name,
565
- input: content.input,
566
- output: null
567
- });
568
- }
569
- }
570
- }
571
-
572
- if (parsed.type === 'user' && parsed.message?.content) {
573
- for (const content of parsed.message.content) {
574
- if (content.type === 'tool_result' && content.tool_use_id) {
575
- const tool = toolsMap.get(content.tool_use_id);
576
- if (tool) {
577
- if (Array.isArray(content.content)) {
578
- const textContent = content.content.find((c: any) => c.type === 'text');
579
- tool.output = textContent?.text || null;
580
- } else {
581
- tool.output = content.content;
582
- }
583
- }
584
- }
585
- }
586
- }
587
-
588
- } catch (e) {
589
- debugLog(`[Debug] Skipping invalid JSON line in Claude output: ${line}`);
590
- }
591
- }
592
-
593
- const tools = Array.from(toolsMap.values());
594
-
595
- if (lastMessage || sessionId || tools.length > 0) {
596
- return {
597
- message: lastMessage,
598
- session_id: sessionId,
599
- tools: tools.length > 0 ? tools : undefined
600
- };
601
- }
602
-
603
- } catch (e) {
604
- debugLog(`[Debug] Failed to parse Claude NDJSON output: ${e}`);
605
- return null;
606
- }
607
-
608
- return null;
609
- }
610
-
611
- export function parseGeminiOutput(stdout: string): any {
612
- if (!stdout) return null;
613
-
614
- try {
615
- const parsed = JSON.parse(stdout.trim());
616
- if (!isGeminiStreamJsonEvent(parsed)) {
617
- return parsed;
618
- }
619
- } catch (e) {
620
- debugLog(`[Debug] Failed to parse Gemini JSON output: ${e}`);
621
- }
622
-
623
- let sessionId: string | null = null;
624
- let assistantBuffer = '';
625
- let lastMessage: string | null = null;
626
- let stats: any = null;
627
- const toolsById = new Map<string, any>();
628
- const toolsWithoutId: any[] = [];
629
- const flushAssistantMessage = () => {
630
- if (assistantBuffer.trim()) {
631
- lastMessage = assistantBuffer;
632
- }
633
- assistantBuffer = '';
634
- };
635
-
636
- for (const line of stdout.split('\n')) {
637
- if (!line.trim()) {
638
- continue;
639
- }
640
-
641
- let parsed: any;
642
- try {
643
- parsed = JSON.parse(line);
644
- } catch (e) {
645
- debugLog(`[Debug] Skipping invalid Gemini stream-json line: ${line}`);
646
- flushAssistantMessage();
647
- continue;
648
- }
649
-
650
- if (parsed.type === 'init' && typeof parsed.session_id === 'string' && parsed.session_id) {
651
- sessionId = parsed.session_id;
652
- continue;
653
- }
654
-
655
- if (isGeminiAssistantMessageEvent(parsed)) {
656
- assistantBuffer += parsed.content;
657
- continue;
658
- }
659
-
660
- flushAssistantMessage();
661
-
662
- if (parsed.type === 'result') {
663
- if (parsed.stats) {
664
- stats = parsed.stats;
665
- }
666
- continue;
667
- }
668
-
669
- if (parsed.type === 'tool_use') {
670
- const tool = {
671
- tool: parsed.tool_name || parsed.name || 'tool_use',
672
- input: parsed.parameters ?? parsed.input ?? null,
673
- output: null,
674
- status: null,
675
- };
676
- if (typeof parsed.tool_id === 'string' && parsed.tool_id) {
677
- toolsById.set(parsed.tool_id, tool);
678
- } else {
679
- toolsWithoutId.push(tool);
680
- }
681
- continue;
682
- }
683
-
684
- if (parsed.type === 'tool_result') {
685
- const toolId = typeof parsed.tool_id === 'string' ? parsed.tool_id : '';
686
- const tool = toolId ? toolsById.get(toolId) : null;
687
- if (tool) {
688
- tool.output = parsed.output ?? parsed.result ?? null;
689
- tool.status = parsed.status ?? null;
690
- } else {
691
- toolsWithoutId.push({
692
- tool: 'tool_result',
693
- input: null,
694
- output: parsed.output ?? parsed.result ?? null,
695
- status: parsed.status ?? null,
696
- });
697
- }
698
- }
699
- }
700
-
701
- flushAssistantMessage();
702
- const tools = [...toolsById.values(), ...toolsWithoutId];
703
-
704
- if (lastMessage || sessionId || stats || tools.length > 0) {
705
- return {
706
- message: lastMessage,
707
- session_id: sessionId,
708
- stats: stats || undefined,
709
- tools: tools.length > 0 ? tools : undefined,
710
- };
711
- }
712
-
713
- return null;
714
- }
715
-
716
- export function parseForgeOutput(stdout: string): any {
717
- if (!stdout) return null;
718
-
719
- const lines = stdout.split('\n');
720
- const markerPattern = /^● \[[^\]]+\] (Initialize|Continue|Finished) (\S+)\s*$/;
721
- let collecting = false;
722
- let currentConversationId: string | null = null;
723
- let currentBody: string[] = [];
724
- let lastConversationId: string | null = null;
725
- let lastMessage: string | null = null;
726
-
727
- for (const line of lines) {
728
- const match = line.match(markerPattern);
729
- if (match) {
730
- const [, action, conversationId] = match;
731
- lastConversationId = conversationId;
732
-
733
- if (action === 'Initialize' || action === 'Continue') {
734
- collecting = true;
735
- currentConversationId = conversationId;
736
- currentBody = [];
737
- } else if (collecting && currentConversationId === conversationId) {
738
- const message = currentBody.join('\n').trim();
739
- if (message) {
740
- lastMessage = message;
741
- }
742
- collecting = false;
743
- currentConversationId = null;
744
- currentBody = [];
745
- }
746
- continue;
747
- }
748
-
749
- if (collecting) {
750
- currentBody.push(line);
751
- }
752
- }
753
-
754
- if (collecting) {
755
- const message = currentBody.join('\n').trim();
756
- if (message) {
757
- lastMessage = message;
758
- }
759
- if (currentConversationId) {
760
- lastConversationId = currentConversationId;
761
- }
762
- }
763
-
764
- if (!lastMessage && !lastConversationId) {
765
- return null;
766
- }
767
-
768
- return {
769
- message: lastMessage,
770
- session_id: lastConversationId,
771
- };
772
- }
773
-
774
- export function parseOpenCodeOutput(stdout: string): any {
775
- if (!stdout) {
776
- return null;
777
- }
778
-
779
- let sessionId: string | null = null;
780
- let currentStepBuffer = '';
781
- let latestCompletedStep: {
782
- message: string;
783
- session_id?: string;
784
- tokens?: any;
785
- cost?: number;
786
- } | null = null;
787
- let hasStepFinish = false;
788
- let hasParseableAssistantText = false;
789
-
790
- for (const line of stdout.split('\n')) {
791
- if (!line.trim()) {
792
- continue;
793
- }
794
-
795
- let parsed: any;
796
- try {
797
- parsed = JSON.parse(line);
798
- } catch {
799
- continue;
800
- }
801
-
802
- if (typeof parsed.sessionID === 'string' && parsed.sessionID) {
803
- sessionId = parsed.sessionID;
804
- }
805
-
806
- if (parsed.type === 'step_start') {
807
- currentStepBuffer = '';
808
- continue;
809
- }
810
-
811
- if (parsed.type === 'text' && parsed.part?.type === 'text' && typeof parsed.part.text === 'string') {
812
- currentStepBuffer += parsed.part.text;
813
- hasParseableAssistantText = true;
814
- continue;
815
- }
816
-
817
- if (parsed.type === 'step_finish') {
818
- hasStepFinish = true;
819
- latestCompletedStep = {
820
- message: currentStepBuffer,
821
- session_id: sessionId || undefined,
822
- tokens: parsed.part?.tokens,
823
- cost: parsed.part?.cost,
824
- };
825
- }
826
- }
827
-
828
- if (hasStepFinish && latestCompletedStep) {
829
- return latestCompletedStep;
830
- }
831
-
832
- if (hasParseableAssistantText) {
833
- return {
834
- message: currentStepBuffer,
835
- session_id: sessionId || undefined,
836
- };
837
- }
838
-
839
- return null;
840
- }