opencastle 0.34.1 → 0.34.3
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/dist/cli/run/adapters/claude.d.ts.map +1 -1
- package/dist/cli/run/adapters/claude.js +46 -3
- package/dist/cli/run/adapters/claude.js.map +1 -1
- package/dist/cli/run/adapters/claude.test.js +85 -0
- package/dist/cli/run/adapters/claude.test.js.map +1 -1
- package/dist/cli/run/adapters/copilot.d.ts.map +1 -1
- package/dist/cli/run/adapters/copilot.js +46 -3
- package/dist/cli/run/adapters/copilot.js.map +1 -1
- package/dist/cli/run/adapters/copilot.test.js +85 -0
- package/dist/cli/run/adapters/copilot.test.js.map +1 -1
- package/dist/cli/run/adapters/cursor.d.ts.map +1 -1
- package/dist/cli/run/adapters/cursor.js +46 -3
- package/dist/cli/run/adapters/cursor.js.map +1 -1
- package/dist/cli/run/adapters/cursor.test.js +85 -0
- package/dist/cli/run/adapters/cursor.test.js.map +1 -1
- package/dist/cli/run/adapters/opencode.d.ts.map +1 -1
- package/dist/cli/run/adapters/opencode.js +47 -4
- package/dist/cli/run/adapters/opencode.js.map +1 -1
- package/dist/cli/run/adapters/opencode.test.js +86 -0
- package/dist/cli/run/adapters/opencode.test.js.map +1 -1
- package/package.json +1 -1
- package/src/cli/run/adapters/claude.test.ts +85 -0
- package/src/cli/run/adapters/claude.ts +44 -3
- package/src/cli/run/adapters/copilot.test.ts +85 -0
- package/src/cli/run/adapters/copilot.ts +44 -3
- package/src/cli/run/adapters/cursor.test.ts +85 -0
- package/src/cli/run/adapters/cursor.ts +44 -3
- package/src/cli/run/adapters/opencode.test.ts +86 -0
- package/src/cli/run/adapters/opencode.ts +45 -4
- package/src/dashboard/dist/data/convoys/demo-api-v2.json +9 -9
- package/src/dashboard/dist/data/convoys/demo-auth-revamp.json +4 -4
- package/src/dashboard/dist/data/convoys/demo-dashboard-ui.json +6 -6
- package/src/dashboard/dist/data/convoys/demo-data-pipeline.json +3 -3
- package/src/dashboard/dist/data/convoys/demo-deploy-ci.json +1 -1
- package/src/dashboard/dist/data/convoys/demo-docs-update.json +3 -3
- package/src/dashboard/dist/data/convoys/demo-perf-opt.json +4 -4
- package/src/dashboard/node_modules/.vite/deps/_metadata.json +6 -6
- package/src/dashboard/public/data/convoys/demo-api-v2.json +9 -9
- package/src/dashboard/public/data/convoys/demo-auth-revamp.json +4 -4
- package/src/dashboard/public/data/convoys/demo-dashboard-ui.json +6 -6
- package/src/dashboard/public/data/convoys/demo-data-pipeline.json +3 -3
- package/src/dashboard/public/data/convoys/demo-deploy-ci.json +1 -1
- package/src/dashboard/public/data/convoys/demo-docs-update.json +3 -3
- package/src/dashboard/public/data/convoys/demo-perf-opt.json +4 -4
|
@@ -233,4 +233,89 @@ describe('copilot adapter — CLI mode', () => {
|
|
|
233
233
|
token: 'abc',
|
|
234
234
|
})
|
|
235
235
|
})
|
|
236
|
+
|
|
237
|
+
it('extracts result from single-line JSON output', async () => {
|
|
238
|
+
mockSpawn.mockImplementation((cmd: string) => {
|
|
239
|
+
if (cmd === 'which') return makeMockProc(0, '')
|
|
240
|
+
return makeMockProc(0, '{"result":"# My PRD\\n\\nThe actual content"}')
|
|
241
|
+
})
|
|
242
|
+
const { execute } = await import('./copilot.js')
|
|
243
|
+
const result = await execute(makeTask(), { cwd: tmpDir })
|
|
244
|
+
expect(result.output).toBe('# My PRD\n\nThe actual content')
|
|
245
|
+
})
|
|
246
|
+
|
|
247
|
+
it('extracts result from JSONL output (last line has result)', async () => {
|
|
248
|
+
const jsonl = [
|
|
249
|
+
'{"type":"progress","content":"thinking..."}',
|
|
250
|
+
'{"type":"tool_use","tool":"read_file","args":{}}',
|
|
251
|
+
'{"type":"result","result":"# My PRD\\n\\nThe actual markdown content","usage":{"input_tokens":500,"output_tokens":1000}}',
|
|
252
|
+
].join('\n')
|
|
253
|
+
mockSpawn.mockImplementation((cmd: string) => {
|
|
254
|
+
if (cmd === 'which') return makeMockProc(0, '')
|
|
255
|
+
return makeMockProc(0, jsonl)
|
|
256
|
+
})
|
|
257
|
+
const { execute } = await import('./copilot.js')
|
|
258
|
+
const result = await execute(makeTask(), { cwd: tmpDir })
|
|
259
|
+
expect(result.output).toBe('# My PRD\n\nThe actual markdown content')
|
|
260
|
+
expect(result.usage?.prompt_tokens).toBe(500)
|
|
261
|
+
expect(result.usage?.completion_tokens).toBe(1000)
|
|
262
|
+
})
|
|
263
|
+
|
|
264
|
+
it('scans JSONL and finds result line among non-result lines', async () => {
|
|
265
|
+
const jsonl = [
|
|
266
|
+
'{"type":"progress","content":"thinking..."}',
|
|
267
|
+
'{"type":"tool_use","tool":"edit","args":{}}',
|
|
268
|
+
'{"type":"result","result":"# Final Content","usage":{"input_tokens":100,"output_tokens":200}}',
|
|
269
|
+
'{"type":"done"}',
|
|
270
|
+
].join('\n')
|
|
271
|
+
mockSpawn.mockImplementation((cmd: string) => {
|
|
272
|
+
if (cmd === 'which') return makeMockProc(0, '')
|
|
273
|
+
return makeMockProc(0, jsonl)
|
|
274
|
+
})
|
|
275
|
+
const { execute } = await import('./copilot.js')
|
|
276
|
+
const result = await execute(makeTask(), { cwd: tmpDir })
|
|
277
|
+
expect(result.output).toBe('# Final Content')
|
|
278
|
+
})
|
|
279
|
+
|
|
280
|
+
it('extracts content from Copilot-style assistant.message JSONL', async () => {
|
|
281
|
+
const jsonl = [
|
|
282
|
+
'{"type":"session.tools_updated","data":{"model":"claude-sonnet-4.6"}}',
|
|
283
|
+
'{"type":"user.message","data":{"content":"Generate a PRD"}}',
|
|
284
|
+
'{"type":"assistant.message","data":{"content":"# My PRD\\n\\nThe generated content","outputTokens":50}}',
|
|
285
|
+
'{"type":"assistant.turn_end","data":{"turnId":"0"}}',
|
|
286
|
+
'{"type":"result","sessionId":"abc","exitCode":0,"usage":{"premiumRequests":1}}',
|
|
287
|
+
].join('\n')
|
|
288
|
+
mockSpawn.mockImplementation((cmd: string) => {
|
|
289
|
+
if (cmd === 'which') return makeMockProc(0, '')
|
|
290
|
+
return makeMockProc(0, jsonl)
|
|
291
|
+
})
|
|
292
|
+
const { execute } = await import('./copilot.js')
|
|
293
|
+
const result = await execute(makeTask(), { cwd: tmpDir })
|
|
294
|
+
expect(result.output).toBe('# My PRD\n\nThe generated content')
|
|
295
|
+
})
|
|
296
|
+
|
|
297
|
+
it('uses last assistant.message when multiple turns exist', async () => {
|
|
298
|
+
const jsonl = [
|
|
299
|
+
'{"type":"assistant.message","data":{"content":"Let me check..."}}',
|
|
300
|
+
'{"type":"assistant.message","data":{"content":"# Final PRD\\n\\nComplete document"}}',
|
|
301
|
+
'{"type":"result","sessionId":"abc","exitCode":0}',
|
|
302
|
+
].join('\n')
|
|
303
|
+
mockSpawn.mockImplementation((cmd: string) => {
|
|
304
|
+
if (cmd === 'which') return makeMockProc(0, '')
|
|
305
|
+
return makeMockProc(0, jsonl)
|
|
306
|
+
})
|
|
307
|
+
const { execute } = await import('./copilot.js')
|
|
308
|
+
const result = await execute(makeTask(), { cwd: tmpDir })
|
|
309
|
+
expect(result.output).toBe('# Final PRD\n\nComplete document')
|
|
310
|
+
})
|
|
311
|
+
|
|
312
|
+
it('falls back to raw output when JSON has no result field', async () => {
|
|
313
|
+
mockSpawn.mockImplementation((cmd: string) => {
|
|
314
|
+
if (cmd === 'which') return makeMockProc(0, '')
|
|
315
|
+
return makeMockProc(0, '{"status":"ok","data":"something"}')
|
|
316
|
+
})
|
|
317
|
+
const { execute } = await import('./copilot.js')
|
|
318
|
+
const result = await execute(makeTask(), { cwd: tmpDir })
|
|
319
|
+
expect(result.output).toBe('{"status":"ok","data":"something"}')
|
|
320
|
+
})
|
|
236
321
|
})
|
|
@@ -187,10 +187,14 @@ async function executeViaCli(task: Task, options: ExecuteOptions = {}): Promise<
|
|
|
187
187
|
}
|
|
188
188
|
})
|
|
189
189
|
proc.on('close', (code) => {
|
|
190
|
-
|
|
190
|
+
let textOutput = [stdout, stderr].filter(Boolean).join('\n')
|
|
191
191
|
let usage: TokenUsage | undefined
|
|
192
192
|
try {
|
|
193
|
+
// Try single JSON object first (claude CLI)
|
|
193
194
|
const parsedJson = JSON.parse(stdout) as Record<string, unknown>
|
|
195
|
+
if (typeof parsedJson.result === 'string') {
|
|
196
|
+
textOutput = parsedJson.result
|
|
197
|
+
}
|
|
194
198
|
const u = parsedJson?.usage as Record<string, number> | undefined
|
|
195
199
|
if (u) {
|
|
196
200
|
const promptTokens = (u.input_tokens ?? u.prompt_tokens) as number | undefined
|
|
@@ -198,10 +202,47 @@ async function executeViaCli(task: Task, options: ExecuteOptions = {}): Promise<
|
|
|
198
202
|
const total = ((promptTokens ?? 0) + (completionTokens ?? 0)) || undefined
|
|
199
203
|
usage = { prompt_tokens: promptTokens, completion_tokens: completionTokens, total_tokens: total }
|
|
200
204
|
}
|
|
201
|
-
} catch {
|
|
205
|
+
} catch {
|
|
206
|
+
// Fallback: parse JSONL (one JSON object per line)
|
|
207
|
+
// Claude CLI uses {"result": "text"}, Copilot CLI uses
|
|
208
|
+
// {"type":"assistant.message","data":{"content":"text"}} for the AI
|
|
209
|
+
// response and a separate {"type":"result"} line for session metadata.
|
|
210
|
+
const lines = stdout.split('\n')
|
|
211
|
+
let lastAssistantContent: string | undefined
|
|
212
|
+
for (const rawLine of lines) {
|
|
213
|
+
const line = rawLine.trim()
|
|
214
|
+
if (!line) continue
|
|
215
|
+
try {
|
|
216
|
+
const parsed = JSON.parse(line) as Record<string, unknown>
|
|
217
|
+
// Claude-style: result text in the result line
|
|
218
|
+
if (typeof parsed.result === 'string' && parsed.result) {
|
|
219
|
+
textOutput = parsed.result
|
|
220
|
+
const u = parsed?.usage as Record<string, number> | undefined
|
|
221
|
+
if (u) {
|
|
222
|
+
const promptTokens = (u.input_tokens ?? u.prompt_tokens) as number | undefined
|
|
223
|
+
const completionTokens = (u.output_tokens ?? u.completion_tokens) as number | undefined
|
|
224
|
+
const total = ((promptTokens ?? 0) + (completionTokens ?? 0)) || undefined
|
|
225
|
+
usage = { prompt_tokens: promptTokens, completion_tokens: completionTokens, total_tokens: total }
|
|
226
|
+
}
|
|
227
|
+
lastAssistantContent = undefined // prefer explicit result field
|
|
228
|
+
break
|
|
229
|
+
}
|
|
230
|
+
// Copilot-style: AI response in assistant.message events
|
|
231
|
+
if (parsed.type === 'assistant.message') {
|
|
232
|
+
const data = parsed.data as Record<string, unknown> | undefined
|
|
233
|
+
if (data && typeof data.content === 'string') {
|
|
234
|
+
lastAssistantContent = data.content
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
} catch { /* skip non-JSON lines */ }
|
|
238
|
+
}
|
|
239
|
+
if (lastAssistantContent !== undefined) {
|
|
240
|
+
textOutput = lastAssistantContent
|
|
241
|
+
}
|
|
242
|
+
}
|
|
202
243
|
resolve({
|
|
203
244
|
success: code === 0,
|
|
204
|
-
output:
|
|
245
|
+
output: textOutput.slice(0, 500_000),
|
|
205
246
|
exitCode: code ?? -1,
|
|
206
247
|
usage,
|
|
207
248
|
})
|
|
@@ -157,4 +157,89 @@ describe('cursor adapter — MCP support', () => {
|
|
|
157
157
|
expect(capturedArgs).toContain('--approve-mcps')
|
|
158
158
|
expect(existsSync(join(tmpDir, 'mcp.json'))).toBe(false)
|
|
159
159
|
})
|
|
160
|
+
|
|
161
|
+
it('extracts result from single-line JSON output', async () => {
|
|
162
|
+
mockSpawn.mockImplementation((cmd: string) => {
|
|
163
|
+
if (cmd === 'which') return makeMockProc(0, '')
|
|
164
|
+
return makeMockProc(0, '{"result":"# My PRD\\n\\nThe actual content"}')
|
|
165
|
+
})
|
|
166
|
+
const { execute } = await import('./cursor.js')
|
|
167
|
+
const result = await execute(makeTask(), { cwd: tmpDir })
|
|
168
|
+
expect(result.output).toBe('# My PRD\n\nThe actual content')
|
|
169
|
+
})
|
|
170
|
+
|
|
171
|
+
it('extracts result from JSONL output (last line has result)', async () => {
|
|
172
|
+
const jsonl = [
|
|
173
|
+
'{"type":"progress","content":"thinking..."}',
|
|
174
|
+
'{"type":"tool_use","tool":"read_file","args":{}}',
|
|
175
|
+
'{"type":"result","result":"# My PRD\\n\\nThe actual markdown content","usage":{"input_tokens":500,"output_tokens":1000}}',
|
|
176
|
+
].join('\n')
|
|
177
|
+
mockSpawn.mockImplementation((cmd: string) => {
|
|
178
|
+
if (cmd === 'which') return makeMockProc(0, '')
|
|
179
|
+
return makeMockProc(0, jsonl)
|
|
180
|
+
})
|
|
181
|
+
const { execute } = await import('./cursor.js')
|
|
182
|
+
const result = await execute(makeTask(), { cwd: tmpDir })
|
|
183
|
+
expect(result.output).toBe('# My PRD\n\nThe actual markdown content')
|
|
184
|
+
expect(result.usage?.prompt_tokens).toBe(500)
|
|
185
|
+
expect(result.usage?.completion_tokens).toBe(1000)
|
|
186
|
+
})
|
|
187
|
+
|
|
188
|
+
it('scans JSONL and finds result line among non-result lines', async () => {
|
|
189
|
+
const jsonl = [
|
|
190
|
+
'{"type":"progress","content":"thinking..."}',
|
|
191
|
+
'{"type":"tool_use","tool":"edit","args":{}}',
|
|
192
|
+
'{"type":"result","result":"# Final Content","usage":{"input_tokens":100,"output_tokens":200}}',
|
|
193
|
+
'{"type":"done"}',
|
|
194
|
+
].join('\n')
|
|
195
|
+
mockSpawn.mockImplementation((cmd: string) => {
|
|
196
|
+
if (cmd === 'which') return makeMockProc(0, '')
|
|
197
|
+
return makeMockProc(0, jsonl)
|
|
198
|
+
})
|
|
199
|
+
const { execute } = await import('./cursor.js')
|
|
200
|
+
const result = await execute(makeTask(), { cwd: tmpDir })
|
|
201
|
+
expect(result.output).toBe('# Final Content')
|
|
202
|
+
})
|
|
203
|
+
|
|
204
|
+
it('extracts content from Copilot-style assistant.message JSONL', async () => {
|
|
205
|
+
const jsonl = [
|
|
206
|
+
'{"type":"session.tools_updated","data":{"model":"claude-sonnet-4.6"}}',
|
|
207
|
+
'{"type":"user.message","data":{"content":"Generate a PRD"}}',
|
|
208
|
+
'{"type":"assistant.message","data":{"content":"# My PRD\\n\\nThe generated content","outputTokens":50}}',
|
|
209
|
+
'{"type":"assistant.turn_end","data":{"turnId":"0"}}',
|
|
210
|
+
'{"type":"result","sessionId":"abc","exitCode":0,"usage":{"premiumRequests":1}}',
|
|
211
|
+
].join('\n')
|
|
212
|
+
mockSpawn.mockImplementation((cmd: string) => {
|
|
213
|
+
if (cmd === 'which') return makeMockProc(0, '')
|
|
214
|
+
return makeMockProc(0, jsonl)
|
|
215
|
+
})
|
|
216
|
+
const { execute } = await import('./cursor.js')
|
|
217
|
+
const result = await execute(makeTask(), { cwd: tmpDir })
|
|
218
|
+
expect(result.output).toBe('# My PRD\n\nThe generated content')
|
|
219
|
+
})
|
|
220
|
+
|
|
221
|
+
it('uses last assistant.message when multiple turns exist', async () => {
|
|
222
|
+
const jsonl = [
|
|
223
|
+
'{"type":"assistant.message","data":{"content":"Let me check..."}}',
|
|
224
|
+
'{"type":"assistant.message","data":{"content":"# Final PRD\\n\\nComplete document"}}',
|
|
225
|
+
'{"type":"result","sessionId":"abc","exitCode":0}',
|
|
226
|
+
].join('\n')
|
|
227
|
+
mockSpawn.mockImplementation((cmd: string) => {
|
|
228
|
+
if (cmd === 'which') return makeMockProc(0, '')
|
|
229
|
+
return makeMockProc(0, jsonl)
|
|
230
|
+
})
|
|
231
|
+
const { execute } = await import('./cursor.js')
|
|
232
|
+
const result = await execute(makeTask(), { cwd: tmpDir })
|
|
233
|
+
expect(result.output).toBe('# Final PRD\n\nComplete document')
|
|
234
|
+
})
|
|
235
|
+
|
|
236
|
+
it('falls back to raw output when JSON has no result field', async () => {
|
|
237
|
+
mockSpawn.mockImplementation((cmd: string) => {
|
|
238
|
+
if (cmd === 'which') return makeMockProc(0, '')
|
|
239
|
+
return makeMockProc(0, '{"status":"ok","data":"something"}')
|
|
240
|
+
})
|
|
241
|
+
const { execute } = await import('./cursor.js')
|
|
242
|
+
const result = await execute(makeTask(), { cwd: tmpDir })
|
|
243
|
+
expect(result.output).toBe('{"status":"ok","data":"something"}')
|
|
244
|
+
})
|
|
160
245
|
})
|
|
@@ -85,10 +85,14 @@ export async function execute(task: Task, options: ExecuteOptions = {}): Promise
|
|
|
85
85
|
})
|
|
86
86
|
|
|
87
87
|
proc.on('close', (code) => {
|
|
88
|
-
|
|
88
|
+
let textOutput = [stdout, stderr].filter(Boolean).join('\n')
|
|
89
89
|
let usage: TokenUsage | undefined
|
|
90
90
|
try {
|
|
91
|
+
// Try single JSON object first (claude CLI)
|
|
91
92
|
const parsed = JSON.parse(stdout) as Record<string, unknown>
|
|
93
|
+
if (typeof parsed.result === 'string') {
|
|
94
|
+
textOutput = parsed.result
|
|
95
|
+
}
|
|
92
96
|
const u = parsed?.usage as Record<string, number> | undefined
|
|
93
97
|
if (u) {
|
|
94
98
|
const promptTokens = (u.input_tokens ?? u.prompt_tokens) as number | undefined
|
|
@@ -96,10 +100,47 @@ export async function execute(task: Task, options: ExecuteOptions = {}): Promise
|
|
|
96
100
|
const total = ((promptTokens ?? 0) + (completionTokens ?? 0)) || undefined
|
|
97
101
|
usage = { prompt_tokens: promptTokens, completion_tokens: completionTokens, total_tokens: total }
|
|
98
102
|
}
|
|
99
|
-
} catch {
|
|
103
|
+
} catch {
|
|
104
|
+
// Fallback: parse JSONL (one JSON object per line)
|
|
105
|
+
// Claude CLI uses {"result": "text"}, Copilot CLI uses
|
|
106
|
+
// {"type":"assistant.message","data":{"content":"text"}} for the AI
|
|
107
|
+
// response and a separate {"type":"result"} line for session metadata.
|
|
108
|
+
const lines = stdout.split('\n')
|
|
109
|
+
let lastAssistantContent: string | undefined
|
|
110
|
+
for (const rawLine of lines) {
|
|
111
|
+
const line = rawLine.trim()
|
|
112
|
+
if (!line) continue
|
|
113
|
+
try {
|
|
114
|
+
const parsed = JSON.parse(line) as Record<string, unknown>
|
|
115
|
+
// Claude-style: result text in the result line
|
|
116
|
+
if (typeof parsed.result === 'string' && parsed.result) {
|
|
117
|
+
textOutput = parsed.result
|
|
118
|
+
const u = parsed?.usage as Record<string, number> | undefined
|
|
119
|
+
if (u) {
|
|
120
|
+
const promptTokens = (u.input_tokens ?? u.prompt_tokens) as number | undefined
|
|
121
|
+
const completionTokens = (u.output_tokens ?? u.completion_tokens) as number | undefined
|
|
122
|
+
const total = ((promptTokens ?? 0) + (completionTokens ?? 0)) || undefined
|
|
123
|
+
usage = { prompt_tokens: promptTokens, completion_tokens: completionTokens, total_tokens: total }
|
|
124
|
+
}
|
|
125
|
+
lastAssistantContent = undefined // prefer explicit result field
|
|
126
|
+
break
|
|
127
|
+
}
|
|
128
|
+
// Copilot-style: AI response in assistant.message events
|
|
129
|
+
if (parsed.type === 'assistant.message') {
|
|
130
|
+
const data = parsed.data as Record<string, unknown> | undefined
|
|
131
|
+
if (data && typeof data.content === 'string') {
|
|
132
|
+
lastAssistantContent = data.content
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
} catch { /* skip non-JSON lines */ }
|
|
136
|
+
}
|
|
137
|
+
if (lastAssistantContent !== undefined) {
|
|
138
|
+
textOutput = lastAssistantContent
|
|
139
|
+
}
|
|
140
|
+
}
|
|
100
141
|
resolve({
|
|
101
142
|
success: code === 0,
|
|
102
|
-
output:
|
|
143
|
+
output: textOutput.slice(0, 500_000),
|
|
103
144
|
exitCode: code ?? -1,
|
|
104
145
|
usage,
|
|
105
146
|
})
|
|
@@ -130,6 +130,7 @@ describe('opencode adapter — MCP support', () => {
|
|
|
130
130
|
})
|
|
131
131
|
const { execute } = await import('./opencode.js')
|
|
132
132
|
await execute(makeTask(), { cwd: tmpDir })
|
|
133
|
+
expect(capturedArgs).toContain('run')
|
|
133
134
|
expect(capturedArgs).not.toContain('--mcp-config')
|
|
134
135
|
})
|
|
135
136
|
|
|
@@ -156,4 +157,89 @@ describe('opencode adapter — MCP support', () => {
|
|
|
156
157
|
await execute(makeTask(), { cwd: tmpDir })
|
|
157
158
|
expect(capturedArgs).not.toContain('--approve-mcps')
|
|
158
159
|
})
|
|
160
|
+
|
|
161
|
+
it('extracts result from single-line JSON output', async () => {
|
|
162
|
+
mockSpawn.mockImplementation((cmd: string) => {
|
|
163
|
+
if (cmd === 'which') return makeMockProc(0, '')
|
|
164
|
+
return makeMockProc(0, '{"result":"# My PRD\\n\\nThe actual content"}')
|
|
165
|
+
})
|
|
166
|
+
const { execute } = await import('./opencode.js')
|
|
167
|
+
const result = await execute(makeTask(), { cwd: tmpDir })
|
|
168
|
+
expect(result.output).toBe('# My PRD\n\nThe actual content')
|
|
169
|
+
})
|
|
170
|
+
|
|
171
|
+
it('extracts result from JSONL output (last line has result)', async () => {
|
|
172
|
+
const jsonl = [
|
|
173
|
+
'{"type":"progress","content":"thinking..."}',
|
|
174
|
+
'{"type":"tool_use","tool":"read_file","args":{}}',
|
|
175
|
+
'{"type":"result","result":"# My PRD\\n\\nThe actual markdown content","usage":{"input_tokens":500,"output_tokens":1000}}',
|
|
176
|
+
].join('\n')
|
|
177
|
+
mockSpawn.mockImplementation((cmd: string) => {
|
|
178
|
+
if (cmd === 'which') return makeMockProc(0, '')
|
|
179
|
+
return makeMockProc(0, jsonl)
|
|
180
|
+
})
|
|
181
|
+
const { execute } = await import('./opencode.js')
|
|
182
|
+
const result = await execute(makeTask(), { cwd: tmpDir })
|
|
183
|
+
expect(result.output).toBe('# My PRD\n\nThe actual markdown content')
|
|
184
|
+
expect(result.usage?.prompt_tokens).toBe(500)
|
|
185
|
+
expect(result.usage?.completion_tokens).toBe(1000)
|
|
186
|
+
})
|
|
187
|
+
|
|
188
|
+
it('scans JSONL and finds result line among non-result lines', async () => {
|
|
189
|
+
const jsonl = [
|
|
190
|
+
'{"type":"progress","content":"thinking..."}',
|
|
191
|
+
'{"type":"tool_use","tool":"edit","args":{}}',
|
|
192
|
+
'{"type":"result","result":"# Final Content","usage":{"input_tokens":100,"output_tokens":200}}',
|
|
193
|
+
'{"type":"done"}',
|
|
194
|
+
].join('\n')
|
|
195
|
+
mockSpawn.mockImplementation((cmd: string) => {
|
|
196
|
+
if (cmd === 'which') return makeMockProc(0, '')
|
|
197
|
+
return makeMockProc(0, jsonl)
|
|
198
|
+
})
|
|
199
|
+
const { execute } = await import('./opencode.js')
|
|
200
|
+
const result = await execute(makeTask(), { cwd: tmpDir })
|
|
201
|
+
expect(result.output).toBe('# Final Content')
|
|
202
|
+
})
|
|
203
|
+
|
|
204
|
+
it('extracts content from Copilot-style assistant.message JSONL', async () => {
|
|
205
|
+
const jsonl = [
|
|
206
|
+
'{"type":"session.tools_updated","data":{"model":"claude-sonnet-4.6"}}',
|
|
207
|
+
'{"type":"user.message","data":{"content":"Generate a PRD"}}',
|
|
208
|
+
'{"type":"assistant.message","data":{"content":"# My PRD\\n\\nThe generated content","outputTokens":50}}',
|
|
209
|
+
'{"type":"assistant.turn_end","data":{"turnId":"0"}}',
|
|
210
|
+
'{"type":"result","sessionId":"abc","exitCode":0,"usage":{"premiumRequests":1}}',
|
|
211
|
+
].join('\n')
|
|
212
|
+
mockSpawn.mockImplementation((cmd: string) => {
|
|
213
|
+
if (cmd === 'which') return makeMockProc(0, '')
|
|
214
|
+
return makeMockProc(0, jsonl)
|
|
215
|
+
})
|
|
216
|
+
const { execute } = await import('./opencode.js')
|
|
217
|
+
const result = await execute(makeTask(), { cwd: tmpDir })
|
|
218
|
+
expect(result.output).toBe('# My PRD\n\nThe generated content')
|
|
219
|
+
})
|
|
220
|
+
|
|
221
|
+
it('uses last assistant.message when multiple turns exist', async () => {
|
|
222
|
+
const jsonl = [
|
|
223
|
+
'{"type":"assistant.message","data":{"content":"Let me check..."}}',
|
|
224
|
+
'{"type":"assistant.message","data":{"content":"# Final PRD\\n\\nComplete document"}}',
|
|
225
|
+
'{"type":"result","sessionId":"abc","exitCode":0}',
|
|
226
|
+
].join('\n')
|
|
227
|
+
mockSpawn.mockImplementation((cmd: string) => {
|
|
228
|
+
if (cmd === 'which') return makeMockProc(0, '')
|
|
229
|
+
return makeMockProc(0, jsonl)
|
|
230
|
+
})
|
|
231
|
+
const { execute } = await import('./opencode.js')
|
|
232
|
+
const result = await execute(makeTask(), { cwd: tmpDir })
|
|
233
|
+
expect(result.output).toBe('# Final PRD\n\nComplete document')
|
|
234
|
+
})
|
|
235
|
+
|
|
236
|
+
it('falls back to raw output when JSON has no result field', async () => {
|
|
237
|
+
mockSpawn.mockImplementation((cmd: string) => {
|
|
238
|
+
if (cmd === 'which') return makeMockProc(0, '')
|
|
239
|
+
return makeMockProc(0, '{"status":"ok","data":"something"}')
|
|
240
|
+
})
|
|
241
|
+
const { execute } = await import('./opencode.js')
|
|
242
|
+
const result = await execute(makeTask(), { cwd: tmpDir })
|
|
243
|
+
expect(result.output).toBe('{"status":"ok","data":"something"}')
|
|
244
|
+
})
|
|
159
245
|
})
|
|
@@ -28,7 +28,7 @@ export async function execute(task: Task, options: ExecuteOptions = {}): Promise
|
|
|
28
28
|
prompt += `\n\nOnly modify files under: ${task.files.join(', ')}`
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
-
const args = ['
|
|
31
|
+
const args = ['run', prompt, '--format', 'json']
|
|
32
32
|
|
|
33
33
|
const cwd = options?.cwd ?? process.cwd()
|
|
34
34
|
const mcpJsonPath = join(cwd, 'mcp.json')
|
|
@@ -79,10 +79,14 @@ export async function execute(task: Task, options: ExecuteOptions = {}): Promise
|
|
|
79
79
|
})
|
|
80
80
|
|
|
81
81
|
proc.on('close', (code) => {
|
|
82
|
-
|
|
82
|
+
let textOutput = [stdout, stderr].filter(Boolean).join('\n')
|
|
83
83
|
let usage: TokenUsage | undefined
|
|
84
84
|
try {
|
|
85
|
+
// Try single JSON object first (claude CLI)
|
|
85
86
|
const parsed = JSON.parse(stdout) as Record<string, unknown>
|
|
87
|
+
if (typeof parsed.result === 'string') {
|
|
88
|
+
textOutput = parsed.result
|
|
89
|
+
}
|
|
86
90
|
const u = parsed?.usage as Record<string, number> | undefined
|
|
87
91
|
if (u) {
|
|
88
92
|
const promptTokens = (u.input_tokens ?? u.prompt_tokens) as number | undefined
|
|
@@ -90,10 +94,47 @@ export async function execute(task: Task, options: ExecuteOptions = {}): Promise
|
|
|
90
94
|
const total = ((promptTokens ?? 0) + (completionTokens ?? 0)) || undefined
|
|
91
95
|
usage = { prompt_tokens: promptTokens, completion_tokens: completionTokens, total_tokens: total }
|
|
92
96
|
}
|
|
93
|
-
} catch {
|
|
97
|
+
} catch {
|
|
98
|
+
// Fallback: parse JSONL (one JSON object per line)
|
|
99
|
+
// Claude CLI uses {"result": "text"}, Copilot CLI uses
|
|
100
|
+
// {"type":"assistant.message","data":{"content":"text"}} for the AI
|
|
101
|
+
// response and a separate {"type":"result"} line for session metadata.
|
|
102
|
+
const lines = stdout.split('\n')
|
|
103
|
+
let lastAssistantContent: string | undefined
|
|
104
|
+
for (const rawLine of lines) {
|
|
105
|
+
const line = rawLine.trim()
|
|
106
|
+
if (!line) continue
|
|
107
|
+
try {
|
|
108
|
+
const parsed = JSON.parse(line) as Record<string, unknown>
|
|
109
|
+
// Claude-style: result text in the result line
|
|
110
|
+
if (typeof parsed.result === 'string' && parsed.result) {
|
|
111
|
+
textOutput = parsed.result
|
|
112
|
+
const u = parsed?.usage as Record<string, number> | undefined
|
|
113
|
+
if (u) {
|
|
114
|
+
const promptTokens = (u.input_tokens ?? u.prompt_tokens) as number | undefined
|
|
115
|
+
const completionTokens = (u.output_tokens ?? u.completion_tokens) as number | undefined
|
|
116
|
+
const total = ((promptTokens ?? 0) + (completionTokens ?? 0)) || undefined
|
|
117
|
+
usage = { prompt_tokens: promptTokens, completion_tokens: completionTokens, total_tokens: total }
|
|
118
|
+
}
|
|
119
|
+
lastAssistantContent = undefined // prefer explicit result field
|
|
120
|
+
break
|
|
121
|
+
}
|
|
122
|
+
// Copilot-style: AI response in assistant.message events
|
|
123
|
+
if (parsed.type === 'assistant.message') {
|
|
124
|
+
const data = parsed.data as Record<string, unknown> | undefined
|
|
125
|
+
if (data && typeof data.content === 'string') {
|
|
126
|
+
lastAssistantContent = data.content
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
} catch { /* skip non-JSON lines */ }
|
|
130
|
+
}
|
|
131
|
+
if (lastAssistantContent !== undefined) {
|
|
132
|
+
textOutput = lastAssistantContent
|
|
133
|
+
}
|
|
134
|
+
}
|
|
94
135
|
resolve({
|
|
95
136
|
success: code === 0,
|
|
96
|
-
output:
|
|
137
|
+
output: textOutput.slice(0, 500_000),
|
|
97
138
|
exitCode: code ?? -1,
|
|
98
139
|
usage,
|
|
99
140
|
})
|
|
@@ -51,21 +51,21 @@
|
|
|
51
51
|
"name": "docs/api-v2-contract.json",
|
|
52
52
|
"type": "json",
|
|
53
53
|
"task_id": "api-t1",
|
|
54
|
-
"created_at": "2026-04-
|
|
55
|
-
},
|
|
56
|
-
{
|
|
57
|
-
"id": "artifact-demo-api-v2-reports-security-gate-failure-md",
|
|
58
|
-
"name": "reports/security-gate-failure.md",
|
|
59
|
-
"type": "summary",
|
|
60
|
-
"task_id": "api-t3",
|
|
61
|
-
"created_at": "2026-04-09T22:20:56.923Z"
|
|
54
|
+
"created_at": "2026-04-10T12:41:00.577Z"
|
|
62
55
|
},
|
|
63
56
|
{
|
|
64
57
|
"id": "artifact-demo-api-v2-src-api-rate-limiter-ts",
|
|
65
58
|
"name": "src/api/rate-limiter.ts",
|
|
66
59
|
"type": "file",
|
|
67
60
|
"task_id": "api-t2",
|
|
68
|
-
"created_at": "2026-04-
|
|
61
|
+
"created_at": "2026-04-10T12:41:00.577Z"
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
"id": "artifact-demo-api-v2-reports-security-gate-failure-md",
|
|
65
|
+
"name": "reports/security-gate-failure.md",
|
|
66
|
+
"type": "summary",
|
|
67
|
+
"task_id": "api-t3",
|
|
68
|
+
"created_at": "2026-04-10T12:41:00.578Z"
|
|
69
69
|
}
|
|
70
70
|
],
|
|
71
71
|
"has_more_events": false,
|
|
@@ -42,28 +42,28 @@
|
|
|
42
42
|
"name": "libs/auth/src/jwt-middleware.ts",
|
|
43
43
|
"type": "file",
|
|
44
44
|
"task_id": "auth-t2",
|
|
45
|
-
"created_at": "2026-04-
|
|
45
|
+
"created_at": "2026-04-10T12:41:00.577Z"
|
|
46
46
|
},
|
|
47
47
|
{
|
|
48
48
|
"id": "artifact-demo-auth-revamp-libs-auth-src-rls-policies-sql",
|
|
49
49
|
"name": "libs/auth/src/rls-policies.sql",
|
|
50
50
|
"type": "file",
|
|
51
51
|
"task_id": "auth-t3",
|
|
52
|
-
"created_at": "2026-04-
|
|
52
|
+
"created_at": "2026-04-10T12:41:00.577Z"
|
|
53
53
|
},
|
|
54
54
|
{
|
|
55
55
|
"id": "artifact-demo-auth-revamp-reports-auth-review-summary-md",
|
|
56
56
|
"name": "reports/auth-review-summary.md",
|
|
57
57
|
"type": "summary",
|
|
58
58
|
"task_id": "auth-t5",
|
|
59
|
-
"created_at": "2026-04-
|
|
59
|
+
"created_at": "2026-04-10T12:41:00.577Z"
|
|
60
60
|
},
|
|
61
61
|
{
|
|
62
62
|
"id": "artifact-demo-auth-revamp-tests-auth-integration-test-ts",
|
|
63
63
|
"name": "tests/auth/integration.test.ts",
|
|
64
64
|
"type": "file",
|
|
65
65
|
"task_id": "auth-t4",
|
|
66
|
-
"created_at": "2026-04-
|
|
66
|
+
"created_at": "2026-04-10T12:41:00.577Z"
|
|
67
67
|
}
|
|
68
68
|
],
|
|
69
69
|
"has_more_events": false,
|
|
@@ -51,42 +51,42 @@
|
|
|
51
51
|
"name": "reports/panel-review-dashboard.md",
|
|
52
52
|
"type": "summary",
|
|
53
53
|
"task_id": "ui-t7",
|
|
54
|
-
"created_at": "2026-04-
|
|
54
|
+
"created_at": "2026-04-10T12:41:00.577Z"
|
|
55
55
|
},
|
|
56
56
|
{
|
|
57
57
|
"id": "artifact-demo-dashboard-ui-reports-visual-regression-json",
|
|
58
58
|
"name": "reports/visual-regression.json",
|
|
59
59
|
"type": "json",
|
|
60
60
|
"task_id": "ui-t6",
|
|
61
|
-
"created_at": "2026-04-
|
|
61
|
+
"created_at": "2026-04-10T12:41:00.577Z"
|
|
62
62
|
},
|
|
63
63
|
{
|
|
64
64
|
"id": "artifact-demo-dashboard-ui-src-components-DonutChart-tsx",
|
|
65
65
|
"name": "src/components/DonutChart.tsx",
|
|
66
66
|
"type": "file",
|
|
67
67
|
"task_id": "ui-t3",
|
|
68
|
-
"created_at": "2026-04-
|
|
68
|
+
"created_at": "2026-04-10T12:41:00.577Z"
|
|
69
69
|
},
|
|
70
70
|
{
|
|
71
71
|
"id": "artifact-demo-dashboard-ui-src-components-KpiCard-tsx",
|
|
72
72
|
"name": "src/components/KpiCard.tsx",
|
|
73
73
|
"type": "file",
|
|
74
74
|
"task_id": "ui-t2",
|
|
75
|
-
"created_at": "2026-04-
|
|
75
|
+
"created_at": "2026-04-10T12:41:00.577Z"
|
|
76
76
|
},
|
|
77
77
|
{
|
|
78
78
|
"id": "artifact-demo-dashboard-ui-src-components-design-tokens-ts",
|
|
79
79
|
"name": "src/components/design-tokens.ts",
|
|
80
80
|
"type": "file",
|
|
81
81
|
"task_id": "ui-t1",
|
|
82
|
-
"created_at": "2026-04-
|
|
82
|
+
"created_at": "2026-04-10T12:41:00.577Z"
|
|
83
83
|
},
|
|
84
84
|
{
|
|
85
85
|
"id": "artifact-demo-dashboard-ui-src-styles-animations-css",
|
|
86
86
|
"name": "src/styles/animations.css",
|
|
87
87
|
"type": "file",
|
|
88
88
|
"task_id": "ui-t4",
|
|
89
|
-
"created_at": "2026-04-
|
|
89
|
+
"created_at": "2026-04-10T12:41:00.577Z"
|
|
90
90
|
}
|
|
91
91
|
],
|
|
92
92
|
"has_more_events": false,
|
|
@@ -42,21 +42,21 @@
|
|
|
42
42
|
"name": "src/etl/pipeline.ts",
|
|
43
43
|
"type": "file",
|
|
44
44
|
"task_id": "etl-t2",
|
|
45
|
-
"created_at": "2026-04-
|
|
45
|
+
"created_at": "2026-04-10T12:41:00.578Z"
|
|
46
46
|
},
|
|
47
47
|
{
|
|
48
48
|
"id": "artifact-demo-data-pipeline-src-etl-schema-ts",
|
|
49
49
|
"name": "src/etl/schema.ts",
|
|
50
50
|
"type": "file",
|
|
51
51
|
"task_id": "etl-t1",
|
|
52
|
-
"created_at": "2026-04-
|
|
52
|
+
"created_at": "2026-04-10T12:41:00.578Z"
|
|
53
53
|
},
|
|
54
54
|
{
|
|
55
55
|
"id": "artifact-demo-data-pipeline-tests-etl-pipeline-test-ts",
|
|
56
56
|
"name": "tests/etl/pipeline.test.ts",
|
|
57
57
|
"type": "file",
|
|
58
58
|
"task_id": "etl-t3",
|
|
59
|
-
"created_at": "2026-04-
|
|
59
|
+
"created_at": "2026-04-10T12:41:00.578Z"
|
|
60
60
|
}
|
|
61
61
|
],
|
|
62
62
|
"has_more_events": false,
|