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
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { spawn } from 'node:child_process';
|
|
2
|
-
import { closeSync, existsSync, mkdirSync, openSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, unlinkSync, writeFileSync, } from 'node:fs';
|
|
2
|
+
import { chmodSync, closeSync, existsSync, mkdirSync, openSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, unlinkSync, writeFileSync, } from 'node:fs';
|
|
3
3
|
import { join, basename, dirname } from 'node:path';
|
|
4
4
|
import { homedir } from 'node:os';
|
|
5
5
|
import { buildCliCommand } from './cli-builder.js';
|
|
6
|
-
import { findClaudeCli, findCodexCli, findForgeCli, findGeminiCli } from './cli-utils.js';
|
|
7
|
-
import { parseClaudeOutput, parseCodexOutput, parseForgeOutput, parseGeminiOutput } from './parsers.js';
|
|
6
|
+
import { findClaudeCli, findCodexCli, findForgeCli, findGeminiCli, findOpencodeCli } from './cli-utils.js';
|
|
7
|
+
import { parseClaudeOutput, parseCodexOutput, parseForgeOutput, parseGeminiOutput, parseOpenCodeOutput } from './parsers.js';
|
|
8
8
|
import { buildProcessResult } from './process-result.js';
|
|
9
9
|
function resolveDefaultStateDir() {
|
|
10
10
|
return process.env.AI_CLI_STATE_DIR || join(homedir(), '.local', 'state', 'ai-cli');
|
|
@@ -27,6 +27,27 @@ function normalizeCwdForStorage(cwd) {
|
|
|
27
27
|
.map((char) => (/^[A-Za-z0-9.-]$/.test(char) ? char : `_${char.charCodeAt(0).toString(16).padStart(2, '0')}`))
|
|
28
28
|
.join('');
|
|
29
29
|
}
|
|
30
|
+
function parseAgentOutput(agent, stdout, stderr) {
|
|
31
|
+
if (agent === 'codex') {
|
|
32
|
+
return parseCodexOutput(`${stdout}\n${stderr}`);
|
|
33
|
+
}
|
|
34
|
+
if (!stdout) {
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
if (agent === 'claude') {
|
|
38
|
+
return parseClaudeOutput(stdout);
|
|
39
|
+
}
|
|
40
|
+
if (agent === 'gemini') {
|
|
41
|
+
return parseGeminiOutput(stdout);
|
|
42
|
+
}
|
|
43
|
+
if (agent === 'forge') {
|
|
44
|
+
return parseForgeOutput(stdout);
|
|
45
|
+
}
|
|
46
|
+
if (agent === 'opencode') {
|
|
47
|
+
return parseOpenCodeOutput(stdout);
|
|
48
|
+
}
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
30
51
|
export class CliProcessService {
|
|
31
52
|
stateDir;
|
|
32
53
|
cliPaths;
|
|
@@ -37,6 +58,7 @@ export class CliProcessService {
|
|
|
37
58
|
codex: findCodexCli(),
|
|
38
59
|
gemini: findGeminiCli(),
|
|
39
60
|
forge: findForgeCli(),
|
|
61
|
+
opencode: findOpencodeCli(),
|
|
40
62
|
};
|
|
41
63
|
mkdirSync(this.stateDir, { recursive: true });
|
|
42
64
|
}
|
|
@@ -50,6 +72,9 @@ export class CliProcessService {
|
|
|
50
72
|
reasoning_effort: options.reasoning_effort,
|
|
51
73
|
cliPaths: this.cliPaths,
|
|
52
74
|
});
|
|
75
|
+
if (cmd.agent === 'opencode') {
|
|
76
|
+
return this.startDetachedOpenCodeProcess(cmd, options.model);
|
|
77
|
+
}
|
|
53
78
|
const stdoutPath = this.resolveStdoutPathForPidPlaceholder();
|
|
54
79
|
const stderrPath = this.resolveStderrPathForPidPlaceholder();
|
|
55
80
|
let stdoutFd;
|
|
@@ -119,26 +144,12 @@ export class CliProcessService {
|
|
|
119
144
|
const refreshed = this.refreshStatus(storedProcess);
|
|
120
145
|
const stdout = this.readTextFileSafe(refreshed.stdoutPath);
|
|
121
146
|
const stderr = this.readTextFileSafe(refreshed.stderrPath);
|
|
122
|
-
|
|
123
|
-
if (refreshed.toolType === 'codex') {
|
|
124
|
-
agentOutput = parseCodexOutput(`${stdout}\n${stderr}`);
|
|
125
|
-
}
|
|
126
|
-
else if (stdout) {
|
|
127
|
-
if (refreshed.toolType === 'claude') {
|
|
128
|
-
agentOutput = parseClaudeOutput(stdout);
|
|
129
|
-
}
|
|
130
|
-
else if (refreshed.toolType === 'gemini') {
|
|
131
|
-
agentOutput = parseGeminiOutput(stdout);
|
|
132
|
-
}
|
|
133
|
-
else if (refreshed.toolType === 'forge') {
|
|
134
|
-
agentOutput = parseForgeOutput(stdout);
|
|
135
|
-
}
|
|
136
|
-
}
|
|
147
|
+
const agentOutput = parseAgentOutput(refreshed.toolType, stdout, stderr);
|
|
137
148
|
return buildProcessResult({
|
|
138
149
|
pid,
|
|
139
150
|
agent: refreshed.toolType,
|
|
140
151
|
status: refreshed.status,
|
|
141
|
-
exitCode:
|
|
152
|
+
exitCode: refreshed.exitCode,
|
|
142
153
|
startTime: refreshed.startTime,
|
|
143
154
|
workFolder: refreshed.workFolder,
|
|
144
155
|
prompt: refreshed.prompt,
|
|
@@ -209,6 +220,49 @@ export class CliProcessService {
|
|
|
209
220
|
message: `Removed ${removed} processes`,
|
|
210
221
|
};
|
|
211
222
|
}
|
|
223
|
+
async startDetachedOpenCodeProcess(cmd, model) {
|
|
224
|
+
const cwdKey = this.resolveCwdKey(cmd.cwd);
|
|
225
|
+
const wrapperPath = this.ensureOpenCodeWrapperScript();
|
|
226
|
+
const childProcess = spawn(wrapperPath, [this.stateDir, cwdKey, cmd.cliPath, ...cmd.args], {
|
|
227
|
+
cwd: cmd.cwd,
|
|
228
|
+
detached: true,
|
|
229
|
+
stdio: 'ignore',
|
|
230
|
+
});
|
|
231
|
+
const pid = childProcess.pid;
|
|
232
|
+
childProcess.unref();
|
|
233
|
+
if (!pid) {
|
|
234
|
+
throw new Error(`Failed to start ${cmd.agent} CLI process`);
|
|
235
|
+
}
|
|
236
|
+
const processDir = this.resolveProcessDir(cmd.cwd, pid);
|
|
237
|
+
mkdirSync(processDir, { recursive: true });
|
|
238
|
+
const stdoutPath = this.resolveStdoutPath(processDir);
|
|
239
|
+
const stderrPath = this.resolveStderrPath(processDir);
|
|
240
|
+
if (!existsSync(stdoutPath)) {
|
|
241
|
+
writeFileSync(stdoutPath, '');
|
|
242
|
+
}
|
|
243
|
+
if (!existsSync(stderrPath)) {
|
|
244
|
+
writeFileSync(stderrPath, '');
|
|
245
|
+
}
|
|
246
|
+
const storedProcess = {
|
|
247
|
+
pid,
|
|
248
|
+
prompt: cmd.prompt,
|
|
249
|
+
workFolder: cmd.cwd,
|
|
250
|
+
cwdKey,
|
|
251
|
+
model,
|
|
252
|
+
toolType: cmd.agent,
|
|
253
|
+
startTime: new Date().toISOString(),
|
|
254
|
+
stdoutPath,
|
|
255
|
+
stderrPath,
|
|
256
|
+
status: 'running',
|
|
257
|
+
};
|
|
258
|
+
this.writeProcess(storedProcess);
|
|
259
|
+
return {
|
|
260
|
+
pid,
|
|
261
|
+
status: 'started',
|
|
262
|
+
agent: cmd.agent,
|
|
263
|
+
message: `${cmd.agent} process started successfully`,
|
|
264
|
+
};
|
|
265
|
+
}
|
|
212
266
|
readAllProcesses() {
|
|
213
267
|
const cwdsDir = this.resolveCwdsDir();
|
|
214
268
|
if (!existsSync(cwdsDir)) {
|
|
@@ -246,12 +300,41 @@ export class CliProcessService {
|
|
|
246
300
|
writeFileSync(this.resolveMetaPath(processDir), JSON.stringify(process, null, 2));
|
|
247
301
|
}
|
|
248
302
|
refreshStatus(process) {
|
|
249
|
-
if (process.status
|
|
303
|
+
if (process.status !== 'running') {
|
|
304
|
+
return process;
|
|
305
|
+
}
|
|
306
|
+
const persistedExitStatus = this.readExitStatus(process);
|
|
307
|
+
if (persistedExitStatus) {
|
|
308
|
+
process.status = persistedExitStatus.status;
|
|
309
|
+
process.exitCode = persistedExitStatus.exitCode;
|
|
310
|
+
this.writeProcess(process);
|
|
311
|
+
return process;
|
|
312
|
+
}
|
|
313
|
+
if (!isProcessRunning(process.pid)) {
|
|
250
314
|
process.status = 'completed';
|
|
251
315
|
this.writeProcess(process);
|
|
252
316
|
}
|
|
253
317
|
return process;
|
|
254
318
|
}
|
|
319
|
+
readExitStatus(process) {
|
|
320
|
+
if (process.toolType !== 'opencode') {
|
|
321
|
+
return null;
|
|
322
|
+
}
|
|
323
|
+
const exitMetaPath = this.resolveExitStatusPath(this.resolveStoredProcessDir(process));
|
|
324
|
+
if (!existsSync(exitMetaPath)) {
|
|
325
|
+
return null;
|
|
326
|
+
}
|
|
327
|
+
try {
|
|
328
|
+
const parsed = JSON.parse(readFileSync(exitMetaPath, 'utf-8'));
|
|
329
|
+
if (parsed.status === 'completed' || parsed.status === 'failed') {
|
|
330
|
+
return parsed;
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
catch {
|
|
334
|
+
return null;
|
|
335
|
+
}
|
|
336
|
+
return null;
|
|
337
|
+
}
|
|
255
338
|
readTextFileSafe(filePath) {
|
|
256
339
|
if (!existsSync(filePath)) {
|
|
257
340
|
return '';
|
|
@@ -282,12 +365,48 @@ export class CliProcessService {
|
|
|
282
365
|
resolveStderrPath(processDir) {
|
|
283
366
|
return join(processDir, 'stderr.log');
|
|
284
367
|
}
|
|
368
|
+
resolveExitStatusPath(processDir) {
|
|
369
|
+
return join(processDir, 'exit-status.json');
|
|
370
|
+
}
|
|
371
|
+
resolveOpenCodeWrapperPath() {
|
|
372
|
+
return join(this.stateDir, 'opencode-detached-wrapper.sh');
|
|
373
|
+
}
|
|
285
374
|
resolveStdoutPathForPidPlaceholder() {
|
|
286
375
|
return join(this.stateDir, `pending-${Date.now()}-${Math.random().toString(36).slice(2)}.stdout.log`);
|
|
287
376
|
}
|
|
288
377
|
resolveStderrPathForPidPlaceholder() {
|
|
289
378
|
return join(this.stateDir, `pending-${Date.now()}-${Math.random().toString(36).slice(2)}.stderr.log`);
|
|
290
379
|
}
|
|
380
|
+
ensureOpenCodeWrapperScript() {
|
|
381
|
+
const wrapperPath = this.resolveOpenCodeWrapperPath();
|
|
382
|
+
if (existsSync(wrapperPath)) {
|
|
383
|
+
return wrapperPath;
|
|
384
|
+
}
|
|
385
|
+
writeFileSync(wrapperPath, `#!/bin/sh
|
|
386
|
+
set +e
|
|
387
|
+
state_dir="$1"
|
|
388
|
+
cwd_key="$2"
|
|
389
|
+
shift 2
|
|
390
|
+
pid="$$"
|
|
391
|
+
process_dir="$state_dir/cwds/$cwd_key/$pid"
|
|
392
|
+
stdout_path="$process_dir/stdout.log"
|
|
393
|
+
stderr_path="$process_dir/stderr.log"
|
|
394
|
+
exit_meta_path="$process_dir/exit-status.json"
|
|
395
|
+
mkdir -p "$process_dir"
|
|
396
|
+
: > "$stdout_path"
|
|
397
|
+
: > "$stderr_path"
|
|
398
|
+
"$@" >> "$stdout_path" 2>> "$stderr_path"
|
|
399
|
+
exit_code="$?"
|
|
400
|
+
status="completed"
|
|
401
|
+
if [ "$exit_code" -ne 0 ]; then
|
|
402
|
+
status="failed"
|
|
403
|
+
fi
|
|
404
|
+
printf '{\n "status": "%s",\n "exitCode": %s\n}\n' "$status" "$exit_code" > "$exit_meta_path"
|
|
405
|
+
exit "$exit_code"
|
|
406
|
+
`);
|
|
407
|
+
chmodSync(wrapperPath, 0o755);
|
|
408
|
+
return wrapperPath;
|
|
409
|
+
}
|
|
291
410
|
renamePlaceholderFile(fromPath, toPath) {
|
|
292
411
|
renameSync(fromPath, toPath);
|
|
293
412
|
}
|
package/dist/cli-utils.js
CHANGED
|
@@ -2,9 +2,7 @@ import { accessSync, constants } from 'node:fs';
|
|
|
2
2
|
import { homedir } from 'node:os';
|
|
3
3
|
import { join } from 'node:path';
|
|
4
4
|
import * as path from 'path';
|
|
5
|
-
// Define debugMode globally using const
|
|
6
5
|
const debugMode = process.env.MCP_CLAUDE_DEBUG === 'true';
|
|
7
|
-
// Dedicated debug logging function
|
|
8
6
|
export function debugLog(message, ...optionalParams) {
|
|
9
7
|
if (debugMode) {
|
|
10
8
|
console.error(message, ...optionalParams);
|
|
@@ -77,7 +75,7 @@ function inspectCliBinary(options) {
|
|
|
77
75
|
lookup: 'env',
|
|
78
76
|
};
|
|
79
77
|
}
|
|
80
|
-
if (isExecutableFile(options.localInstallPath)) {
|
|
78
|
+
if (options.localInstallPath && isExecutableFile(options.localInstallPath)) {
|
|
81
79
|
return {
|
|
82
80
|
configuredCommand,
|
|
83
81
|
resolvedPath: options.localInstallPath,
|
|
@@ -136,6 +134,13 @@ function getCliBinaryConfig(name) {
|
|
|
136
134
|
localInstallPath: join(homedir(), '.forge', 'local', 'forge'),
|
|
137
135
|
};
|
|
138
136
|
}
|
|
137
|
+
if (name === 'opencode') {
|
|
138
|
+
return {
|
|
139
|
+
envVarName: 'OPENCODE_CLI_NAME',
|
|
140
|
+
customCliName: process.env.OPENCODE_CLI_NAME,
|
|
141
|
+
defaultCliName: 'opencode',
|
|
142
|
+
};
|
|
143
|
+
}
|
|
139
144
|
return {
|
|
140
145
|
envVarName: 'GEMINI_CLI_NAME',
|
|
141
146
|
customCliName: process.env.GEMINI_CLI_NAME,
|
|
@@ -152,43 +157,29 @@ export function getCliDoctorStatus() {
|
|
|
152
157
|
codex: getCliBinaryStatus('codex'),
|
|
153
158
|
gemini: getCliBinaryStatus('gemini'),
|
|
154
159
|
forge: getCliBinaryStatus('forge'),
|
|
160
|
+
opencode: getCliBinaryStatus('opencode'),
|
|
155
161
|
};
|
|
156
162
|
}
|
|
157
|
-
/**
|
|
158
|
-
* Determine the Gemini CLI command/path.
|
|
159
|
-
* Similar to findClaudeCli but for Gemini
|
|
160
|
-
*/
|
|
161
163
|
export function findGeminiCli() {
|
|
162
164
|
debugLog('[Debug] Attempting to find Gemini CLI...');
|
|
163
165
|
const status = getCliBinaryStatus('gemini');
|
|
164
166
|
return getCliCommandOrThrow(status);
|
|
165
167
|
}
|
|
166
|
-
/**
|
|
167
|
-
* Determine the Codex CLI command/path.
|
|
168
|
-
* Similar to findClaudeCli but for Codex
|
|
169
|
-
*/
|
|
170
168
|
export function findCodexCli() {
|
|
171
169
|
debugLog('[Debug] Attempting to find Codex CLI...');
|
|
172
170
|
const status = getCliBinaryStatus('codex');
|
|
173
171
|
return getCliCommandOrThrow(status);
|
|
174
172
|
}
|
|
175
|
-
/**
|
|
176
|
-
* Determine the Forge CLI command/path.
|
|
177
|
-
*/
|
|
178
173
|
export function findForgeCli() {
|
|
179
174
|
debugLog('[Debug] Attempting to find Forge CLI...');
|
|
180
175
|
const status = getCliBinaryStatus('forge');
|
|
181
176
|
return getCliCommandOrThrow(status);
|
|
182
177
|
}
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
* - If simple name, continues with path resolution
|
|
189
|
-
* 2. Checks for Claude CLI at the local user path: ~/.claude/local/claude.
|
|
190
|
-
* 3. If not found, defaults to the CLI name (or 'claude'), relying on the system's PATH for lookup.
|
|
191
|
-
*/
|
|
178
|
+
export function findOpencodeCli() {
|
|
179
|
+
debugLog('[Debug] Attempting to find OpenCode CLI...');
|
|
180
|
+
const status = getCliBinaryStatus('opencode');
|
|
181
|
+
return getCliCommandOrThrow(status);
|
|
182
|
+
}
|
|
192
183
|
export function findClaudeCli() {
|
|
193
184
|
debugLog('[Debug] Attempting to find Claude CLI...');
|
|
194
185
|
const status = getCliBinaryStatus('claude');
|
package/dist/cli.js
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
|
* Minimal argv parser. No external dependencies.
|
|
7
7
|
* Supports: --key value, --key=value
|
|
@@ -35,17 +35,18 @@ function parseArgs(argv) {
|
|
|
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
|
async function main() {
|
|
51
52
|
const args = parseArgs(process.argv.slice(2));
|
|
@@ -69,6 +70,7 @@ async function main() {
|
|
|
69
70
|
codex: findCodexCli(),
|
|
70
71
|
gemini: findGeminiCli(),
|
|
71
72
|
forge: findForgeCli(),
|
|
73
|
+
opencode: findOpencodeCli(),
|
|
72
74
|
};
|
|
73
75
|
// Build command
|
|
74
76
|
let cmd;
|
package/dist/model-catalog.js
CHANGED
|
@@ -20,6 +20,7 @@ export const GEMINI_MODELS = [
|
|
|
20
20
|
'gemini-3-flash-preview',
|
|
21
21
|
];
|
|
22
22
|
export const FORGE_MODELS = ['forge'];
|
|
23
|
+
export const OPENCODE_MODELS = ['opencode'];
|
|
23
24
|
export const MODEL_ALIASES = {
|
|
24
25
|
'claude-ultra': 'opus',
|
|
25
26
|
'codex-ultra': 'gpt-5.4',
|
|
@@ -37,10 +38,12 @@ export function getSupportedModelsDescription() {
|
|
|
37
38
|
...CODEX_MODELS.map((model) => `"${model}"`),
|
|
38
39
|
...GEMINI_MODELS.map((model) => `"${model}"`),
|
|
39
40
|
...FORGE_MODELS.map((model) => `"${model}"`),
|
|
41
|
+
...OPENCODE_MODELS.map((model) => `"${model}"`),
|
|
42
|
+
'"oc-<provider/model>"',
|
|
40
43
|
].join(', ');
|
|
41
44
|
}
|
|
42
45
|
export function getModelParameterDescription() {
|
|
43
|
-
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.`;
|
|
46
|
+
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.`;
|
|
44
47
|
}
|
|
45
48
|
export function getModelsPayload() {
|
|
46
49
|
return {
|
|
@@ -49,5 +52,14 @@ export function getModelsPayload() {
|
|
|
49
52
|
codex: CODEX_MODELS,
|
|
50
53
|
gemini: GEMINI_MODELS,
|
|
51
54
|
forge: FORGE_MODELS,
|
|
55
|
+
opencode: OPENCODE_MODELS,
|
|
56
|
+
dynamicModelBackends: {
|
|
57
|
+
opencode: {
|
|
58
|
+
explicitPrefix: 'oc-',
|
|
59
|
+
explicitPattern: 'oc-<provider/model>',
|
|
60
|
+
discoveryCommand: 'opencode models',
|
|
61
|
+
modelsAreDynamic: true,
|
|
62
|
+
},
|
|
63
|
+
},
|
|
52
64
|
};
|
|
53
65
|
}
|
package/dist/parsers.js
CHANGED
|
@@ -1,7 +1,4 @@
|
|
|
1
1
|
import { debugLog } from './cli-utils.js';
|
|
2
|
-
/**
|
|
3
|
-
* Parse Codex NDJSON output to extract the last agent message and token count
|
|
4
|
-
*/
|
|
5
2
|
export function parseCodexOutput(stdout) {
|
|
6
3
|
if (!stdout)
|
|
7
4
|
return null;
|
|
@@ -25,7 +22,6 @@ export function parseCodexOutput(stdout) {
|
|
|
25
22
|
lastMessage = parsed.msg.message;
|
|
26
23
|
}
|
|
27
24
|
else if (parsed.item?.type === 'reasoning') {
|
|
28
|
-
// Ignore reasoning-only items for message selection.
|
|
29
25
|
}
|
|
30
26
|
else if (parsed.msg?.type === 'token_count') {
|
|
31
27
|
tokenCount = parsed.msg;
|
|
@@ -34,7 +30,7 @@ export function parseCodexOutput(stdout) {
|
|
|
34
30
|
tools.push({
|
|
35
31
|
server: parsed.item.server,
|
|
36
32
|
tool: parsed.item.tool,
|
|
37
|
-
input: parsed.item.arguments,
|
|
33
|
+
input: parsed.item.arguments,
|
|
38
34
|
output: parsed.item.result
|
|
39
35
|
});
|
|
40
36
|
}
|
|
@@ -48,7 +44,6 @@ export function parseCodexOutput(stdout) {
|
|
|
48
44
|
}
|
|
49
45
|
}
|
|
50
46
|
catch (e) {
|
|
51
|
-
// Skip invalid JSON lines
|
|
52
47
|
debugLog(`[Debug] Skipping invalid JSON line: ${line}`);
|
|
53
48
|
}
|
|
54
49
|
}
|
|
@@ -67,56 +62,46 @@ export function parseCodexOutput(stdout) {
|
|
|
67
62
|
}
|
|
68
63
|
return null;
|
|
69
64
|
}
|
|
70
|
-
/**
|
|
71
|
-
* Parse Claude Output (supports both JSON and stream-json/NDJSON)
|
|
72
|
-
*/
|
|
73
65
|
export function parseClaudeOutput(stdout) {
|
|
74
66
|
if (!stdout)
|
|
75
67
|
return null;
|
|
76
|
-
// First try parsing as a single JSON object (backward compatibility)
|
|
77
68
|
try {
|
|
78
69
|
return JSON.parse(stdout);
|
|
79
70
|
}
|
|
80
71
|
catch (e) {
|
|
81
|
-
// If not valid single JSON, proceed to parse as NDJSON
|
|
82
72
|
}
|
|
83
73
|
try {
|
|
84
74
|
const lines = stdout.trim().split('\n');
|
|
85
75
|
let lastMessage = null;
|
|
86
76
|
let sessionId = null;
|
|
87
|
-
const toolsMap = new Map();
|
|
77
|
+
const toolsMap = new Map();
|
|
88
78
|
for (const line of lines) {
|
|
89
79
|
if (!line.trim())
|
|
90
80
|
continue;
|
|
91
81
|
try {
|
|
92
82
|
const parsed = JSON.parse(line);
|
|
93
|
-
// Extract session ID from any message that has it
|
|
94
83
|
if (parsed.session_id) {
|
|
95
84
|
sessionId = parsed.session_id;
|
|
96
85
|
}
|
|
97
|
-
// Extract final result message
|
|
98
86
|
if (parsed.type === 'result' && parsed.result) {
|
|
99
87
|
lastMessage = parsed.result;
|
|
100
88
|
}
|
|
101
|
-
// Extract tool usage from assistant messages
|
|
102
89
|
if (parsed.type === 'assistant' && parsed.message?.content) {
|
|
103
90
|
for (const content of parsed.message.content) {
|
|
104
91
|
if (content.type === 'tool_use') {
|
|
105
92
|
toolsMap.set(content.id, {
|
|
106
93
|
tool: content.name,
|
|
107
94
|
input: content.input,
|
|
108
|
-
output: null
|
|
95
|
+
output: null
|
|
109
96
|
});
|
|
110
97
|
}
|
|
111
98
|
}
|
|
112
99
|
}
|
|
113
|
-
// Match tool results from user messages
|
|
114
100
|
if (parsed.type === 'user' && parsed.message?.content) {
|
|
115
101
|
for (const content of parsed.message.content) {
|
|
116
102
|
if (content.type === 'tool_result' && content.tool_use_id) {
|
|
117
103
|
const tool = toolsMap.get(content.tool_use_id);
|
|
118
104
|
if (tool) {
|
|
119
|
-
// Extract text from content array
|
|
120
105
|
if (Array.isArray(content.content)) {
|
|
121
106
|
const textContent = content.content.find((c) => c.type === 'text');
|
|
122
107
|
tool.output = textContent?.text || null;
|
|
@@ -133,11 +118,10 @@ export function parseClaudeOutput(stdout) {
|
|
|
133
118
|
debugLog(`[Debug] Skipping invalid JSON line in Claude output: ${line}`);
|
|
134
119
|
}
|
|
135
120
|
}
|
|
136
|
-
// Convert Map to array
|
|
137
121
|
const tools = Array.from(toolsMap.values());
|
|
138
122
|
if (lastMessage || sessionId || tools.length > 0) {
|
|
139
123
|
return {
|
|
140
|
-
message: lastMessage,
|
|
124
|
+
message: lastMessage,
|
|
141
125
|
session_id: sessionId,
|
|
142
126
|
tools: tools.length > 0 ? tools : undefined
|
|
143
127
|
};
|
|
@@ -149,9 +133,6 @@ export function parseClaudeOutput(stdout) {
|
|
|
149
133
|
}
|
|
150
134
|
return null;
|
|
151
135
|
}
|
|
152
|
-
/**
|
|
153
|
-
* Parse Gemini JSON output
|
|
154
|
-
*/
|
|
155
136
|
export function parseGeminiOutput(stdout) {
|
|
156
137
|
if (!stdout)
|
|
157
138
|
return null;
|
|
@@ -163,9 +144,6 @@ export function parseGeminiOutput(stdout) {
|
|
|
163
144
|
return null;
|
|
164
145
|
}
|
|
165
146
|
}
|
|
166
|
-
/**
|
|
167
|
-
* Parse Forge output framed by Initialize/Continue/Finished markers.
|
|
168
|
-
*/
|
|
169
147
|
export function parseForgeOutput(stdout) {
|
|
170
148
|
if (!stdout)
|
|
171
149
|
return null;
|
|
@@ -218,3 +196,56 @@ export function parseForgeOutput(stdout) {
|
|
|
218
196
|
session_id: lastConversationId,
|
|
219
197
|
};
|
|
220
198
|
}
|
|
199
|
+
export function parseOpenCodeOutput(stdout) {
|
|
200
|
+
if (!stdout) {
|
|
201
|
+
return null;
|
|
202
|
+
}
|
|
203
|
+
let sessionId = null;
|
|
204
|
+
let currentStepBuffer = '';
|
|
205
|
+
let latestCompletedStep = null;
|
|
206
|
+
let hasStepFinish = false;
|
|
207
|
+
let hasParseableAssistantText = false;
|
|
208
|
+
for (const line of stdout.split('\n')) {
|
|
209
|
+
if (!line.trim()) {
|
|
210
|
+
continue;
|
|
211
|
+
}
|
|
212
|
+
let parsed;
|
|
213
|
+
try {
|
|
214
|
+
parsed = JSON.parse(line);
|
|
215
|
+
}
|
|
216
|
+
catch {
|
|
217
|
+
continue;
|
|
218
|
+
}
|
|
219
|
+
if (typeof parsed.sessionID === 'string' && parsed.sessionID) {
|
|
220
|
+
sessionId = parsed.sessionID;
|
|
221
|
+
}
|
|
222
|
+
if (parsed.type === 'step_start') {
|
|
223
|
+
currentStepBuffer = '';
|
|
224
|
+
continue;
|
|
225
|
+
}
|
|
226
|
+
if (parsed.type === 'text' && parsed.part?.type === 'text' && typeof parsed.part.text === 'string') {
|
|
227
|
+
currentStepBuffer += parsed.part.text;
|
|
228
|
+
hasParseableAssistantText = true;
|
|
229
|
+
continue;
|
|
230
|
+
}
|
|
231
|
+
if (parsed.type === 'step_finish') {
|
|
232
|
+
hasStepFinish = true;
|
|
233
|
+
latestCompletedStep = {
|
|
234
|
+
message: currentStepBuffer,
|
|
235
|
+
session_id: sessionId || undefined,
|
|
236
|
+
tokens: parsed.part?.tokens,
|
|
237
|
+
cost: parsed.part?.cost,
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
if (hasStepFinish && latestCompletedStep) {
|
|
242
|
+
return latestCompletedStep;
|
|
243
|
+
}
|
|
244
|
+
if (hasParseableAssistantText) {
|
|
245
|
+
return {
|
|
246
|
+
message: currentStepBuffer,
|
|
247
|
+
session_id: sessionId || undefined,
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
return null;
|
|
251
|
+
}
|
package/dist/process-result.js
CHANGED
|
@@ -23,6 +23,9 @@ function hasMeaningfulParsedOutput(agentOutput) {
|
|
|
23
23
|
return true;
|
|
24
24
|
});
|
|
25
25
|
}
|
|
26
|
+
function shouldPreserveRawFailureOutput(context) {
|
|
27
|
+
return context.agent === 'opencode' && context.status === 'failed';
|
|
28
|
+
}
|
|
26
29
|
export function buildProcessResult(context, agentOutput, verbose = false) {
|
|
27
30
|
const response = {
|
|
28
31
|
pid: context.pid,
|
|
@@ -40,12 +43,16 @@ export function buildProcessResult(context, agentOutput, verbose = false) {
|
|
|
40
43
|
response.session_id = agentOutput.session_id;
|
|
41
44
|
}
|
|
42
45
|
const shapedAgentOutput = verbose ? agentOutput : compactAgentOutput(agentOutput);
|
|
43
|
-
|
|
46
|
+
const preserveRawFailureOutput = shouldPreserveRawFailureOutput(context);
|
|
47
|
+
if (hasMeaningfulParsedOutput(shapedAgentOutput) && (verbose || !preserveRawFailureOutput)) {
|
|
44
48
|
response.agentOutput = shapedAgentOutput;
|
|
45
49
|
}
|
|
46
|
-
if (!response.agentOutput) {
|
|
50
|
+
if (!response.agentOutput || preserveRawFailureOutput) {
|
|
47
51
|
response.stdout = context.stdout;
|
|
48
52
|
response.stderr = context.stderr;
|
|
49
53
|
}
|
|
54
|
+
if (verbose && preserveRawFailureOutput && hasMeaningfulParsedOutput(shapedAgentOutput)) {
|
|
55
|
+
response.agentOutput = shapedAgentOutput;
|
|
56
|
+
}
|
|
50
57
|
return response;
|
|
51
58
|
}
|
package/dist/process-service.js
CHANGED
|
@@ -1,7 +1,28 @@
|
|
|
1
1
|
import { spawn } from 'node:child_process';
|
|
2
2
|
import { buildCliCommand } 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
|
+
function parseAgentOutput(agent, stdout, stderr) {
|
|
6
|
+
if (agent === 'codex') {
|
|
7
|
+
return parseCodexOutput(`${stdout || ''}\n${stderr || ''}`);
|
|
8
|
+
}
|
|
9
|
+
if (!stdout) {
|
|
10
|
+
return null;
|
|
11
|
+
}
|
|
12
|
+
if (agent === 'claude') {
|
|
13
|
+
return parseClaudeOutput(stdout);
|
|
14
|
+
}
|
|
15
|
+
if (agent === 'gemini') {
|
|
16
|
+
return parseGeminiOutput(stdout);
|
|
17
|
+
}
|
|
18
|
+
if (agent === 'forge') {
|
|
19
|
+
return parseForgeOutput(stdout);
|
|
20
|
+
}
|
|
21
|
+
if (agent === 'opencode') {
|
|
22
|
+
return parseOpenCodeOutput(stdout);
|
|
23
|
+
}
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
5
26
|
export class ProcessService {
|
|
6
27
|
processManager = new Map();
|
|
7
28
|
cliPaths;
|
|
@@ -85,22 +106,7 @@ export class ProcessService {
|
|
|
85
106
|
if (!process) {
|
|
86
107
|
throw new Error(`Process with PID ${pid} not found`);
|
|
87
108
|
}
|
|
88
|
-
|
|
89
|
-
if (process.toolType === 'codex') {
|
|
90
|
-
const combinedOutput = (process.stdout || '') + '\n' + (process.stderr || '');
|
|
91
|
-
agentOutput = parseCodexOutput(combinedOutput);
|
|
92
|
-
}
|
|
93
|
-
else if (process.stdout) {
|
|
94
|
-
if (process.toolType === 'claude') {
|
|
95
|
-
agentOutput = parseClaudeOutput(process.stdout);
|
|
96
|
-
}
|
|
97
|
-
else if (process.toolType === 'gemini') {
|
|
98
|
-
agentOutput = parseGeminiOutput(process.stdout);
|
|
99
|
-
}
|
|
100
|
-
else if (process.toolType === 'forge') {
|
|
101
|
-
agentOutput = parseForgeOutput(process.stdout);
|
|
102
|
-
}
|
|
103
|
-
}
|
|
109
|
+
const agentOutput = parseAgentOutput(process.toolType, process.stdout, process.stderr);
|
|
104
110
|
return buildProcessResult({
|
|
105
111
|
pid,
|
|
106
112
|
agent: process.toolType,
|
package/dist/server.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
export { debugLog, findClaudeCli, findCodexCli, findForgeCli, findGeminiCli } from './cli-utils.js';
|
|
1
|
+
export { debugLog, findClaudeCli, findCodexCli, findForgeCli, findGeminiCli, findOpencodeCli } from './cli-utils.js';
|
|
3
2
|
export { resolveModelAlias } from './cli-builder.js';
|
|
4
3
|
export { ClaudeCodeServer, runMcpServer, spawnAsync } from './app/mcp.js';
|
|
5
4
|
import { runMcpServer } from './app/mcp.js';
|