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
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
|
+
});
|