ai-cli-mcp 2.14.1 → 2.15.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 (56) hide show
  1. package/.github/dependabot.yml +28 -0
  2. package/.github/workflows/ci.yml +4 -1
  3. package/.github/workflows/dependency-review.yml +22 -0
  4. package/CHANGELOG.md +7 -0
  5. package/README.ja.md +25 -6
  6. package/README.md +25 -7
  7. package/dist/__tests__/app-cli.test.js +24 -4
  8. package/dist/__tests__/cli-bin-smoke.test.js +43 -0
  9. package/dist/__tests__/cli-builder.test.js +92 -14
  10. package/dist/__tests__/cli-process-service.test.js +103 -0
  11. package/dist/__tests__/cli-utils.test.js +31 -0
  12. package/dist/__tests__/e2e.test.js +77 -51
  13. package/dist/__tests__/mcp-contract.test.js +154 -0
  14. package/dist/__tests__/parsers.test.js +62 -1
  15. package/dist/__tests__/process-management.test.js +1 -1
  16. package/dist/__tests__/server.test.js +35 -6
  17. package/dist/__tests__/utils/opencode-mock.js +91 -0
  18. package/dist/__tests__/validation.test.js +40 -2
  19. package/dist/app/cli.js +4 -4
  20. package/dist/app/mcp.js +8 -4
  21. package/dist/cli-builder.js +66 -27
  22. package/dist/cli-parse.js +11 -5
  23. package/dist/cli-process-service.js +139 -20
  24. package/dist/cli-utils.js +14 -23
  25. package/dist/cli.js +6 -4
  26. package/dist/model-catalog.js +13 -1
  27. package/dist/parsers.js +57 -26
  28. package/dist/process-result.js +9 -2
  29. package/dist/process-service.js +23 -17
  30. package/dist/server.js +1 -2
  31. package/package.json +9 -6
  32. package/src/__tests__/app-cli.test.ts +24 -4
  33. package/src/__tests__/cli-bin-smoke.test.ts +62 -1
  34. package/src/__tests__/cli-builder.test.ts +110 -14
  35. package/src/__tests__/cli-process-service.test.ts +112 -0
  36. package/src/__tests__/cli-utils.test.ts +34 -0
  37. package/src/__tests__/e2e.test.ts +85 -54
  38. package/src/__tests__/mcp-contract.test.ts +179 -0
  39. package/src/__tests__/parsers.test.ts +73 -1
  40. package/src/__tests__/process-management.test.ts +1 -1
  41. package/src/__tests__/server.test.ts +45 -10
  42. package/src/__tests__/utils/opencode-mock.ts +108 -0
  43. package/src/__tests__/validation.test.ts +48 -2
  44. package/src/app/cli.ts +4 -4
  45. package/src/app/mcp.ts +8 -4
  46. package/src/cli-builder.ts +90 -31
  47. package/src/cli-parse.ts +11 -5
  48. package/src/cli-process-service.ts +171 -17
  49. package/src/cli-utils.ts +37 -33
  50. package/src/cli.ts +6 -4
  51. package/src/model-catalog.ts +24 -1
  52. package/src/parsers.ts +77 -31
  53. package/src/process-result.ts +11 -2
  54. package/src/process-service.ts +28 -15
  55. package/src/server.ts +2 -2
  56. package/vitest.config.unit.ts +2 -3
package/src/parsers.ts CHANGED
@@ -1,18 +1,15 @@
1
1
  import { debugLog } from './cli-utils.js';
2
2
 
3
- /**
4
- * Parse Codex NDJSON output to extract the last agent message and token count
5
- */
6
3
  export function parseCodexOutput(stdout: string): any {
7
4
  if (!stdout) return null;
8
-
5
+
9
6
  try {
10
7
  const lines = stdout.trim().split('\n');
11
8
  let lastMessage = null;
12
9
  let tokenCount = null;
13
10
  let threadId = null;
14
11
  const tools: any[] = [];
15
-
12
+
16
13
  for (const line of lines) {
17
14
  if (line.trim()) {
18
15
  try {
@@ -24,14 +21,13 @@ export function parseCodexOutput(stdout: string): any {
24
21
  } else if (parsed.msg?.type === 'agent_message') {
25
22
  lastMessage = parsed.msg.message;
26
23
  } else if (parsed.item?.type === 'reasoning') {
27
- // Ignore reasoning-only items for message selection.
28
24
  } else if (parsed.msg?.type === 'token_count') {
29
25
  tokenCount = parsed.msg;
30
26
  } else if (parsed.type === 'item.completed' && parsed.item?.type === 'mcp_tool_call') {
31
27
  tools.push({
32
28
  server: parsed.item.server,
33
29
  tool: parsed.item.tool,
34
- input: parsed.item.arguments, // Map arguments to input to match common patterns
30
+ input: parsed.item.arguments,
35
31
  output: parsed.item.result
36
32
  });
37
33
  } else if (parsed.type === 'item.completed' && parsed.item?.type === 'command_execution') {
@@ -43,12 +39,11 @@ export function parseCodexOutput(stdout: string): any {
43
39
  });
44
40
  }
45
41
  } catch (e) {
46
- // Skip invalid JSON lines
47
42
  debugLog(`[Debug] Skipping invalid JSON line: ${line}`);
48
43
  }
49
44
  }
50
45
  }
51
-
46
+
52
47
  if (lastMessage || tokenCount || threadId || tools.length > 0) {
53
48
  return {
54
49
  message: lastMessage,
@@ -60,28 +55,23 @@ export function parseCodexOutput(stdout: string): any {
60
55
  } catch (e) {
61
56
  debugLog(`[Debug] Failed to parse Codex NDJSON output: ${e}`);
62
57
  }
63
-
58
+
64
59
  return null;
65
60
  }
66
61
 
67
- /**
68
- * Parse Claude Output (supports both JSON and stream-json/NDJSON)
69
- */
70
62
  export function parseClaudeOutput(stdout: string): any {
71
63
  if (!stdout) return null;
72
64
 
73
- // First try parsing as a single JSON object (backward compatibility)
74
65
  try {
75
66
  return JSON.parse(stdout);
76
67
  } catch (e) {
77
- // If not valid single JSON, proceed to parse as NDJSON
78
68
  }
79
69
 
80
70
  try {
81
71
  const lines = stdout.trim().split('\n');
82
72
  let lastMessage = null;
83
73
  let sessionId = null;
84
- const toolsMap = new Map<string, any>(); // Map by tool_use id for matching results
74
+ const toolsMap = new Map<string, any>();
85
75
 
86
76
  for (const line of lines) {
87
77
  if (!line.trim()) continue;
@@ -89,36 +79,31 @@ export function parseClaudeOutput(stdout: string): any {
89
79
  try {
90
80
  const parsed = JSON.parse(line);
91
81
 
92
- // Extract session ID from any message that has it
93
82
  if (parsed.session_id) {
94
83
  sessionId = parsed.session_id;
95
84
  }
96
85
 
97
- // Extract final result message
98
86
  if (parsed.type === 'result' && parsed.result) {
99
87
  lastMessage = parsed.result;
100
88
  }
101
89
 
102
- // Extract tool usage from assistant messages
103
90
  if (parsed.type === 'assistant' && parsed.message?.content) {
104
91
  for (const content of parsed.message.content) {
105
92
  if (content.type === 'tool_use') {
106
93
  toolsMap.set(content.id, {
107
94
  tool: content.name,
108
95
  input: content.input,
109
- output: null // Will be filled when tool_result is found
96
+ output: null
110
97
  });
111
98
  }
112
99
  }
113
100
  }
114
101
 
115
- // Match tool results from user messages
116
102
  if (parsed.type === 'user' && parsed.message?.content) {
117
103
  for (const content of parsed.message.content) {
118
104
  if (content.type === 'tool_result' && content.tool_use_id) {
119
105
  const tool = toolsMap.get(content.tool_use_id);
120
106
  if (tool) {
121
- // Extract text from content array
122
107
  if (Array.isArray(content.content)) {
123
108
  const textContent = content.content.find((c: any) => c.type === 'text');
124
109
  tool.output = textContent?.text || null;
@@ -135,12 +120,11 @@ export function parseClaudeOutput(stdout: string): any {
135
120
  }
136
121
  }
137
122
 
138
- // Convert Map to array
139
123
  const tools = Array.from(toolsMap.values());
140
124
 
141
125
  if (lastMessage || sessionId || tools.length > 0) {
142
126
  return {
143
- message: lastMessage, // This is the final result text
127
+ message: lastMessage,
144
128
  session_id: sessionId,
145
129
  tools: tools.length > 0 ? tools : undefined
146
130
  };
@@ -150,13 +134,10 @@ export function parseClaudeOutput(stdout: string): any {
150
134
  debugLog(`[Debug] Failed to parse Claude NDJSON output: ${e}`);
151
135
  return null;
152
136
  }
153
-
137
+
154
138
  return null;
155
139
  }
156
140
 
157
- /**
158
- * Parse Gemini JSON output
159
- */
160
141
  export function parseGeminiOutput(stdout: string): any {
161
142
  if (!stdout) return null;
162
143
 
@@ -168,9 +149,6 @@ export function parseGeminiOutput(stdout: string): any {
168
149
  }
169
150
  }
170
151
 
171
- /**
172
- * Parse Forge output framed by Initialize/Continue/Finished markers.
173
- */
174
152
  export function parseForgeOutput(stdout: string): any {
175
153
  if (!stdout) return null;
176
154
 
@@ -228,3 +206,71 @@ export function parseForgeOutput(stdout: string): any {
228
206
  session_id: lastConversationId,
229
207
  };
230
208
  }
209
+
210
+ export function parseOpenCodeOutput(stdout: string): any {
211
+ if (!stdout) {
212
+ return null;
213
+ }
214
+
215
+ let sessionId: string | null = null;
216
+ let currentStepBuffer = '';
217
+ let latestCompletedStep: {
218
+ message: string;
219
+ session_id?: string;
220
+ tokens?: any;
221
+ cost?: number;
222
+ } | null = null;
223
+ let hasStepFinish = false;
224
+ let hasParseableAssistantText = false;
225
+
226
+ for (const line of stdout.split('\n')) {
227
+ if (!line.trim()) {
228
+ continue;
229
+ }
230
+
231
+ let parsed: any;
232
+ try {
233
+ parsed = JSON.parse(line);
234
+ } catch {
235
+ continue;
236
+ }
237
+
238
+ if (typeof parsed.sessionID === 'string' && parsed.sessionID) {
239
+ sessionId = parsed.sessionID;
240
+ }
241
+
242
+ if (parsed.type === 'step_start') {
243
+ currentStepBuffer = '';
244
+ continue;
245
+ }
246
+
247
+ if (parsed.type === 'text' && parsed.part?.type === 'text' && typeof parsed.part.text === 'string') {
248
+ currentStepBuffer += parsed.part.text;
249
+ hasParseableAssistantText = true;
250
+ continue;
251
+ }
252
+
253
+ if (parsed.type === 'step_finish') {
254
+ hasStepFinish = true;
255
+ latestCompletedStep = {
256
+ message: currentStepBuffer,
257
+ session_id: sessionId || undefined,
258
+ tokens: parsed.part?.tokens,
259
+ cost: parsed.part?.cost,
260
+ };
261
+ }
262
+ }
263
+
264
+ if (hasStepFinish && latestCompletedStep) {
265
+ return latestCompletedStep;
266
+ }
267
+
268
+ if (hasParseableAssistantText) {
269
+ return {
270
+ message: currentStepBuffer,
271
+ session_id: sessionId || undefined,
272
+ };
273
+ }
274
+
275
+ return null;
276
+ }
@@ -45,6 +45,10 @@ function hasMeaningfulParsedOutput(agentOutput: any): boolean {
45
45
  });
46
46
  }
47
47
 
48
+ function shouldPreserveRawFailureOutput(context: ProcessResultContext): boolean {
49
+ return context.agent === 'opencode' && context.status === 'failed';
50
+ }
51
+
48
52
  export function buildProcessResult(context: ProcessResultContext, agentOutput: any, verbose = false): any {
49
53
  const response: any = {
50
54
  pid: context.pid,
@@ -65,15 +69,20 @@ export function buildProcessResult(context: ProcessResultContext, agentOutput: a
65
69
  }
66
70
 
67
71
  const shapedAgentOutput = verbose ? agentOutput : compactAgentOutput(agentOutput);
72
+ const preserveRawFailureOutput = shouldPreserveRawFailureOutput(context);
68
73
 
69
- if (hasMeaningfulParsedOutput(shapedAgentOutput)) {
74
+ if (hasMeaningfulParsedOutput(shapedAgentOutput) && (verbose || !preserveRawFailureOutput)) {
70
75
  response.agentOutput = shapedAgentOutput;
71
76
  }
72
77
 
73
- if (!response.agentOutput) {
78
+ if (!response.agentOutput || preserveRawFailureOutput) {
74
79
  response.stdout = context.stdout;
75
80
  response.stderr = context.stderr;
76
81
  }
77
82
 
83
+ if (verbose && preserveRawFailureOutput && hasMeaningfulParsedOutput(shapedAgentOutput)) {
84
+ response.agentOutput = shapedAgentOutput;
85
+ }
86
+
78
87
  return response;
79
88
  }
@@ -1,9 +1,9 @@
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 } from './parsers.js';
3
+ import { parseClaudeOutput, parseCodexOutput, parseForgeOutput, parseGeminiOutput, parseOpenCodeOutput } from './parsers.js';
4
4
  import { buildProcessResult } from './process-result.js';
5
5
 
6
- export type AgentType = 'claude' | 'codex' | 'gemini' | 'forge';
6
+ export type AgentType = 'claude' | 'codex' | 'gemini' | 'forge' | 'opencode';
7
7
  export type ProcessStatus = 'running' | 'completed' | 'failed';
8
8
 
9
9
  interface TrackedProcess {
@@ -37,6 +37,31 @@ interface ProcessServiceOptions {
37
37
  cliPaths: BuildCliCommandOptions['cliPaths'];
38
38
  }
39
39
 
40
+ function parseAgentOutput(agent: AgentType, stdout: string, stderr: string): any {
41
+ if (agent === 'codex') {
42
+ return parseCodexOutput(`${stdout || ''}\n${stderr || ''}`);
43
+ }
44
+
45
+ if (!stdout) {
46
+ return null;
47
+ }
48
+
49
+ if (agent === 'claude') {
50
+ return parseClaudeOutput(stdout);
51
+ }
52
+ if (agent === 'gemini') {
53
+ return parseGeminiOutput(stdout);
54
+ }
55
+ if (agent === 'forge') {
56
+ return parseForgeOutput(stdout);
57
+ }
58
+ if (agent === 'opencode') {
59
+ return parseOpenCodeOutput(stdout);
60
+ }
61
+
62
+ return null;
63
+ }
64
+
40
65
  export class ProcessService {
41
66
  private readonly processManager = new Map<number, TrackedProcess>();
42
67
  private readonly cliPaths: BuildCliCommandOptions['cliPaths'];
@@ -136,19 +161,7 @@ export class ProcessService {
136
161
  throw new Error(`Process with PID ${pid} not found`);
137
162
  }
138
163
 
139
- let agentOutput: any = null;
140
- if (process.toolType === 'codex') {
141
- const combinedOutput = (process.stdout || '') + '\n' + (process.stderr || '');
142
- agentOutput = parseCodexOutput(combinedOutput);
143
- } else if (process.stdout) {
144
- if (process.toolType === 'claude') {
145
- agentOutput = parseClaudeOutput(process.stdout);
146
- } else if (process.toolType === 'gemini') {
147
- agentOutput = parseGeminiOutput(process.stdout);
148
- } else if (process.toolType === 'forge') {
149
- agentOutput = parseForgeOutput(process.stdout);
150
- }
151
- }
164
+ const agentOutput = parseAgentOutput(process.toolType, process.stdout, process.stderr);
152
165
 
153
166
  return buildProcessResult({
154
167
  pid,
package/src/server.ts CHANGED
@@ -1,5 +1,5 @@
1
- #!/usr/bin/env node
2
- export { debugLog, findClaudeCli, findCodexCli, findForgeCli, findGeminiCli } from './cli-utils.js';
1
+ import { debugLog, findClaudeCli, findCodexCli, findForgeCli, findGeminiCli, findOpencodeCli } from './cli-utils.js';
2
+ export { debugLog, findClaudeCli, findCodexCli, findForgeCli, findGeminiCli, findOpencodeCli } from './cli-utils.js';
3
3
  export { resolveModelAlias } from './cli-builder.js';
4
4
  export { ClaudeCodeServer, runMcpServer, spawnAsync } from './app/mcp.js';
5
5
 
@@ -17,13 +17,12 @@ export default defineConfig({
17
17
  },
18
18
  exclude: [
19
19
  'node_modules/**',
20
+ 'dist/**',
20
21
  'src/__tests__/e2e.test.ts',
21
22
  'src/__tests__/edge-cases.test.ts',
22
- 'dist/__tests__/e2e.test.js',
23
- 'dist/__tests__/edge-cases.test.js',
24
23
  ],
25
24
  mockReset: true,
26
25
  clearMocks: true,
27
26
  restoreMocks: true,
28
27
  },
29
- });
28
+ });