From 753767da74b8c993f4d413a996d22d9456373c2d Mon Sep 17 00:00:00 2001 From: yorkeccak Date: Tue, 23 Jun 2026 22:08:31 +0100 Subject: [PATCH] Add workflows command for reusable deep research templates Workflows are versioned, templated deep research starting points: a saved prompt, research strategy, report format, deliverables, and recommended mode with typed variables. Fill in the variables and run one to start a normal deep research task - same auth, billing, and lifecycle. New `valyu workflows` command: - list / get / versions discover curated and org-owned templates - preview resolve a template against params (no credits spent) - run run a template, with optional mode/version overrides and --watch - create / update / delete manage org-owned templates from JSON definitions Adds the matching client methods and types, a SKILL reference, README section, and client tests. Bumps the CLI to 1.1.0. --- README.md | 32 +- package.json | 2 +- skills/valyu-cli/SKILL.md | 11 +- skills/valyu-cli/references/workflows.md | 184 ++++++ src/__tests__/client.test.ts | 117 ++++ src/cli.ts | 7 + src/commands/deepresearch/index.ts | 2 +- src/commands/workflows/index.ts | 756 +++++++++++++++++++++++ src/lib/client.ts | 200 +++++- src/lib/version.ts | 2 +- 10 files changed, 1307 insertions(+), 6 deletions(-) create mode 100644 skills/valyu-cli/references/workflows.md create mode 100644 src/commands/workflows/index.ts diff --git a/README.md b/README.md index 3f23889..0a9af61 100644 --- a/README.md +++ b/README.md @@ -125,7 +125,37 @@ Research modes: | `fast` | ~5 min | Quick lookups, simple questions | | `standard` | ~10-20 min | Balanced research (default) | | `heavy` | ~60 min | In-depth analysis, long reports | -| `max` | ~90 min | Maximum depth and quality | +| `max` | up to ~2 hrs | Maximum depth and quality | + +### `valyu workflows` + +Reusable, versioned deep research templates. A workflow bundles a prompt, research +strategy, report format, deliverables, and recommended mode with typed `{variables}`. +Fill in the variables and run it - the template expands into a normal deep research task. + +Curated Valyu workflows (e.g. an IC memo, a drug competitive landscape, a company +profile) are available to everyone; workflows you create are private to your org. + +```bash +# Browse curated templates +valyu workflows list --scope valyu +valyu workflows list --vertical investment-banking + +# Inspect a template and its variables +valyu workflows get ib-company-profile + +# Preview the resolved prompt without spending credits +valyu workflows preview ib-company-profile --param company="NVIDIA (NVDA)" + +# Run it (starts a deep research task) +valyu workflows run ib-company-profile --param company="NVIDIA (NVDA)" --watch +valyu workflows run ib-company-profile -P company="Apple" -m heavy + +# Manage your own org workflows +valyu workflows create --file workflow.json +valyu workflows update my-flow --file patch.json +valyu workflows delete my-flow +``` ### `valyu sources` diff --git a/package.json b/package.json index ff2db18..ab29fbd 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@valyu/cli", - "version": "1.0.8", + "version": "1.1.0", "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 312b860..6f89312 100644 --- a/skills/valyu-cli/SKILL.md +++ b/skills/valyu-cli/SKILL.md @@ -12,7 +12,7 @@ description: > license: MIT metadata: author: valyu - version: "1.0.8" + version: "1.1.0" homepage: https://valyu.ai source: https://github.com/valyuAI/valyu-cli inputs: @@ -24,6 +24,7 @@ references: - references/answer.md - references/contents.md - references/deepresearch.md + - references/workflows.md - references/auth.md - references/error-codes.md --- @@ -43,6 +44,11 @@ valyu │ ├── create [options] # with steering, deliverables, HITL, structured output │ ├── list / status / watch │ ├── update / cancel / delete / share +├── workflows # reusable, versioned deepresearch templates +│ ├── list / get / versions # discover curated (Valyu) + org templates +│ ├── preview [--param k=v] # resolve template, no credits spent +│ ├── run [--param k=v] # run template -> starts a deepresearch task +│ ├── create / update / delete # manage your org's templates (file-based) ├── batch # parallel deepresearch jobs with shared config ├── sources # list available proprietary data sources ├── login / logout / whoami # auth @@ -193,6 +199,8 @@ valyu deepresearch create \ | Find papers / filings / trials / patents on a topic | `valyu search "..."` | | Pull clean text from a URL (or extract structured data) | `valyu contents [--structured]` | | Comprehensive research + cited report (± deliverables) | `valyu deepresearch create "..."` | +| Repeatable research from a saved template (e.g. company profile, IC memo) | `valyu workflows run --param key=value` | +| Discover available research templates | `valyu workflows list` | | Many parallel deepresearch tasks with shared config | `valyu batch create ...` | | Discover available proprietary data sources | `valyu sources list` | | Upgrade the CLI itself | `valyu upgrade` | @@ -235,6 +243,7 @@ valyu deepresearch create \ ## When to load each reference - **Deep research / deliverables / HITL / structured output** → [references/deepresearch.md](references/deepresearch.md) +- **Workflows (reusable research templates)** → [references/workflows.md](references/workflows.md) - **Search (web / paper / finance / sec / bio / patent / economics / news)** → [references/search.md](references/search.md) - **AI answer (`answer`)** → [references/answer.md](references/answer.md) - **URL content extraction (`contents`)** → [references/contents.md](references/contents.md) diff --git a/skills/valyu-cli/references/workflows.md b/skills/valyu-cli/references/workflows.md new file mode 100644 index 0000000..c3fa861 --- /dev/null +++ b/skills/valyu-cli/references/workflows.md @@ -0,0 +1,184 @@ +# valyu workflows + +Reusable, versioned deep research templates. A workflow bundles a prompt template (with typed `{variables}`), a research strategy, a report format, deliverables, and a recommended mode. You fill in the variables and run it — the template expands into a normal `deepresearch` task with the same auth, billing, and lifecycle. + +Two scopes: + +- **Valyu (curated)** — read-only templates available to every org (`is_valyu: true`). 44+ across verticals: investment-banking, private-equity, hedge-funds, consulting, life-sciences, legal-regulatory, sales-intelligence, supply-chain. +- **Org** — templates your organization creates and versions, private to your org. + +A workflow is a *template*; running one creates a single deep research **task**. This is orthogonal to `batch` (which groups many tasks). + +## Subcommand tree + +``` +valyu workflows +├── list [options] # discover curated + org templates +├── get [--version N] # full template detail (variables, prompt, deliverables) +├── versions # version history +├── preview [--param k=v ...] # resolve the template against params (no credits) +├── run [--param k=v ...] # run it → starts a deepresearch task +├── create --file # create an org workflow (JSON definition) +├── update --file # edit metadata / publish a new version +└── delete [-y] # delete an org workflow +``` + +## Quick start + +```bash +# Browse curated templates +valyu workflows list --scope valyu +valyu workflows list --vertical investment-banking + +# Inspect what a template needs (its variables) +valyu workflows get ib-company-profile + +# Dry-run: resolve the prompt without spending credits +valyu workflows preview ib-company-profile --param company="NVIDIA (NVDA)" + +# Run it and wait for the report +valyu workflows run ib-company-profile --param company="NVIDIA (NVDA)" --watch +``` + +## Passing parameters + +Workflow variables are filled with `--param key=value` (alias `-P`), repeatable. Values that look like numbers or `true`/`false` are coerced; everything else stays a string. + +```bash +valyu workflows run pe-investment-memo \ + -P company="Databricks" \ + -P thesis="Best independent data + AI platform as the lakehouse consolidates" \ + --watch +``` + +For values with awkward punctuation or for many params, use a JSON file (or stdin): + +```bash +valyu workflows run my-org/quarterly-review --params-file params.json --watch +echo '{"company":"Stripe"}' | valyu workflows preview ib-company-profile --params-file - +``` + +`params.json`: + +```json +{ "company": "NVIDIA (NVDA)", "peers": ["AMD", "Intel"] } +``` + +Required variables are marked with `*` in `valyu workflows get `. Omitting a required variable returns `validation_failed`. + +## Running + +`run` overrides are optional — by default the template's own recommended mode and output formats apply. + +| Flag | Purpose | +|------|---------| +| `-P, --param ` | Template parameter (repeatable) | +| `--params-file ` | JSON object of params (`-` for stdin) | +| `--version ` | Pin a workflow version (default: current) | +| `-m, --mode ` | Override mode: `fast`, `standard`, `heavy`, `max` | +| `-w, --watch` | Block until the task completes and print the result | +| `--webhook-url ` | HMAC-signed completion webhook | +| `--alert-email ` | Email notification on completion | +| `--alert-email-url ` | Custom report link for the alert email (must include `{id}`) | + +`run` returns a normal deep research task. Track it with `valyu deepresearch watch ` / `status `, exactly like a freeform task. + +```bash +# Pinned version, heavy mode, JSON for scripting +ID=$(valyu workflows run ib-company-profile -P company="Apple" --version 1 -m heavy -q | jq -r .deepresearch_id) +valyu deepresearch watch "$ID" +``` + +## Listing and filtering + +```bash +valyu workflows list # everything visible to you +valyu workflows list --scope valyu # curated only +valyu workflows list --scope org # your org only +valyu workflows list --vertical hedge-funds +valyu workflows list --search "company profile" +valyu workflows list --tag screening --expand # include template fields +``` + +## Creating an org workflow + +File-based. Required top-level fields: `slug`, `title`, `version`. The `version` object holds the template. Any `{variable}` used in `prompt` / `strategy` / `report_format` / deliverable descriptions must be declared in `variables`. + +```bash +valyu workflows create --file workflow.json +cat workflow.json | valyu workflows create --file - +``` + +`workflow.json`: + +```json +{ + "slug": "quarterly-company-profile", + "title": "Quarterly Company Profile", + "subtitle": "Standardised profile for IC screening", + "vertical": "investment-banking", + "tags": ["screening", "profile"], + "version": { + "prompt": "Build a company profile for {company}. Cover business, financials, competitors, and risks.", + "strategy": "Prioritise filings and earnings calls over press coverage.", + "report_format": "2-page analyst brief with a peer comparison table.", + "variables": [ + { "key": "company", "label": "Company", "type": "text", "required": true, + "placeholder": "Databricks", "examples": ["Stripe", "Ramp"] } + ], + "deliverables": [ + { "type": "xlsx", "description": "Peer comparison workbook for {company}" } + ], + "tools": { "charts": true }, + "recommended_mode": "standard", + "estimated_time": "7-12 min", + "output_formats": ["markdown", "pdf"] + } +} +``` + +Variable types: `text`, `textarea`, `number`, `date`, `enum`. For `enum`, set `validation.enum`. Deliverable `type`: `csv`, `xlsx`, `pptx`, `docx`, `pdf`. Limits: ≤50 variables, ≤20 deliverables, ≤20 tags, template fields ≤64KB. Default org quota: 100 workflows. + +## Updating and versioning + +`update` takes a patch file. It can change metadata (`title`, `subtitle`, `description`, `vertical`, `tags`) and/or publish a new immutable version. When the patch includes a `version` object, `version.changelog` is **required**. Pass `"set_current": false` to add a version without promoting it. + +```bash +# Metadata-only edit +echo '{"title":"Quarterly Company Profile (v2)"}' | valyu workflows update quarterly-company-profile --file - + +# Publish a new version +valyu workflows update quarterly-company-profile --file new-version.json +``` + +`new-version.json`: + +```json +{ + "version": { + "prompt": "Build a company profile for {company}, including a competitive landscape section.", + "strategy": "Prioritise filings and earnings calls.", + "report_format": "2-page analyst brief.", + "variables": [{ "key": "company", "label": "Company", "type": "text", "required": true }], + "changelog": "Added a competitive landscape section." + } +} +``` + +Curated Valyu workflows are read-only: `update` / `delete` on them returns `cannot_edit_valyu_workflow` / `cannot_delete_valyu_workflow` (403). + +## Agent protocol + +```bash +# Discover, then run — JSON for scripting +valyu workflows list --scope valyu -q | jq -r '.workflows[] | "\(.slug)\t\(.title)"' +valyu workflows get ib-company-profile -q | jq '.variables[] | {key, required, type}' + +# Resolve before running to confirm the prompt (free) +valyu workflows preview ib-company-profile -P company="NVIDIA (NVDA)" -q | jq -r .resolved.input + +# Run and capture the task id +valyu workflows run ib-company-profile -P company="NVIDIA (NVDA)" -q | jq -r .deepresearch_id +``` + +Errors return `{"error":{"message":"...","code":"..."}}` with exit code 1. Common codes: `validation_failed` (a required/typed param is wrong), `workflow_not_found`, `cannot_edit_valyu_workflow`, `changelog_required`, `workflow_quota_exceeded`. diff --git a/src/__tests__/client.test.ts b/src/__tests__/client.test.ts index 9922929..c942afa 100644 --- a/src/__tests__/client.test.ts +++ b/src/__tests__/client.test.ts @@ -154,6 +154,123 @@ describe('ValyuClient.contents', () => { }); }); +describe('ValyuClient workflows', () => { + let client: ValyuClient; + + beforeEach(() => { + client = new ValyuClient('test-key-1234567890'); + mockFetch.mockReset(); + }); + + it('listWorkflows builds query string from filters', async () => { + mockFetch.mockResolvedValueOnce( + makeResponse({ workflows: [], total: 0, next_cursor: null, verticals: [] }), + ); + + await client.listWorkflows({ + vertical: 'investment-banking', + scope: 'valyu', + query: 'company profile', + tags: ['ib', 'profile'], + limit: 25, + expand: true, + }); + + const url = mockFetch.mock.calls[0][0] as string; + expect(url).toContain('/v1/workflows?'); + expect(url).toContain('vertical=investment-banking'); + expect(url).toContain('scope=valyu'); + expect(url).toContain('q=company+profile'); + expect(url).toContain('tags=ib%2Cprofile'); + expect(url).toContain('limit=25'); + expect(url).toContain('expand=true'); + }); + + it('getWorkflow appends version query when provided', async () => { + mockFetch.mockResolvedValueOnce(makeResponse({ slug: 'ib-company-profile', version: 2 })); + + await client.getWorkflow('ib-company-profile', 2); + + const url = mockFetch.mock.calls[0][0] as string; + expect(url).toContain('/v1/workflows/ib-company-profile?'); + expect(url).toContain('version=2'); + }); + + it('getWorkflow url-encodes org-prefixed slugs', async () => { + mockFetch.mockResolvedValueOnce(makeResponse({ slug: 'my-org/flow', version: 1 })); + + await client.getWorkflow('my-org/flow'); + + const url = mockFetch.mock.calls[0][0] as string; + expect(url).toContain('/v1/workflows/my-org%2Fflow'); + }); + + it('previewWorkflow posts workflow_params and version', async () => { + mockFetch.mockResolvedValueOnce( + makeResponse({ workflow: { slug: 's', version: 1 }, resolved: {}, estimated_credits: null }), + ); + + await client.previewWorkflow('ib-company-profile', { company: 'NVIDIA' }, 1); + + const [url, init] = mockFetch.mock.calls[0]; + expect(url).toBe('https://api.valyu.ai/v1/workflows/ib-company-profile/preview'); + expect(init.method).toBe('POST'); + const body = JSON.parse(init.body); + expect(body.workflow_params).toEqual({ company: 'NVIDIA' }); + expect(body.workflow_version).toBe(1); + }); + + it('runWorkflow sends workflow_id and only includes provided overrides', async () => { + mockFetch.mockResolvedValueOnce(makeResponse({ deepresearch_id: 'dr_1', status: 'running' })); + + await client.runWorkflow({ + slug: 'ib-company-profile', + workflowParams: { company: 'Stripe' }, + mode: 'fast', + }); + + const [url, init] = mockFetch.mock.calls[0]; + expect(url).toBe('https://api.valyu.ai/v1/deepresearch/tasks'); + const body = JSON.parse(init.body); + expect(body.workflow_id).toBe('ib-company-profile'); + expect(body.workflow_params).toEqual({ company: 'Stripe' }); + expect(body.mode).toBe('fast'); + // No params beyond what was passed; version/webhook left undefined and stripped + expect('workflow_version' in body).toBe(false); + expect('webhook_url' in body).toBe(false); + }); + + it('runWorkflow omits empty workflow_params so the template applies', async () => { + mockFetch.mockResolvedValueOnce(makeResponse({ deepresearch_id: 'dr_1', status: 'running' })); + + await client.runWorkflow({ slug: 'ib-company-profile', workflowParams: {} }); + + const body = JSON.parse(mockFetch.mock.calls[0][1].body); + expect(body.workflow_id).toBe('ib-company-profile'); + expect('workflow_params' in body).toBe(false); + expect('mode' in body).toBe(false); + }); + + it('updateWorkflow uses the PATCH method', async () => { + mockFetch.mockResolvedValueOnce(makeResponse({ slug: 'my-flow', version: 2 })); + + await client.updateWorkflow('my-flow', { version: { changelog: 'tweak' } }); + + const init = mockFetch.mock.calls[0][1]; + expect(init.method).toBe('PATCH'); + }); + + it('deleteWorkflow uses the DELETE method', async () => { + mockFetch.mockResolvedValueOnce(makeResponse({ slug: 'my-flow', deleted_at: 'now' })); + + await client.deleteWorkflow('my-flow'); + + const [url, init] = mockFetch.mock.calls[0]; + expect(url).toBe('https://api.valyu.ai/v1/workflows/my-flow'); + expect(init.method).toBe('DELETE'); + }); +}); + describe('ValyuClient.validateKey', () => { let client: ValyuClient; diff --git a/src/cli.ts b/src/cli.ts index 1134ebf..69f4830 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -6,6 +6,7 @@ import { searchCommand } from './commands/search/index.js'; import { answerCommand } from './commands/answer/index.js'; import { contentsCommand } from './commands/contents/index.js'; import { deepresearchCommand } from './commands/deepresearch/index.js'; +import { workflowsCommand } from './commands/workflows/index.js'; import { batchCommand } from './commands/batch/index.js'; import { whoamiCommand } from './commands/whoami.js'; import { doctorCommand } from './commands/doctor.js'; @@ -64,6 +65,11 @@ ${pc.dim('Examples:')} - Start deep research ${pc.cyan('$ valyu deepresearch create "Global AI infrastructure investment trends" --watch')} + +- Run a workflow (reusable research template) + + ${pc.cyan('$ valyu workflows list --scope valyu')} + ${pc.cyan('$ valyu workflows run ib-company-profile --param company="NVIDIA (NVDA)" --watch')} `, ) .action(() => { @@ -89,6 +95,7 @@ ${pc.dim('Examples:')} .addCommand(answerCommand) .addCommand(contentsCommand) .addCommand(deepresearchCommand) + .addCommand(workflowsCommand) .addCommand(batchCommand) .addCommand(sourcesCommand) .addCommand(whoamiCommand) diff --git a/src/commands/deepresearch/index.ts b/src/commands/deepresearch/index.ts index e8f5105..13d6211 100644 --- a/src/commands/deepresearch/index.ts +++ b/src/commands/deepresearch/index.ts @@ -1202,7 +1202,7 @@ async function handleInteraction( // ─── watch loop ───────────────────────────────────────────────────────────── -async function watchResearch( +export async function watchResearch( client: ValyuClient, id: string, globalOpts: GlobalOpts, diff --git a/src/commands/workflows/index.ts b/src/commands/workflows/index.ts new file mode 100644 index 0000000..764cb72 --- /dev/null +++ b/src/commands/workflows/index.ts @@ -0,0 +1,756 @@ +import { readFileSync } from 'node:fs'; +import { resolve } from 'node:path'; +import * as p from '@clack/prompts'; +import { Command } from '@commander-js/extra-typings'; +import pc from 'picocolors'; +import type { + GlobalOpts, + Workflow, + WorkflowDeliverable, + WorkflowDetail, + WorkflowTools, + WorkflowVariable, +} from '../../lib/client.js'; +import { requireApiKey, ValyuClient } from '../../lib/client.js'; +import { relTime } from '../../lib/format.js'; +import { outputError, outputResult } from '../../lib/output.js'; +import { createSpinner } from '../../lib/spinner.js'; +import { readStdin } from '../../lib/stdin.js'; +import { watchResearch } from '../deepresearch/index.js'; + +const MODES = ['fast', 'standard', 'heavy', 'max'] as const; +type Mode = (typeof MODES)[number]; + +const SCOPES = ['valyu', 'org', 'all'] as const; + +const SEPARATOR = pc.dim(' ' + '─'.repeat(50)); + +const collect = (v: string, prev: string[] = []): string[] => [...prev, v]; + +// Parse repeatable --param key=value pairs. Values are coerced to boolean / +// number when they look like one, matching how the template variables are typed. +// Use --params-file for values that must stay strings or carry punctuation. +function parseParams(pairs: string[]): Record { + const out: Record = {}; + for (const kv of pairs) { + const eq = kv.indexOf('='); + if (eq <= 0) throw new Error(`Invalid --param '${kv}'. Expected format: key=value`); + const key = kv.slice(0, eq).trim(); + const raw = kv.slice(eq + 1); + if (!key) throw new Error(`Invalid --param '${kv}'. Empty key.`); + if (raw === 'true') out[key] = true; + else if (raw === 'false') out[key] = false; + else if (raw !== '' && !Number.isNaN(Number(raw))) out[key] = Number(raw); + else out[key] = raw; + } + return out; +} + +function readJsonFile(filePath: string, label: string): T { + try { + const raw = readFileSync(resolve(filePath), 'utf-8'); + return JSON.parse(raw) as T; + } catch (err) { + if (err instanceof Error && 'code' in err && (err as NodeJS.ErrnoException).code === 'ENOENT') { + throw new Error(`File not found: ${filePath}`); + } + throw new Error( + `Invalid JSON in ${label} file ${filePath}: ${err instanceof Error ? err.message : 'parse error'}`, + ); + } +} + +// Merge --param pairs and --params-file (file first, flags override). +async function resolveParams( + paramPairs: string[], + paramsFile: string | undefined, +): Promise> { + let params: Record = {}; + if (paramsFile) { + const raw = paramsFile === '-' ? await readStdin() : undefined; + const loaded = + paramsFile === '-' + ? (JSON.parse(raw || '{}') as unknown) + : readJsonFile(paramsFile, 'params'); + if (typeof loaded !== 'object' || loaded === null || Array.isArray(loaded)) { + throw new Error('--params-file must contain a JSON object of { key: value }'); + } + params = loaded as Record; + } + if (paramPairs.length > 0) { + params = { ...params, ...parseParams(paramPairs) }; + } + return params; +} + +function scopeLabel(w: { is_valyu: boolean }): string { + return w.is_valyu ? pc.cyan('valyu') : pc.magenta('org'); +} + +// ─── list ───────────────────────────────────────────────────────────────── + +const listCmd = new Command('list') + .description('List available workflows (curated Valyu templates + your org)') + .option('--vertical ', 'Filter by vertical (e.g. investment-banking, life-sciences)') + .option('--scope ', `Filter by scope: ${SCOPES.join(', ')} (default: all)`) + .option('--search ', 'Free-text search over title / description') + .option('--tag ', 'Filter by tag (repeatable)', collect, [] as string[]) + .option('-n, --limit ', 'Max results (default 50, max 100)') + .option('--expand', 'Include template fields (prompt, strategy, report format)') + .addHelpText( + 'after', + ` +${pc.dim('Examples:')} + + ${pc.dim('$ valyu workflows list')} + ${pc.dim('$ valyu workflows list --vertical investment-banking')} + ${pc.dim('$ valyu workflows list --scope valyu --search "company profile"')} +`, + ) + .action(async (opts, cmd) => { + const globalOpts = cmd.optsWithGlobals() as GlobalOpts; + const fail = (message: string, code: string) => + outputError({ message, code }, { json: globalOpts.json }); + + if (opts.scope && !SCOPES.includes(opts.scope as (typeof SCOPES)[number])) { + fail(`Invalid --scope '${opts.scope}'. Valid: ${SCOPES.join(', ')}`, 'invalid_option'); + return; + } + const limit = opts.limit ? Number(opts.limit) : undefined; + if (limit != null && (!Number.isInteger(limit) || limit < 1)) { + fail('--limit must be a positive integer', 'invalid_option'); + return; + } + + const resolved = requireApiKey(globalOpts); + const client = new ValyuClient(resolved.key); + const spinner = createSpinner('Loading workflows...', globalOpts.quiet); + + const { data, error } = await client.listWorkflows({ + vertical: opts.vertical, + scope: opts.scope, + query: opts.search, + tags: opts.tag.length > 0 ? opts.tag : undefined, + limit, + expand: opts.expand, + }); + + if (error) { + spinner.fail('Failed to list workflows'); + outputError({ message: error.message, code: error.code }, { json: globalOpts.json }); + return; + } + + const workflows = data?.workflows ?? []; + spinner.stop(`${workflows.length} workflow${workflows.length === 1 ? '' : 's'}`); + + if (globalOpts.json || !process.stdout.isTTY) { + outputResult(data, { json: true }); + return; + } + + if (workflows.length === 0) { + console.log( + `\n ${pc.dim('No workflows found. Browse curated templates with:')} valyu workflows list --scope valyu\n`, + ); + return; + } + + console.log(''); + for (const w of workflows) { + const badge = scopeLabel(w); + const vertical = w.vertical ? pc.dim(w.vertical) : pc.dim('-'); + console.log(` ${pc.bold(w.slug)} ${badge} ${pc.dim(`v${w.version}`)}`); + console.log(` ${w.title}`); + console.log( + ` ${vertical} ${pc.dim('·')} ${pc.dim(w.recommended_mode)}${w.estimated_time ? ` ${pc.dim('·')} ${pc.dim(w.estimated_time)}` : ''}`, + ); + console.log(''); + } + console.log( + ` ${pc.dim('Inspect:')} valyu workflows get ${pc.dim('Run:')} valyu workflows run --param key=value\n`, + ); + }); + +// ─── get ──────────────────────────────────────────────────────────────────── + +function renderVariables(variables: WorkflowVariable[]): void { + if (!variables.length) return; + console.log(` ${pc.bold('Variables:')}`); + for (const v of variables) { + const req = v.required ? pc.red('*') : pc.dim('?'); + const type = pc.dim(`(${v.type ?? 'text'})`); + console.log(` ${req} ${pc.cyan(v.key)} ${type} ${v.label}`); + if (v.help) console.log(` ${pc.dim(v.help)}`); + if (v.validation?.enum?.length) { + console.log(` ${pc.dim('one of:')} ${pc.dim(v.validation.enum.join(', '))}`); + } + } + console.log(''); +} + +function renderDeliverables(deliverables: WorkflowDeliverable[]): void { + if (!deliverables.length) return; + console.log(` ${pc.bold('Deliverables:')}`); + for (const d of deliverables) { + console.log(` ${pc.dim('·')} ${d.type.toUpperCase()} ${d.description}`); + } + console.log(''); +} + +function renderTools(tools: WorkflowTools | undefined): void { + if (!tools) return; + const enabled = Object.entries(tools) + .filter(([, on]) => on) + .map(([k]) => k); + if (!enabled.length) return; + console.log(` ${pc.bold('Tools:')} ${enabled.join(', ')}`); +} + +function renderTemplateSection(label: string, text: string | undefined): void { + if (!text) return; + console.log(''); + console.log(` ${pc.bold(label)}`); + console.log(SEPARATOR); + for (const line of text.split('\n')) { + console.log(` ${pc.dim(line)}`); + } +} + +function renderWorkflowDetail(w: WorkflowDetail): void { + console.log(''); + console.log(` ${pc.bold(w.title)}`); + console.log(` ${pc.cyan(w.slug)} ${scopeLabel(w)} ${pc.dim(`v${w.version}`)}`); + if (w.subtitle) console.log(` ${pc.dim(w.subtitle)}`); + console.log(''); + if (w.vertical) console.log(` ${pc.bold('Vertical:')} ${w.vertical}`); + console.log(` ${pc.bold('Mode:')} ${w.recommended_mode}`); + if (w.estimated_time) console.log(` ${pc.bold('Est. time:')} ${w.estimated_time}`); + if (w.tags?.length) console.log(` ${pc.bold('Tags:')} ${pc.dim(w.tags.join(', '))}`); + renderTools(w.tools); + if (w.description) { + console.log(''); + console.log(` ${w.description}`); + } + console.log(''); + renderVariables(w.variables ?? []); + renderDeliverables(w.deliverables ?? []); + renderTemplateSection('Prompt', w.prompt); + renderTemplateSection('Research strategy', w.strategy); + renderTemplateSection('Report format', w.report_format); + console.log(''); + const example = + (w.variables ?? []).find((v) => v.required)?.key ?? (w.variables ?? [])[0]?.key ?? 'key'; + console.log(` ${pc.dim('Run:')} valyu workflows run ${w.slug} --param ${example}=value\n`); +} + +const getCmd = new Command('get') + .description('Show a workflow template in detail') + .argument('', 'Workflow slug') + .option('--version ', 'Specific version (default: current)') + .action(async (slug, opts, cmd) => { + const globalOpts = cmd.optsWithGlobals() as GlobalOpts; + const version = opts.version ? Number(opts.version) : undefined; + if (version != null && (!Number.isInteger(version) || version < 1)) { + outputError( + { message: '--version must be a positive integer', code: 'invalid_option' }, + { json: globalOpts.json }, + ); + return; + } + const resolved = requireApiKey(globalOpts); + const client = new ValyuClient(resolved.key); + const spinner = createSpinner('Loading workflow...', globalOpts.quiet); + + const { data, error } = await client.getWorkflow(slug, version); + + if (error) { + spinner.fail('Failed to load workflow'); + outputError({ message: error.message, code: error.code }, { json: globalOpts.json }); + return; + } + + spinner.stop(`${data!.slug}`); + + if (globalOpts.json || !process.stdout.isTTY) { + outputResult(data, { json: true }); + return; + } + + renderWorkflowDetail(data!); + }); + +// ─── versions ───────────────────────────────────────────────────────────── + +const versionsCmd = new Command('versions') + .description('List the version history of a workflow') + .argument('', 'Workflow slug') + .action(async (slug, _opts, cmd) => { + const globalOpts = cmd.optsWithGlobals() as GlobalOpts; + const resolved = requireApiKey(globalOpts); + const client = new ValyuClient(resolved.key); + const spinner = createSpinner('Loading versions...', globalOpts.quiet); + + const { data, error } = await client.listWorkflowVersions(slug); + + if (error) { + spinner.fail('Failed to load versions'); + outputError({ message: error.message, code: error.code }, { json: globalOpts.json }); + return; + } + + const versions = data?.versions ?? []; + spinner.stop(`${versions.length} version${versions.length === 1 ? '' : 's'}`); + + if (globalOpts.json || !process.stdout.isTTY) { + outputResult(data, { json: true }); + return; + } + + console.log(''); + console.log(` ${pc.bold(`Versions of ${slug}`)}`); + console.log(SEPARATOR); + for (const v of versions) { + const marker = v.is_current ? pc.green('✔ current') : pc.dim(' -'); + const changelog = v.changelog ? ` ${v.changelog}` : ''; + console.log( + ` ${pc.cyan(`v${v.version}`)} ${marker} ${pc.dim(v.recommended_mode)} ${pc.dim(relTime(v.created_at))}${changelog}`, + ); + } + console.log(''); + }); + +// ─── preview ──────────────────────────────────────────────────────────────── + +const previewCmd = new Command('preview') + .description('Resolve a workflow template against params without running it (no credits)') + .argument('', 'Workflow slug') + .option( + '-P, --param ', + 'Template parameter in key=value form (repeatable)', + collect, + [] as string[], + ) + .option('--params-file ', 'JSON file of { key: value } params (use "-" for stdin)') + .option('--version ', 'Specific version (default: current)') + .addHelpText( + 'after', + ` +${pc.dim('Examples:')} + + ${pc.dim('$ valyu workflows preview ib-company-profile --param company="NVIDIA (NVDA)"')} + ${pc.dim('$ valyu workflows preview ib-company-profile --params-file params.json')} +`, + ) + .action(async (slug, opts, cmd) => { + const globalOpts = cmd.optsWithGlobals() as GlobalOpts; + const fail = (message: string, code: string) => + outputError({ message, code }, { json: globalOpts.json }); + + const version = opts.version ? Number(opts.version) : undefined; + if (version != null && (!Number.isInteger(version) || version < 1)) { + fail('--version must be a positive integer', 'invalid_option'); + return; + } + + let params: Record; + try { + params = await resolveParams(opts.param, opts.paramsFile); + } catch (err) { + fail(err instanceof Error ? err.message : 'Invalid params', 'invalid_option'); + return; + } + + const resolved = requireApiKey(globalOpts); + const client = new ValyuClient(resolved.key); + const spinner = createSpinner('Resolving template...', globalOpts.quiet); + + const { data, error } = await client.previewWorkflow(slug, params, version); + + if (error) { + spinner.fail('Preview failed'); + outputError({ message: error.message, code: error.code }, { json: globalOpts.json }); + return; + } + + spinner.stop('Resolved'); + + if (globalOpts.json || !process.stdout.isTTY) { + outputResult(data, { json: true }); + return; + } + + const r = data!.resolved; + console.log(''); + console.log( + ` ${pc.bold('Workflow:')} ${pc.cyan(data!.workflow.slug)} ${pc.dim(`v${data!.workflow.version}`)}`, + ); + console.log(` ${pc.bold('Mode:')} ${r.mode}`); + renderDeliverables(r.deliverables ?? []); + renderTemplateSection('Resolved prompt', r.input); + renderTemplateSection('Research strategy', r.research_strategy); + renderTemplateSection('Report format', r.report_format); + console.log(''); + console.log( + ` ${pc.dim('Run it:')} valyu workflows run ${data!.workflow.slug} ${opts.param.map((p) => `--param ${p}`).join(' ')}\n`, + ); + }); + +// ─── run ────────────────────────────────────────────────────────────────── + +const runCmd = new Command('run') + .description('Run a workflow - resolves the template and starts a deep research task') + .argument('', 'Workflow slug') + .option( + '-P, --param ', + 'Template parameter in key=value form (repeatable)', + collect, + [] as string[], + ) + .option('--params-file ', 'JSON file of { key: value } params (use "-" for stdin)') + .option('--version ', 'Pin a workflow version (default: current)') + .option('-m, --mode ', `Override the template's recommended mode: ${MODES.join(', ')}`) + .option('-w, --watch', 'Wait for completion and display the result') + .option('--webhook-url ', 'HTTPS URL to receive completion webhook (HMAC-signed)') + .option( + '--alert-email ', + 'Email to notify on completion (must belong to your organization)', + ) + .option( + '--alert-email-url ', + 'Custom report link for the alert email. Must include {id} placeholder', + ) + .addHelpText( + 'after', + ` +${pc.dim('Examples:')} + + ${pc.dim('$ valyu workflows run ib-company-profile --param company="NVIDIA (NVDA)" --watch')} + ${pc.dim('$ valyu workflows run ib-company-profile -P company="Apple" -m heavy')} + ${pc.dim('$ valyu workflows run my-org/quarterly-review --params-file params.json --watch')} +`, + ) + .action(async (slug, opts, cmd) => { + const globalOpts = cmd.optsWithGlobals() as GlobalOpts; + const fail = (message: string, code: string) => + outputError({ message, code }, { json: globalOpts.json }); + + if (opts.mode && !MODES.includes(opts.mode as Mode)) { + fail(`Invalid mode '${opts.mode}'. Must be one of: ${MODES.join(', ')}`, 'invalid_mode'); + return; + } + + const version = opts.version ? Number(opts.version) : undefined; + if (version != null && (!Number.isInteger(version) || version < 1)) { + fail('--version must be a positive integer', 'invalid_option'); + return; + } + + let params: Record; + try { + params = await resolveParams(opts.param, opts.paramsFile); + } catch (err) { + fail(err instanceof Error ? err.message : 'Invalid params', 'invalid_option'); + return; + } + + if (opts.alertEmailUrl && !opts.alertEmail) { + fail('--alert-email-url requires --alert-email', 'invalid_option'); + return; + } + + const alertEmailValue: string | { email: string; custom_url?: string } | undefined = + opts.alertEmail + ? opts.alertEmailUrl + ? { email: opts.alertEmail, custom_url: opts.alertEmailUrl } + : opts.alertEmail + : undefined; + + const resolved = requireApiKey(globalOpts); + const client = new ValyuClient(resolved.key); + const spinner = createSpinner('Starting workflow...', globalOpts.quiet); + + const { data, error } = await client.runWorkflow({ + slug, + workflowParams: params, + version, + mode: opts.mode, + webhookUrl: opts.webhookUrl, + alertEmail: alertEmailValue, + }); + + if (error) { + spinner.fail('Failed to start workflow'); + outputError({ message: error.message, code: error.code }, { json: globalOpts.json }); + return; + } + + const task = data! as unknown as Record; + const id = String(task.deepresearch_id ?? task.id ?? task.task_id ?? ''); + if (!id) { + spinner.fail('Workflow started but no task id was returned'); + outputError( + { message: 'Workflow run returned no task id', code: 'unexpected_response' }, + { json: globalOpts.json }, + ); + return; + } + spinner.stop(`Workflow started: ${pc.cyan(id)}`); + + if (!opts.watch) { + if (globalOpts.json || !process.stdout.isTTY) { + outputResult(task, { json: true }); + return; + } + // Mode is template-defined; only assert it when the API echoed it back. + const modeLabel = task.mode ?? opts.mode ?? pc.dim('template default'); + console.log(''); + console.log(` ${pc.bold('Task ID:')} ${pc.cyan(id)}`); + console.log(` ${pc.bold('Mode:')} ${modeLabel}`); + console.log(` ${pc.bold('Status:')} ${task.status ?? 'queued'}`); + console.log(''); + console.log(` ${pc.dim('Watch:')} valyu deepresearch watch ${id}`); + console.log(` ${pc.dim('Status:')} valyu deepresearch status ${id}`); + console.log(''); + return; + } + + await watchResearch(client, id, globalOpts); + }); + +// ─── create ───────────────────────────────────────────────────────────────── + +const createCmd = new Command('create') + .description('Create an org workflow from a JSON definition file') + .option('--file ', 'JSON file with the workflow definition (use "-" for stdin)') + .addHelpText( + 'after', + ` +${pc.dim('The file must contain the full create body. Required: slug, title, version.')} +${pc.dim('The version object holds the template (prompt, strategy, report_format) and variables.')} + +${pc.dim('Example file:')} + + ${pc.dim('{')} + ${pc.dim(' "slug": "quarterly-company-profile",')} + ${pc.dim(' "title": "Quarterly Company Profile",')} + ${pc.dim(' "vertical": "investment-banking",')} + ${pc.dim(' "version": {')} + ${pc.dim(' "prompt": "Produce a company profile for {company}.",')} + ${pc.dim(' "strategy": "Prioritise filings and earnings calls.",')} + ${pc.dim(' "report_format": "2-page analyst brief.",')} + ${pc.dim(' "variables": [{ "key": "company", "label": "Company", "type": "text", "required": true }],')} + ${pc.dim(' "recommended_mode": "standard",')} + ${pc.dim(' "output_formats": ["markdown", "pdf"]')} + ${pc.dim(' }')} + ${pc.dim('}')} + +${pc.dim('Examples:')} + + ${pc.dim('$ valyu workflows create --file workflow.json')} + ${pc.dim('$ cat workflow.json | valyu workflows create --file -')} +`, + ) + .action(async (opts, cmd) => { + const globalOpts = cmd.optsWithGlobals() as GlobalOpts; + const fail = (message: string, code: string) => + outputError({ message, code }, { json: globalOpts.json }); + + if (!opts.file) { + fail( + 'Provide a workflow definition with --file (or --file - for stdin)', + 'missing_file', + ); + return; + } + + let body: Record; + try { + if (opts.file === '-') { + const piped = await readStdin(); + if (!piped) { + fail('No workflow definition provided on stdin', 'missing_file'); + return; + } + body = JSON.parse(piped) as Record; + } else { + body = readJsonFile>(opts.file, 'workflow'); + } + } catch (err) { + fail( + err instanceof Error ? err.message : 'Failed to read workflow definition', + 'invalid_file', + ); + return; + } + + if (typeof body !== 'object' || body === null || Array.isArray(body)) { + fail('Workflow definition must be a JSON object', 'invalid_file'); + return; + } + for (const required of ['slug', 'title', 'version'] as const) { + if (!body[required]) { + fail(`Workflow definition is missing required field '${required}'`, 'invalid_file'); + return; + } + } + + const resolved = requireApiKey(globalOpts); + const client = new ValyuClient(resolved.key); + const spinner = createSpinner('Creating workflow...', globalOpts.quiet); + + const { data, error } = await client.createWorkflow(body); + + if (error) { + spinner.fail('Failed to create workflow'); + outputError({ message: error.message, code: error.code }, { json: globalOpts.json }); + return; + } + + spinner.stop(`Workflow created: ${pc.cyan(data!.slug)} ${pc.dim(`v${data!.version}`)}`); + + if (globalOpts.json || !process.stdout.isTTY) { + outputResult(data, { json: true }); + return; + } + console.log(''); + console.log(` ${pc.dim('Inspect:')} valyu workflows get ${data!.slug}`); + console.log(` ${pc.dim('Run:')} valyu workflows run ${data!.slug} --param key=value`); + console.log(''); + }); + +// ─── update ───────────────────────────────────────────────────────────────── + +const updateCmd = new Command('update') + .description('Update an org workflow, or publish a new version, from a JSON file') + .argument('', 'Workflow slug') + .option('--file ', 'JSON file with the patch body (use "-" for stdin)') + .addHelpText( + 'after', + ` +${pc.dim('The file may contain metadata fields (title, subtitle, description, vertical, tags)')} +${pc.dim('and/or a "version" object to publish a new immutable version. When publishing a new')} +${pc.dim('version, "version.changelog" is required. Pass "set_current": false to add a version')} +${pc.dim('without promoting it to current.')} + +${pc.dim('Examples:')} + + ${pc.dim('$ valyu workflows update my-flow --file patch.json')} + ${pc.dim('$ echo \'{"title":"New Title"}\' | valyu workflows update my-flow --file -')} +`, + ) + .action(async (slug, opts, cmd) => { + const globalOpts = cmd.optsWithGlobals() as GlobalOpts; + const fail = (message: string, code: string) => + outputError({ message, code }, { json: globalOpts.json }); + + if (!opts.file) { + fail('Provide the patch body with --file (or --file - for stdin)', 'missing_file'); + return; + } + + let body: Record; + try { + if (opts.file === '-') { + const piped = await readStdin(); + if (!piped) { + fail('No patch body provided on stdin', 'missing_file'); + return; + } + body = JSON.parse(piped) as Record; + } else { + body = readJsonFile>(opts.file, 'patch'); + } + } catch (err) { + fail(err instanceof Error ? err.message : 'Failed to read patch body', 'invalid_file'); + return; + } + + if (typeof body !== 'object' || body === null || Array.isArray(body)) { + fail('Patch body must be a JSON object', 'invalid_file'); + return; + } + + const resolved = requireApiKey(globalOpts); + const client = new ValyuClient(resolved.key); + const spinner = createSpinner('Updating workflow...', globalOpts.quiet); + + const { data, error } = await client.updateWorkflow(slug, body); + + if (error) { + spinner.fail('Failed to update workflow'); + outputError({ message: error.message, code: error.code }, { json: globalOpts.json }); + return; + } + + spinner.stop(`Workflow updated: ${pc.cyan(data!.slug)} ${pc.dim(`v${data!.version}`)}`); + + if (globalOpts.json || !process.stdout.isTTY) { + outputResult(data, { json: true }); + } + }); + +// ─── delete ───────────────────────────────────────────────────────────────── + +const deleteCmd = new Command('delete') + .description('Delete an org workflow') + .argument('', 'Workflow slug') + .option('-y, --yes', 'Skip confirmation prompt') + .action(async (slug, opts, cmd) => { + const globalOpts = cmd.optsWithGlobals() as GlobalOpts; + const resolved = requireApiKey(globalOpts); + const client = new ValyuClient(resolved.key); + + if (!opts.yes) { + const confirm = await p.confirm({ + message: `Delete workflow ${pc.cyan(slug)}? This cannot be undone.`, + }); + if (p.isCancel(confirm) || !confirm) { + console.log(` ${pc.dim('Cancelled')}`); + return; + } + } + + const spinner = createSpinner('Deleting workflow...', globalOpts.quiet); + + const { data, error } = await client.deleteWorkflow(slug); + + if (error) { + spinner.fail('Failed to delete workflow'); + outputError({ message: error.message, code: error.code }, { json: globalOpts.json }); + return; + } + + spinner.stop(`Workflow ${pc.cyan(slug)} deleted`); + + if (globalOpts.json || !process.stdout.isTTY) { + outputResult(data, { json: true }); + } + }); + +// ─── export ───────────────────────────────────────────────────────────────── + +export const workflowsCommand = new Command('workflows') + .description('Reusable, versioned deep research templates') + .addHelpText( + 'after', + ` +${pc.dim('Workflows are templated deep research starting points. Pick one, fill in its typed')} +${pc.dim('variables, and run it - the template expands into a normal deep research task.')} +${pc.dim('Curated Valyu workflows are read-only; workflows you create are private to your org.')} + +${pc.dim('Quick start:')} + + ${pc.dim('$ valyu workflows list --scope valyu')} + ${pc.dim('$ valyu workflows get ib-company-profile')} + ${pc.dim('$ valyu workflows run ib-company-profile --param company="NVIDIA (NVDA)" --watch')} +`, + ) + .addCommand(listCmd) + .addCommand(getCmd) + .addCommand(versionsCmd) + .addCommand(previewCmd) + .addCommand(runCmd) + .addCommand(createCmd) + .addCommand(updateCmd) + .addCommand(deleteCmd); diff --git a/src/lib/client.ts b/src/lib/client.ts index d7ac294..37c9446 100644 --- a/src/lib/client.ts +++ b/src/lib/client.ts @@ -191,6 +191,7 @@ export class ValyuClient { private async request( path: string, payload: Record, + method: 'POST' | 'PATCH' = 'POST', ): Promise<{ data: T | null; error: ValyuApiError | null }> { // Strip undefined values const body = Object.fromEntries( @@ -199,7 +200,7 @@ export class ValyuClient { try { const res = await fetch(`${VALYU_API_BASE}${path}`, { - method: 'POST', + method, headers: { 'Content-Type': 'application/json', 'x-api-key': this.apiKey, @@ -539,6 +540,106 @@ export class ValyuClient { }); } + // ─── DeepResearch Workflows ─────────────────────────────────────────────── + // Workflows are templated, versioned deep-research starting points. List and + // inspect curated (Valyu) or org-owned templates, preview the resolved task, + // then run one via POST /deepresearch/tasks with workflow_id + workflow_params. + + async listWorkflows(params?: { + vertical?: string; + scope?: string; + query?: string; + tags?: string[]; + limit?: number; + expand?: boolean; + }): Promise<{ data: WorkflowListResult | null; error: ValyuApiError | null }> { + const qs: Record = {}; + if (params?.vertical) qs.vertical = params.vertical; + if (params?.scope) qs.scope = params.scope; + if (params?.query) qs.q = params.query; + if (params?.tags?.length) qs.tags = params.tags.join(','); + if (params?.limit) qs.limit = String(params.limit); + if (params?.expand) qs.expand = 'true'; + return this.get('/workflows', Object.keys(qs).length ? qs : undefined); + } + + async getWorkflow( + slug: string, + version?: number, + ): Promise<{ data: WorkflowDetail | null; error: ValyuApiError | null }> { + return this.get( + `/workflows/${encodeURIComponent(slug)}`, + version != null ? { version: String(version) } : undefined, + ); + } + + async listWorkflowVersions( + slug: string, + ): Promise<{ data: WorkflowVersionsResult | null; error: ValyuApiError | null }> { + return this.get(`/workflows/${encodeURIComponent(slug)}/versions`); + } + + async getWorkflowVersion( + slug: string, + version: number, + ): Promise<{ data: WorkflowDetail | null; error: ValyuApiError | null }> { + return this.get(`/workflows/${encodeURIComponent(slug)}/versions/${version}`); + } + + async previewWorkflow( + slug: string, + workflowParams?: Record, + version?: number, + ): Promise<{ data: WorkflowPreviewResult | null; error: ValyuApiError | null }> { + return this.request(`/workflows/${encodeURIComponent(slug)}/preview`, { + workflow_params: workflowParams ?? {}, + workflow_version: version, + }); + } + + async createWorkflow( + body: Record, + ): Promise<{ data: WorkflowDetail | null; error: ValyuApiError | null }> { + return this.request('/workflows', body); + } + + async updateWorkflow( + slug: string, + body: Record, + ): Promise<{ data: WorkflowDetail | null; error: ValyuApiError | null }> { + return this.request(`/workflows/${encodeURIComponent(slug)}`, body, 'PATCH'); + } + + async deleteWorkflow( + slug: string, + ): Promise<{ data: Record | null; error: ValyuApiError | null }> { + return this.del>(`/workflows/${encodeURIComponent(slug)}`); + } + + // Run a workflow: creates a normal deep-research task from the template. Only + // sends optional overrides when provided so the template's own recommended + // mode / output formats apply by default. + async runWorkflow(params: { + slug: string; + workflowParams?: Record; + version?: number; + mode?: string; + webhookUrl?: string; + alertEmail?: string | { email: string; custom_url?: string }; + }): Promise<{ data: ResearchTask | null; error: ValyuApiError | null }> { + return this.request('/deepresearch/tasks', { + workflow_id: params.slug, + workflow_params: + params.workflowParams && Object.keys(params.workflowParams).length + ? params.workflowParams + : undefined, + workflow_version: params.version, + mode: params.mode, + webhook_url: params.webhookUrl, + alert_email: params.alertEmail, + }); + } + // ─── Contents async jobs ────────────────────────────────────────────────── async getContentsJob( @@ -798,3 +899,100 @@ export interface DatasourcesResult { export interface DatasourceCategoriesResult { categories: DatasourceCategory[]; } + +// ─── DeepResearch Workflows ─────────────────────────────────────────────────── + +export interface WorkflowVariable { + key: string; + label: string; + type?: 'text' | 'textarea' | 'number' | 'date' | 'enum'; + required?: boolean; + placeholder?: string; + help?: string; + examples?: string[]; + validation?: { + min_length?: number; + max_length?: number; + pattern?: string; + enum?: string[]; + }; +} + +export interface WorkflowDeliverable { + type: string; + description: string; +} + +export interface WorkflowTools { + code_execution?: boolean; + screenshots?: boolean; + browser_use?: boolean; + charts?: boolean; +} + +// List item; template fields (prompt/strategy/...) present only with ?expand=true. +export interface Workflow { + slug: string; + version: number; + vertical: string | null; + tags: string[]; + title: string; + subtitle: string | null; + description: string | null; + popular: boolean; + recommended_mode: 'fast' | 'standard' | 'heavy' | 'max'; + estimated_time: string | null; + is_valyu: boolean; + owner_org_id: string | null; + variables: WorkflowVariable[]; + deliverables: WorkflowDeliverable[]; + created_at: string; + updated_at: string; + prompt?: string; + strategy?: string; + report_format?: string; + tools?: WorkflowTools; +} + +// Detail / create / update response: template fields always present. +export interface WorkflowDetail extends Workflow { + prompt: string; + strategy: string; + report_format: string; + tools: WorkflowTools; + changelog: string | null; +} + +export interface WorkflowListResult { + workflows: Workflow[]; + total: number; + next_cursor: string | null; + verticals: string[]; +} + +export interface WorkflowVersionSummary { + version: number; + recommended_mode: 'fast' | 'standard' | 'heavy' | 'max'; + estimated_time: string | null; + changelog: string | null; + created_at: string; + is_current: boolean; +} + +export interface WorkflowVersionsResult { + slug: string; + versions: WorkflowVersionSummary[]; +} + +export interface WorkflowPreviewResult { + workflow: { slug: string; version: number }; + resolved: { + input: string; + research_strategy: string; + report_format: string; + deliverables: WorkflowDeliverable[]; + mode: string; + tools: WorkflowTools; + }; + estimated_credits: number | null; +} diff --git a/src/lib/version.ts b/src/lib/version.ts index b0c931f..30f4e08 100644 --- a/src/lib/version.ts +++ b/src/lib/version.ts @@ -1,2 +1,2 @@ -export const VERSION = '1.0.8'; +export const VERSION = '1.1.0'; export const PACKAGE_NAME = '@valyu/cli';