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.
- package/.github/dependabot.yml +28 -0
- package/.github/workflows/ci.yml +4 -1
- package/.github/workflows/dependency-review.yml +22 -0
- package/CHANGELOG.md +14 -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 +187 -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 +158 -25
- 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 +217 -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 +193 -22
- 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
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
|
|
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
|
|
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
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
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
|
package/src/model-catalog.ts
CHANGED
|
@@ -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,
|
|
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>();
|
|
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
|
|
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,
|
|
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
|
+
}
|
package/src/process-result.ts
CHANGED
|
@@ -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
|
}
|
package/src/process-service.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
|
package/vitest.config.unit.ts
CHANGED
|
@@ -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
|
+
});
|