opencastle 0.34.2 → 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 +43 -5
- 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 +43 -5
- 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 +43 -5
- 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 +44 -6
- 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 +41 -7
- package/src/cli/run/adapters/copilot.test.ts +85 -0
- package/src/cli/run/adapters/copilot.ts +41 -7
- package/src/cli/run/adapters/cursor.test.ts +85 -0
- package/src/cli/run/adapters/cursor.ts +41 -7
- package/src/cli/run/adapters/opencode.test.ts +86 -0
- package/src/cli/run/adapters/opencode.ts +42 -8
- 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 +12 -12
- 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 +10 -10
- 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 +12 -12
- 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 +10 -10
|
@@ -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
|
})
|
|
@@ -190,14 +190,11 @@ async function executeViaCli(task: Task, options: ExecuteOptions = {}): Promise<
|
|
|
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>
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
const result = parsedJson.result as string | undefined
|
|
197
|
-
if (typeof result === 'string') {
|
|
198
|
-
textOutput = result
|
|
195
|
+
if (typeof parsedJson.result === 'string') {
|
|
196
|
+
textOutput = parsedJson.result
|
|
199
197
|
}
|
|
200
|
-
|
|
201
198
|
const u = parsedJson?.usage as Record<string, number> | undefined
|
|
202
199
|
if (u) {
|
|
203
200
|
const promptTokens = (u.input_tokens ?? u.prompt_tokens) as number | undefined
|
|
@@ -205,7 +202,44 @@ async function executeViaCli(task: Task, options: ExecuteOptions = {}): Promise<
|
|
|
205
202
|
const total = ((promptTokens ?? 0) + (completionTokens ?? 0)) || undefined
|
|
206
203
|
usage = { prompt_tokens: promptTokens, completion_tokens: completionTokens, total_tokens: total }
|
|
207
204
|
}
|
|
208
|
-
} 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
|
+
}
|
|
209
243
|
resolve({
|
|
210
244
|
success: code === 0,
|
|
211
245
|
output: textOutput.slice(0, 500_000),
|
|
@@ -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
|
})
|
|
@@ -88,14 +88,11 @@ export async function execute(task: Task, options: ExecuteOptions = {}): Promise
|
|
|
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>
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
const result = parsed.result as string | undefined
|
|
95
|
-
if (typeof result === 'string') {
|
|
96
|
-
textOutput = result
|
|
93
|
+
if (typeof parsed.result === 'string') {
|
|
94
|
+
textOutput = parsed.result
|
|
97
95
|
}
|
|
98
|
-
|
|
99
96
|
const u = parsed?.usage as Record<string, number> | undefined
|
|
100
97
|
if (u) {
|
|
101
98
|
const promptTokens = (u.input_tokens ?? u.prompt_tokens) as number | undefined
|
|
@@ -103,7 +100,44 @@ export async function execute(task: Task, options: ExecuteOptions = {}): Promise
|
|
|
103
100
|
const total = ((promptTokens ?? 0) + (completionTokens ?? 0)) || undefined
|
|
104
101
|
usage = { prompt_tokens: promptTokens, completion_tokens: completionTokens, total_tokens: total }
|
|
105
102
|
}
|
|
106
|
-
} 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
|
+
}
|
|
107
141
|
resolve({
|
|
108
142
|
success: code === 0,
|
|
109
143
|
output: textOutput.slice(0, 500_000),
|
|
@@ -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')
|
|
@@ -82,14 +82,11 @@ export async function execute(task: Task, options: ExecuteOptions = {}): Promise
|
|
|
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>
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
const result = parsed.result as string | undefined
|
|
89
|
-
if (typeof result === 'string') {
|
|
90
|
-
textOutput = result
|
|
87
|
+
if (typeof parsed.result === 'string') {
|
|
88
|
+
textOutput = parsed.result
|
|
91
89
|
}
|
|
92
|
-
|
|
93
90
|
const u = parsed?.usage as Record<string, number> | undefined
|
|
94
91
|
if (u) {
|
|
95
92
|
const promptTokens = (u.input_tokens ?? u.prompt_tokens) as number | undefined
|
|
@@ -97,7 +94,44 @@ export async function execute(task: Task, options: ExecuteOptions = {}): Promise
|
|
|
97
94
|
const total = ((promptTokens ?? 0) + (completionTokens ?? 0)) || undefined
|
|
98
95
|
usage = { prompt_tokens: promptTokens, completion_tokens: completionTokens, total_tokens: total }
|
|
99
96
|
}
|
|
100
|
-
} 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
|
+
}
|
|
101
135
|
resolve({
|
|
102
136
|
success: code === 0,
|
|
103
137
|
output: textOutput.slice(0, 500_000),
|
|
@@ -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-10T09:30:29.641Z"
|
|
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,
|
|
@@ -46,47 +46,47 @@
|
|
|
46
46
|
],
|
|
47
47
|
"artifact_count": 6,
|
|
48
48
|
"artifacts": [
|
|
49
|
-
{
|
|
50
|
-
"id": "artifact-demo-dashboard-ui-src-components-design-tokens-ts",
|
|
51
|
-
"name": "src/components/design-tokens.ts",
|
|
52
|
-
"type": "file",
|
|
53
|
-
"task_id": "ui-t1",
|
|
54
|
-
"created_at": "2026-04-10T09:30:29.640Z"
|
|
55
|
-
},
|
|
56
49
|
{
|
|
57
50
|
"id": "artifact-demo-dashboard-ui-reports-panel-review-dashboard-md",
|
|
58
51
|
"name": "reports/panel-review-dashboard.md",
|
|
59
52
|
"type": "summary",
|
|
60
53
|
"task_id": "ui-t7",
|
|
61
|
-
"created_at": "2026-04-
|
|
54
|
+
"created_at": "2026-04-10T12:41:00.577Z"
|
|
62
55
|
},
|
|
63
56
|
{
|
|
64
57
|
"id": "artifact-demo-dashboard-ui-reports-visual-regression-json",
|
|
65
58
|
"name": "reports/visual-regression.json",
|
|
66
59
|
"type": "json",
|
|
67
60
|
"task_id": "ui-t6",
|
|
68
|
-
"created_at": "2026-04-
|
|
61
|
+
"created_at": "2026-04-10T12:41:00.577Z"
|
|
69
62
|
},
|
|
70
63
|
{
|
|
71
64
|
"id": "artifact-demo-dashboard-ui-src-components-DonutChart-tsx",
|
|
72
65
|
"name": "src/components/DonutChart.tsx",
|
|
73
66
|
"type": "file",
|
|
74
67
|
"task_id": "ui-t3",
|
|
75
|
-
"created_at": "2026-04-
|
|
68
|
+
"created_at": "2026-04-10T12:41:00.577Z"
|
|
76
69
|
},
|
|
77
70
|
{
|
|
78
71
|
"id": "artifact-demo-dashboard-ui-src-components-KpiCard-tsx",
|
|
79
72
|
"name": "src/components/KpiCard.tsx",
|
|
80
73
|
"type": "file",
|
|
81
74
|
"task_id": "ui-t2",
|
|
82
|
-
"created_at": "2026-04-
|
|
75
|
+
"created_at": "2026-04-10T12:41:00.577Z"
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
"id": "artifact-demo-dashboard-ui-src-components-design-tokens-ts",
|
|
79
|
+
"name": "src/components/design-tokens.ts",
|
|
80
|
+
"type": "file",
|
|
81
|
+
"task_id": "ui-t1",
|
|
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,
|