From a14bcc90467754eaecc209c5330779a7d319af58 Mon Sep 17 00:00:00 2001 From: yorkeccak Date: Tue, 31 Mar 2026 12:50:26 +0100 Subject: [PATCH] Add structured output support for deepresearch Support JSON schema via --structured (inline) and --structured-file (read from file) on deepresearch create. Passes structured_output to the API and renders structured data on task completion. Bump to 1.0.2. --- package.json | 2 +- skills/valyu-cli/SKILL.md | 2 +- src/commands/research/index.ts | 63 ++++++++++++++++++++++++++++++++++ src/lib/client.ts | 3 ++ src/lib/version.ts | 2 +- 5 files changed, 69 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index 1ed1ac7..92b20d4 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@valyu/cli", - "version": "1.0.1", + "version": "1.0.2", "description": "The search CLI for knowledge workers", "license": "MIT", "repository": { diff --git a/skills/valyu-cli/SKILL.md b/skills/valyu-cli/SKILL.md index d839393..3a9b488 100644 --- a/skills/valyu-cli/SKILL.md +++ b/skills/valyu-cli/SKILL.md @@ -9,7 +9,7 @@ description: > license: MIT metadata: author: valyu - version: "1.0.0" + version: "1.0.2" homepage: https://valyu.ai source: https://github.com/valyu-network/valyu-cli inputs: diff --git a/src/commands/research/index.ts b/src/commands/research/index.ts index ec02afb..4f9aad9 100644 --- a/src/commands/research/index.ts +++ b/src/commands/research/index.ts @@ -1,3 +1,5 @@ +import { readFileSync } from 'node:fs'; +import { resolve } from 'node:path'; import { Command } from '@commander-js/extra-typings'; import pc from 'picocolors'; import * as p from '@clack/prompts'; @@ -42,6 +44,8 @@ const createCmd = new Command('create') .option('-m, --mode ', `Research depth: ${MODES.join(', ')} (default: standard)`, 'standard') .option('--no-pdf', 'Skip PDF generation') .option('-w, --watch', 'Wait for completion and display result') + .option('--structured ', 'JSON schema for structured output (inline JSON string)') + .option('--structured-file ', 'Path to JSON schema file for structured output') .addHelpText( 'after', ` @@ -49,11 +53,18 @@ ${pc.dim('Modes:')} ${MODES.map((m) => ` ${pc.cyan(m.padEnd(10))} ${MODE_DESC[m]}`).join('\n')} +${pc.dim('Structured output:')} + + Pass a JSON schema to get structured data back alongside the report. + Use ${pc.cyan('--structured')} for inline JSON or ${pc.cyan('--structured-file')} to read from a file. + ${pc.dim('Examples:')} ${pc.dim('$ valyu deepresearch create "AI infrastructure market analysis 2025"')} ${pc.dim('$ valyu deepresearch create "CRISPR therapeutics landscape" --mode heavy')} ${pc.dim('$ valyu deepresearch create "Tesla competitive positioning" --watch')} + ${pc.dim('$ valyu deepresearch create "Compare top 5 CRM tools" --structured-file schema.json --watch')} + ${pc.dim('$ valyu deepresearch create "NVIDIA financials" --structured \'{"revenue":"number","yoy_growth":"string"}\'')} `, ) .action(async (query, opts, cmd) => { @@ -70,6 +81,42 @@ ${pc.dim('Examples:')} return; } + if (opts.structured && opts.structuredFile) { + outputError( + { message: 'Use --structured or --structured-file, not both', code: 'invalid_options' }, + { json: globalOpts.json }, + ); + return; + } + + let structuredOutput: Record | undefined; + if (opts.structuredFile) { + try { + const filePath = resolve(opts.structuredFile); + const raw = readFileSync(filePath, 'utf-8'); + structuredOutput = JSON.parse(raw); + } catch (err) { + let msg: string; + if (err instanceof Error && 'code' in err && (err as NodeJS.ErrnoException).code === 'ENOENT') { + msg = `File not found: ${opts.structuredFile}`; + } else { + msg = `Invalid JSON in ${opts.structuredFile}: ${err instanceof Error ? err.message : 'parse error'}`; + } + outputError({ message: msg, code: 'invalid_schema' }, { json: globalOpts.json }); + return; + } + } else if (opts.structured) { + try { + structuredOutput = JSON.parse(opts.structured); + } catch { + outputError( + { message: 'Invalid JSON for --structured schema. Tip: use --structured-file to read from a file instead.', code: 'invalid_schema' }, + { json: globalOpts.json }, + ); + return; + } + } + const resolved = requireApiKey(globalOpts); const client = new ValyuClient(resolved.key); const spinner = createSpinner('Creating research task...', globalOpts.quiet); @@ -80,6 +127,7 @@ ${pc.dim('Examples:')} query, mode: opts.mode, outputFormats: formats, + structuredOutput, }); if (error) { @@ -103,6 +151,9 @@ ${pc.dim('Examples:')} console.log(` ${pc.bold('Mode:')} ${task.mode ?? opts.mode}`); console.log(` ${pc.bold('Status:')} ${colorStatus(String(task.status))}`); console.log(` ${pc.bold('PDF:')} ${formats.includes('pdf') ? pc.green('yes') : pc.dim('no')}`); + if (structuredOutput) { + console.log(` ${pc.bold('Schema:')} ${pc.green('yes')} (structured output enabled)`); + } console.log(''); console.log(` ${pc.dim('Watch:')} valyu deepresearch watch ${id}`); console.log(` ${pc.dim('Status:')} valyu deepresearch status ${id}`); @@ -758,6 +809,18 @@ function renderResearchStatus(status: ResearchStatus): void { } if (status.status === 'completed') { + // Structured output + if (status.structured_output && typeof status.structured_output === 'object') { + console.log(''); + console.log(` ${pc.bold('Structured Output:')}`); + console.log(SEPARATOR); + console.log(''); + const formatted = JSON.stringify(status.structured_output, null, 2); + for (const line of formatted.split('\n')) { + console.log(` ${line}`); + } + } + // Output - render full report if (status.output && typeof status.output === 'string') { console.log(''); diff --git a/src/lib/client.ts b/src/lib/client.ts index 7cc38cd..3f5ed4a 100644 --- a/src/lib/client.ts +++ b/src/lib/client.ts @@ -342,11 +342,13 @@ export class ValyuClient { mode?: string; outputFormats?: string[]; deliverables?: string[]; + structuredOutput?: Record; }): Promise<{ data: ResearchTask | null; error: ValyuApiError | null }> { const body: Record = { query: params.query, mode: params.mode ?? 'fast', output_formats: params.outputFormats ?? ['markdown', 'pdf'], + structured_output: params.structuredOutput, }; if (params.deliverables?.length) { body.deliverables = params.deliverables; @@ -560,6 +562,7 @@ export interface ResearchStatus { mode?: string; output?: string; output_type?: string; + structured_output?: Record; pdf_url?: string; images?: Array<{ image_id: string; title: string; image_url: string }>; deliverables?: Array<{ diff --git a/src/lib/version.ts b/src/lib/version.ts index 09d2951..64c3509 100644 --- a/src/lib/version.ts +++ b/src/lib/version.ts @@ -1,2 +1,2 @@ -export const VERSION = '1.0.1'; +export const VERSION = '1.0.2'; export const PACKAGE_NAME = '@valyu/cli';