ai-cli-mcp 2.14.0 → 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 +14 -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 +187 -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 +158 -25
  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 +217 -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 +193 -22
  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/cli-utils.ts CHANGED
@@ -3,10 +3,8 @@ import { homedir } from 'node:os';
3
3
  import { join } from 'node:path';
4
4
  import * as path from 'path';
5
5
 
6
- // Define debugMode globally using const
7
6
  const debugMode = process.env.MCP_CLAUDE_DEBUG === 'true';
8
7
 
9
- // Dedicated debug logging function
10
8
  export function debugLog(message?: any, ...optionalParams: any[]): void {
11
9
  if (debugMode) {
12
10
  console.error(message, ...optionalParams);
@@ -21,6 +19,24 @@ export interface CliBinaryStatus {
21
19
  error?: string;
22
20
  }
23
21
 
22
+ export type CliBinaryName = 'claude' | 'codex' | 'gemini' | 'forge' | 'opencode';
23
+
24
+ export interface CliPaths {
25
+ claude: string;
26
+ codex: string;
27
+ gemini: string;
28
+ forge: string;
29
+ opencode: string;
30
+ }
31
+
32
+ export interface CliDoctorStatus {
33
+ claude: CliBinaryStatus;
34
+ codex: CliBinaryStatus;
35
+ gemini: CliBinaryStatus;
36
+ forge: CliBinaryStatus;
37
+ opencode: CliBinaryStatus;
38
+ }
39
+
24
40
  function getPathDelimiter(): string {
25
41
  return process.platform === 'win32' ? ';' : ':';
26
42
  }
@@ -75,7 +91,7 @@ function inspectCliBinary(options: {
75
91
  envVarName: string;
76
92
  customCliName: string | undefined;
77
93
  defaultCliName: string;
78
- localInstallPath: string;
94
+ localInstallPath?: string;
79
95
  }): CliBinaryStatus {
80
96
  const configuredCommand = options.customCliName || options.defaultCliName;
81
97
 
@@ -109,7 +125,7 @@ function inspectCliBinary(options: {
109
125
  };
110
126
  }
111
127
 
112
- if (isExecutableFile(options.localInstallPath)) {
128
+ if (options.localInstallPath && isExecutableFile(options.localInstallPath)) {
113
129
  return {
114
130
  configuredCommand,
115
131
  resolvedPath: options.localInstallPath,
@@ -148,13 +164,11 @@ function isExecutableFile(filePath: string): boolean {
148
164
  }
149
165
  }
150
166
 
151
- type CliBinaryName = 'claude' | 'codex' | 'gemini' | 'forge';
152
-
153
167
  function getCliBinaryConfig(name: CliBinaryName): {
154
168
  envVarName: string;
155
169
  customCliName: string | undefined;
156
170
  defaultCliName: string;
157
- localInstallPath: string;
171
+ localInstallPath?: string;
158
172
  } {
159
173
  if (name === 'claude') {
160
174
  return {
@@ -183,6 +197,14 @@ function getCliBinaryConfig(name: CliBinaryName): {
183
197
  };
184
198
  }
185
199
 
200
+ if (name === 'opencode') {
201
+ return {
202
+ envVarName: 'OPENCODE_CLI_NAME',
203
+ customCliName: process.env.OPENCODE_CLI_NAME,
204
+ defaultCliName: 'opencode',
205
+ };
206
+ }
207
+
186
208
  return {
187
209
  envVarName: 'GEMINI_CLI_NAME',
188
210
  customCliName: process.env.GEMINI_CLI_NAME,
@@ -195,58 +217,40 @@ function getCliBinaryStatus(name: CliBinaryName): CliBinaryStatus {
195
217
  return inspectCliBinary(getCliBinaryConfig(name));
196
218
  }
197
219
 
198
- export function getCliDoctorStatus(): {
199
- claude: CliBinaryStatus;
200
- codex: CliBinaryStatus;
201
- gemini: CliBinaryStatus;
202
- forge: CliBinaryStatus;
203
- } {
220
+ export function getCliDoctorStatus(): CliDoctorStatus {
204
221
  return {
205
222
  claude: getCliBinaryStatus('claude'),
206
223
  codex: getCliBinaryStatus('codex'),
207
224
  gemini: getCliBinaryStatus('gemini'),
208
225
  forge: getCliBinaryStatus('forge'),
226
+ opencode: getCliBinaryStatus('opencode'),
209
227
  };
210
228
  }
211
229
 
212
- /**
213
- * Determine the Gemini CLI command/path.
214
- * Similar to findClaudeCli but for Gemini
215
- */
216
230
  export function findGeminiCli(): string {
217
231
  debugLog('[Debug] Attempting to find Gemini CLI...');
218
232
  const status = getCliBinaryStatus('gemini');
219
233
  return getCliCommandOrThrow(status);
220
234
  }
221
235
 
222
- /**
223
- * Determine the Codex CLI command/path.
224
- * Similar to findClaudeCli but for Codex
225
- */
226
236
  export function findCodexCli(): string {
227
237
  debugLog('[Debug] Attempting to find Codex CLI...');
228
238
  const status = getCliBinaryStatus('codex');
229
239
  return getCliCommandOrThrow(status);
230
240
  }
231
241
 
232
- /**
233
- * Determine the Forge CLI command/path.
234
- */
235
242
  export function findForgeCli(): string {
236
243
  debugLog('[Debug] Attempting to find Forge CLI...');
237
244
  const status = getCliBinaryStatus('forge');
238
245
  return getCliCommandOrThrow(status);
239
246
  }
240
247
 
241
- /**
242
- * Determine the Claude CLI command/path.
243
- * 1. Checks for CLAUDE_CLI_NAME environment variable:
244
- * - If absolute path, uses it directly
245
- * - If relative path, throws error
246
- * - If simple name, continues with path resolution
247
- * 2. Checks for Claude CLI at the local user path: ~/.claude/local/claude.
248
- * 3. If not found, defaults to the CLI name (or 'claude'), relying on the system's PATH for lookup.
249
- */
248
+ export function findOpencodeCli(): string {
249
+ debugLog('[Debug] Attempting to find OpenCode CLI...');
250
+ const status = getCliBinaryStatus('opencode');
251
+ return getCliCommandOrThrow(status);
252
+ }
253
+
250
254
  export function findClaudeCli(): string {
251
255
  debugLog('[Debug] Attempting to find Claude CLI...');
252
256
  const status = getCliBinaryStatus('claude');
package/src/cli.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import { spawn } from 'node:child_process';
3
3
  import { buildCliCommand } from './cli-builder.js';
4
- import { findClaudeCli, findCodexCli, findForgeCli, findGeminiCli } from './cli-utils.js';
4
+ import { findClaudeCli, findCodexCli, findForgeCli, findGeminiCli, findOpencodeCli } from './cli-utils.js';
5
5
 
6
6
  /**
7
7
  * Minimal argv parser. No external dependencies.
@@ -35,17 +35,18 @@ function parseArgs(argv: string[]): Record<string, string> {
35
35
  const USAGE = `Usage: npm run -s cli.run -- --model <model> --workFolder <path> --prompt "..." [options]
36
36
 
37
37
  Options:
38
- --model Model name or alias (e.g. sonnet, opus, gpt-5.2-codex, gemini-2.5-pro, forge)
38
+ --model Model name or alias (e.g. sonnet, opus, gpt-5.2-codex, gemini-2.5-pro, forge, opencode, oc-openai/gpt-5.4)
39
39
  --workFolder Working directory (absolute path)
40
40
  --prompt Prompt string (mutually exclusive with --prompt_file)
41
41
  --prompt_file Path to a file containing the prompt
42
- --session_id Session ID to resume
43
- --reasoning_effort Claude/Codex only: Claude=low|medium|high, Codex=low|medium|high|xhigh
42
+ --session_id Session ID to resume, including OpenCode in-place resumes
43
+ --reasoning_effort Claude/Codex only: Claude=low|medium|high, Codex=low|medium|high|xhigh; unsupported for Gemini, Forge, and OpenCode
44
44
  --help Show this help message
45
45
 
46
46
  Raw CLI output goes to stdout. Use cli.run.parse to parse the output:
47
47
  npm run -s cli.run -- ... > raw.txt
48
48
  npm run -s cli.run.parse -- --agent claude < raw.txt
49
+ npm run -s cli.run.parse -- --agent opencode < raw.txt
49
50
  `;
50
51
 
51
52
  async function main(): Promise<void> {
@@ -74,6 +75,7 @@ async function main(): Promise<void> {
74
75
  codex: findCodexCli(),
75
76
  gemini: findGeminiCli(),
76
77
  forge: findForgeCli(),
78
+ opencode: findOpencodeCli(),
77
79
  };
78
80
 
79
81
  // Build command
@@ -20,6 +20,7 @@ export const GEMINI_MODELS = [
20
20
  'gemini-3-flash-preview',
21
21
  ] as const;
22
22
  export const FORGE_MODELS = ['forge'] as const;
23
+ export const OPENCODE_MODELS = ['opencode'] as const;
23
24
 
24
25
  export const MODEL_ALIASES: Record<string, string> = {
25
26
  'claude-ultra': 'opus',
@@ -33,6 +34,13 @@ export const MODEL_ALIAS_DETAILS = [
33
34
  { name: 'gemini-ultra', resolvesTo: 'gemini-3.1-pro-preview', agent: 'gemini' },
34
35
  ] as const;
35
36
 
37
+ export interface DynamicModelBackendDescription {
38
+ explicitPrefix: string;
39
+ explicitPattern: string;
40
+ discoveryCommand: string;
41
+ modelsAreDynamic: boolean;
42
+ }
43
+
36
44
  export function getSupportedModelsDescription(): string {
37
45
  return [
38
46
  '"claude-ultra", "codex-ultra", "gemini-ultra"',
@@ -40,11 +48,13 @@ export function getSupportedModelsDescription(): string {
40
48
  ...CODEX_MODELS.map((model) => `"${model}"`),
41
49
  ...GEMINI_MODELS.map((model) => `"${model}"`),
42
50
  ...FORGE_MODELS.map((model) => `"${model}"`),
51
+ ...OPENCODE_MODELS.map((model) => `"${model}"`),
52
+ '"oc-<provider/model>"',
43
53
  ].join(', ');
44
54
  }
45
55
 
46
56
  export function getModelParameterDescription(): string {
47
- return `The model to use. Aliases: "claude-ultra" (auto high effort), "codex-ultra" (auto xhigh reasoning), "gemini-ultra". Standard: ${[...CLAUDE_MODELS, ...CODEX_MODELS, ...GEMINI_MODELS, ...FORGE_MODELS].map((model) => `"${model}"`).join(', ')}. "forge" is a provider key, not a Forge model family selector.`;
57
+ return `The model to use. Aliases: "claude-ultra" (auto high effort), "codex-ultra" (auto xhigh reasoning), "gemini-ultra". Standard: ${[...CLAUDE_MODELS, ...CODEX_MODELS, ...GEMINI_MODELS, ...FORGE_MODELS, ...OPENCODE_MODELS].map((model) => `"${model}"`).join(', ')}. OpenCode also accepts explicit dynamic models using "oc-<provider/model>". "forge" is a provider key, not a Forge model family selector.`;
48
58
  }
49
59
 
50
60
  export function getModelsPayload(): {
@@ -53,6 +63,10 @@ export function getModelsPayload(): {
53
63
  codex: ReadonlyArray<string>;
54
64
  gemini: ReadonlyArray<string>;
55
65
  forge: ReadonlyArray<string>;
66
+ opencode: ReadonlyArray<string>;
67
+ dynamicModelBackends: {
68
+ opencode: DynamicModelBackendDescription;
69
+ };
56
70
  } {
57
71
  return {
58
72
  aliases: MODEL_ALIAS_DETAILS,
@@ -60,5 +74,14 @@ export function getModelsPayload(): {
60
74
  codex: CODEX_MODELS,
61
75
  gemini: GEMINI_MODELS,
62
76
  forge: FORGE_MODELS,
77
+ opencode: OPENCODE_MODELS,
78
+ dynamicModelBackends: {
79
+ opencode: {
80
+ explicitPrefix: 'oc-',
81
+ explicitPattern: 'oc-<provider/model>',
82
+ discoveryCommand: 'opencode models',
83
+ modelsAreDynamic: true,
84
+ },
85
+ },
63
86
  };
64
87
  }
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
+ });