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.
- package/.github/dependabot.yml +28 -0
- package/.github/workflows/ci.yml +4 -1
- package/.github/workflows/dependency-review.yml +22 -0
- package/CHANGELOG.md +7 -0
- package/README.ja.md +25 -6
- package/README.md +25 -7
- package/dist/__tests__/app-cli.test.js +24 -4
- package/dist/__tests__/cli-bin-smoke.test.js +43 -0
- package/dist/__tests__/cli-builder.test.js +92 -14
- package/dist/__tests__/cli-process-service.test.js +103 -0
- package/dist/__tests__/cli-utils.test.js +31 -0
- package/dist/__tests__/e2e.test.js +77 -51
- package/dist/__tests__/mcp-contract.test.js +154 -0
- package/dist/__tests__/parsers.test.js +62 -1
- package/dist/__tests__/process-management.test.js +1 -1
- package/dist/__tests__/server.test.js +35 -6
- package/dist/__tests__/utils/opencode-mock.js +91 -0
- package/dist/__tests__/validation.test.js +40 -2
- package/dist/app/cli.js +4 -4
- package/dist/app/mcp.js +8 -4
- package/dist/cli-builder.js +66 -27
- package/dist/cli-parse.js +11 -5
- package/dist/cli-process-service.js +139 -20
- package/dist/cli-utils.js +14 -23
- package/dist/cli.js +6 -4
- package/dist/model-catalog.js +13 -1
- package/dist/parsers.js +57 -26
- package/dist/process-result.js +9 -2
- package/dist/process-service.js +23 -17
- package/dist/server.js +1 -2
- package/package.json +9 -6
- package/src/__tests__/app-cli.test.ts +24 -4
- package/src/__tests__/cli-bin-smoke.test.ts +62 -1
- package/src/__tests__/cli-builder.test.ts +110 -14
- package/src/__tests__/cli-process-service.test.ts +112 -0
- package/src/__tests__/cli-utils.test.ts +34 -0
- package/src/__tests__/e2e.test.ts +85 -54
- package/src/__tests__/mcp-contract.test.ts +179 -0
- package/src/__tests__/parsers.test.ts +73 -1
- package/src/__tests__/process-management.test.ts +1 -1
- package/src/__tests__/server.test.ts +45 -10
- package/src/__tests__/utils/opencode-mock.ts +108 -0
- package/src/__tests__/validation.test.ts +48 -2
- package/src/app/cli.ts +4 -4
- package/src/app/mcp.ts +8 -4
- package/src/cli-builder.ts +90 -31
- package/src/cli-parse.ts +11 -5
- package/src/cli-process-service.ts +171 -17
- package/src/cli-utils.ts +37 -33
- package/src/cli.ts +6 -4
- package/src/model-catalog.ts +24 -1
- package/src/parsers.ts +77 -31
- package/src/process-result.ts +11 -2
- package/src/process-service.ts +28 -15
- package/src/server.ts +2 -2
- 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
|
|
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
|
|
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
|
|
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'],
|
package/src/cli-builder.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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 =
|
|
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:
|
|
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:
|
|
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 =
|
|
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
|
|
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
|
|
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
|
|
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');
|