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
@@ -69,7 +69,6 @@ describe('Argument Validation Tests', () => {
69
69
  beforeEach(() => {
70
70
  vi.clearAllMocks();
71
71
  vi.resetModules();
72
- vi.unmock('../server.js');
73
72
  consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
74
73
  // Set up process.env
75
74
  process.env = { ...process.env };
@@ -80,6 +79,7 @@ describe('Argument Validation Tests', () => {
80
79
  mockHomedir.mockReturnValue('/home/user');
81
80
  mockExistsSync.mockReturnValue(true);
82
81
  setupServerMock();
82
+ vi.doUnmock('../server.js');
83
83
  const module = await import('../server.js');
84
84
  // @ts-ignore
85
85
  const { ClaudeCodeServer } = module;
@@ -114,6 +114,7 @@ describe('Argument Validation Tests', () => {
114
114
  mockHomedir.mockReturnValue('/home/user');
115
115
  mockExistsSync.mockReturnValue(true);
116
116
  setupServerMock();
117
+ vi.doUnmock('../server.js');
117
118
  const module = await import('../server.js');
118
119
  // @ts-ignore
119
120
  const { ClaudeCodeServer } = module;
@@ -216,7 +217,7 @@ describe('Argument Validation Tests', () => {
216
217
 
217
218
  const { Server } = await import('@modelcontextprotocol/sdk/server/index.js');
218
219
 
219
- vi.mocked(Server).mockImplementation(() => {
220
+ vi.mocked(Server).mockImplementation(function(this: any) {
220
221
  mockServerInstance = {
221
222
  setRequestHandler: vi.fn((schema: any, handler: Function) => {
222
223
  handlers.set(schema.name, handler);
@@ -228,6 +229,7 @@ describe('Argument Validation Tests', () => {
228
229
  return mockServerInstance as any;
229
230
  });
230
231
 
232
+ vi.doUnmock('../server.js');
231
233
  const module = await import('../server.js');
232
234
  // @ts-ignore
233
235
  const { ClaudeCodeServer } = module;
@@ -319,5 +321,49 @@ describe('Argument Validation Tests', () => {
319
321
  })
320
322
  ).rejects.toThrow(/reasoning_effort/i);
321
323
  });
324
+
325
+ it.each([
326
+ 'oc-',
327
+ 'oc-openai',
328
+ 'oc-/gpt-5.4',
329
+ 'oc-openai/',
330
+ ' oc-openai/gpt-5.4',
331
+ 'oc-openai/gpt-5.4 ',
332
+ ])('should reject malformed OpenCode model syntax at runtime: %s', async (model) => {
333
+ await setupServer();
334
+ const handler = handlers.get('callTool')!;
335
+
336
+ await expect(
337
+ handler({
338
+ params: {
339
+ name: 'run',
340
+ arguments: {
341
+ prompt: 'test',
342
+ workFolder: '/tmp',
343
+ model,
344
+ }
345
+ }
346
+ })
347
+ ).rejects.toThrow('Invalid OpenCode model. Expected exact syntax oc-<provider/model>.');
348
+ });
349
+
350
+ it('should reject reasoning_effort for OpenCode runtime requests', async () => {
351
+ await setupServer();
352
+ const handler = handlers.get('callTool')!;
353
+
354
+ await expect(
355
+ handler({
356
+ params: {
357
+ name: 'run',
358
+ arguments: {
359
+ prompt: 'test',
360
+ workFolder: '/tmp',
361
+ model: 'opencode',
362
+ reasoning_effort: 'high',
363
+ }
364
+ }
365
+ })
366
+ ).rejects.toThrow('reasoning_effort is not supported for opencode.');
367
+ });
322
368
  });
323
369
  });
package/src/app/cli.ts CHANGED
@@ -26,9 +26,9 @@ Options:
26
26
  --cwd <path> Working directory
27
27
  --prompt <text> Prompt text
28
28
  --prompt-file <path> Path to a prompt file
29
- --model <model> Model name or alias (e.g. sonnet, claude-ultra, gpt-5.2-codex, codex-ultra, gemini-2.5-pro, gemini-ultra, forge)
30
- --session-id <id> Resume a previous session
31
- --reasoning-effort <level> Reasoning level for Claude/Codex only
29
+ --model <model> Model name or alias (e.g. sonnet, claude-ultra, gpt-5.2-codex, codex-ultra, gemini-2.5-pro, gemini-ultra, forge, opencode, oc-openai/gpt-5.4)
30
+ --session-id <id> Resume a previous session, including OpenCode in-place resumes
31
+ --reasoning-effort <level> Reasoning level for Claude/Codex only; unsupported for Gemini, Forge, and OpenCode
32
32
  --help, -h Show this help message
33
33
 
34
34
  Compatibility aliases:
@@ -92,7 +92,7 @@ Options:
92
92
 
93
93
  export const DOCTOR_HELP_TEXT = `Usage: ai-cli doctor
94
94
 
95
- Check whether supported AI CLI binaries are available.
95
+ Check whether supported AI CLI binaries are available, including OpenCode.
96
96
 
97
97
  Options:
98
98
  --help, -h Show this help message
package/src/app/mcp.ts CHANGED
@@ -8,7 +8,7 @@ import {
8
8
  type ServerResult,
9
9
  } from '@modelcontextprotocol/sdk/types.js';
10
10
  import { spawn } from 'node:child_process';
11
- import { debugLog, findClaudeCli, findCodexCli, findForgeCli, findGeminiCli } from '../cli-utils.js';
11
+ import { debugLog, findClaudeCli, findCodexCli, findForgeCli, findGeminiCli, findOpencodeCli } from '../cli-utils.js';
12
12
  import { getModelParameterDescription, getSupportedModelsDescription } from '../model-catalog.js';
13
13
  import { ProcessService } from '../process-service.js';
14
14
 
@@ -73,6 +73,7 @@ export class ClaudeCodeServer {
73
73
  private codexCliPath: string;
74
74
  private geminiCliPath: string;
75
75
  private forgeCliPath: string;
76
+ private opencodeCliPath: string;
76
77
  private processService: ProcessService;
77
78
  private sigintHandler?: () => Promise<void>;
78
79
  private packageVersion: string;
@@ -82,10 +83,12 @@ export class ClaudeCodeServer {
82
83
  this.codexCliPath = findCodexCli();
83
84
  this.geminiCliPath = findGeminiCli();
84
85
  this.forgeCliPath = findForgeCli();
86
+ this.opencodeCliPath = findOpencodeCli();
85
87
  console.error(`[Setup] Using Claude CLI command/path: ${this.claudeCliPath}`);
86
88
  console.error(`[Setup] Using Codex CLI command/path: ${this.codexCliPath}`);
87
89
  console.error(`[Setup] Using Gemini CLI command/path: ${this.geminiCliPath}`);
88
90
  console.error(`[Setup] Using Forge CLI command/path: ${this.forgeCliPath}`);
91
+ console.error(`[Setup] Using OpenCode CLI command/path: ${this.opencodeCliPath}`);
89
92
  this.packageVersion = SERVER_VERSION;
90
93
  this.processService = new ProcessService({
91
94
  cliPaths: {
@@ -93,6 +96,7 @@ export class ClaudeCodeServer {
93
96
  codex: this.codexCliPath,
94
97
  gemini: this.geminiCliPath,
95
98
  forge: this.forgeCliPath,
99
+ opencode: this.opencodeCliPath,
96
100
  },
97
101
  });
98
102
 
@@ -123,7 +127,7 @@ export class ClaudeCodeServer {
123
127
  tools: [
124
128
  {
125
129
  name: 'run',
126
- description: `AI Agent Runner: Starts a Claude, Codex, Gemini, or Forge CLI process in the background and returns a PID immediately. Use list_processes and get_result to monitor progress.
130
+ description: `AI Agent Runner: Starts a Claude, Codex, Gemini, Forge, or OpenCode CLI process in the background and returns a PID immediately. Use list_processes and get_result to monitor progress.
127
131
 
128
132
  • File ops: Create, read, (fuzzy) edit, move, copy, delete, list files, analyze/ocr images, file content analysis
129
133
  • Code: Generate / analyse / refactor / fix
@@ -167,11 +171,11 @@ ${getSupportedModelsDescription()}
167
171
  },
168
172
  reasoning_effort: {
169
173
  type: 'string',
170
- description: 'Reasoning control for Claude and Codex. Claude uses --effort with "low", "medium", "high". Codex uses model_reasoning_effort with "low", "medium", "high", "xhigh". Forge does not support reasoning_effort in this integration.',
174
+ description: 'Reasoning control for Claude and Codex. Claude uses --effort with "low", "medium", "high". Codex uses model_reasoning_effort with "low", "medium", "high", "xhigh". Gemini, Forge, and OpenCode do not support reasoning_effort in this integration.',
171
175
  },
172
176
  session_id: {
173
177
  type: 'string',
174
- description: 'Optional session ID to resume a previous session. Supported for: haiku, sonnet, opus, gemini-2.5-pro, gemini-2.5-flash, gemini-3.1-pro-preview, gemini-3-pro-preview, gemini-3-flash-preview, forge.',
178
+ description: 'Optional session ID to resume a previous session. Supported for Claude, Codex, Gemini, Forge, and OpenCode. OpenCode resumes in-place via --session and may also be combined with explicit oc-<provider/model> selection.',
175
179
  },
176
180
  },
177
181
  required: ['workFolder'],
@@ -1,11 +1,21 @@
1
1
  import { existsSync, readFileSync } from 'node:fs';
2
2
  import { resolve as pathResolve, isAbsolute } from 'node:path';
3
+ import type { CliPaths } from './cli-utils.js';
3
4
  import { MODEL_ALIASES } from './model-catalog.js';
4
5
 
5
6
  export const ALLOWED_REASONING_EFFORTS = new Set(['low', 'medium', 'high', 'xhigh']);
6
7
  const CLAUDE_REASONING_EFFORTS = new Set(['low', 'medium', 'high']);
8
+ const OPENCODE_MODEL_ERROR = 'Invalid OpenCode model. Expected exact syntax oc-<provider/model>.';
7
9
 
8
- function getAgentForModel(model: string): 'codex' | 'claude' | 'gemini' | 'forge' {
10
+ type Agent = 'codex' | 'claude' | 'gemini' | 'forge' | 'opencode';
11
+
12
+ interface ModelSelection {
13
+ agent: Agent;
14
+ resolvedModel: string;
15
+ openCodeModel: string | null;
16
+ }
17
+
18
+ function getStandardAgentForModel(model: string): Exclude<Agent, 'opencode'> {
9
19
  if (model === 'forge') {
10
20
  return 'forge';
11
21
  }
@@ -18,20 +28,63 @@ function getAgentForModel(model: string): 'codex' | 'claude' | 'gemini' | 'forge
18
28
  return 'claude';
19
29
  }
20
30
 
21
- /**
22
- * Resolves model aliases to their full model names
23
- * @param model - The model name or alias to resolve
24
- * @returns The full model name, or the original value if no alias exists
25
- */
31
+ function isPotentialOpenCodeExplicitModel(rawModel: string): boolean {
32
+ return rawModel.startsWith('oc-') || rawModel.trim().startsWith('oc-');
33
+ }
34
+
35
+ function extractOpenCodeModel(rawModel: string): string {
36
+ if (rawModel !== rawModel.trim()) {
37
+ throw new Error(OPENCODE_MODEL_ERROR);
38
+ }
39
+
40
+ if (!rawModel.startsWith('oc-')) {
41
+ throw new Error(OPENCODE_MODEL_ERROR);
42
+ }
43
+
44
+ const remainder = rawModel.slice(3);
45
+ const slashIndex = remainder.indexOf('/');
46
+ if (slashIndex === -1) {
47
+ throw new Error(OPENCODE_MODEL_ERROR);
48
+ }
49
+
50
+ const provider = remainder.slice(0, slashIndex);
51
+ const model = remainder.slice(slashIndex + 1);
52
+ if (!provider || !model) {
53
+ throw new Error(OPENCODE_MODEL_ERROR);
54
+ }
55
+
56
+ return remainder;
57
+ }
58
+
59
+ function resolveModelSelection(rawModel: string): ModelSelection {
60
+ if (rawModel === 'opencode') {
61
+ return {
62
+ agent: 'opencode',
63
+ resolvedModel: rawModel,
64
+ openCodeModel: null,
65
+ };
66
+ }
67
+
68
+ if (isPotentialOpenCodeExplicitModel(rawModel)) {
69
+ return {
70
+ agent: 'opencode',
71
+ resolvedModel: rawModel,
72
+ openCodeModel: extractOpenCodeModel(rawModel),
73
+ };
74
+ }
75
+
76
+ const resolvedModel = resolveModelAlias(rawModel);
77
+ return {
78
+ agent: getStandardAgentForModel(resolvedModel),
79
+ resolvedModel,
80
+ openCodeModel: null,
81
+ };
82
+ }
83
+
26
84
  export function resolveModelAlias(model: string): string {
27
85
  return MODEL_ALIASES[model] || model;
28
86
  }
29
87
 
30
- /**
31
- * Validates and normalizes reasoning effort parameter.
32
- * @returns normalized reasoning effort string, or '' if not applicable
33
- * @throws Error for invalid values (plain Error, not MCP-specific)
34
- */
35
88
  export function getReasoningEffort(model: string, rawValue: unknown): string {
36
89
  if (typeof rawValue !== 'string') {
37
90
  return '';
@@ -40,13 +93,18 @@ export function getReasoningEffort(model: string, rawValue: unknown): string {
40
93
  if (!trimmed) {
41
94
  return '';
42
95
  }
96
+
97
+ if (model === 'opencode' || model.startsWith('oc-')) {
98
+ throw new Error('reasoning_effort is not supported for opencode.');
99
+ }
100
+
43
101
  const normalized = trimmed.toLowerCase();
44
102
  if (!ALLOWED_REASONING_EFFORTS.has(normalized)) {
45
103
  throw new Error(
46
104
  `Invalid reasoning_effort: ${rawValue}. Allowed values: low, medium, high, xhigh.`
47
105
  );
48
106
  }
49
- const agent = getAgentForModel(model);
107
+ const agent = getStandardAgentForModel(model);
50
108
  if (agent === 'forge') {
51
109
  throw new Error('reasoning_effort is not supported for forge.');
52
110
  }
@@ -67,7 +125,7 @@ export interface CliCommand {
67
125
  cliPath: string;
68
126
  args: string[];
69
127
  cwd: string;
70
- agent: 'claude' | 'codex' | 'gemini' | 'forge';
128
+ agent: Agent;
71
129
  prompt: string;
72
130
  resolvedModel: string;
73
131
  }
@@ -79,21 +137,14 @@ export interface BuildCliCommandOptions {
79
137
  model?: string;
80
138
  session_id?: string;
81
139
  reasoning_effort?: string;
82
- cliPaths: { claude: string; codex: string; gemini: string; forge: string };
140
+ cliPaths: CliPaths;
83
141
  }
84
142
 
85
- /**
86
- * Build a CLI command from the given options.
87
- * This is a pure function (aside from filesystem reads for prompt_file / workFolder validation).
88
- * @throws Error on validation failures
89
- */
90
143
  export function buildCliCommand(options: BuildCliCommandOptions): CliCommand {
91
- // Validate workFolder
92
144
  if (!options.workFolder || typeof options.workFolder !== 'string') {
93
145
  throw new Error('Missing or invalid required parameter: workFolder');
94
146
  }
95
147
 
96
- // Validate prompt / prompt_file
97
148
  const hasPrompt = !!options.prompt && typeof options.prompt === 'string' && options.prompt.trim() !== '';
98
149
  const hasPromptFile = !!options.prompt_file && typeof options.prompt_file === 'string' && options.prompt_file.trim() !== '';
99
150
 
@@ -105,7 +156,6 @@ export function buildCliCommand(options: BuildCliCommandOptions): CliCommand {
105
156
  throw new Error('Cannot specify both prompt and prompt_file. Please use only one.');
106
157
  }
107
158
 
108
- // Determine prompt
109
159
  let prompt: string;
110
160
  if (hasPrompt) {
111
161
  prompt = options.prompt!;
@@ -125,18 +175,14 @@ export function buildCliCommand(options: BuildCliCommandOptions): CliCommand {
125
175
  }
126
176
  }
127
177
 
128
- // Resolve workFolder
129
178
  const cwd = pathResolve(options.workFolder);
130
179
  if (!existsSync(cwd)) {
131
180
  throw new Error(`Working folder does not exist: ${options.workFolder}`);
132
181
  }
133
182
 
134
- // Resolve model
135
183
  const rawModel = options.model || '';
136
- const resolvedModel = resolveModelAlias(rawModel);
137
- const agent = getAgentForModel(resolvedModel);
184
+ const { agent, resolvedModel, openCodeModel } = resolveModelSelection(rawModel);
138
185
 
139
- // Special handling for ultra aliases: default to higher reasoning if not specified
140
186
  let reasoningEffortArg: string | undefined = options.reasoning_effort;
141
187
  if (!reasoningEffortArg) {
142
188
  if (rawModel === 'codex-ultra') {
@@ -146,9 +192,11 @@ export function buildCliCommand(options: BuildCliCommandOptions): CliCommand {
146
192
  }
147
193
  }
148
194
 
149
- const reasoningEffort = getReasoningEffort(resolvedModel, reasoningEffortArg);
195
+ const reasoningTargetModel = rawModel === 'opencode' || rawModel.startsWith('oc-')
196
+ ? rawModel
197
+ : (resolvedModel || rawModel);
198
+ const reasoningEffort = getReasoningEffort(reasoningTargetModel, reasoningEffortArg);
150
199
 
151
- // Build CLI path and args
152
200
  let cliPath: string;
153
201
  let args: string[];
154
202
 
@@ -169,7 +217,6 @@ export function buildCliCommand(options: BuildCliCommandOptions): CliCommand {
169
217
  }
170
218
 
171
219
  args.push('--skip-git-repo-check', '--full-auto', '--json', prompt);
172
-
173
220
  } else if (agent === 'gemini') {
174
221
  cliPath = options.cliPaths.gemini;
175
222
  args = ['-y', '--output-format', 'json'];
@@ -183,7 +230,6 @@ export function buildCliCommand(options: BuildCliCommandOptions): CliCommand {
183
230
  }
184
231
 
185
232
  args.push(prompt);
186
-
187
233
  } else if (agent === 'forge') {
188
234
  cliPath = options.cliPaths.forge;
189
235
  args = ['-C', cwd];
@@ -193,6 +239,19 @@ export function buildCliCommand(options: BuildCliCommandOptions): CliCommand {
193
239
  }
194
240
 
195
241
  args.push('-p', prompt);
242
+ } else if (agent === 'opencode') {
243
+ cliPath = options.cliPaths.opencode;
244
+ args = ['run', '--format', 'json', '--dir', cwd];
245
+
246
+ if (options.session_id && typeof options.session_id === 'string') {
247
+ args.push('--session', options.session_id);
248
+ }
249
+
250
+ if (openCodeModel) {
251
+ args.push('--model', openCodeModel);
252
+ }
253
+
254
+ args.push(prompt);
196
255
  } else {
197
256
  cliPath = options.cliPaths.claude;
198
257
  args = ['--dangerously-skip-permissions', '--output-format', 'stream-json', '--verbose'];
package/src/cli-parse.ts CHANGED
@@ -1,21 +1,24 @@
1
1
  #!/usr/bin/env node
2
- import { parseClaudeOutput, parseCodexOutput, parseForgeOutput, parseGeminiOutput } from './parsers.js';
2
+ import { parseClaudeOutput, parseCodexOutput, parseForgeOutput, parseGeminiOutput, parseOpenCodeOutput } from './parsers.js';
3
3
 
4
- const AGENTS = ['claude', 'codex', 'gemini', 'forge'] as const;
4
+ const AGENTS = ['claude', 'codex', 'gemini', 'forge', 'opencode'] as const;
5
5
  type Agent = typeof AGENTS[number];
6
6
 
7
- const USAGE = `Usage: npm run -s cli.run.parse -- --agent <claude|codex|gemini|forge>
7
+ const USAGE = `Usage: npm run -s cli.run.parse -- --agent <claude|codex|gemini|forge|opencode>
8
8
 
9
9
  Reads raw CLI output from stdin and outputs parsed JSON to stdout.
10
10
 
11
11
  Options:
12
- --agent Agent type: claude, codex, gemini, or forge (required)
12
+ --agent Agent type: claude, codex, gemini, forge, or opencode (required)
13
13
  --help Show this help message
14
14
 
15
15
  Examples:
16
16
  npm run -s cli.run -- --model sonnet --workFolder /tmp --prompt "hi" > raw.txt
17
17
  npm run -s cli.run.parse -- --agent claude < raw.txt
18
18
 
19
+ npm run -s cli.run -- --model opencode --workFolder /tmp --prompt "hi" > raw.txt
20
+ npm run -s cli.run.parse -- --agent opencode < raw.txt
21
+
19
22
  # Or pipe directly
20
23
  npm run -s cli.run -- --model sonnet --workFolder /tmp --prompt "hi" | npm run -s cli.run.parse -- --agent claude
21
24
  `;
@@ -62,7 +65,7 @@ async function main(): Promise<void> {
62
65
 
63
66
  const agent = args.agent as Agent;
64
67
  if (!agent || !AGENTS.includes(agent)) {
65
- process.stderr.write(`Error: --agent is required (claude, codex, gemini, or forge)\n\n`);
68
+ process.stderr.write(`Error: --agent is required (claude, codex, gemini, forge, or opencode)\n\n`);
66
69
  process.stderr.write(USAGE);
67
70
  process.exit(1);
68
71
  }
@@ -88,6 +91,9 @@ async function main(): Promise<void> {
88
91
  case 'forge':
89
92
  parsed = parseForgeOutput(input);
90
93
  break;
94
+ case 'opencode':
95
+ parsed = parseOpenCodeOutput(input);
96
+ break;
91
97
  }
92
98
 
93
99
  process.stdout.write(JSON.stringify(parsed, null, 2) + '\n');