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/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,141 @@
|
|
|
1
1
|
import { debugLog } from './cli-utils.js';
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
export interface PeekMessage {
|
|
4
|
+
ts: string;
|
|
5
|
+
text: string;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
type PeekAgent = 'claude' | 'codex' | string | null;
|
|
9
|
+
|
|
10
|
+
function isGeminiAssistantMessageEvent(parsed: any): boolean {
|
|
11
|
+
return parsed.type === 'message' && parsed.role === 'assistant' && typeof parsed.content === 'string';
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const GEMINI_STREAM_EVENT_TYPES = new Set([
|
|
15
|
+
'init',
|
|
16
|
+
'message',
|
|
17
|
+
'tool_use',
|
|
18
|
+
'tool_result',
|
|
19
|
+
'result',
|
|
20
|
+
'error',
|
|
21
|
+
'stats',
|
|
22
|
+
]);
|
|
23
|
+
|
|
24
|
+
function isGeminiStreamJsonEvent(parsed: any): boolean {
|
|
25
|
+
return parsed && typeof parsed === 'object' && !Array.isArray(parsed) && GEMINI_STREAM_EVENT_TYPES.has(parsed.type);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function extractPeekMessagesFromParsedEvent(agent: PeekAgent, parsed: any, observedAt: string): PeekMessage[] {
|
|
29
|
+
if (agent === 'codex') {
|
|
30
|
+
if (parsed.item?.type === 'agent_message' && typeof parsed.item.text === 'string' && parsed.item.text.trim()) {
|
|
31
|
+
return [{ ts: observedAt, text: parsed.item.text }];
|
|
32
|
+
}
|
|
33
|
+
if (parsed.msg?.type === 'agent_message' && typeof parsed.msg.message === 'string' && parsed.msg.message.trim()) {
|
|
34
|
+
return [{ ts: observedAt, text: parsed.msg.message }];
|
|
35
|
+
}
|
|
36
|
+
return [];
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (agent === 'claude' && parsed.type === 'assistant' && Array.isArray(parsed.message?.content)) {
|
|
40
|
+
return parsed.message.content
|
|
41
|
+
.filter((content: any) => content?.type === 'text' && typeof content.text === 'string' && content.text.trim())
|
|
42
|
+
.map((content: any) => ({ ts: observedAt, text: content.text }));
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (agent === 'opencode' && parsed.type === 'text' && parsed.part?.type === 'text' && typeof parsed.part.text === 'string' && parsed.part.text.trim()) {
|
|
46
|
+
return [{ ts: observedAt, text: parsed.part.text }];
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return [];
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export class PeekMessageExtractor {
|
|
53
|
+
private pending = '';
|
|
54
|
+
private geminiAssistantBuffer = '';
|
|
55
|
+
|
|
56
|
+
constructor(private readonly agent: PeekAgent) {}
|
|
57
|
+
|
|
58
|
+
push(chunk: string, observedAt = new Date().toISOString()): PeekMessage[] {
|
|
59
|
+
if (!chunk) {
|
|
60
|
+
return [];
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const lines = `${this.pending}${chunk}`.split(/\r?\n/);
|
|
64
|
+
this.pending = lines.pop() || '';
|
|
65
|
+
return this.extractLines(lines, observedAt);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
flush(observedAt = new Date().toISOString()): PeekMessage[] {
|
|
69
|
+
const messages: PeekMessage[] = [];
|
|
70
|
+
|
|
71
|
+
if (this.pending) {
|
|
72
|
+
const line = this.pending;
|
|
73
|
+
this.pending = '';
|
|
74
|
+
messages.push(...this.extractLines([line], observedAt));
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
messages.push(...this.flushGeminiAssistantBuffer(observedAt));
|
|
78
|
+
return messages;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
private extractLines(lines: string[], observedAt: string): PeekMessage[] {
|
|
82
|
+
const messages: PeekMessage[] = [];
|
|
83
|
+
|
|
84
|
+
for (const line of lines) {
|
|
85
|
+
if (!line.trim()) {
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
try {
|
|
90
|
+
messages.push(...this.extractParsedEvent(JSON.parse(line), observedAt));
|
|
91
|
+
} catch {
|
|
92
|
+
debugLog(`[Debug] Skipping invalid peek JSON line: ${line}`);
|
|
93
|
+
messages.push(...this.flushGeminiAssistantBuffer(observedAt));
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return messages;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
private extractParsedEvent(parsed: any, observedAt: string): PeekMessage[] {
|
|
101
|
+
if (this.agent !== 'gemini') {
|
|
102
|
+
return extractPeekMessagesFromParsedEvent(this.agent, parsed, observedAt);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (isGeminiAssistantMessageEvent(parsed)) {
|
|
106
|
+
this.geminiAssistantBuffer += parsed.content;
|
|
107
|
+
return [];
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return this.flushGeminiAssistantBuffer(observedAt);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
private flushGeminiAssistantBuffer(observedAt: string): PeekMessage[] {
|
|
114
|
+
if (this.agent !== 'gemini' || !this.geminiAssistantBuffer) {
|
|
115
|
+
return [];
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const text = this.geminiAssistantBuffer;
|
|
119
|
+
this.geminiAssistantBuffer = '';
|
|
120
|
+
|
|
121
|
+
if (!text.trim()) {
|
|
122
|
+
return [];
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return [{ ts: observedAt, text }];
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
6
129
|
export function parseCodexOutput(stdout: string): any {
|
|
7
130
|
if (!stdout) return null;
|
|
8
|
-
|
|
131
|
+
|
|
9
132
|
try {
|
|
10
133
|
const lines = stdout.trim().split('\n');
|
|
11
134
|
let lastMessage = null;
|
|
12
135
|
let tokenCount = null;
|
|
13
136
|
let threadId = null;
|
|
14
137
|
const tools: any[] = [];
|
|
15
|
-
|
|
138
|
+
|
|
16
139
|
for (const line of lines) {
|
|
17
140
|
if (line.trim()) {
|
|
18
141
|
try {
|
|
@@ -24,14 +147,13 @@ export function parseCodexOutput(stdout: string): any {
|
|
|
24
147
|
} else if (parsed.msg?.type === 'agent_message') {
|
|
25
148
|
lastMessage = parsed.msg.message;
|
|
26
149
|
} else if (parsed.item?.type === 'reasoning') {
|
|
27
|
-
// Ignore reasoning-only items for message selection.
|
|
28
150
|
} else if (parsed.msg?.type === 'token_count') {
|
|
29
151
|
tokenCount = parsed.msg;
|
|
30
152
|
} else if (parsed.type === 'item.completed' && parsed.item?.type === 'mcp_tool_call') {
|
|
31
153
|
tools.push({
|
|
32
154
|
server: parsed.item.server,
|
|
33
155
|
tool: parsed.item.tool,
|
|
34
|
-
input: parsed.item.arguments,
|
|
156
|
+
input: parsed.item.arguments,
|
|
35
157
|
output: parsed.item.result
|
|
36
158
|
});
|
|
37
159
|
} else if (parsed.type === 'item.completed' && parsed.item?.type === 'command_execution') {
|
|
@@ -43,12 +165,11 @@ export function parseCodexOutput(stdout: string): any {
|
|
|
43
165
|
});
|
|
44
166
|
}
|
|
45
167
|
} catch (e) {
|
|
46
|
-
// Skip invalid JSON lines
|
|
47
168
|
debugLog(`[Debug] Skipping invalid JSON line: ${line}`);
|
|
48
169
|
}
|
|
49
170
|
}
|
|
50
171
|
}
|
|
51
|
-
|
|
172
|
+
|
|
52
173
|
if (lastMessage || tokenCount || threadId || tools.length > 0) {
|
|
53
174
|
return {
|
|
54
175
|
message: lastMessage,
|
|
@@ -60,28 +181,23 @@ export function parseCodexOutput(stdout: string): any {
|
|
|
60
181
|
} catch (e) {
|
|
61
182
|
debugLog(`[Debug] Failed to parse Codex NDJSON output: ${e}`);
|
|
62
183
|
}
|
|
63
|
-
|
|
184
|
+
|
|
64
185
|
return null;
|
|
65
186
|
}
|
|
66
187
|
|
|
67
|
-
/**
|
|
68
|
-
* Parse Claude Output (supports both JSON and stream-json/NDJSON)
|
|
69
|
-
*/
|
|
70
188
|
export function parseClaudeOutput(stdout: string): any {
|
|
71
189
|
if (!stdout) return null;
|
|
72
190
|
|
|
73
|
-
// First try parsing as a single JSON object (backward compatibility)
|
|
74
191
|
try {
|
|
75
192
|
return JSON.parse(stdout);
|
|
76
193
|
} catch (e) {
|
|
77
|
-
// If not valid single JSON, proceed to parse as NDJSON
|
|
78
194
|
}
|
|
79
195
|
|
|
80
196
|
try {
|
|
81
197
|
const lines = stdout.trim().split('\n');
|
|
82
198
|
let lastMessage = null;
|
|
83
199
|
let sessionId = null;
|
|
84
|
-
const toolsMap = new Map<string, any>();
|
|
200
|
+
const toolsMap = new Map<string, any>();
|
|
85
201
|
|
|
86
202
|
for (const line of lines) {
|
|
87
203
|
if (!line.trim()) continue;
|
|
@@ -89,36 +205,31 @@ export function parseClaudeOutput(stdout: string): any {
|
|
|
89
205
|
try {
|
|
90
206
|
const parsed = JSON.parse(line);
|
|
91
207
|
|
|
92
|
-
// Extract session ID from any message that has it
|
|
93
208
|
if (parsed.session_id) {
|
|
94
209
|
sessionId = parsed.session_id;
|
|
95
210
|
}
|
|
96
211
|
|
|
97
|
-
// Extract final result message
|
|
98
212
|
if (parsed.type === 'result' && parsed.result) {
|
|
99
213
|
lastMessage = parsed.result;
|
|
100
214
|
}
|
|
101
215
|
|
|
102
|
-
// Extract tool usage from assistant messages
|
|
103
216
|
if (parsed.type === 'assistant' && parsed.message?.content) {
|
|
104
217
|
for (const content of parsed.message.content) {
|
|
105
218
|
if (content.type === 'tool_use') {
|
|
106
219
|
toolsMap.set(content.id, {
|
|
107
220
|
tool: content.name,
|
|
108
221
|
input: content.input,
|
|
109
|
-
output: null
|
|
222
|
+
output: null
|
|
110
223
|
});
|
|
111
224
|
}
|
|
112
225
|
}
|
|
113
226
|
}
|
|
114
227
|
|
|
115
|
-
// Match tool results from user messages
|
|
116
228
|
if (parsed.type === 'user' && parsed.message?.content) {
|
|
117
229
|
for (const content of parsed.message.content) {
|
|
118
230
|
if (content.type === 'tool_result' && content.tool_use_id) {
|
|
119
231
|
const tool = toolsMap.get(content.tool_use_id);
|
|
120
232
|
if (tool) {
|
|
121
|
-
// Extract text from content array
|
|
122
233
|
if (Array.isArray(content.content)) {
|
|
123
234
|
const textContent = content.content.find((c: any) => c.type === 'text');
|
|
124
235
|
tool.output = textContent?.text || null;
|
|
@@ -135,12 +246,11 @@ export function parseClaudeOutput(stdout: string): any {
|
|
|
135
246
|
}
|
|
136
247
|
}
|
|
137
248
|
|
|
138
|
-
// Convert Map to array
|
|
139
249
|
const tools = Array.from(toolsMap.values());
|
|
140
250
|
|
|
141
251
|
if (lastMessage || sessionId || tools.length > 0) {
|
|
142
252
|
return {
|
|
143
|
-
message: lastMessage,
|
|
253
|
+
message: lastMessage,
|
|
144
254
|
session_id: sessionId,
|
|
145
255
|
tools: tools.length > 0 ? tools : undefined
|
|
146
256
|
};
|
|
@@ -150,27 +260,115 @@ export function parseClaudeOutput(stdout: string): any {
|
|
|
150
260
|
debugLog(`[Debug] Failed to parse Claude NDJSON output: ${e}`);
|
|
151
261
|
return null;
|
|
152
262
|
}
|
|
153
|
-
|
|
263
|
+
|
|
154
264
|
return null;
|
|
155
265
|
}
|
|
156
266
|
|
|
157
|
-
/**
|
|
158
|
-
* Parse Gemini JSON output
|
|
159
|
-
*/
|
|
160
267
|
export function parseGeminiOutput(stdout: string): any {
|
|
161
268
|
if (!stdout) return null;
|
|
162
269
|
|
|
163
270
|
try {
|
|
164
|
-
|
|
271
|
+
const parsed = JSON.parse(stdout.trim());
|
|
272
|
+
if (!isGeminiStreamJsonEvent(parsed)) {
|
|
273
|
+
return parsed;
|
|
274
|
+
}
|
|
165
275
|
} catch (e) {
|
|
166
276
|
debugLog(`[Debug] Failed to parse Gemini JSON output: ${e}`);
|
|
167
|
-
return null;
|
|
168
277
|
}
|
|
278
|
+
|
|
279
|
+
let sessionId: string | null = null;
|
|
280
|
+
let assistantBuffer = '';
|
|
281
|
+
let lastMessage: string | null = null;
|
|
282
|
+
let stats: any = null;
|
|
283
|
+
const toolsById = new Map<string, any>();
|
|
284
|
+
const toolsWithoutId: any[] = [];
|
|
285
|
+
const flushAssistantMessage = () => {
|
|
286
|
+
if (assistantBuffer.trim()) {
|
|
287
|
+
lastMessage = assistantBuffer;
|
|
288
|
+
}
|
|
289
|
+
assistantBuffer = '';
|
|
290
|
+
};
|
|
291
|
+
|
|
292
|
+
for (const line of stdout.split('\n')) {
|
|
293
|
+
if (!line.trim()) {
|
|
294
|
+
continue;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
let parsed: any;
|
|
298
|
+
try {
|
|
299
|
+
parsed = JSON.parse(line);
|
|
300
|
+
} catch (e) {
|
|
301
|
+
debugLog(`[Debug] Skipping invalid Gemini stream-json line: ${line}`);
|
|
302
|
+
flushAssistantMessage();
|
|
303
|
+
continue;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
if (parsed.type === 'init' && typeof parsed.session_id === 'string' && parsed.session_id) {
|
|
307
|
+
sessionId = parsed.session_id;
|
|
308
|
+
continue;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
if (isGeminiAssistantMessageEvent(parsed)) {
|
|
312
|
+
assistantBuffer += parsed.content;
|
|
313
|
+
continue;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
flushAssistantMessage();
|
|
317
|
+
|
|
318
|
+
if (parsed.type === 'result') {
|
|
319
|
+
if (parsed.stats) {
|
|
320
|
+
stats = parsed.stats;
|
|
321
|
+
}
|
|
322
|
+
continue;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
if (parsed.type === 'tool_use') {
|
|
326
|
+
const tool = {
|
|
327
|
+
tool: parsed.tool_name || parsed.name || 'tool_use',
|
|
328
|
+
input: parsed.parameters ?? parsed.input ?? null,
|
|
329
|
+
output: null,
|
|
330
|
+
status: null,
|
|
331
|
+
};
|
|
332
|
+
if (typeof parsed.tool_id === 'string' && parsed.tool_id) {
|
|
333
|
+
toolsById.set(parsed.tool_id, tool);
|
|
334
|
+
} else {
|
|
335
|
+
toolsWithoutId.push(tool);
|
|
336
|
+
}
|
|
337
|
+
continue;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
if (parsed.type === 'tool_result') {
|
|
341
|
+
const toolId = typeof parsed.tool_id === 'string' ? parsed.tool_id : '';
|
|
342
|
+
const tool = toolId ? toolsById.get(toolId) : null;
|
|
343
|
+
if (tool) {
|
|
344
|
+
tool.output = parsed.output ?? parsed.result ?? null;
|
|
345
|
+
tool.status = parsed.status ?? null;
|
|
346
|
+
} else {
|
|
347
|
+
toolsWithoutId.push({
|
|
348
|
+
tool: 'tool_result',
|
|
349
|
+
input: null,
|
|
350
|
+
output: parsed.output ?? parsed.result ?? null,
|
|
351
|
+
status: parsed.status ?? null,
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
flushAssistantMessage();
|
|
358
|
+
const tools = [...toolsById.values(), ...toolsWithoutId];
|
|
359
|
+
|
|
360
|
+
if (lastMessage || sessionId || stats || tools.length > 0) {
|
|
361
|
+
return {
|
|
362
|
+
message: lastMessage,
|
|
363
|
+
session_id: sessionId,
|
|
364
|
+
stats: stats || undefined,
|
|
365
|
+
tools: tools.length > 0 ? tools : undefined,
|
|
366
|
+
};
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
return null;
|
|
169
370
|
}
|
|
170
371
|
|
|
171
|
-
/**
|
|
172
|
-
* Parse Forge output framed by Initialize/Continue/Finished markers.
|
|
173
|
-
*/
|
|
174
372
|
export function parseForgeOutput(stdout: string): any {
|
|
175
373
|
if (!stdout) return null;
|
|
176
374
|
|
|
@@ -228,3 +426,71 @@ export function parseForgeOutput(stdout: string): any {
|
|
|
228
426
|
session_id: lastConversationId,
|
|
229
427
|
};
|
|
230
428
|
}
|
|
429
|
+
|
|
430
|
+
export function parseOpenCodeOutput(stdout: string): any {
|
|
431
|
+
if (!stdout) {
|
|
432
|
+
return null;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
let sessionId: string | null = null;
|
|
436
|
+
let currentStepBuffer = '';
|
|
437
|
+
let latestCompletedStep: {
|
|
438
|
+
message: string;
|
|
439
|
+
session_id?: string;
|
|
440
|
+
tokens?: any;
|
|
441
|
+
cost?: number;
|
|
442
|
+
} | null = null;
|
|
443
|
+
let hasStepFinish = false;
|
|
444
|
+
let hasParseableAssistantText = false;
|
|
445
|
+
|
|
446
|
+
for (const line of stdout.split('\n')) {
|
|
447
|
+
if (!line.trim()) {
|
|
448
|
+
continue;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
let parsed: any;
|
|
452
|
+
try {
|
|
453
|
+
parsed = JSON.parse(line);
|
|
454
|
+
} catch {
|
|
455
|
+
continue;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
if (typeof parsed.sessionID === 'string' && parsed.sessionID) {
|
|
459
|
+
sessionId = parsed.sessionID;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
if (parsed.type === 'step_start') {
|
|
463
|
+
currentStepBuffer = '';
|
|
464
|
+
continue;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
if (parsed.type === 'text' && parsed.part?.type === 'text' && typeof parsed.part.text === 'string') {
|
|
468
|
+
currentStepBuffer += parsed.part.text;
|
|
469
|
+
hasParseableAssistantText = true;
|
|
470
|
+
continue;
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
if (parsed.type === 'step_finish') {
|
|
474
|
+
hasStepFinish = true;
|
|
475
|
+
latestCompletedStep = {
|
|
476
|
+
message: currentStepBuffer,
|
|
477
|
+
session_id: sessionId || undefined,
|
|
478
|
+
tokens: parsed.part?.tokens,
|
|
479
|
+
cost: parsed.part?.cost,
|
|
480
|
+
};
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
if (hasStepFinish && latestCompletedStep) {
|
|
485
|
+
return latestCompletedStep;
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
if (hasParseableAssistantText) {
|
|
489
|
+
return {
|
|
490
|
+
message: currentStepBuffer,
|
|
491
|
+
session_id: sessionId || undefined,
|
|
492
|
+
};
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
return null;
|
|
496
|
+
}
|
package/src/peek.ts
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import type { PeekMessage } from './parsers.js';
|
|
2
|
+
import type { AgentType, ProcessStatus } from './process-service.js';
|
|
3
|
+
|
|
4
|
+
export const DEFAULT_PEEK_TIME_SEC = 10;
|
|
5
|
+
export const MAX_PEEK_TIME_SEC = 60;
|
|
6
|
+
export const MAX_PEEK_PIDS = 32;
|
|
7
|
+
export const PEEK_MESSAGE_CAP = 50;
|
|
8
|
+
|
|
9
|
+
export type PeekStatus = ProcessStatus | 'not_found';
|
|
10
|
+
export type PeekAgent = AgentType | string | null;
|
|
11
|
+
|
|
12
|
+
export interface PeekProcessResult {
|
|
13
|
+
pid: number;
|
|
14
|
+
agent: PeekAgent;
|
|
15
|
+
status: PeekStatus;
|
|
16
|
+
messages: PeekMessage[];
|
|
17
|
+
truncated: boolean;
|
|
18
|
+
error: string | null;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface PeekResponse {
|
|
22
|
+
peek_started_at: string;
|
|
23
|
+
observed_duration_sec: number;
|
|
24
|
+
processes: PeekProcessResult[];
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function validatePeekPids(value: unknown): number[] {
|
|
28
|
+
if (!Array.isArray(value)) {
|
|
29
|
+
throw new Error('Missing or invalid required parameter: pids (must be an array of positive safe integers)');
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const deduped: number[] = [];
|
|
33
|
+
const seen = new Set<number>();
|
|
34
|
+
|
|
35
|
+
for (const pid of value) {
|
|
36
|
+
if (typeof pid !== 'number' || !Number.isSafeInteger(pid) || pid <= 0) {
|
|
37
|
+
throw new Error('All pids must be positive safe integers');
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (!seen.has(pid)) {
|
|
41
|
+
seen.add(pid);
|
|
42
|
+
deduped.push(pid);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (deduped.length === 0 || deduped.length > MAX_PEEK_PIDS) {
|
|
47
|
+
throw new Error(`pids must contain 1..${MAX_PEEK_PIDS} entries after dedupe`);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return deduped;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function validatePeekTimeSec(value: unknown): number {
|
|
54
|
+
if (value === undefined || value === null) {
|
|
55
|
+
return DEFAULT_PEEK_TIME_SEC;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (typeof value !== 'number' || !Number.isSafeInteger(value) || value <= 0 || value > MAX_PEEK_TIME_SEC) {
|
|
59
|
+
throw new Error(`peek_time_sec must be a positive integer no greater than ${MAX_PEEK_TIME_SEC}`);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return value;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function buildNotFoundPeekProcess(pid: number): PeekProcessResult {
|
|
66
|
+
return {
|
|
67
|
+
pid,
|
|
68
|
+
agent: null,
|
|
69
|
+
status: 'not_found',
|
|
70
|
+
messages: [],
|
|
71
|
+
truncated: false,
|
|
72
|
+
error: 'process not found',
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function appendPeekMessages(target: PeekProcessResult, messages: PeekMessage[]): void {
|
|
77
|
+
for (const message of messages) {
|
|
78
|
+
if (target.messages.length < PEEK_MESSAGE_CAP) {
|
|
79
|
+
target.messages.push(message);
|
|
80
|
+
} else {
|
|
81
|
+
target.truncated = true;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function observedDurationSec(startedAtMs: number, endedAtMs = Date.now()): number {
|
|
87
|
+
return Number(((endedAtMs - startedAtMs) / 1000).toFixed(2));
|
|
88
|
+
}
|
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
|
}
|