diff --git a/src/docs/ask.ts b/src/docs/ask.ts index a0ffc5b3..aaf9840b 100644 --- a/src/docs/ask.ts +++ b/src/docs/ask.ts @@ -35,6 +35,14 @@ export function createAskCommand (deps: AskDeps = defaultDeps): OpaqueCommandHan const spinner = interactive ? startSpinner(deps.stderr, 'Thinking…') : undefined try { + if (parsed.options['json'] === true) { + const chunks: string[] = [] + for await (const event of deps.docsAskStream(question, conversationId)) { + if (event.kind === 'chunk') chunks.push(event.text) + } + return { answer: chunks.join('') } + } + const gen = deps.docsAskStream(question, conversationId) await streamAnswer(gen, renderMarkdown, deps.stdout, spinner) } catch (err) { diff --git a/src/docs/chat.ts b/src/docs/chat.ts index 690dca38..7c5d2cb7 100644 --- a/src/docs/chat.ts +++ b/src/docs/chat.ts @@ -54,6 +54,24 @@ export function createChatCommand (deps: ChatDeps = defaultDeps): OpaqueCommandH const interactive = process.stderr.isTTY === true && parsed.options['json'] !== true const conversationId = newUuid() + + if (parsed.options['json'] === true) { + const chunks: string[] = [] + try { + for await (const event of deps.docsAskStream(question, conversationId)) { + if (event.kind === 'chunk') chunks.push(event.text) + } + } catch (err) { + return { + error: { + code: 'docs_error', + message: err instanceof Error ? err.message : String(err), + }, + } + } + return { answer: chunks.join('') } + } + await askQuestion(question, conversationId, deps, interactive ? startSpinner(deps.stderr, 'Thinking…') : undefined) if (interactive) { diff --git a/test/docs/ask.test.ts b/test/docs/ask.test.ts index a0678da8..99cc1ac6 100644 --- a/test/docs/ask.test.ts +++ b/test/docs/ask.test.ts @@ -93,6 +93,29 @@ describe('createAskCommand', () => { } }) + it('returns structured JSON with buffered answer when --json is active', async () => { + const captured: string[] = [] + const origWrite = process.stdout.write + process.stdout.write = ((s: string) => { captured.push(s); return true }) as typeof process.stdout.write + try { + const cmd = createAskCommand({ + docsAskStream: streamFrom(['chunk1', ' chunk2']), + stdout: { write: () => true }, + stderr: { write: () => true }, + }) + cmd.option('--json', 'output as JSON') + cmd.exitOverride() + cmd.configureOutput({ writeOut: () => {}, writeErr: () => {} }) + await cmd.parseAsync(['what is elasticsearch', '--json'], { from: 'user' }) + + const output = captured.join('') + const parsed = JSON.parse(output) + assert.equal(parsed.answer, 'chunk1 chunk2') + } finally { + process.stdout.write = origWrite + } + }) + it('stringifies non-Error thrown values into the docs_error message', async () => { const stderrWrites: string[] = [] const cmd = createAskCommand({ diff --git a/test/docs/chat.test.ts b/test/docs/chat.test.ts index 5f38ef43..e2bc9674 100644 --- a/test/docs/chat.test.ts +++ b/test/docs/chat.test.ts @@ -101,6 +101,30 @@ describe('createChatCommand', () => { } }) + it('returns structured JSON with buffered answer when --json is active', async () => { + const captured: string[] = [] + const origWrite = process.stdout.write + process.stdout.write = ((s: string) => { captured.push(s); return true }) as typeof process.stdout.write + try { + const cmd = createChatCommand({ + docsAskStream: streamFrom(['chunk1', ' chunk2']), + stdout: { write: () => true }, + stderr: { write: () => true }, + getStdin: () => Readable.from([]), + }) + cmd.option('--json', 'output as JSON') + cmd.exitOverride() + cmd.configureOutput({ writeOut: () => {}, writeErr: () => {} }) + await cmd.parseAsync(['what is elasticsearch', '--json'], { from: 'user' }) + + const output = captured.join('') + const parsed = JSON.parse(output) + assert.equal(parsed.answer, 'chunk1 chunk2') + } finally { + process.stdout.write = origWrite + } + }) + it('stringifies non-Error thrown values into the stderr message', async () => { const stderrWrites: string[] = [] const cmd = createChatCommand({