ai-cli-mcp 2.14.1 → 2.16.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 +83 -6
- package/README.md +83 -7
- package/dist/__tests__/app-cli.test.js +80 -5
- package/dist/__tests__/cli-bin-smoke.test.js +43 -0
- package/dist/__tests__/cli-builder.test.js +93 -15
- package/dist/__tests__/cli-process-service.test.js +162 -0
- package/dist/__tests__/cli-utils.test.js +31 -0
- package/dist/__tests__/e2e.test.js +79 -52
- package/dist/__tests__/mcp-contract.test.js +162 -0
- package/dist/__tests__/parsers.test.js +224 -1
- package/dist/__tests__/peek.test.js +35 -0
- package/dist/__tests__/process-management.test.js +160 -1
- package/dist/__tests__/server.test.js +39 -9
- package/dist/__tests__/utils/opencode-mock.js +91 -0
- package/dist/__tests__/validation.test.js +40 -2
- package/dist/app/cli.js +47 -5
- package/dist/app/mcp.js +53 -4
- package/dist/cli-builder.js +67 -28
- package/dist/cli-parse.js +11 -5
- package/dist/cli-process-service.js +241 -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 +242 -28
- package/dist/peek.js +56 -0
- package/dist/process-result.js +9 -2
- package/dist/process-service.js +103 -17
- package/dist/server.js +1 -2
- package/package.json +9 -6
- package/src/__tests__/app-cli.test.ts +95 -4
- package/src/__tests__/cli-bin-smoke.test.ts +62 -1
- package/src/__tests__/cli-builder.test.ts +111 -15
- package/src/__tests__/cli-process-service.test.ts +180 -0
- package/src/__tests__/cli-utils.test.ts +34 -0
- package/src/__tests__/e2e.test.ts +87 -55
- package/src/__tests__/mcp-contract.test.ts +188 -0
- package/src/__tests__/parsers.test.ts +260 -1
- package/src/__tests__/peek.test.ts +43 -0
- package/src/__tests__/process-management.test.ts +185 -1
- package/src/__tests__/server.test.ts +49 -13
- package/src/__tests__/utils/opencode-mock.ts +108 -0
- package/src/__tests__/validation.test.ts +48 -2
- package/src/app/cli.ts +52 -4
- package/src/app/mcp.ts +54 -4
- package/src/cli-builder.ts +91 -32
- package/src/cli-parse.ts +11 -5
- package/src/cli-process-service.ts +304 -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 +299 -33
- package/src/peek.ts +88 -0
- package/src/process-result.ts +11 -2
- package/src/process-service.ts +134 -15
- package/src/server.ts +2 -2
- package/vitest.config.unit.ts +2 -3
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,102 @@
|
|
|
1
1
|
import { debugLog } from './cli-utils.js';
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
2
|
+
function isGeminiAssistantMessageEvent(parsed) {
|
|
3
|
+
return parsed.type === 'message' && parsed.role === 'assistant' && typeof parsed.content === 'string';
|
|
4
|
+
}
|
|
5
|
+
const GEMINI_STREAM_EVENT_TYPES = new Set([
|
|
6
|
+
'init',
|
|
7
|
+
'message',
|
|
8
|
+
'tool_use',
|
|
9
|
+
'tool_result',
|
|
10
|
+
'result',
|
|
11
|
+
'error',
|
|
12
|
+
'stats',
|
|
13
|
+
]);
|
|
14
|
+
function isGeminiStreamJsonEvent(parsed) {
|
|
15
|
+
return parsed && typeof parsed === 'object' && !Array.isArray(parsed) && GEMINI_STREAM_EVENT_TYPES.has(parsed.type);
|
|
16
|
+
}
|
|
17
|
+
function extractPeekMessagesFromParsedEvent(agent, parsed, observedAt) {
|
|
18
|
+
if (agent === 'codex') {
|
|
19
|
+
if (parsed.item?.type === 'agent_message' && typeof parsed.item.text === 'string' && parsed.item.text.trim()) {
|
|
20
|
+
return [{ ts: observedAt, text: parsed.item.text }];
|
|
21
|
+
}
|
|
22
|
+
if (parsed.msg?.type === 'agent_message' && typeof parsed.msg.message === 'string' && parsed.msg.message.trim()) {
|
|
23
|
+
return [{ ts: observedAt, text: parsed.msg.message }];
|
|
24
|
+
}
|
|
25
|
+
return [];
|
|
26
|
+
}
|
|
27
|
+
if (agent === 'claude' && parsed.type === 'assistant' && Array.isArray(parsed.message?.content)) {
|
|
28
|
+
return parsed.message.content
|
|
29
|
+
.filter((content) => content?.type === 'text' && typeof content.text === 'string' && content.text.trim())
|
|
30
|
+
.map((content) => ({ ts: observedAt, text: content.text }));
|
|
31
|
+
}
|
|
32
|
+
if (agent === 'opencode' && parsed.type === 'text' && parsed.part?.type === 'text' && typeof parsed.part.text === 'string' && parsed.part.text.trim()) {
|
|
33
|
+
return [{ ts: observedAt, text: parsed.part.text }];
|
|
34
|
+
}
|
|
35
|
+
return [];
|
|
36
|
+
}
|
|
37
|
+
export class PeekMessageExtractor {
|
|
38
|
+
agent;
|
|
39
|
+
pending = '';
|
|
40
|
+
geminiAssistantBuffer = '';
|
|
41
|
+
constructor(agent) {
|
|
42
|
+
this.agent = agent;
|
|
43
|
+
}
|
|
44
|
+
push(chunk, observedAt = new Date().toISOString()) {
|
|
45
|
+
if (!chunk) {
|
|
46
|
+
return [];
|
|
47
|
+
}
|
|
48
|
+
const lines = `${this.pending}${chunk}`.split(/\r?\n/);
|
|
49
|
+
this.pending = lines.pop() || '';
|
|
50
|
+
return this.extractLines(lines, observedAt);
|
|
51
|
+
}
|
|
52
|
+
flush(observedAt = new Date().toISOString()) {
|
|
53
|
+
const messages = [];
|
|
54
|
+
if (this.pending) {
|
|
55
|
+
const line = this.pending;
|
|
56
|
+
this.pending = '';
|
|
57
|
+
messages.push(...this.extractLines([line], observedAt));
|
|
58
|
+
}
|
|
59
|
+
messages.push(...this.flushGeminiAssistantBuffer(observedAt));
|
|
60
|
+
return messages;
|
|
61
|
+
}
|
|
62
|
+
extractLines(lines, observedAt) {
|
|
63
|
+
const messages = [];
|
|
64
|
+
for (const line of lines) {
|
|
65
|
+
if (!line.trim()) {
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
try {
|
|
69
|
+
messages.push(...this.extractParsedEvent(JSON.parse(line), observedAt));
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
debugLog(`[Debug] Skipping invalid peek JSON line: ${line}`);
|
|
73
|
+
messages.push(...this.flushGeminiAssistantBuffer(observedAt));
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return messages;
|
|
77
|
+
}
|
|
78
|
+
extractParsedEvent(parsed, observedAt) {
|
|
79
|
+
if (this.agent !== 'gemini') {
|
|
80
|
+
return extractPeekMessagesFromParsedEvent(this.agent, parsed, observedAt);
|
|
81
|
+
}
|
|
82
|
+
if (isGeminiAssistantMessageEvent(parsed)) {
|
|
83
|
+
this.geminiAssistantBuffer += parsed.content;
|
|
84
|
+
return [];
|
|
85
|
+
}
|
|
86
|
+
return this.flushGeminiAssistantBuffer(observedAt);
|
|
87
|
+
}
|
|
88
|
+
flushGeminiAssistantBuffer(observedAt) {
|
|
89
|
+
if (this.agent !== 'gemini' || !this.geminiAssistantBuffer) {
|
|
90
|
+
return [];
|
|
91
|
+
}
|
|
92
|
+
const text = this.geminiAssistantBuffer;
|
|
93
|
+
this.geminiAssistantBuffer = '';
|
|
94
|
+
if (!text.trim()) {
|
|
95
|
+
return [];
|
|
96
|
+
}
|
|
97
|
+
return [{ ts: observedAt, text }];
|
|
98
|
+
}
|
|
99
|
+
}
|
|
5
100
|
export function parseCodexOutput(stdout) {
|
|
6
101
|
if (!stdout)
|
|
7
102
|
return null;
|
|
@@ -25,7 +120,6 @@ export function parseCodexOutput(stdout) {
|
|
|
25
120
|
lastMessage = parsed.msg.message;
|
|
26
121
|
}
|
|
27
122
|
else if (parsed.item?.type === 'reasoning') {
|
|
28
|
-
// Ignore reasoning-only items for message selection.
|
|
29
123
|
}
|
|
30
124
|
else if (parsed.msg?.type === 'token_count') {
|
|
31
125
|
tokenCount = parsed.msg;
|
|
@@ -34,7 +128,7 @@ export function parseCodexOutput(stdout) {
|
|
|
34
128
|
tools.push({
|
|
35
129
|
server: parsed.item.server,
|
|
36
130
|
tool: parsed.item.tool,
|
|
37
|
-
input: parsed.item.arguments,
|
|
131
|
+
input: parsed.item.arguments,
|
|
38
132
|
output: parsed.item.result
|
|
39
133
|
});
|
|
40
134
|
}
|
|
@@ -48,7 +142,6 @@ export function parseCodexOutput(stdout) {
|
|
|
48
142
|
}
|
|
49
143
|
}
|
|
50
144
|
catch (e) {
|
|
51
|
-
// Skip invalid JSON lines
|
|
52
145
|
debugLog(`[Debug] Skipping invalid JSON line: ${line}`);
|
|
53
146
|
}
|
|
54
147
|
}
|
|
@@ -67,56 +160,46 @@ export function parseCodexOutput(stdout) {
|
|
|
67
160
|
}
|
|
68
161
|
return null;
|
|
69
162
|
}
|
|
70
|
-
/**
|
|
71
|
-
* Parse Claude Output (supports both JSON and stream-json/NDJSON)
|
|
72
|
-
*/
|
|
73
163
|
export function parseClaudeOutput(stdout) {
|
|
74
164
|
if (!stdout)
|
|
75
165
|
return null;
|
|
76
|
-
// First try parsing as a single JSON object (backward compatibility)
|
|
77
166
|
try {
|
|
78
167
|
return JSON.parse(stdout);
|
|
79
168
|
}
|
|
80
169
|
catch (e) {
|
|
81
|
-
// If not valid single JSON, proceed to parse as NDJSON
|
|
82
170
|
}
|
|
83
171
|
try {
|
|
84
172
|
const lines = stdout.trim().split('\n');
|
|
85
173
|
let lastMessage = null;
|
|
86
174
|
let sessionId = null;
|
|
87
|
-
const toolsMap = new Map();
|
|
175
|
+
const toolsMap = new Map();
|
|
88
176
|
for (const line of lines) {
|
|
89
177
|
if (!line.trim())
|
|
90
178
|
continue;
|
|
91
179
|
try {
|
|
92
180
|
const parsed = JSON.parse(line);
|
|
93
|
-
// Extract session ID from any message that has it
|
|
94
181
|
if (parsed.session_id) {
|
|
95
182
|
sessionId = parsed.session_id;
|
|
96
183
|
}
|
|
97
|
-
// Extract final result message
|
|
98
184
|
if (parsed.type === 'result' && parsed.result) {
|
|
99
185
|
lastMessage = parsed.result;
|
|
100
186
|
}
|
|
101
|
-
// Extract tool usage from assistant messages
|
|
102
187
|
if (parsed.type === 'assistant' && parsed.message?.content) {
|
|
103
188
|
for (const content of parsed.message.content) {
|
|
104
189
|
if (content.type === 'tool_use') {
|
|
105
190
|
toolsMap.set(content.id, {
|
|
106
191
|
tool: content.name,
|
|
107
192
|
input: content.input,
|
|
108
|
-
output: null
|
|
193
|
+
output: null
|
|
109
194
|
});
|
|
110
195
|
}
|
|
111
196
|
}
|
|
112
197
|
}
|
|
113
|
-
// Match tool results from user messages
|
|
114
198
|
if (parsed.type === 'user' && parsed.message?.content) {
|
|
115
199
|
for (const content of parsed.message.content) {
|
|
116
200
|
if (content.type === 'tool_result' && content.tool_use_id) {
|
|
117
201
|
const tool = toolsMap.get(content.tool_use_id);
|
|
118
202
|
if (tool) {
|
|
119
|
-
// Extract text from content array
|
|
120
203
|
if (Array.isArray(content.content)) {
|
|
121
204
|
const textContent = content.content.find((c) => c.type === 'text');
|
|
122
205
|
tool.output = textContent?.text || null;
|
|
@@ -133,11 +216,10 @@ export function parseClaudeOutput(stdout) {
|
|
|
133
216
|
debugLog(`[Debug] Skipping invalid JSON line in Claude output: ${line}`);
|
|
134
217
|
}
|
|
135
218
|
}
|
|
136
|
-
// Convert Map to array
|
|
137
219
|
const tools = Array.from(toolsMap.values());
|
|
138
220
|
if (lastMessage || sessionId || tools.length > 0) {
|
|
139
221
|
return {
|
|
140
|
-
message: lastMessage,
|
|
222
|
+
message: lastMessage,
|
|
141
223
|
session_id: sessionId,
|
|
142
224
|
tools: tools.length > 0 ? tools : undefined
|
|
143
225
|
};
|
|
@@ -149,23 +231,102 @@ export function parseClaudeOutput(stdout) {
|
|
|
149
231
|
}
|
|
150
232
|
return null;
|
|
151
233
|
}
|
|
152
|
-
/**
|
|
153
|
-
* Parse Gemini JSON output
|
|
154
|
-
*/
|
|
155
234
|
export function parseGeminiOutput(stdout) {
|
|
156
235
|
if (!stdout)
|
|
157
236
|
return null;
|
|
158
237
|
try {
|
|
159
|
-
|
|
238
|
+
const parsed = JSON.parse(stdout.trim());
|
|
239
|
+
if (!isGeminiStreamJsonEvent(parsed)) {
|
|
240
|
+
return parsed;
|
|
241
|
+
}
|
|
160
242
|
}
|
|
161
243
|
catch (e) {
|
|
162
244
|
debugLog(`[Debug] Failed to parse Gemini JSON output: ${e}`);
|
|
163
|
-
return null;
|
|
164
245
|
}
|
|
246
|
+
let sessionId = null;
|
|
247
|
+
let assistantBuffer = '';
|
|
248
|
+
let lastMessage = null;
|
|
249
|
+
let stats = null;
|
|
250
|
+
const toolsById = new Map();
|
|
251
|
+
const toolsWithoutId = [];
|
|
252
|
+
const flushAssistantMessage = () => {
|
|
253
|
+
if (assistantBuffer.trim()) {
|
|
254
|
+
lastMessage = assistantBuffer;
|
|
255
|
+
}
|
|
256
|
+
assistantBuffer = '';
|
|
257
|
+
};
|
|
258
|
+
for (const line of stdout.split('\n')) {
|
|
259
|
+
if (!line.trim()) {
|
|
260
|
+
continue;
|
|
261
|
+
}
|
|
262
|
+
let parsed;
|
|
263
|
+
try {
|
|
264
|
+
parsed = JSON.parse(line);
|
|
265
|
+
}
|
|
266
|
+
catch (e) {
|
|
267
|
+
debugLog(`[Debug] Skipping invalid Gemini stream-json line: ${line}`);
|
|
268
|
+
flushAssistantMessage();
|
|
269
|
+
continue;
|
|
270
|
+
}
|
|
271
|
+
if (parsed.type === 'init' && typeof parsed.session_id === 'string' && parsed.session_id) {
|
|
272
|
+
sessionId = parsed.session_id;
|
|
273
|
+
continue;
|
|
274
|
+
}
|
|
275
|
+
if (isGeminiAssistantMessageEvent(parsed)) {
|
|
276
|
+
assistantBuffer += parsed.content;
|
|
277
|
+
continue;
|
|
278
|
+
}
|
|
279
|
+
flushAssistantMessage();
|
|
280
|
+
if (parsed.type === 'result') {
|
|
281
|
+
if (parsed.stats) {
|
|
282
|
+
stats = parsed.stats;
|
|
283
|
+
}
|
|
284
|
+
continue;
|
|
285
|
+
}
|
|
286
|
+
if (parsed.type === 'tool_use') {
|
|
287
|
+
const tool = {
|
|
288
|
+
tool: parsed.tool_name || parsed.name || 'tool_use',
|
|
289
|
+
input: parsed.parameters ?? parsed.input ?? null,
|
|
290
|
+
output: null,
|
|
291
|
+
status: null,
|
|
292
|
+
};
|
|
293
|
+
if (typeof parsed.tool_id === 'string' && parsed.tool_id) {
|
|
294
|
+
toolsById.set(parsed.tool_id, tool);
|
|
295
|
+
}
|
|
296
|
+
else {
|
|
297
|
+
toolsWithoutId.push(tool);
|
|
298
|
+
}
|
|
299
|
+
continue;
|
|
300
|
+
}
|
|
301
|
+
if (parsed.type === 'tool_result') {
|
|
302
|
+
const toolId = typeof parsed.tool_id === 'string' ? parsed.tool_id : '';
|
|
303
|
+
const tool = toolId ? toolsById.get(toolId) : null;
|
|
304
|
+
if (tool) {
|
|
305
|
+
tool.output = parsed.output ?? parsed.result ?? null;
|
|
306
|
+
tool.status = parsed.status ?? null;
|
|
307
|
+
}
|
|
308
|
+
else {
|
|
309
|
+
toolsWithoutId.push({
|
|
310
|
+
tool: 'tool_result',
|
|
311
|
+
input: null,
|
|
312
|
+
output: parsed.output ?? parsed.result ?? null,
|
|
313
|
+
status: parsed.status ?? null,
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
flushAssistantMessage();
|
|
319
|
+
const tools = [...toolsById.values(), ...toolsWithoutId];
|
|
320
|
+
if (lastMessage || sessionId || stats || tools.length > 0) {
|
|
321
|
+
return {
|
|
322
|
+
message: lastMessage,
|
|
323
|
+
session_id: sessionId,
|
|
324
|
+
stats: stats || undefined,
|
|
325
|
+
tools: tools.length > 0 ? tools : undefined,
|
|
326
|
+
};
|
|
327
|
+
}
|
|
328
|
+
return null;
|
|
165
329
|
}
|
|
166
|
-
/**
|
|
167
|
-
* Parse Forge output framed by Initialize/Continue/Finished markers.
|
|
168
|
-
*/
|
|
169
330
|
export function parseForgeOutput(stdout) {
|
|
170
331
|
if (!stdout)
|
|
171
332
|
return null;
|
|
@@ -218,3 +379,56 @@ export function parseForgeOutput(stdout) {
|
|
|
218
379
|
session_id: lastConversationId,
|
|
219
380
|
};
|
|
220
381
|
}
|
|
382
|
+
export function parseOpenCodeOutput(stdout) {
|
|
383
|
+
if (!stdout) {
|
|
384
|
+
return null;
|
|
385
|
+
}
|
|
386
|
+
let sessionId = null;
|
|
387
|
+
let currentStepBuffer = '';
|
|
388
|
+
let latestCompletedStep = null;
|
|
389
|
+
let hasStepFinish = false;
|
|
390
|
+
let hasParseableAssistantText = false;
|
|
391
|
+
for (const line of stdout.split('\n')) {
|
|
392
|
+
if (!line.trim()) {
|
|
393
|
+
continue;
|
|
394
|
+
}
|
|
395
|
+
let parsed;
|
|
396
|
+
try {
|
|
397
|
+
parsed = JSON.parse(line);
|
|
398
|
+
}
|
|
399
|
+
catch {
|
|
400
|
+
continue;
|
|
401
|
+
}
|
|
402
|
+
if (typeof parsed.sessionID === 'string' && parsed.sessionID) {
|
|
403
|
+
sessionId = parsed.sessionID;
|
|
404
|
+
}
|
|
405
|
+
if (parsed.type === 'step_start') {
|
|
406
|
+
currentStepBuffer = '';
|
|
407
|
+
continue;
|
|
408
|
+
}
|
|
409
|
+
if (parsed.type === 'text' && parsed.part?.type === 'text' && typeof parsed.part.text === 'string') {
|
|
410
|
+
currentStepBuffer += parsed.part.text;
|
|
411
|
+
hasParseableAssistantText = true;
|
|
412
|
+
continue;
|
|
413
|
+
}
|
|
414
|
+
if (parsed.type === 'step_finish') {
|
|
415
|
+
hasStepFinish = true;
|
|
416
|
+
latestCompletedStep = {
|
|
417
|
+
message: currentStepBuffer,
|
|
418
|
+
session_id: sessionId || undefined,
|
|
419
|
+
tokens: parsed.part?.tokens,
|
|
420
|
+
cost: parsed.part?.cost,
|
|
421
|
+
};
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
if (hasStepFinish && latestCompletedStep) {
|
|
425
|
+
return latestCompletedStep;
|
|
426
|
+
}
|
|
427
|
+
if (hasParseableAssistantText) {
|
|
428
|
+
return {
|
|
429
|
+
message: currentStepBuffer,
|
|
430
|
+
session_id: sessionId || undefined,
|
|
431
|
+
};
|
|
432
|
+
}
|
|
433
|
+
return null;
|
|
434
|
+
}
|
package/dist/peek.js
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
export const DEFAULT_PEEK_TIME_SEC = 10;
|
|
2
|
+
export const MAX_PEEK_TIME_SEC = 60;
|
|
3
|
+
export const MAX_PEEK_PIDS = 32;
|
|
4
|
+
export const PEEK_MESSAGE_CAP = 50;
|
|
5
|
+
export function validatePeekPids(value) {
|
|
6
|
+
if (!Array.isArray(value)) {
|
|
7
|
+
throw new Error('Missing or invalid required parameter: pids (must be an array of positive safe integers)');
|
|
8
|
+
}
|
|
9
|
+
const deduped = [];
|
|
10
|
+
const seen = new Set();
|
|
11
|
+
for (const pid of value) {
|
|
12
|
+
if (typeof pid !== 'number' || !Number.isSafeInteger(pid) || pid <= 0) {
|
|
13
|
+
throw new Error('All pids must be positive safe integers');
|
|
14
|
+
}
|
|
15
|
+
if (!seen.has(pid)) {
|
|
16
|
+
seen.add(pid);
|
|
17
|
+
deduped.push(pid);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
if (deduped.length === 0 || deduped.length > MAX_PEEK_PIDS) {
|
|
21
|
+
throw new Error(`pids must contain 1..${MAX_PEEK_PIDS} entries after dedupe`);
|
|
22
|
+
}
|
|
23
|
+
return deduped;
|
|
24
|
+
}
|
|
25
|
+
export function validatePeekTimeSec(value) {
|
|
26
|
+
if (value === undefined || value === null) {
|
|
27
|
+
return DEFAULT_PEEK_TIME_SEC;
|
|
28
|
+
}
|
|
29
|
+
if (typeof value !== 'number' || !Number.isSafeInteger(value) || value <= 0 || value > MAX_PEEK_TIME_SEC) {
|
|
30
|
+
throw new Error(`peek_time_sec must be a positive integer no greater than ${MAX_PEEK_TIME_SEC}`);
|
|
31
|
+
}
|
|
32
|
+
return value;
|
|
33
|
+
}
|
|
34
|
+
export function buildNotFoundPeekProcess(pid) {
|
|
35
|
+
return {
|
|
36
|
+
pid,
|
|
37
|
+
agent: null,
|
|
38
|
+
status: 'not_found',
|
|
39
|
+
messages: [],
|
|
40
|
+
truncated: false,
|
|
41
|
+
error: 'process not found',
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
export function appendPeekMessages(target, messages) {
|
|
45
|
+
for (const message of messages) {
|
|
46
|
+
if (target.messages.length < PEEK_MESSAGE_CAP) {
|
|
47
|
+
target.messages.push(message);
|
|
48
|
+
}
|
|
49
|
+
else {
|
|
50
|
+
target.truncated = true;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
export function observedDurationSec(startedAtMs, endedAtMs = Date.now()) {
|
|
55
|
+
return Number(((endedAtMs - startedAtMs) / 1000).toFixed(2));
|
|
56
|
+
}
|
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,29 @@
|
|
|
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, PeekMessageExtractor } from './parsers.js';
|
|
4
|
+
import { appendPeekMessages, buildNotFoundPeekProcess, observedDurationSec, validatePeekPids, validatePeekTimeSec, } from './peek.js';
|
|
4
5
|
import { buildProcessResult } from './process-result.js';
|
|
6
|
+
function parseAgentOutput(agent, stdout, stderr) {
|
|
7
|
+
if (agent === 'codex') {
|
|
8
|
+
return parseCodexOutput(`${stdout || ''}\n${stderr || ''}`);
|
|
9
|
+
}
|
|
10
|
+
if (!stdout) {
|
|
11
|
+
return null;
|
|
12
|
+
}
|
|
13
|
+
if (agent === 'claude') {
|
|
14
|
+
return parseClaudeOutput(stdout);
|
|
15
|
+
}
|
|
16
|
+
if (agent === 'gemini') {
|
|
17
|
+
return parseGeminiOutput(stdout);
|
|
18
|
+
}
|
|
19
|
+
if (agent === 'forge') {
|
|
20
|
+
return parseForgeOutput(stdout);
|
|
21
|
+
}
|
|
22
|
+
if (agent === 'opencode') {
|
|
23
|
+
return parseOpenCodeOutput(stdout);
|
|
24
|
+
}
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
5
27
|
export class ProcessService {
|
|
6
28
|
processManager = new Map();
|
|
7
29
|
cliPaths;
|
|
@@ -85,22 +107,7 @@ export class ProcessService {
|
|
|
85
107
|
if (!process) {
|
|
86
108
|
throw new Error(`Process with PID ${pid} not found`);
|
|
87
109
|
}
|
|
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
|
-
}
|
|
110
|
+
const agentOutput = parseAgentOutput(process.toolType, process.stdout, process.stderr);
|
|
104
111
|
return buildProcessResult({
|
|
105
112
|
pid,
|
|
106
113
|
agent: process.toolType,
|
|
@@ -149,6 +156,85 @@ export class ProcessService {
|
|
|
149
156
|
}
|
|
150
157
|
}
|
|
151
158
|
}
|
|
159
|
+
async peekProcesses(pids, peekTimeSec = 10) {
|
|
160
|
+
const targetPids = validatePeekPids(pids);
|
|
161
|
+
const targetPeekTimeSec = validatePeekTimeSec(peekTimeSec);
|
|
162
|
+
const processes = [];
|
|
163
|
+
const observers = [];
|
|
164
|
+
for (const pid of targetPids) {
|
|
165
|
+
const entry = this.processManager.get(pid);
|
|
166
|
+
if (!entry) {
|
|
167
|
+
processes.push(buildNotFoundPeekProcess(pid));
|
|
168
|
+
continue;
|
|
169
|
+
}
|
|
170
|
+
const result = {
|
|
171
|
+
pid,
|
|
172
|
+
agent: entry.toolType,
|
|
173
|
+
status: entry.status,
|
|
174
|
+
messages: [],
|
|
175
|
+
truncated: false,
|
|
176
|
+
error: null,
|
|
177
|
+
};
|
|
178
|
+
processes.push(result);
|
|
179
|
+
const stdoutExtractor = new PeekMessageExtractor(entry.toolType);
|
|
180
|
+
const stderrExtractor = new PeekMessageExtractor(entry.toolType);
|
|
181
|
+
const onStdout = (data) => {
|
|
182
|
+
appendPeekMessages(result, stdoutExtractor.push(data.toString(), new Date().toISOString()));
|
|
183
|
+
};
|
|
184
|
+
const onStderr = (data) => {
|
|
185
|
+
appendPeekMessages(result, stderrExtractor.push(data.toString(), new Date().toISOString()));
|
|
186
|
+
};
|
|
187
|
+
if (entry.status === 'running') {
|
|
188
|
+
entry.process.stdout?.on('data', onStdout);
|
|
189
|
+
entry.process.stderr?.on('data', onStderr);
|
|
190
|
+
}
|
|
191
|
+
observers.push({ entry, result, stdoutExtractor, stderrExtractor, onStdout, onStderr });
|
|
192
|
+
}
|
|
193
|
+
const startedAt = new Date();
|
|
194
|
+
const startedAtMs = Date.now();
|
|
195
|
+
const runningObservers = observers.filter((observer) => observer.entry.status === 'running');
|
|
196
|
+
const terminalPromise = Promise.all(runningObservers.map((observer) => this.waitForProcessTerminal(observer.entry)));
|
|
197
|
+
let timeoutHandle;
|
|
198
|
+
const timeoutPromise = new Promise((resolve) => {
|
|
199
|
+
timeoutHandle = setTimeout(resolve, targetPeekTimeSec * 1000);
|
|
200
|
+
timeoutHandle.unref?.();
|
|
201
|
+
});
|
|
202
|
+
try {
|
|
203
|
+
await Promise.race([terminalPromise, timeoutPromise]);
|
|
204
|
+
}
|
|
205
|
+
finally {
|
|
206
|
+
if (timeoutHandle) {
|
|
207
|
+
clearTimeout(timeoutHandle);
|
|
208
|
+
}
|
|
209
|
+
const flushTs = new Date().toISOString();
|
|
210
|
+
for (const observer of observers) {
|
|
211
|
+
observer.entry.process.stdout?.off('data', observer.onStdout);
|
|
212
|
+
observer.entry.process.stderr?.off('data', observer.onStderr);
|
|
213
|
+
appendPeekMessages(observer.result, observer.stdoutExtractor.flush(flushTs));
|
|
214
|
+
appendPeekMessages(observer.result, observer.stderrExtractor.flush(flushTs));
|
|
215
|
+
observer.result.status = observer.entry.status;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
return {
|
|
219
|
+
peek_started_at: startedAt.toISOString(),
|
|
220
|
+
observed_duration_sec: observedDurationSec(startedAtMs),
|
|
221
|
+
processes,
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
waitForProcessTerminal(processEntry) {
|
|
225
|
+
if (processEntry.status !== 'running') {
|
|
226
|
+
return Promise.resolve();
|
|
227
|
+
}
|
|
228
|
+
return new Promise((resolve) => {
|
|
229
|
+
const done = () => {
|
|
230
|
+
processEntry.process.off('close', done);
|
|
231
|
+
processEntry.process.off('error', done);
|
|
232
|
+
resolve();
|
|
233
|
+
};
|
|
234
|
+
processEntry.process.once('close', done);
|
|
235
|
+
processEntry.process.once('error', done);
|
|
236
|
+
});
|
|
237
|
+
}
|
|
152
238
|
killProcess(pid) {
|
|
153
239
|
const processEntry = this.processManager.get(pid);
|
|
154
240
|
if (!processEntry) {
|
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';
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ai-cli-mcp",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.16.0",
|
|
4
4
|
"mcpName": "io.github.mkXultra/ai-cli-mcp",
|
|
5
|
-
"description": "MCP server for AI CLI tools (Claude, Codex, Gemini, and
|
|
5
|
+
"description": "MCP server for AI CLI tools (Claude, Codex, Gemini, Forge, and OpenCode) with background process management",
|
|
6
6
|
"author": "mkXultra",
|
|
7
7
|
"license": "MIT",
|
|
8
8
|
"main": "dist/server.js",
|
|
@@ -25,21 +25,24 @@
|
|
|
25
25
|
"prepare": "husky"
|
|
26
26
|
},
|
|
27
27
|
"dependencies": {
|
|
28
|
-
"@modelcontextprotocol/sdk": "^1.
|
|
28
|
+
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
29
29
|
"zod": "^3.24.4"
|
|
30
30
|
},
|
|
31
|
+
"engines": {
|
|
32
|
+
"node": "^20.19.0 || >=22.12.0"
|
|
33
|
+
},
|
|
31
34
|
"type": "module",
|
|
32
35
|
"devDependencies": {
|
|
33
36
|
"@eslint/js": "^9.26.0",
|
|
34
37
|
"@semantic-release/changelog": "^6.0.3",
|
|
35
38
|
"@semantic-release/git": "^10.0.1",
|
|
36
39
|
"@types/node": "^22.15.17",
|
|
37
|
-
"@vitest/coverage-v8": "^
|
|
40
|
+
"@vitest/coverage-v8": "^4.1.3",
|
|
38
41
|
"husky": "^9.1.7",
|
|
39
|
-
"semantic-release": "^25.0.
|
|
42
|
+
"semantic-release": "^25.0.3",
|
|
40
43
|
"tsx": "^4.19.4",
|
|
41
44
|
"typescript": "^5.8.3",
|
|
42
|
-
"vitest": "^
|
|
45
|
+
"vitest": "^4.1.3"
|
|
43
46
|
},
|
|
44
47
|
"repository": {
|
|
45
48
|
"type": "git",
|