diff --git a/.github/workflows/contract-drift.yml b/.github/workflows/contract-drift.yml index 6fef5a56..7c1a3c54 100644 --- a/.github/workflows/contract-drift.yml +++ b/.github/workflows/contract-drift.yml @@ -1,21 +1,14 @@ name: Contract Drift -# Fails the PR if widget's oRPC contract mirrors (src/sdk/routes.ts, -# src/sdk/schema.ts) have drifted STRUCTURALLY from the source of truth in -# Marketrix-ai/api (routes.ts, schema.ts). "Structural" = presence of routes, -# exported schema identifiers, and discriminated-union event tags — comments, -# import paths, and whitespace are ignored. -# -# Widget is a strict SUBSET of the api contract: it intentionally omits the -# dashboard-only `simulationIssue*` routes and `SimulationIssue*` schemas. The -# drift script is run with --allow-subset, which tolerates those (and only -# those) being absent. Anything else missing — or anything EXTRA in the widget -# mirror — is still drift. See scripts/check-contract-drift.mjs for the known -# field-level gap. +# Fails the PR if widget's scoped SDK mirror (src/sdk/contract.ts + contracts/*) +# has drifted from the source of truth in Marketrix-ai/api (sdk/widget.ts + +# contracts/*). Uses the same sync-consumers.mjs generator in --check mode so +# the gate and the write path are provably equivalent. # # TOKEN REQUIREMENT # ----------------- -# This workflow reads two files from the PRIVATE Marketrix-ai/api repo, so it -# needs a token with read access. Create a repo or org secret: +# This workflow sparse-checkouts the relevant paths from the PRIVATE +# Marketrix-ai/api repo, so it needs a token with read access. Create a +# repo or org secret: # # CONTRACTS_READ_TOKEN = fine-grained PAT with "Contents: read" on # Marketrix-ai/api (read-only, no write scopes). @@ -25,9 +18,7 @@ name: Contract Drift on: pull_request: paths: - - src/sdk/routes.ts - - src/sdk/schema.ts - - scripts/check-contract-drift.mjs + - src/sdk/** - .github/workflows/contract-drift.yml workflow_dispatch: schedule: @@ -57,34 +48,26 @@ jobs: exit 1 fi - - name: Fetch api contract source (routes.ts, schema.ts) from Marketrix-ai/api@dev + - name: Sparse-checkout api contracts + sync generator from Marketrix-ai/api@dev env: GH_TOKEN: ${{ secrets.CONTRACTS_READ_TOKEN }} run: | set -euo pipefail mkdir -p .api-src - for f in routes.ts schema.ts; do - echo "Fetching api/${f}@dev ..." - gh api "repos/Marketrix-ai/api/contents/${f}?ref=dev" \ - -H 'Accept: application/vnd.github.raw' > ".api-src/${f}" - if [ ! -s ".api-src/${f}" ]; then - echo "::error::Fetched empty .api-src/${f} — check the token has Contents:read on Marketrix-ai/api and that the file exists on dev." - exit 1 - fi - done - - - name: Check routes drift (subset mode) - run: | - node scripts/check-contract-drift.mjs \ - --api .api-src/routes.ts \ - --mirror src/sdk/routes.ts \ - --kind routes \ - --allow-subset + git -C .api-src init + git -C .api-src remote add origin "https://x-access-token:${GH_TOKEN}@github.com/Marketrix-ai/api.git" + git -C .api-src config core.sparseCheckout true + printf 'contracts/\nscripts/sync-consumers.mjs\nsdk/\n' > .api-src/.git/info/sparse-checkout + git -C .api-src fetch --depth=1 origin dev + git -C .api-src checkout FETCH_HEAD + if [ ! -f ".api-src/scripts/sync-consumers.mjs" ]; then + echo "::error::sync-consumers.mjs not found in .api-src — check token and branch." + exit 1 + fi - - name: Check schema drift (subset mode) + - name: Check contract drift (sync-consumers --check) run: | - node scripts/check-contract-drift.mjs \ - --api .api-src/schema.ts \ - --mirror src/sdk/schema.ts \ - --kind schema \ - --allow-subset + node .api-src/scripts/sync-consumers.mjs widget \ + --check \ + --api-root .api-src \ + --dest src/sdk diff --git a/package.json b/package.json index c2aa4963..dacaeb33 100644 --- a/package.json +++ b/package.json @@ -2,6 +2,7 @@ "name": "@marketrix.ai/widget", "version": "3.8.3", "type": "module", + "sideEffects": false, "main": "./dist/widget.mjs", "module": "./dist/widget.mjs", "types": "./dist/src/index.d.ts", diff --git a/scripts/check-contract-drift.mjs b/scripts/check-contract-drift.mjs deleted file mode 100644 index 0339b3c1..00000000 --- a/scripts/check-contract-drift.mjs +++ /dev/null @@ -1,204 +0,0 @@ -#!/usr/bin/env node -// check-contract-drift.mjs — structural drift gate for the oRPC contract mirrors. -// -// The app/widget SDK files (`src/sdk/routes.ts`, `src/sdk/schema.ts`) are -// hand-maintained mirrors of the source of truth in the `api` repo -// (`api/routes.ts`, `api/schema.ts`). They are intentionally NOT byte-identical: -// the mirrors trim some comments and use different import paths, and the widget -// mirror is a strict SUBSET — it omits the dashboard-only `simulationIssue*` -// routes and the `SimulationIssue*` schemas. This script compares STRUCTURE -// (presence of routes / schemas / discriminated-union event tags) while ignoring -// comments, import lines, and whitespace, and fails on any structural difference -// other than the known widget subset allowlist. -// -// WHAT IT CHECKS (presence-level drift): -// --kind routes : the set of top-level route keys in `const contract = { ... }` -// (e.g. `getIndex:`, `authMe:`). -// --kind schema : the set of top-level `export const|type|interface ` -// identifiers, PLUS the set of `type: z.literal('')` -// member tags inside discriminated unions (AppEventSchema, -// WidgetEventSchema, WidgetCommandSchema, SSEEventSchema, ...). -// -// KNOWN GAP (documented, not checked): FIELD-LEVEL type drift inside a schema — -// e.g. a field changing from `z.string()` to `z.number()`, or a field being -// added/removed inside an existing object — is NOT detected. Only the presence -// of routes, exported schema identifiers, and union member tags is compared. -// A future iteration could parse the Zod AST for field-level coverage. -// -// USAGE: -// node scripts/check-contract-drift.mjs \ -// --api --mirror \ -// --kind routes|schema [--allow-subset] -// -// `--allow-subset` (widget only) tolerates identifiers that are ABSENT from the -// mirror as long as they match the widget allowlist (the `simulationIssue*` -// routes and `SimulationIssue*` schemas). Anything else absent is still drift. -// Identifiers ADDED in the mirror but missing from the api source are ALWAYS -// drift, subset mode or not. -// -// Exit codes: 0 = in sync, 1 = structural drift, 2 = bad invocation / IO error. -// Pure Node, no dependencies. - -import { readFileSync } from 'node:fs'; - -// Widget is a strict subset of the api contract. These identifiers MAY be -// absent from the widget mirror under --allow-subset. Matched case-insensitively -// by prefix so both routes (`simulationIssuePersonaCounts`, lowercase-camel) and -// schemas (`SimulationIssueEntitySchema`, PascalCase) are covered. -const WIDGET_SUBSET_PREFIX = 'simulationissue'; - -function parseArgs(argv) { - const args = { allowSubset: false }; - for (let i = 0; i < argv.length; i++) { - const a = argv[i]; - if (a === '--api') args.api = argv[++i]; - else if (a === '--mirror') args.mirror = argv[++i]; - else if (a === '--kind') args.kind = argv[++i]; - else if (a === '--allow-subset') args.allowSubset = true; - else { - console.error(`Unknown argument: ${a}`); - return null; - } - } - if (!args.api || !args.mirror || !args.kind) { - console.error('Missing required arg. Need --api --mirror --kind routes|schema'); - return null; - } - if (args.kind !== 'routes' && args.kind !== 'schema') { - console.error(`--kind must be "routes" or "schema", got "${args.kind}"`); - return null; - } - return args; -} - -function read(path) { - try { - return readFileSync(path, 'utf8'); - } catch (err) { - console.error(`Cannot read ${path}: ${err.message}`); - process.exit(2); - } -} - -// Top-level route keys are the keys of the single `const contract = { ... }` -// object literal — lines like ` getIndex: oc` at exactly 2-space indent. -function extractRouteKeys(src) { - const set = new Set(); - const re = /^ {2}([A-Za-z][A-Za-z0-9_]*)\s*:\s*oc\b/gm; - let m; - while ((m = re.exec(src)) !== null) set.add(m[1]); - return set; -} - -// Top-level exported identifiers: `export const|type|interface `. -function extractSchemaExports(src) { - const set = new Set(); - const re = /^export\s+(?:const|type|interface)\s+([A-Za-z][A-Za-z0-9_]*)/gm; - let m; - while ((m = re.exec(src)) !== null) set.add(m[1]); - return set; -} - -// Discriminated-union member tags: `type: z.literal('')`. Prefixed with -// `tag:` so they live in the same comparison space as exports without colliding -// with a same-named export. -function extractEventTags(src) { - const set = new Set(); - const re = /\btype:\s*z\.literal\(\s*'([^']+)'\s*\)/g; - let m; - while ((m = re.exec(src)) !== null) set.add(`tag:${m[1]}`); - return set; -} - -function isAllowlisted(name) { - return name.toLowerCase().startsWith(WIDGET_SUBSET_PREFIX); -} - -function diffSets(apiSet, mirrorSet) { - const removed = []; // in api, missing from mirror - const added = []; // in mirror, missing from api - for (const name of apiSet) if (!mirrorSet.has(name)) removed.push(name); - for (const name of mirrorSet) if (!apiSet.has(name)) added.push(name); - removed.sort(); - added.sort(); - return { removed, added }; -} - -function main() { - const args = parseArgs(process.argv.slice(2)); - if (!args) process.exit(2); - - const apiSrc = read(args.api); - const mirrorSrc = read(args.mirror); - - let apiSet; - let mirrorSet; - if (args.kind === 'routes') { - apiSet = extractRouteKeys(apiSrc); - mirrorSet = extractRouteKeys(mirrorSrc); - } else { - apiSet = new Set([...extractSchemaExports(apiSrc), ...extractEventTags(apiSrc)]); - mirrorSet = new Set([...extractSchemaExports(mirrorSrc), ...extractEventTags(mirrorSrc)]); - } - - if (apiSet.size === 0) { - console.error( - `No ${args.kind} identifiers extracted from api source ${args.api}. ` + - `The file format may have changed — refusing to pass vacuously.`, - ); - process.exit(2); - } - - let { removed, added } = diffSets(apiSet, mirrorSet); - - // Under --allow-subset, identifiers ABSENT from the mirror are tolerated only - // if they are on the widget allowlist. Anything else removed is still drift. - const allowedAbsent = []; - if (args.allowSubset) { - const stillRemoved = []; - for (const name of removed) { - if (isAllowlisted(name)) allowedAbsent.push(name); - else stillRemoved.push(name); - } - removed = stillRemoved; - } - - // Added-in-mirror is always drift. Pair removed+added of equal count as a hint - // that a rename happened (one removed + one added), purely for the message. - const renameHint = removed.length === 1 && added.length === 1; - - const label = `${args.kind}${args.allowSubset ? ' (subset mode)' : ''}`; - - if (removed.length === 0 && added.length === 0) { - const note = allowedAbsent.length - ? ` (${allowedAbsent.length} allowlisted subset identifier(s) intentionally absent: ${allowedAbsent.join(', ')})` - : ''; - console.log(`OK: ${label} in sync — ${apiSet.size} identifiers match.${note}`); - process.exit(0); - } - - console.error(`DRIFT DETECTED in ${label}:`); - console.error(` api source : ${args.api}`); - console.error(` mirror : ${args.mirror}`); - if (renameHint) { - console.error(` likely RENAME: "${removed[0]}" -> "${added[0]}"`); - } - if (removed.length) { - console.error(` MISSING from mirror (present in api, ${removed.length}): ${removed.join(', ')}`); - if (!args.allowSubset && removed.some(isAllowlisted)) { - console.error( - ` hint: these look like widget-subset identifiers — pass --allow-subset only in the widget repo.`, - ); - } - } - if (added.length) { - console.error(` EXTRA in mirror (absent in api, ${added.length}): ${added.join(', ')}`); - } - if (allowedAbsent.length) { - console.error(` (allowlisted subset, ignored: ${allowedAbsent.join(', ')})`); - } - console.error(`\nFix: re-run the sync-contracts skill to bring the mirror back in line with api ${args.kind}.`); - process.exit(1); -} - -main(); diff --git a/src/context/TaskContext.tsx b/src/context/TaskContext.tsx index 18e29873..518ee83d 100644 --- a/src/context/TaskContext.tsx +++ b/src/context/TaskContext.tsx @@ -1,6 +1,6 @@ import React, { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react'; -import type { WidgetEvent } from '../sdk/schema'; +import type { WidgetEvent } from '../sdk'; import { StreamClient, type StreamStatus } from '../services/StreamClient'; import { toolExecutionService } from '../services/ToolService'; import type { ChatMessage, InstructionType, TaskProgress } from '../types'; diff --git a/src/context/__tests__/sseReducer.test.ts b/src/context/__tests__/sseReducer.test.ts index 52f52b3d..8128bd4b 100644 --- a/src/context/__tests__/sseReducer.test.ts +++ b/src/context/__tests__/sseReducer.test.ts @@ -5,7 +5,7 @@ */ import { describe, expect, it } from 'vitest'; -import type { WidgetEvent } from '@/sdk/schema'; +import type { WidgetEvent } from '@/sdk'; import type { ChatMessage } from '@/types'; import { hasThinkingMarker } from '@/utils/chat'; diff --git a/src/context/sseReducer.ts b/src/context/sseReducer.ts index 7d4a3acb..587d7b1b 100644 --- a/src/context/sseReducer.ts +++ b/src/context/sseReducer.ts @@ -8,7 +8,7 @@ * Keeping this pure makes the previously-untestable ~180-line SSE handler unit * testable and removes the nested setState-within-setState that hid bugs. */ -import type { WidgetEvent } from '../sdk/schema'; +import type { WidgetEvent } from '../sdk'; import type { ChatMessage, InstructionType } from '../types'; import { addProgressLine, diff --git a/src/sdk/contract.ts b/src/sdk/contract.ts new file mode 100644 index 00000000..03a8a1df --- /dev/null +++ b/src/sdk/contract.ts @@ -0,0 +1,16 @@ +import { activityLogCreate } from './contracts/activityLog'; +import { agentGet } from './contracts/agent'; +import { applicationGet } from './contracts/application'; +import { chatCreate } from './contracts/chat'; +import { widgetGetDefaults, widgetMessage, widgetSearch, widgetStream } from './contracts/widget'; + +export const widgetContract = { + activityLogCreate, + agentGet, + applicationGet, + chatCreate, + widgetGetDefaults, + widgetSearch, + widgetMessage, + widgetStream, +}; diff --git a/src/sdk/contracts/activityLog.ts b/src/sdk/contracts/activityLog.ts new file mode 100644 index 00000000..e707b58f --- /dev/null +++ b/src/sdk/contracts/activityLog.ts @@ -0,0 +1,44 @@ +import { oc } from '@orpc/contract'; +import { z } from 'zod'; + +import { paginatedListOf, PaginationSchema } from './common'; +import { ActionLogCreateSchema, ActionLogEntitySchema, ActionLogTypeSchema } from './entities'; + +// ---- procedures ---- + +export const activityLogCreate = oc + .route({ + method: 'POST', + tags: ['Activity Log'], + path: '/log', + summary: 'Create new activity log entry', + description: 'Records user or system action for auditing and tracking purposes', + }) + .input(ActionLogCreateSchema) + .output(ActionLogEntitySchema); + +export const activityLogSearch = oc + .route({ + method: 'GET', + tags: ['Activity Log'], + path: '/log', + summary: 'Search and filter activity logs', + description: 'Returns list of activity logs matching search parameters (workspace, type)', + }) + .input( + z + .object({ + workspace_id: z.coerce.number().optional(), + type: ActionLogTypeSchema.optional(), + application_id: z.coerce.number().optional(), + }) + .extend(PaginationSchema.shape), + ) + .output(paginatedListOf(ActionLogEntitySchema)); + +// ---- domain aggregate ---- + +export const activityLogRoutes = { + activityLogCreate, + activityLogSearch, +}; diff --git a/src/sdk/contracts/agent.ts b/src/sdk/contracts/agent.ts new file mode 100644 index 00000000..483c0bb9 --- /dev/null +++ b/src/sdk/contracts/agent.ts @@ -0,0 +1,235 @@ +import { oc } from '@orpc/contract'; +import { z } from 'zod'; + +import { ByAgentIdSchema, MindMapSchema, paginatedListOf, PaginationSchema, SuccessSchema } from './common'; +import { AgentEntitySchema, AgentTypeSchema, AgentVoiceSchema } from './entities'; + +// ---- private helpers ---- + +const parseIds = (val: unknown): number[] => { + if (Array.isArray(val)) return val.map(v => Number(v)); + if (typeof val === 'string') { + const parsed: unknown = JSON.parse(val); + return Array.isArray(parsed) ? (parsed as unknown[]).map(v => Number(v)) : []; + } + return []; +}; + +const KnowledgeIdsSchema = z.union([z.array(z.number()), z.string()]).transform(parseIds); +const SimulationIdsSchema = z.union([z.array(z.number()), z.string()]).transform(parseIds); + +// ---- agent-only schemas ---- + +export const AgentCreateSchema = AgentEntitySchema.partial().extend({ + application_id: z.coerce.number(), + agent_name: z.string(), + agent_type: AgentTypeSchema, + agent_voice: AgentVoiceSchema, + agent_description: z.string(), + instructions: z.string(), + file: z.instanceof(File).optional(), + image_url: z.string().optional(), + knowledge_ids: KnowledgeIdsSchema, + simulation_ids: SimulationIdsSchema, +}); +export type AgentCreationData = z.infer; + +export const AgentUpdateSchema = AgentEntitySchema.partial().extend({ + file: z.instanceof(File).optional(), + knowledge_ids: KnowledgeIdsSchema, + simulation_ids: SimulationIdsSchema, +}); +export type AgentUpdateData = z.infer; + +/** + * Agent task start response schema + * Response from agent server when starting a task + */ +export const AgentTaskStartResponseSchema = z.object({ + text: z.string(), + task_id: z.string().optional(), +}); +export type AgentTaskStartResponseData = z.infer; + +/** + * Agent task stop response schema + * Response from agent server when stopping a task + */ +export const AgentTaskStopResponseSchema = z.object({ + status: z.string(), + message: z.string().optional(), +}); +export type AgentTaskStopResponseData = z.infer; + +/** + * Agent task status response schema + * Response from agent server for task status queries + */ +export const AgentTaskStatusResponseSchema = z.object({ + task_id: z.string().optional(), + status: z.string().optional(), + current_step: z.string().optional(), + error: z.string().optional(), + message: z.string().optional(), +}); +export type AgentTaskStatusResponseData = z.infer; + +export const AgentSearchConfigSchema = z.object({ + contentType: z.enum(['document', 'video', 'automation_log', 'screenshot', 'all']).optional(), + context: z.string().optional(), + previousActions: z.array(z.string()).optional(), + top: z.coerce.number().min(1).max(100).optional(), + minConfidence: z.coerce.number().min(0).max(1).optional(), + entities: z.array(z.string()).optional(), + useVectorSearch: z.boolean().optional(), + vectorThreshold: z.coerce.number().min(0).max(1).optional(), +}); +export type AgentSearchConfig = z.infer; + +/** + * Search document schema (matches Azure Search document structure) + */ +export const SearchDocumentSchema = z.object({ + id: z.string(), + content: z.string(), + contentType: z.enum(['document', 'video', 'automation_log', 'screenshot']), + sourceFile: z.string(), + sourceType: z.string(), + metadata: z.string(), // JSON stringified + confidence: z.number(), + keyPhrases: z.array(z.string()), + entities: z.array(z.string()), // Format: "text:category:confidence" + sentiment: z.string(), // JSON stringified + vectorContent: z.array(z.number()), +}); +export type SearchDocument = z.infer; + +/** + * Search result schema (Azure Search result wrapper) + */ +export const SearchResultSchema = z.object({ + document: SearchDocumentSchema, + score: z.number(), + highlights: z.record(z.string(), z.array(z.string())).optional(), +}); +export type SearchResult = z.infer; + +export const AgentSimulationIndexRequestSchema = z.object({ + simulation_id: z.coerce.number().positive('Simulation ID must be a positive number'), +}); +export type AgentSimulationIndexRequest = z.infer; + +export const AgentSimulationIndexResponseSchema = z.object({ + agent: AgentEntitySchema, + simulation_id: z.number(), + knowledge_id: z.number(), + message: z.string(), +}); +export type AgentSimulationIndexResponse = z.infer; + +// ---- procedures ---- + +export const agentCreate = oc + .route({ + method: 'POST', + tags: ['Agent'], + path: '/agent', + summary: 'Create new AI agent with configuration', + description: 'Creates agent with specified settings, prompts, and returns agent entity', + }) + .input(AgentCreateSchema) + .output(AgentEntitySchema); + +export const agentSearch = oc + .route({ + method: 'GET', + tags: ['Agent'], + path: '/agent', + summary: 'Search and filter agents by workspace or user', + description: + 'Returns list of agents matching search parameters. Supports filtering by workspace_id, user_id, and application_id.', + }) + .input( + z + .object({ + workspace_id: z.coerce.number().optional(), + user_id: z.coerce.number().optional(), + application_id: z.coerce.number().optional(), + include: z.array(z.enum(['knowledge', 'simulations'])).optional(), + }) + .extend(PaginationSchema.shape), + ) + .output(paginatedListOf(AgentEntitySchema)); + +export const agentGet = oc + .route({ + method: 'GET', + tags: ['Agent'], + path: '/agent/{agent_id}', + summary: 'Get specific agent details by ID', + description: 'Returns complete agent information including configuration and settings', + }) + .input(ByAgentIdSchema.extend({ include: z.array(z.enum(['knowledge', 'simulations'])).optional() })) + .output(AgentEntitySchema); + +export const agentMindmap = oc + .route({ + method: 'GET', + tags: ['Agent'], + path: '/agent/{agent_id}/mindmap', + summary: 'Get agent mindmap by ID', + description: 'Returns the mindmap knowledge graph for the specified agent', + }) + .input(ByAgentIdSchema) + .output(MindMapSchema); + +export const agentUpdate = oc + .route({ + method: 'PUT', + tags: ['Agent'], + path: '/agent/{agent_id}', + summary: 'Update agent configuration and settings', + description: + 'Updates agent details. For JSON requests, handled by oRPC. Also accepts multipart/form-data for logo file uploads (handled by raw Express route).', + }) + .input( + AgentUpdateSchema.extend({ + agent_id: z.coerce.number(), + force_reset_learning: z.coerce.boolean().optional(), + }), + ) + .output(AgentEntitySchema); + +export const agentDelete = oc + .route({ + method: 'DELETE', + tags: ['Agent'], + path: '/agent/{agent_id}', + summary: 'Delete agent and all associated data', + description: 'Permanently deletes an agent and cleans up all related resources. This action cannot be undone.', + }) + .input(ByAgentIdSchema) + .output(SuccessSchema); + +export const agentIndexSimulation = oc + .route({ + method: 'POST', + tags: ['Agent'], + path: '/agent/{agent_id}/index', + summary: 'Index simulation document into agent knowledge base', + description: "Adds a simulation document to an agent's knowledge base and refreshes the search index", + }) + .input(AgentSimulationIndexRequestSchema.extend({ agent_id: z.coerce.number() })) + .output(AgentSimulationIndexResponseSchema); + +// ---- domain aggregate ---- + +export const agentRoutes = { + agentCreate, + agentSearch, + agentGet, + agentMindmap, + agentUpdate, + agentDelete, + agentIndexSimulation, +}; diff --git a/src/sdk/contracts/application.ts b/src/sdk/contracts/application.ts new file mode 100644 index 00000000..dc4bfbf0 --- /dev/null +++ b/src/sdk/contracts/application.ts @@ -0,0 +1,107 @@ +import { oc } from '@orpc/contract'; +import { z } from 'zod'; + +import { ByApplicationIdSchema, paginatedListOf, PaginationSchema, SuccessSchema } from './common'; +import { ApplicationEntitySchema, ApplicationReadSchema, ApplicationTypeSchema, WidgetEntitySchema } from './entities'; + +// ---- application-only schemas ---- + +export const ApplicationFilterSchema = z.object({ + application_id: z.coerce.number().optional(), +}); + +export const ApplicationCreateSchema = ApplicationEntitySchema.partial().extend({ + type: ApplicationTypeSchema, + name: z.string().min(1), + url: z.string(), + allowed_domains: z.array(z.string()).optional().default([]), +}); +export type ApplicationCreateData = z.infer; + +export const ApplicationUpdateSchema = ApplicationEntitySchema.partial().omit({ workspace_id: true, slug: true }); +export type ApplicationUpdateData = z.infer; + +// ---- procedures ---- + +export const applicationCreate = oc + .route({ + method: 'POST', + tags: ['Application'], + path: '/applications', + summary: 'Create a new application', + description: 'Creates a new application for the authenticated workspace and returns the created entity.', + }) + .input(ApplicationCreateSchema) + .output(ApplicationReadSchema); + +export const applicationSearch = oc + .route({ + method: 'GET', + tags: ['Application'], + path: '/applications', + summary: 'Search applications for workspace', + description: + 'Returns applications for the authenticated workspace, optionally filtered by type, always includes widgets', + }) + .input( + z + .object({ + type: ApplicationTypeSchema.optional(), + include: z.array(z.enum(['widgets'])).optional(), + }) + .extend(PaginationSchema.shape), + ) + .output( + paginatedListOf( + ApplicationReadSchema.extend({ + widgets: z.array(WidgetEntitySchema).optional(), + }), + ), + ); + +export const applicationGet = oc + .route({ + method: 'GET', + tags: ['Application'], + path: '/applications/{application_id}', + summary: 'Get application by ID', + description: 'Returns specific application details by ID, always includes widgets', + }) + .input(ByApplicationIdSchema) + .output( + ApplicationReadSchema.extend({ + widgets: z.array(WidgetEntitySchema), + }), + ); + +export const applicationUpdate = oc + .route({ + method: 'PUT', + tags: ['Application'], + path: '/applications/{application_id}', + summary: 'Update application', + description: 'Updates application details and configuration', + }) + .input(ApplicationUpdateSchema.extend({ application_id: z.coerce.number() })) + .output(ApplicationReadSchema); + +export const applicationDelete = oc + .route({ + method: 'DELETE', + tags: ['Application'], + path: '/applications/{application_id}', + summary: 'Delete application', + description: 'Permanently deletes an application and all associated widgets. This action cannot be undone.', + }) + .input(ByApplicationIdSchema) + .output(SuccessSchema); + +// ---- domain aggregate ---- + +export const applicationRoutes = { + applicationCreate, + applicationSearch, + applicationGet, + applicationUpdate, + applicationDelete, +}; diff --git a/src/sdk/contracts/chat.ts b/src/sdk/contracts/chat.ts new file mode 100644 index 00000000..495e4a0a --- /dev/null +++ b/src/sdk/contracts/chat.ts @@ -0,0 +1,20 @@ +import { oc } from '@orpc/contract'; +import { z } from 'zod'; + +// ---- procedures ---- + +export const chatCreate = oc + .route({ + method: 'POST', + tags: ['Chat'], + path: '/chat', + summary: 'Create a new chat thread', + description: 'Initializes new chat thread and returns session ID', + }) + .output(z.string()); + +// ---- domain aggregate ---- + +export const chatRoutes = { + chatCreate, +}; diff --git a/src/sdk/contracts/common.ts b/src/sdk/contracts/common.ts new file mode 100644 index 00000000..609c3b12 --- /dev/null +++ b/src/sdk/contracts/common.ts @@ -0,0 +1,142 @@ +import { z } from 'zod'; + +export const EntityStatusSchema = z.enum(['created', 'active', 'suspended', 'pending_approval']); + +/** + * Base entity schema with common fields for all database entities + */ +export const BaseEntitySchema = z.object({ + id: z.number().optional(), + created_at: z.coerce.date().optional(), + updated_at: z.coerce.date().optional(), +}); + +// ── Shared input helpers ── +export const ByIdSchema = z.object({ id: z.coerce.number() }); +export const BySlugSchema = z.object({ slug: z.string() }); +export const ByAgentIdSchema = z.object({ agent_id: z.coerce.number() }); +export const ByWidgetIdSchema = z.object({ widget_id: z.coerce.number() }); +export const BySimulationIdSchema = z.object({ simulation_id: z.coerce.number() }); +export const ByApplicationIdSchema = z.object({ application_id: z.coerce.number() }); +export const ByUserIdSchema = z.object({ user_id: z.coerce.number() }); + +export const PaginationSchema = z.object({ + limit: z.coerce.number().optional().default(50), + offset: z.coerce.number().optional().default(0), +}); + +// ── List wrappers ── + +/** Paginated list for unbounded queries — includes total/limit/offset for pagination */ +export const paginatedListOf = (schema: T) => + z.object({ + items: z.array(schema), + total: z.number(), + limit: z.number(), + offset: z.number(), + }); + +/** Simple list for bounded results (scoped to parent entity) — includes count */ +export const listOf = (schema: T) => + z.object({ + items: z.array(schema), + count: z.number(), + }); + +/** Value types in QA test case version diffs */ +export const DiffValueSchema = z.union([z.string(), z.number(), z.boolean(), z.array(z.string()), z.null()]); + +export const ContextRefSchema = z.object({ + type: z.enum(['doc', 'sim', 'session']), + id: z.string(), + label: z.string(), +}); + +export const SuccessSchema = z.object({ success: z.literal(true) }); +export const SuccessWithMessageSchema = SuccessSchema.extend({ message: z.string() }); + +/** + * One tool_call recorded inside a SimulationStep — the underlying browser_op + * invocation that contributed to the step's action. + */ +export const ToolCallRecordSchema = z.object({ + name: z.string().min(1), + params: z.record(z.string(), z.unknown()).default({}), + result: z.record(z.string(), z.unknown()).default({}), +}); +export type ToolCallRecord = z.infer; + +/** + * Slack incoming-webhook URL validator — shared by the workspace update path + * and the slack-test endpoint so both surface a 400 BAD_REQUEST for the same + * malformed input. + */ +export const SlackWebhookUrlSchema = z + .string() + .url() + .refine(u => /^https:\/\/hooks\.slack\.com\//.test(u), { + message: 'Slack webhook URL must start with https://hooks.slack.com/', + }); + +/** + * Mindmap edge schema - transition from one node to another via an action + */ +export const MindMapEdgeSchema = z + .object({ + start: z.string(), + end: z.string(), + action: z.string(), + }) + .passthrough(); +export type MindMapEdgeData = z.infer; + +/** + * Mindmap section schema - functional UI section within a page node + */ +export const MindMapSectionSchema = z + .object({ + id: z.string(), + label: z.string(), + purpose: z.string(), + elements: z.array(z.record(z.string(), z.unknown())).default([]), + bbox: z.record(z.string(), z.unknown()).default({}), + screenshot: z.string().default(''), + embedding: z.array(z.number()).nullish(), + }) + .passthrough(); + +/** + * Mindmap node schema - unique page state observed during simulation. + * Matches agent's PageNode model (perception/graph.py). + * Uses passthrough() because the agent model may evolve faster than the schema. + */ +export const MindMapNodeSchema = z + .object({ + id: z.string(), + title: z.string(), + url: z.string(), + summary: z.string().default(''), + screenshot: z.string().default(''), + sections: z.array(MindMapSectionSchema).default([]), + sequence_ids: z.array(z.number()).default([]), + embedding: z.array(z.number()).nullish(), + }) + .passthrough(); +export type MindMapNodeData = z.infer; + +export const MindMapSchema = z.object({ + nodes: z.array(MindMapNodeSchema), + edges: z.array(MindMapEdgeSchema), +}); +export type MindMapData = z.infer; + +/** + * Skill invocation request — used by SimulationCreateSchema to launch a + * simulation against a specific skill. Lives in common so simulation.ts + * can import it without crossing domain boundaries. + */ +export const SkillInvocationRequestSchema = z.object({ + skill_id: z.number().int(), + params: z.record(z.string(), z.string()), +}); +export type SkillInvocationRequest = z.infer; diff --git a/src/sdk/contracts/entities.ts b/src/sdk/contracts/entities.ts new file mode 100644 index 00000000..25634f98 --- /dev/null +++ b/src/sdk/contracts/entities.ts @@ -0,0 +1,453 @@ +import { z } from 'zod'; + +import { BaseEntitySchema, EntityStatusSchema } from './common'; + +export const WorkspacePackageSchema = z.enum(['free', 'startup', 'growth', 'enterprise']); +export type WorkspacePackage = z.infer; + +export const AgentTypeSchema = z.enum(['human', 'ai']); +export type AgentType = z.infer; + +export const AgentVoiceSchema = z.enum(['male', 'female']); +export type AgentVoice = z.infer; + +export const AgentStatusSchema = z.enum(['active', 'learning', 'error']); +export type AgentStatus = z.infer; + +// Learning progress uses nullable boolean: +// - null: callback not yet received +// - true: callback received with success +// - false: callback received with failure +export const LearningProgressSchema = z.object({ + graph_index_created: z.boolean().nullable(), +}); + +export const KnowledgeTypeSchema = z.enum(['document', 'video']); +export type KnowledgeType = z.infer; + +export const KnowledgeSourceSchema = z.enum(['user', 'research']); +export type KnowledgeSource = z.infer; + +export const QAFlowStatusSchema = z.enum(['pending', 'processing', 'waiting_review', 'completed', 'failed']); + +/** + * Simulation parent status — canonical wire vocabulary matching the + * `simulation_status` Postgres ENUM and the `SimulationStatus` proto enum. + * Wave-14 added `has_question` (parent reflects a task awaiting answer); + * V56 migration adds the corresponding PG enum value. + */ +export const SimulationStatusSchema = z.enum([ + 'queued', + 'running', + 'creating_knowledge', + 'has_question', + 'completed', + 'failed', + 'stopped', +]); +export type SimulationStatus = z.infer; + +/** + * Mindmap generation lifecycle status on a simulation row. Values written by + * `mindmapDispatch.ts` and `simulationHooks.ts`; relayed on the + * `simulation/mindmap-updated` app event. + */ +export const MindmapStatusSchema = z.enum(['pending', 'generating', 'completed', 'failed']); + +/** + * Status carried on the `agent/updated` app event. The event has two emitter + * lineages: (1) `agentService` + `agentLearningHooks` emit an agent-entity + * status (`AgentStatusSchema`), and (2) the agentTask flow's `eventMapping` + * emits a task-status (`SimulationTaskStatusSchema`). The union captures both + * vocabularies so downstream consumers get exhaustiveness rather than `string`. + */ +export const AgentUpdatedStatusSchema = z.enum([ + // Agent entity statuses (AgentStatusSchema) + 'active', + 'learning', + 'error', + // Task-level statuses surfaced via agentTask flow transitions + 'queued', + 'running', + 'completed', + 'failed', + 'has_question', + 'stopped', +]); + +export const ApplicationTypeSchema = z.enum(['app', 'website']); +export type ApplicationType = z.infer; + +export const WidgetTypeSchema = z.enum(['widget']); +export type WidgetType = z.infer; + +export const InstructionTypeSchema = z.enum(['tell', 'show', 'do']); +export type InstructionType = z.infer; + +/** + * Authentication method schema + * password: Email/password authentication + * oauth: Social login (Google, Microsoft, Apple, etc.) or SSO + */ +export const AuthMethodSchema = z.enum(['password', 'oauth']); + +/** + * Complete user entity schema + * Note: Users don't have plans - plans belong to workspaces (via workspace_plan table) + */ +export const UserEntitySchema = BaseEntitySchema.extend({ + is_super: z.boolean(), + status: EntityStatusSchema, + email: z.string().email(), + external_id: z.string().nullish(), + first_name: z.string().nullish(), + last_name: z.string().nullish(), + password: z.string().nullish(), + image_url: z.string().nullish(), + prompt_limit: z.number().nullish(), + last_login_at: z.coerce.date().nullish(), + auth_method: AuthMethodSchema.nullish(), +}); +export type UserData = z.infer; + +export const UserCreateSchema = UserEntitySchema.partial().extend({ + email: z.string().email(), + password: z.string(), +}); +export type UserCreateData = z.infer; + +/** + * Complete workspace entity schema + * Note: package and ending_date come from workspace_plan table (joined when fetching workspace data). + * They are NOT stored on the workspace table itself. + */ +export const WorkspaceEntitySchema = BaseEntitySchema.extend({ + name: z.string(), + slug: z.string(), + status: EntityStatusSchema, + package: WorkspacePackageSchema, + ending_date: z.coerce.date().nullish(), + external_workspace_id: z.string().nullish(), + // Read-only flag derived from `slack_webhook_url`'s presence. The URL itself + // is a secret and is never returned to clients — only this boolean is. + slack_webhook_configured: z.boolean().optional(), + notify_all_members_on_question: z.boolean().optional(), +}); +export type WorkspaceData = z.infer; + +/** + * Lightweight agent badge for embedding in other entities + */ +export const AgentBadgeSchema = z.object({ + id: z.number(), + agent_name: z.string(), + image_url: z.string().nullish(), +}); +export type AgentBadgeData = z.infer; + +/** + * Knowledge base document schema + */ +export const KnowledgeEntitySchema = BaseEntitySchema.extend({ + workspace_id: z.number(), + application_id: z.number().optional(), + file_name: z.string().min(1), + file_size: z.coerce.number(), + file_type: KnowledgeTypeSchema, + file_url: z.string(), + source_url: z.string().nullish(), // Original URL for URL-based documents + source: KnowledgeSourceSchema.default('user').optional(), + agents: z.array(AgentBadgeSchema).optional(), +}); +export type KnowledgeData = z.infer; + +/** + * QA verdict entity schemas — LLM evaluator output per (qa_run, qa_test_case). + * Independent of simulation task status (which keeps raw execution semantics). + */ +export const QAVerdictSchema = z.enum(['passed', 'needs_healing', 'failed']); +export type QAVerdict = z.infer; + +// ── QA Insight Response (completion payload for process/refine streams) ── +export const QAInsightResponseSchema = z.object({ + ultimate_goal: z.string(), + test_cases: z.array( + z.object({ + test_title: z.string(), + test_objective: z.string(), + test_steps: z.array(z.string()), + expected_outcome: z.string(), + priority: z.enum(['Low', 'Medium', 'High']), + }), + ), + summary: z.object({ + total_tests: z.number(), + high_priority: z.number(), + medium_priority: z.number(), + low_priority: z.number(), + estimated_time_minutes: z.number(), + }), +}); + +export const TaskDependencySchema = z.object({ + task_id: z.string(), + condition: z.enum(['pass']).optional(), +}); +export type TaskDependency = z.infer; + +/** + * A single task within a simulation. Direct simulations have 1 task (the prompt). + * QA simulations have N tasks (one per test case). + */ +export const SimulationTaskEntrySchema = z.object({ + task_id: z.string(), + title: z.string(), + instructions: z.string(), + status: z.enum(['pending', 'running', 'has_question', 'passed', 'failed', 'skipped', 'stopped']), + error_message: z.string().nullish(), + started_at: z.string().nullish(), + completed_at: z.string().nullish(), + order_index: z.number().int().nonnegative().default(0), + tab_id: z.string().nullish(), + step_count: z.number().int().nonnegative().default(0), + blocked_by: z.array(TaskDependencySchema).default([]), +}); +export type SimulationTaskEntry = z.infer; + +export const SimulationEntitySchema = BaseEntitySchema.extend({ + application_id: z.number(), + agent_id: z.number(), + job_id: z.string(), + browser_session_id: z.string().nullish(), + status: SimulationStatusSchema, + status_message: z.string().nullish(), + path: z.string().nullish(), + instructions: z.string().nullish(), + pinned: z.boolean().optional(), + source: z.enum(['direct', 'qa']).optional(), + agent_name: z.string().nullish(), + graph_index_id: z.string().nullish(), + source_metadata: z.record(z.string(), z.unknown()).nullish(), + tasks: z.array(SimulationTaskEntrySchema).optional(), + agents: z.array(AgentBadgeSchema).optional(), + mindmap_status: MindmapStatusSchema.optional(), + mindmap_steps_processed: z.number().int().nonnegative().optional(), + mindmap_steps_total: z.number().int().nonnegative().optional(), + mindmap_error: z.string().nullish(), + created_by_user_id: z.number().nullish(), + // Persona selected for this run in the Generate Simulation modal. Nullable + // for "Generic" runs and for reaction-flow runs (which use + // reaction_result.persona_id for many-personas-per-run). See migration V54. + persona_id: z.number().nullish(), + // Derived flag: true if any per-task status is `has_question`. The parent + // `status` itself never holds `has_question` — that's a per-task state. The + // flag drives the "Question" UI pill on the simulation header. + has_question: z.boolean().optional(), +}); +export type SimulationData = z.infer; + +export const AgentEntitySchema = BaseEntitySchema.extend({ + workspace_id: z.number(), + user_id: z.number().nullish(), + application_id: z.number(), + agent_name: z.string(), + agent_type: AgentTypeSchema, + agent_voice: AgentVoiceSchema, + agent_description: z.string().nullish(), + instructions: z.string().nullish(), + image_url: z.string().nullish(), + graph_index_id: z.string().nullish(), + status: AgentStatusSchema, + status_message: z.string().nullish(), + learning_progress: LearningProgressSchema.nullish(), + learning_started_at: z.coerce.date().nullish(), + workspace: WorkspaceEntitySchema.optional(), + user: UserEntitySchema.optional(), + knowledge: z.array(KnowledgeEntitySchema).optional(), + simulations: z.array(SimulationEntitySchema).optional(), + simulation_count: z.number().int().nonnegative().optional(), + knowledge_count: z.number().int().nonnegative().optional(), +}); +export type AgentData = z.infer; + +export const ApplicationEntitySchema = BaseEntitySchema.extend({ + workspace_id: z.number(), + name: z.string(), + slug: z.string(), + type: ApplicationTypeSchema, + url: z.string().nullish(), + username: z.string().nullish(), + password: z.string().nullish(), + allowed_domains: z.array(z.string()).nullish().default([]), +}); +export type ApplicationData = z.infer; + +/** + * Application read schema — entity minus password. Used for all API + * responses; password is write-only and never returned to clients. + */ +export const ApplicationReadSchema = ApplicationEntitySchema.omit({ password: true }); +export type ApplicationReadData = z.infer; + +export const WidgetChipSchema = z.object({ + chip_mode: z.enum(['show', 'tell', 'do']), + chip_text: z.string(), +}); +export type WidgetChip = z.infer; + +export const WidgetSettingsDataSchema = z.object({ + widget_enabled: z.boolean(), + widget_appearance: z.enum(['default', 'compact', 'full']), + widget_position: z.enum(['bottom_left', 'bottom_right', 'top_left', 'top_right']), + widget_device: z.enum(['desktop', 'mobile', 'desktop_mobile']), + widget_header: z.string(), + widget_body: z.string(), + widget_greeting: z.string(), + widget_feature_tell: z.boolean(), + widget_feature_show: z.boolean(), + widget_feature_do: z.boolean(), + widget_feature_human: z.boolean(), + widget_background_color: z.string(), + widget_text_color: z.string(), + widget_border_color: z.string(), + widget_accent_color: z.string(), + widget_secondary_color: z.string(), + widget_border_radius: z.string(), + widget_font_size: z.string(), + widget_width: z.string(), + widget_height: z.string(), + widget_shadow: z.string(), + widget_animation_duration: z.string(), + widget_fade_duration: z.string(), + widget_bounce_effect: z.boolean(), + widget_chips: z.array(WidgetChipSchema), +}); +export type WidgetSettingsData = z.infer; + +export const WidgetEntitySchema = BaseEntitySchema.extend({ + application_id: z.number(), + agent_id: z.number(), + type: WidgetTypeSchema, + settings: WidgetSettingsDataSchema, + status: EntityStatusSchema, + marketrix_id: z.string(), + marketrix_key: z.string(), + snippet: z.string().nullish(), +}); +export type WidgetData = z.infer; + +/** + * State Trigger entity schema - stores URL patterns and messages to show in widget + * `message` is one or more chip texts shown by the widget when the URL pattern matches. + */ +export const StateTriggerEntitySchema = BaseEntitySchema.extend({ + widget_id: z.number(), + url_pattern: z.string(), + message: z.array(z.string()), + description: z.string().optional(), +}); +export type StateTriggerData = z.infer; + +/** + * QA run derived status — set by `deriveQARunStats` based on aggregated + * task states. Emitted by the API on `qa-run/updated` events and returned + * in QA run oRPC payloads. + * + * Canonical wire vocabulary; `deriveQARunStats` returns `'running'` directly. + * `'pending'` is the empty-task initial state. + */ +export const QARunDerivedStatusSchema = z.enum(['pending', 'running', 'completed', 'failed', 'stopped']); +export type QARunDerivedStatus = z.infer; + +// ── Activity Log ── + +export const ActionLogTypeSchema = z.enum([ + 'user_login', + 'url_visit', + 'update_workspace', + 'create_user', + 'update_user', + 'delete_user', + 'create_agent', + 'update_agent', + 'delete_agent', + 'create_application', + 'update_application', + 'delete_application', + 'create_widget', + 'update_widget', + 'delete_widget', + 'create_knowledge', + 'update_knowledge', + 'delete_knowledge', + 'approve_user', + 'deny_user', + 'request_workspace', + 'widget_question', + 'qa_run_started', + 'start_simulation', + 'create_automation', + 'update_automation', + 'delete_automation', + 'toggle_automation', + 'slack_command', +]); +export type ActionLogType = z.infer; + +/** + * Action log metadata schema + * Captures common metadata fields used across different action log types + */ +export const ActionLogMetadataSchema = z + .object({ + details: z.string().optional(), + id: z.number().optional(), + type: z.string().optional(), + name: z.string().optional(), + target_user_id: z.number().optional(), + target_user_email: z.string().optional(), + reason: z.string().optional(), + assigned_role: z.string().optional(), + new_role: z.string().optional(), + previous_role: z.string().optional(), + workspace_name: z.string().optional(), + workspace_slug: z.string().optional(), + ip_address: z.string().optional(), + user_agent: z.string().optional(), + widget_type: z.string().optional(), + created_by: z.number().optional(), + }) + .passthrough(); // Allow additional fields for flexibility (e.g., updatedData, previousData, createdData) +export type ActionLogMetadataData = z.infer; + +export const ActionLogEntitySchema = BaseEntitySchema.extend({ + workspace_id: z.number(), + user_id: z.number(), + type: ActionLogTypeSchema, + metadata: ActionLogMetadataSchema.optional(), +}); +export type ActionLogData = z.infer; + +export const ActionLogCreateSchema = ActionLogEntitySchema.partial().extend({ + type: ActionLogTypeSchema, +}); + +// ── User Quota ── + +export const UserQuotaSchema = z.object({ + user_id: z.number(), + limit: z.number(), + used: z.number(), + remaining: z.number(), +}); +export type UserQuotaData = z.infer; + +// Shared across root (AppEventSchema reaction/run payload) and insight domain. +export const SuggestedSimulationSchema = z.object({ + description: z.string(), + selected: z.boolean(), + simulation_id: z.number().nullable().optional(), + task_id: z.string().nullable().optional(), + status: SimulationStatusSchema.nullable().optional(), +}); +export type SuggestedSimulation = z.infer; diff --git a/src/sdk/contracts/widget.ts b/src/sdk/contracts/widget.ts new file mode 100644 index 00000000..9a1d7fbb --- /dev/null +++ b/src/sdk/contracts/widget.ts @@ -0,0 +1,243 @@ +import { eventIterator, oc } from '@orpc/contract'; +import { z } from 'zod'; + +import { ByWidgetIdSchema, paginatedListOf, PaginationSchema } from './common'; +import { + AgentEntitySchema, + ApplicationReadSchema, + UserEntitySchema, + WidgetEntitySchema, + WidgetSettingsDataSchema, + WidgetTypeSchema, + WorkspaceEntitySchema, +} from './entities'; + +// ---- widget-only schemas ---- + +export const WidgetInfoSchema = WidgetEntitySchema.extend({ + application: ApplicationReadSchema.partial(), + workspace: WorkspaceEntitySchema.partial(), + user: UserEntitySchema.partial(), + agent: AgentEntitySchema.partial(), +}); +export type WidgetInfoData = z.infer; + +/** + * Widget search result schema — includes optional eager-loaded agent + */ +export const WidgetWithAgentSchema = WidgetEntitySchema.extend({ + agent: AgentEntitySchema.partial().optional(), +}); +export type WidgetWithAgentData = z.infer; + +/** + * Application with widgets schema — matches API response structure + */ +export const ApplicationWithWidgetsSchema = ApplicationReadSchema.extend({ + widgets: z.array(WidgetEntitySchema).optional(), + agents: z.array(AgentEntitySchema).optional(), +}); +export type ApplicationWithWidgetsData = z.infer; + +export const WidgetCreateSchema = WidgetEntitySchema.partial().extend({ + application_id: z.number().positive(), + agent_id: z.number().positive(), + type: WidgetTypeSchema, + settings: WidgetSettingsDataSchema.optional(), +}); +export type WidgetCreateData = z.infer; + +export const WidgetUpdateSchema = WidgetEntitySchema.partial(); +export type WidgetUpdateData = z.infer; + +export type WidgetSettingsKey = keyof z.infer; + +/** Server → Widget event (discriminated union on `type`) */ +export const WidgetEventSchema = z.discriminatedUnion('type', [ + z.object({ type: z.literal('registered'), chat_id: z.string(), application_id: z.number().optional() }), + z.object({ type: z.literal('pong') }), + z.object({ type: z.literal('heartbeat') }), + z.object({ + type: z.literal('chat/response'), + request_id: z.string(), + text: z.string(), + task_id: z.string().optional(), + }), + z.object({ + type: z.literal('chat/error'), + request_id: z.string(), + error: z.string(), + }), + z.object({ + type: z.literal('task/status'), + // Matches SimulationTaskStatus / QATaskStatus on the agent side. + status: z.enum(['running', 'completed', 'failed', 'stopped', 'has_question']), + message: z.string().optional(), + task_id: z.string().optional(), + timestamp: z.number().optional(), + }), + z.object({ + type: z.literal('tool/call'), + call_id: z.string(), + tool: z.string(), + args: z.record(z.string(), z.unknown()), + mode: z.enum(['show', 'do']).optional(), + explanation: z.string().optional(), + state_version: z.number().optional(), + }), +]); +export type WidgetEvent = z.infer; + +/** Widget → Server command (discriminated union on `type`) */ +export const WidgetCommandSchema = z.discriminatedUnion('type', [ + z.object({ type: z.literal('chat/tell'), request_id: z.string(), content: z.string() }), + z.object({ type: z.literal('chat/show'), request_id: z.string(), content: z.string() }), + z.object({ type: z.literal('chat/do'), request_id: z.string(), content: z.string() }), + z.object({ type: z.literal('chat/stop'), task_id: z.string().optional() }), + z.object({ + type: z.literal('tool/response'), + call_id: z.string(), + success: z.boolean(), + data: z.string().optional(), + error: z.string().optional(), + state_version: z.number().optional(), + }), + z.object({ type: z.literal('ping') }), + z.object({ + type: z.literal('rrweb/metadata'), + session_id: z.string(), + chat_id: z.string(), + application_id: z.number(), + url: z.string().optional(), + user_agent: z.string().optional(), + timestamp: z.number().optional(), + viewport: z + .object({ + width: z.number(), + height: z.number(), + }) + .optional(), + }), + z.object({ + type: z.literal('rrweb/events'), + session_id: z.string(), + events: z.array(z.unknown()), + }), +]); +export type WidgetCommand = z.infer; + +// ---- procedures ---- + +export const widgetCreate = oc + .route({ + method: 'POST', + tags: ['Widget'], + path: '/widgets', + summary: 'Create a new widget', + description: 'Creates a new widget for an application and returns the created entity. Requires an application_id.', + }) + .input(WidgetCreateSchema) + .output(WidgetEntitySchema); + +export const widgetSearch = oc + .route({ + method: 'GET', + tags: ['Widget'], + path: '/widgets', + summary: 'Search widgets for workspace', + description: 'Search widgets by type, application, marketrix_id, or marketrix_key', + }) + .input( + z + .object({ + type: WidgetTypeSchema.optional(), + application_id: z.coerce.number().optional(), + marketrix_id: z.string().optional(), + marketrix_key: z.string().optional(), + include: z.array(z.enum(['agent'])).optional(), + }) + .extend(PaginationSchema.shape), + ) + .output(paginatedListOf(WidgetWithAgentSchema)); + +export const widgetGetDefaults = oc + .route({ + method: 'GET', + tags: ['Widget'], + path: '/widgets/defaults/{type}', + summary: 'Get default settings for widget type', + description: 'Returns default settings for the specified widget type', + }) + .input(z.object({ type: WidgetTypeSchema })) + .output(WidgetSettingsDataSchema); + +export const widgetUpdate = oc + .route({ + method: 'PUT', + tags: ['Widget'], + path: '/widgets/{widget_id}', + summary: 'Update widget', + description: 'Updates widget settings and configuration', + }) + .input(WidgetUpdateSchema.extend({ widget_id: z.coerce.number() })) + .output(WidgetEntitySchema); + +export const widgetDelete = oc + .route({ + method: 'DELETE', + tags: ['Widget'], + path: '/widgets/{widget_id}', + summary: 'Delete widget', + description: 'Permanently deletes a widget from an application. This action cannot be undone.', + }) + .input(ByWidgetIdSchema) + .output(z.object({ success: z.literal(true) })); + +export const widgetStream = oc + .route({ + method: 'GET', + tags: ['Widget'], + path: '/widget/stream', + summary: 'SSE stream for real-time widget events', + description: + 'Typed event stream delivering tool calls, task status updates, chat responses, and registration confirmation.', + }) + .input( + z.object({ + chat_id: z.string(), + tab_id: z.string().optional(), + marketrix_id: z.string().optional(), + marketrix_key: z.string().optional(), + agent_id: z.coerce.number().optional(), + application_id: z.coerce.number().optional(), + }), + ) + .output(eventIterator(WidgetEventSchema)); + +export const widgetMessage = oc + .route({ + method: 'POST', + tags: ['Widget'], + path: '/widget/message', + summary: 'Send a typed command from widget to server', + description: 'Receives chat commands, tool responses, and keepalive pings from the widget.', + }) + .input( + z.object({ + chat_id: z.string(), + command: WidgetCommandSchema, + }), + ) + .output(z.object({ ok: z.boolean() })); + +// ---- domain aggregate ---- + +export const widgetRoutes = { + widgetCreate, + widgetSearch, + widgetGetDefaults, + widgetUpdate, + widgetDelete, + widgetStream, + widgetMessage, +}; diff --git a/src/sdk/index.ts b/src/sdk/index.ts index d6c82ef0..9e098668 100644 --- a/src/sdk/index.ts +++ b/src/sdk/index.ts @@ -2,13 +2,13 @@ import { createORPCClient } from '@orpc/client'; import { RPCLink } from '@orpc/client/fetch'; import type { ContractRouterClient } from '@orpc/contract'; -import type { contract } from './routes'; +import type { widgetContract } from './contract'; let authToken: string | null = null; let currentApiUrl: string = ''; -let client: ContractRouterClient; +let client: ContractRouterClient; -function createClient(apiUrl: string): ContractRouterClient { +function createClient(apiUrl: string): ContractRouterClient { const link = new RPCLink({ url: apiUrl, headers: () => { @@ -58,7 +58,7 @@ type SdkExtras = typeof sdkExtras; // oRPC's client is a deep Proxy that intercepts all string property accesses // (including .bind, .call, etc.) as route path segments, so we must not call // .bind() on it — just return the property directly. -export const sdk = new Proxy({} as ContractRouterClient & SdkExtras, { +export const sdk = new Proxy({} as ContractRouterClient & SdkExtras, { get(_target, prop) { if (prop in sdkExtras) { return sdkExtras[prop as keyof typeof sdkExtras]; @@ -67,11 +67,25 @@ export const sdk = new Proxy({} as ContractRouterClient & SdkEx }, }); -// Export all types from schema -export * from './schema'; +// Runtime value re-exports (used by services, tests, and widget stream handler) +export { WidgetSettingsDataSchema } from './contracts/entities'; +export { WidgetEventSchema } from './contracts/widget'; + +// Type-only re-exports from entities +export type { + AgentData, + ApplicationData, + InstructionType, + UserData, + WidgetChip, + WidgetData, + WidgetSettingsData, + WorkspaceData, +} from './contracts/entities'; + +// Type-only re-exports from widget contract +export type { WidgetCommand, WidgetEvent, WidgetSettingsKey } from './contracts/widget'; // Type-only export: the oRPC client builds requests from the proxied path and -// never needs the `contract` VALUE at runtime. Re-exporting the value pulled the -// whole routes.ts contract (and the 291 zod schemas it references) into the -// embeddable bundle. Keeping it type-only drops all of that from the shipped JS. -export type { contract } from './routes'; +// never needs the `widgetContract` VALUE at runtime. +export type { widgetContract } from './contract'; diff --git a/src/sdk/routes.ts b/src/sdk/routes.ts deleted file mode 100644 index e25e2725..00000000 --- a/src/sdk/routes.ts +++ /dev/null @@ -1,2696 +0,0 @@ -import { eventIterator, oc } from '@orpc/contract'; -import { z } from 'zod'; - -import { - ActionCreateSchema, - ActionEntitySchema, - ActionLogCreateSchema, - ActionLogEntitySchema, - ActionLogTypeSchema, - ActionSearchSchema, - ActionUpdateSchema, - AgentCreateSchema, - AgentEntitySchema, - AgentSimulationIndexRequestSchema, - AgentSimulationIndexResponseSchema, - AgentUpdateSchema, - AppEventSchema, - AppEventScopeSchema, - ApplicationCreateSchema, - ApplicationReadSchema, - ApplicationTypeSchema, - ApplicationUpdateSchema, - AutomationCreateSchema, - AutomationEntitySchema, - AutomationRunEntitySchema, - AutomationRunSearchSchema, - AutomationSearchSchema, - AutomationUpdateSchema, - BrowserConfigSchema, - BrowserTypeSchema, - ByAgentIdSchema, - ByApplicationIdSchema, - ByIdSchema, - BySimulationIdSchema, - BySlugSchema, - ByUserIdSchema, - ByWidgetIdSchema, - ChatContextResponseSchema, - CheckoutSessionSchema, - ConnectorCapabilitySchema, - ContextRefSchema, - DiffValueSchema, - DomainPersonaSuggestResponseSchema, - EntityStatusSchema, - FailureAnalysisSchema, - HealthResponseSchema, - HeatmapCandidateSchema, - HeatmapJobStatusSchema, - HeatmapPageEntitySchema, - HeatmapSnapshotEntitySchema, - HeatmapTypeSchema, - HeatmapVariationSchema, - IndexResponseSchema, - InsightAudienceCoverageSchema, - InsightFindingsListInputSchema, - InsightFindingsListResponseSchema, - InsightFindingSummarizeInputSchema, - InsightFindingSummarizeResponseSchema, - InsightPersonaCompareInputSchema, - InsightPersonaCompareResponseSchema, - InsightPersonaEntitySchema, - InsightPersonaPinInputSchema, - InsightPersonasOverviewSchema, - InsightPersonasResponseSchema, - InsightPersonasSaveDomainInputSchema, - InsightPersonaStatsSchema, - InsightPersonaUpdateInputSchema, - InsightSimulationTopPagesInputSchema, - InsightSimulationTopPagesResponseSchema, - KnowledgeEntitySchema, - KnowledgeTypeSchema, - listOf, - McpStatusSchema, - McpToolSchema, - MindMapSchema, - NotificationChannelsSchema, - NotificationEntitySchema, - NotificationSlackTestSchema, - paginatedListOf, - PaginationSchema, - PlanCatalogSchema, - PlanInfoSchema, - PortalSessionSchema, - ProviderEntitySchema, - ProviderNameSchema, - PublicConfigSchema, - PushSubscriptionRegisterSchema, - PushSubscriptionUnregisterSchema, - QAFlowCreateSchema, - QAFlowEntitySchema, - QAFlowStatusSchema, - QAHealingAttemptEntrySchema, - QARunEntitySchema, - QARunTaskEntitySchema, - QATestCaseEntitySchema, - QAVersionHistoryEntrySchema, - ReactionEntitySchema, - ReactionRunEntitySchema, - SessionEntitySchema, - SessionStatsResponseSchema, - SimulationAnswerSchema, - SimulationCreateSchema, - SimulationEntitySchema, - SimulationListEntitySchema, - SimulationProgressEntitySchema, - SimulationStepSchema, - SimulationStepSummarySchema, - SimulationUpdateSchema, - SkillDetailSchema, - SkillListRowSchema, - SlackCapabilitySchema, - SlackCommandLogEntitySchema, - SlackCommandLogSearchSchema, - StateTriggerCreateSchema, - StateTriggerEntitySchema, - StripeCheckoutSchema, - StripeConfigSchema, - StripeDowngradeResponseSchema, - StripeDowngradeSchema, - StripePortalSchema, - StripePricingSchema, - StripeTrialSchema, - SubscriptionUsageSchema, - SuccessSchema, - SuccessWithMessageSchema, - SuggestedSimulationSchema, - TrialSubscriptionSchema, - TriggerCreateSchema, - TriggerEntitySchema, - TriggerSearchSchema, - TriggerUpdateSchema, - UserEntitySchema, - UserQuotaSchema, - UserUpdateSchema, - VapidPublicKeyResponseSchema, - WidgetCommandSchema, - WidgetCreateSchema, - WidgetEntitySchema, - WidgetEventSchema, - WidgetSettingsDataSchema, - WidgetTypeSchema, - WidgetUpdateSchema, - WidgetWithAgentSchema, - WorkspaceCreateSchema, - WorkspaceEntitySchema, - WorkspaceMemberEntitySchema, - WorkspaceMemberRoleSchema, - WorkspaceUpdateSchema, -} from './schema'; - -// Main contract with all routes -const contract = { - getIndex: oc - .route({ - method: 'GET', - tags: ['Root'], - path: '/', - summary: 'Get API index information and basic system details', - description: 'Returns general API information, version, and available endpoints', - }) - .output(IndexResponseSchema), - - getHealth: oc - .route({ - method: 'GET', - tags: ['Internal'], - path: '/health', - summary: 'Health check endpoint for monitoring system status', - description: 'Returns current system health, database connectivity, and service status', - }) - .output(HealthResponseSchema), - - validateUrl: oc - .route({ - method: 'GET', - tags: ['Root'], - path: '/validate-url', - summary: 'Validate URL accessibility and DNS resolution', - description: 'Validates a URL for format, DNS resolution, and HTTP accessibility', - }) - .input( - z.object({ - url: z.string().url(), - skip_network: z.string().optional(), - }), - ) - .output( - z.object({ - valid: z.boolean(), - accessible: z.boolean(), - dnsResolves: z.boolean(), - error: z.string().optional().describe('Error message if URL validation failed'), - warnings: z.array(z.string()).optional().describe('Non-fatal issues found during URL validation'), - }), - ), - - getPublicConfig: oc - .route({ - method: 'GET', - tags: ['Root'], - path: '/config/public', - summary: 'Get public client configuration', - description: 'Returns widget key and widget ID. No auth. Values are stored in backend env only.', - }) - .output(PublicConfigSchema), - - authMe: oc - .route({ - method: 'GET', - tags: ['Auth'], - path: '/auth/me', - summary: 'Get current authenticated user and workspace information', - description: 'Returns user profile and workspace data based on session cookie', - }) - .output( - z.object({ - user: UserEntitySchema, - workspace: WorkspaceEntitySchema.nullable(), - workspaces: z.array( - z.object({ - id: z.number(), - name: z.string(), - slug: z.string(), - role: z.enum(['owner', 'member']), - }), - ), - }), - ), - - /** @docs-only — raw Express handler; returns HTML, not JSON */ - emailAction: oc - .route({ - method: 'GET', - tags: ['Auth'], - path: '/email-action/{token}', - summary: 'Process email approval/rejection action', - description: - 'Handles approve/reject links from admin emails. Token-in-URL authentication (no session required). Returns an HTML page, not JSON.', - }) - .input(z.object({ token: z.string().min(1) })) - .output(z.object({ result: z.string().describe('HTML response page') })), - - onboard: oc - .route({ - method: 'POST', - tags: ['Onboarding'], - path: '/onboard', - summary: 'Complete user onboarding in one atomic operation', - description: 'Creates workspace, updates user status, and invites team members', - }) - .input( - z.object({ - name: z.string().min(1).max(45), - slug: z.string().min(1).max(45).optional(), - domain: z.string().min(1).max(200), - team_emails: z.array(z.string().email()).optional().default([]), - app_url: z.string().optional(), - app_username: z.string().optional(), - app_password: z.string().optional(), - }), - ) - .output( - z.object({ - workspace: WorkspaceEntitySchema, - }), - ), - - workspaceCreate: oc - .route({ - method: 'POST', - tags: ['Workspace'], - path: '/workspaces', - summary: 'Create a new workspace', - description: 'Creates new workspace with specified settings and returns workspace entity', - }) - .input(WorkspaceCreateSchema) - .output(WorkspaceEntitySchema), - - workspaceSearch: oc - .route({ - method: 'GET', - tags: ['Workspace'], - path: '/workspaces', - summary: 'Search and filter workspaces by various criteria', - description: 'Returns list of workspaces matching search parameters (name, domain, email, etc.)', - }) - .input( - z - .object({ - name: z.string().optional(), - domain: z.string().optional(), - email: z.string().email().optional(), - application_id: z.coerce.number().optional(), - workspace_id: z.coerce.number().optional(), - user_id: z.coerce.number().optional(), - }) - .extend(PaginationSchema.shape), - ) - .output(paginatedListOf(WorkspaceEntitySchema)), - - workspaceLookup: oc - .route({ - method: 'GET', - tags: ['Workspace'], - path: '/workspaces/lookup/{slug}', - summary: 'Look up workspace by slug for onboarding', - description: 'Returns workspace name and slug for join-workspace flow. Requires onboarding-level auth.', - }) - .input(BySlugSchema) - .output(z.object({ name: z.string(), slug: z.string() }).nullable()), - - workspaceGet: oc - .route({ - method: 'GET', - tags: ['Workspace'], - path: '/workspaces/{slug}', - summary: 'Get specific workspace details by slug', - description: 'Returns complete workspace information including settings and configuration', - }) - .input(BySlugSchema) - .output(WorkspaceEntitySchema), - - workspaceSelect: oc - .route({ - method: 'POST', - tags: ['Workspace'], - path: '/workspaces/{slug}/select', - summary: 'Select the active workspace for the current session', - description: 'Updates the server-side session workspace context to match the selected workspace slug.', - }) - .input(BySlugSchema) - .output(WorkspaceEntitySchema), - - workspaceUpdate: oc - .route({ - method: 'PUT', - tags: ['Workspace'], - path: '/workspaces/{slug}', - summary: 'Update workspace settings and configuration', - description: 'Modifies workspace properties like name, domain, or feature settings', - }) - .input(WorkspaceUpdateSchema.extend({ slug: z.string() })) - .output(WorkspaceEntitySchema), - - // ── Workspace Members ─────────────────────────────────────────────── - workspaceMemberList: oc - .route({ - method: 'GET', - tags: ['Workspace'], - path: '/workspaces/{slug}/members', - summary: 'List workspace members', - description: 'Returns all members of the workspace with their user profile details and roles.', - }) - .input(BySlugSchema) - .output( - listOf( - WorkspaceMemberEntitySchema.extend({ - user: UserEntitySchema.pick({ id: true, email: true, first_name: true, last_name: true, image_url: true }), - }), - ), - ), - - workspaceMemberAdd: oc - .route({ - method: 'POST', - tags: ['Workspace'], - path: '/workspaces/{slug}/members', - summary: 'Add member to workspace', - description: 'Invites a user by email to join the workspace with the specified role. Defaults to member role.', - }) - .input(BySlugSchema.extend({ email: z.string().email(), role: WorkspaceMemberRoleSchema.default('member') })) - .output(WorkspaceMemberEntitySchema), - - workspaceMemberRemove: oc - .route({ - method: 'DELETE', - tags: ['Workspace'], - path: '/workspaces/{slug}/members/{user_id}', - summary: 'Remove member from workspace', - description: 'Removes a user from the workspace by user_id. The user loses access immediately.', - }) - .input(BySlugSchema.extend({ user_id: z.number() })) - .output(SuccessSchema), - - workspaceMemberUpdateRole: oc - .route({ - method: 'PATCH', - tags: ['Workspace'], - path: '/workspaces/{slug}/members/{user_id}', - summary: 'Update member role', - description: 'Changes the role of a workspace member. Valid roles: owner, member.', - }) - .input(BySlugSchema.extend({ user_id: z.number(), role: WorkspaceMemberRoleSchema })) - .output(WorkspaceMemberEntitySchema), - - workspaceListForUser: oc - .route({ - method: 'GET', - tags: ['Workspace'], - path: '/users/me/workspaces', - summary: 'List workspaces for current user', - description: 'Returns all workspaces the authenticated user belongs to, including their role in each.', - }) - .output(listOf(WorkspaceEntitySchema.extend({ role: WorkspaceMemberRoleSchema }))), - - applicationCreate: oc - .route({ - method: 'POST', - tags: ['Application'], - path: '/applications', - summary: 'Create a new application', - description: 'Creates a new application for the authenticated workspace and returns the created entity.', - }) - .input(ApplicationCreateSchema) - .output(ApplicationReadSchema), - - applicationSearch: oc - .route({ - method: 'GET', - tags: ['Application'], - path: '/applications', - summary: 'Search applications for workspace', - description: - 'Returns applications for the authenticated workspace, optionally filtered by type, always includes widgets', - }) - .input( - z - .object({ - type: ApplicationTypeSchema.optional(), - include: z.array(z.enum(['widgets'])).optional(), - }) - .extend(PaginationSchema.shape), - ) - .output( - paginatedListOf( - ApplicationReadSchema.extend({ - widgets: z.array(WidgetEntitySchema).optional(), - }), - ), - ), - - applicationGet: oc - .route({ - method: 'GET', - tags: ['Application'], - path: '/applications/{application_id}', - summary: 'Get application by ID', - description: 'Returns specific application details by ID, always includes widgets', - }) - .input(ByApplicationIdSchema) - .output( - ApplicationReadSchema.extend({ - widgets: z.array(WidgetEntitySchema), - }), - ), - - applicationUpdate: oc - .route({ - method: 'PUT', - tags: ['Application'], - path: '/applications/{application_id}', - summary: 'Update application', - description: 'Updates application details and configuration', - }) - .input(ApplicationUpdateSchema.extend({ application_id: z.coerce.number() })) - .output(ApplicationReadSchema), - - applicationDelete: oc - .route({ - method: 'DELETE', - tags: ['Application'], - path: '/applications/{application_id}', - summary: 'Delete application', - description: 'Permanently deletes an application and all associated widgets. This action cannot be undone.', - }) - .input(ByApplicationIdSchema) - .output(SuccessSchema), - - widgetCreate: oc - .route({ - method: 'POST', - tags: ['Widget'], - path: '/widgets', - summary: 'Create a new widget', - description: - 'Creates a new widget for an application and returns the created entity. Requires an application_id.', - }) - .input(WidgetCreateSchema) - .output(WidgetEntitySchema), - - widgetSearch: oc - .route({ - method: 'GET', - tags: ['Widget'], - path: '/widgets', - summary: 'Search widgets for workspace', - description: 'Search widgets by type, application, marketrix_id, or marketrix_key', - }) - .input( - z - .object({ - type: WidgetTypeSchema.optional(), - application_id: z.coerce.number().optional(), - marketrix_id: z.string().optional(), - marketrix_key: z.string().optional(), - include: z.array(z.enum(['agent'])).optional(), - }) - .extend(PaginationSchema.shape), - ) - .output(paginatedListOf(WidgetWithAgentSchema)), - - widgetGetDefaults: oc - .route({ - method: 'GET', - tags: ['Widget'], - path: '/widgets/defaults/{type}', - summary: 'Get default settings for widget type', - description: 'Returns default settings for the specified widget type', - }) - .input(z.object({ type: WidgetTypeSchema })) - .output(WidgetSettingsDataSchema), - - widgetGet: oc - .route({ - method: 'GET', - tags: ['Widget'], - path: '/widgets/{widget_id}', - summary: 'Get widget by ID', - description: 'Returns specific widget details by widget ID including snippet code', - }) - .input(ByWidgetIdSchema) - .output(WidgetEntitySchema), - - widgetUpdate: oc - .route({ - method: 'PUT', - tags: ['Widget'], - path: '/widgets/{widget_id}', - summary: 'Update widget', - description: 'Updates widget settings and configuration', - }) - .input(WidgetUpdateSchema.extend({ widget_id: z.coerce.number() })) - .output(WidgetEntitySchema), - - widgetDelete: oc - .route({ - method: 'DELETE', - tags: ['Widget'], - path: '/widgets/{widget_id}', - summary: 'Delete widget', - description: 'Permanently deletes a widget from an application. This action cannot be undone.', - }) - .input(ByWidgetIdSchema) - .output(SuccessSchema), - - chatCreate: oc - .route({ - method: 'POST', - tags: ['Chat'], - path: '/chat', - summary: 'Create a new chat thread', - description: 'Initializes new chat thread and returns session ID', - }) - .output(z.string()), - - chatSearch: oc - .route({ - method: 'GET', - tags: ['Chat'], - path: '/chat', - summary: 'Search chats by user', - description: 'Returns chat list and quota for the specified user', - }) - .input( - z.object({ - user_id: z.coerce.number().optional(), - chat_id: z.coerce.number().optional(), - }), - ) - .output(UserQuotaSchema), - - mcpActivate: oc - .route({ - method: 'POST', - tags: ['MCP'], - path: '/mcp/activate', - summary: 'Activate MCP for an application', - description: 'Creates or reactivates an MCP connector with auto-generated credentials.', - }) - .input(z.object({ application_id: z.coerce.number() })) - .output(McpStatusSchema), - - mcpDeactivate: oc - .route({ - method: 'POST', - tags: ['MCP'], - path: '/mcp/deactivate', - summary: 'Deactivate MCP for an application', - description: 'Deactivates the MCP connector. Credentials are preserved but access is revoked.', - }) - .input(z.object({ application_id: z.coerce.number() })) - .output(McpStatusSchema), - - mcpRegenerate: oc - .route({ - method: 'POST', - tags: ['MCP'], - path: '/mcp/regenerate', - summary: 'Regenerate MCP API key', - description: 'Generates a new API key for the MCP connector. Existing integrations will break.', - }) - .input(z.object({ application_id: z.coerce.number() })) - .output(McpStatusSchema), - - mcpStatus: oc - .route({ - method: 'GET', - tags: ['MCP'], - path: '/mcp/status/{application_id}', - summary: 'Get MCP activation status', - description: 'Returns the MCP connector status for an application, or null if not activated.', - }) - .input(z.object({ application_id: z.coerce.number() })) - .output(McpStatusSchema.nullable()), - - mcpTools: oc - .route({ - method: 'GET', - tags: ['MCP'], - path: '/mcp/tools/{application_id}', - summary: 'List MCP platform tools', - description: 'Returns all platform tools with their enabled/disabled state.', - }) - .input(z.object({ application_id: z.coerce.number() })) - .output(z.array(McpToolSchema)), - - mcpToolToggle: oc - .route({ - method: 'PATCH', - tags: ['MCP'], - path: '/mcp/tools/toggle', - summary: 'Toggle a platform tool', - description: 'Enable or disable a specific platform tool for the MCP server.', - }) - .input(z.object({ application_id: z.coerce.number(), tool_name: z.string(), enabled: z.boolean() })) - .output(z.array(McpToolSchema)), - - automationCreate: oc - .route({ - method: 'POST', - tags: ['Automation'], - path: '/automation', - summary: 'Create automation', - description: 'Creates a DAG-based automation workflow', - }) - .input(AutomationCreateSchema) - .output(AutomationEntitySchema), - - automationSearch: oc - .route({ - method: 'GET', - tags: ['Automation'], - path: '/automation', - summary: 'List automations', - description: 'Returns automations for the current workspace', - }) - .input(AutomationSearchSchema) - .output(paginatedListOf(AutomationEntitySchema)), - - automationGet: oc - .route({ - method: 'GET', - tags: ['Automation'], - path: '/automation/{id}', - summary: 'Get automation', - description: 'Returns a single automation with its graph', - }) - .input(ByIdSchema) - .output(AutomationEntitySchema.nullable()), - - automationUpdate: oc - .route({ - method: 'PUT', - tags: ['Automation'], - path: '/automation/{id}', - summary: 'Update automation', - description: 'Updates automation graph and settings', - }) - .input(AutomationUpdateSchema) - .output(AutomationEntitySchema), - - automationDelete: oc - .route({ - method: 'DELETE', - tags: ['Automation'], - path: '/automation/{id}', - summary: 'Delete automation', - description: 'Permanently deletes an automation and deregisters its triggers', - }) - .input(ByIdSchema) - .output(SuccessSchema), - - automationToggle: oc - .route({ - method: 'PATCH', - tags: ['Automation'], - path: '/automation/{id}/toggle', - summary: 'Toggle automation', - description: 'Enable or disable an automation', - }) - .input(z.object({ id: z.coerce.number(), enabled: z.boolean() })) - .output(AutomationEntitySchema), - - automationRun: oc - .route({ - method: 'POST', - tags: ['Automation'], - path: '/automation/{id}/run', - summary: 'Trigger automation run', - description: 'Manually triggers an automation run', - }) - .input(ByIdSchema) - .output(AutomationRunEntitySchema.nullable()), - - automationRunSearch: oc - .route({ - method: 'GET', - tags: ['Automation'], - path: '/automation/{id}/runs', - summary: 'List automation runs', - description: 'Returns execution history for an automation', - }) - .input(z.object({ id: z.coerce.number() }).extend(AutomationRunSearchSchema.shape)) - .output(paginatedListOf(AutomationRunEntitySchema)), - - automationRunGet: oc - .route({ - method: 'GET', - tags: ['Automation'], - path: '/automation/{id}/runs/{run_id}', - summary: 'Get automation run', - description: 'Returns a single run with per-node results', - }) - .input(z.object({ id: z.coerce.number(), run_id: z.coerce.number() })) - .output(AutomationRunEntitySchema.nullable()), - - providerGet: oc - .route({ - method: 'GET', - tags: ['Provider'], - path: '/provider/{provider}', - summary: 'Get provider record for a workspace', - }) - .input(z.object({ provider: ProviderNameSchema })) - .output(ProviderEntitySchema.nullable()), - - providerDelete: oc - .route({ - method: 'DELETE', - tags: ['Provider'], - path: '/provider/{provider}', - summary: 'Disconnect a provider', - }) - .input(z.object({ provider: ProviderNameSchema })) - .output(SuccessSchema), - - providerRefresh: oc - .route({ - method: 'POST', - tags: ['Provider'], - path: '/provider/{provider}/refresh', - summary: 'Re-fetch provider data using stored credentials', - }) - .input(z.object({ provider: ProviderNameSchema })) - .output(ProviderEntitySchema), - - triggerCreate: oc - .route({ - method: 'POST', - tags: ['Trigger'], - path: '/trigger', - summary: 'Create a new trigger', - }) - .input(TriggerCreateSchema) - .output(TriggerEntitySchema), - - triggerGet: oc - .route({ - method: 'GET', - tags: ['Trigger'], - path: '/trigger/{trigger_id}', - summary: 'Get a single trigger', - }) - .input(z.object({ trigger_id: z.coerce.number() })) - .output(TriggerEntitySchema), - - triggerSearch: oc - .route({ - method: 'GET', - tags: ['Trigger'], - path: '/trigger', - summary: 'List triggers', - }) - .input(TriggerSearchSchema) - .output(paginatedListOf(TriggerEntitySchema)), - - triggerUpdate: oc - .route({ - method: 'PUT', - tags: ['Trigger'], - path: '/trigger/{trigger_id}', - summary: 'Update trigger', - }) - .input(TriggerUpdateSchema) - .output(TriggerEntitySchema), - - triggerDelete: oc - .route({ - method: 'DELETE', - tags: ['Trigger'], - path: '/trigger/{trigger_id}', - summary: 'Delete a trigger', - }) - .input(z.object({ trigger_id: z.coerce.number() })) - .output(SuccessSchema), - - triggerToggle: oc - .route({ - method: 'PATCH', - tags: ['Trigger'], - path: '/trigger/{trigger_id}/toggle', - summary: 'Toggle trigger enabled state', - }) - .input(z.object({ trigger_id: z.coerce.number() })) - .output(TriggerEntitySchema), - - triggerAutoInstall: oc - .route({ - method: 'POST', - tags: ['Trigger'], - path: '/trigger/{trigger_id}/install', - summary: 'Auto-install trigger configuration on the provider', - }) - .input(z.object({ trigger_id: z.coerce.number() })) - .output(z.object({ success: z.boolean(), message: z.string() })), - - skillList: oc - .route({ - method: 'GET', - tags: ['Skill'], - path: '/workspaces/{workspace_id}/applications/{application_id}/skills', - summary: 'List active learned skills for an application', - description: - 'Returns the current non-deprecated version of each learned skill for (workspace, application). Browser_ops excluded — they are global and not listed here.', - }) - .input( - z.object({ - workspace_id: z.coerce.number().int(), - application_id: z.coerce.number().int(), - }), - ) - .output(z.array(SkillListRowSchema)), - - skillGet: oc - .route({ - method: 'GET', - tags: ['Skill'], - path: '/skills/{skill_id}', - summary: 'Get a skill with its full version lineage', - }) - .input(z.object({ skill_id: z.coerce.number().int() })) - .output(SkillDetailSchema), - - skillDeprecate: oc - .route({ - method: 'POST', - tags: ['Skill'], - path: '/skills/{skill_id}/deprecate', - summary: 'Soft-delete a learned skill (sets deprecated_at)', - }) - .input(z.object({ skill_id: z.coerce.number().int() })) - .output(z.object({ ok: z.literal(true) })), - - slackCommandLogSearch: oc - .route({ - method: 'GET', - tags: ['Slack'], - path: '/slack/command-log', - summary: 'Search slash command logs', - }) - .input(SlackCommandLogSearchSchema) - .output(paginatedListOf(SlackCommandLogEntitySchema)), - - slackCapabilities: oc - .route({ - method: 'GET', - tags: ['Slack'], - path: '/slack/capabilities', - summary: 'Get Slack capability stats', - }) - .output(z.array(SlackCapabilitySchema)), - - actionCreate: oc - .route({ - method: 'POST', - tags: ['Action'], - path: '/action', - summary: 'Create a custom action', - }) - .input(ActionCreateSchema) - .output(ActionEntitySchema), - - actionGet: oc - .route({ - method: 'GET', - tags: ['Action'], - path: '/action/{action_id}', - summary: 'Get a single action', - }) - .input(z.object({ action_id: z.coerce.number() })) - .output(ActionEntitySchema), - - actionSearch: oc - .route({ - method: 'GET', - tags: ['Action'], - path: '/action', - summary: 'List actions', - }) - .input(ActionSearchSchema) - .output(paginatedListOf(ActionEntitySchema)), - - actionUpdate: oc - .route({ - method: 'PUT', - tags: ['Action'], - path: '/action/{action_id}', - summary: 'Update action', - }) - .input(ActionUpdateSchema) - .output(ActionEntitySchema), - - actionDelete: oc - .route({ - method: 'DELETE', - tags: ['Action'], - path: '/action/{action_id}', - summary: 'Delete an action', - }) - .input(z.object({ action_id: z.coerce.number() })) - .output(SuccessSchema), - - actionToggle: oc - .route({ - method: 'PATCH', - tags: ['Action'], - path: '/action/{action_id}/toggle', - summary: 'Toggle action enabled state', - }) - .input(z.object({ action_id: z.coerce.number() })) - .output(ActionEntitySchema), - - connectorCapabilities: oc - .route({ - method: 'GET', - tags: ['Connector'], - path: '/connector-capabilities', - summary: 'List connector capabilities', - description: 'Returns available triggers and callbacks per connector type', - }) - .input(z.object({})) - .output(z.array(ConnectorCapabilitySchema)), - - userSearch: oc - .route({ - method: 'GET', - tags: ['User'], - path: '/user', - summary: 'Search and filter users by workspace', - description: 'Returns list of users associated with specified workspace', - }) - .input( - z - .object({ - workspace_id: z.coerce.number().optional(), - status: EntityStatusSchema.optional(), - }) - .extend(PaginationSchema.shape), - ) - .output(paginatedListOf(UserEntitySchema)), - - userGet: oc - .route({ - method: 'GET', - tags: ['User'], - path: '/user/{user_id}', - summary: 'Get specific user details by ID', - description: 'Returns complete user information including profile and settings', - }) - .input(ByUserIdSchema) - .output(UserEntitySchema), - - userUpdate: oc - .route({ - method: 'PUT', - tags: ['User'], - path: '/user/{user_id}', - summary: 'Update user profile and settings', - description: 'Modifies user properties like name, email, or preferences', - }) - .input(UserUpdateSchema.extend({ user_id: z.coerce.number() })) - .output(UserEntitySchema), - - agentCreate: oc - .route({ - method: 'POST', - tags: ['Agent'], - path: '/agent', - summary: 'Create new AI agent with configuration', - description: 'Creates agent with specified settings, prompts, and returns agent entity', - }) - .input(AgentCreateSchema) - .output(AgentEntitySchema), - - agentSearch: oc - .route({ - method: 'GET', - tags: ['Agent'], - path: '/agent', - summary: 'Search and filter agents by workspace or user', - description: - 'Returns list of agents matching search parameters. Supports filtering by workspace_id, user_id, and application_id.', - }) - .input( - z - .object({ - workspace_id: z.coerce.number().optional(), - user_id: z.coerce.number().optional(), - application_id: z.coerce.number().optional(), - include: z.array(z.enum(['knowledge', 'simulations'])).optional(), - }) - .extend(PaginationSchema.shape), - ) - .output(paginatedListOf(AgentEntitySchema)), - - agentGet: oc - .route({ - method: 'GET', - tags: ['Agent'], - path: '/agent/{agent_id}', - summary: 'Get specific agent details by ID', - description: 'Returns complete agent information including configuration and settings', - }) - .input(ByAgentIdSchema.extend({ include: z.array(z.enum(['knowledge', 'simulations'])).optional() })) - .output(AgentEntitySchema), - - agentMindmap: oc - .route({ - method: 'GET', - tags: ['Agent'], - path: '/agent/{agent_id}/mindmap', - summary: 'Get agent mindmap by ID', - description: 'Returns the mindmap knowledge graph for the specified agent', - }) - .input(ByAgentIdSchema) - .output(MindMapSchema), - - agentUpdate: oc - .route({ - method: 'PUT', - tags: ['Agent'], - path: '/agent/{agent_id}', - summary: 'Update agent configuration and settings', - description: - 'Updates agent details. For JSON requests, handled by oRPC. Also accepts multipart/form-data for logo file uploads (handled by raw Express route).', - }) - .input( - AgentUpdateSchema.extend({ - agent_id: z.coerce.number(), - force_reset_learning: z.coerce.boolean().optional(), - }), - ) - .output(AgentEntitySchema), - - agentDelete: oc - .route({ - method: 'DELETE', - tags: ['Agent'], - path: '/agent/{agent_id}', - summary: 'Delete agent and all associated data', - description: 'Permanently deletes an agent and cleans up all related resources. This action cannot be undone.', - }) - .input(ByAgentIdSchema) - .output(SuccessSchema), - - agentIndexSimulation: oc - .route({ - method: 'POST', - tags: ['Agent'], - path: '/agent/{agent_id}/index', - summary: 'Index simulation document into agent knowledge base', - description: "Adds a simulation document to an agent's knowledge base and refreshes the search index", - }) - .input(AgentSimulationIndexRequestSchema.extend({ agent_id: z.coerce.number() })) - .output(AgentSimulationIndexResponseSchema), - - activityLogCreate: oc - .route({ - method: 'POST', - tags: ['Activity Log'], - path: '/log', - summary: 'Create new activity log entry', - description: 'Records user or system action for auditing and tracking purposes', - }) - .input(ActionLogCreateSchema) - .output(ActionLogEntitySchema), - - activityLogSearch: oc - .route({ - method: 'GET', - tags: ['Activity Log'], - path: '/log', - summary: 'Search and filter activity logs', - description: 'Returns list of activity logs matching search parameters (workspace, type)', - }) - .input( - z - .object({ - workspace_id: z.coerce.number().optional(), - type: ActionLogTypeSchema.optional(), - application_id: z.coerce.number().optional(), - }) - .extend(PaginationSchema.shape), - ) - .output(paginatedListOf(ActionLogEntitySchema)), - - stateTriggerCreate: oc - .route({ - method: 'POST', - tags: ['State Trigger'], - path: '/state-trigger', - summary: 'Create new state trigger', - description: 'Creates state trigger with pattern and message', - }) - .input(StateTriggerCreateSchema) - .output(StateTriggerEntitySchema), - - simulationGet: oc - .route({ - method: 'GET', - tags: ['Simulation'], - path: '/simulation/{simulation_id}', - summary: 'Get a single simulation by ID', - }) - .input( - z.object({ - simulation_id: z.coerce.number(), - }), - ) - .output(SimulationEntitySchema), - - simulationSearch: oc - .route({ - method: 'GET', - tags: ['Simulation'], - path: '/simulation', - summary: 'Search and filter simulations by workspace', - description: - 'Returns list of simulations associated with specified workspace. Use application_id, agent_id, or pinned to filter.', - }) - .input( - z - .object({ - workspace_id: z.coerce.number().optional(), - application_id: z.coerce.number().optional(), - agent_id: z.coerce.number().optional(), - pinned: z - .union([z.boolean(), z.string()]) - .optional() - .transform(val => - val === undefined - ? undefined - : typeof val === 'boolean' - ? val - : val === 'true' - ? true - : val === 'false' - ? false - : undefined, - ), - source: z.enum(['direct', 'qa']).optional(), - include: z.array(z.enum(['agent', 'application'])).optional(), - }) - .extend(PaginationSchema.shape), - ) - .output(paginatedListOf(SimulationListEntitySchema)), - - simulationStart: oc - .route({ - method: 'POST', - tags: ['Simulation'], - path: '/simulation/start', - summary: 'Start new application simulation', - description: 'Creates and initializes simulation with specified parameters', - }) - .input(SimulationCreateSchema) - .output(SimulationEntitySchema), - - simulationUpdate: oc - .route({ - method: 'PUT', - tags: ['Simulation'], - path: '/simulation/{simulation_id}', - summary: 'Update simulation configuration and status', - description: 'Modifies simulation parameters and updates execution state', - }) - .input(SimulationUpdateSchema.extend({ simulation_id: z.coerce.number() })) - .output(SimulationEntitySchema), - - simulationAssignAgents: oc - .route({ - method: 'PUT', - tags: ['Simulation'], - path: '/simulation/{simulation_id}/agents', - summary: 'Assign agents to a simulation', - description: 'Sets which agents use this simulation for their knowledge. Triggers learning on affected agents.', - }) - .input( - BySimulationIdSchema.extend({ - agent_ids: z.array(z.number()), - }), - ) - .output(SimulationEntitySchema), - - simulationProgress: oc - .route({ - method: 'GET', - tags: ['Simulation'], - path: '/simulation/{simulation_id}/progress', - summary: 'Get simulation progress history as chat transcript', - description: 'Returns all progress updates for a simulation ordered chronologically', - }) - .input(BySimulationIdSchema.extend({ task_id: z.string().optional() })) - .output(listOf(SimulationProgressEntitySchema)), - - simulationTaskLiveView: oc - .route({ - method: 'GET', - tags: ['Simulation'], - path: '/simulation/{simulation_id}/task/{task_id}/live-view', - summary: 'Get per-task live view URL', - description: 'Returns Browserbase per-tab live view URL for a specific task within a simulation.', - }) - .input( - z.object({ - simulation_id: z.coerce.number().int().positive(), - task_id: z.string(), - }), - ) - .output( - z.object({ - live_view_url: z.string(), - status: z.string(), - }), - ), - - simulationHistory: oc - .route({ - method: 'GET', - tags: ['Simulation'], - path: '/simulation/{simulation_id}/history', - summary: 'Get simulation history by ID', - description: 'Returns the history array for the specified simulation', - }) - .input(BySimulationIdSchema.extend({ task_id: z.string() })) - .output(listOf(SimulationStepSchema)), - - simulationSteps: oc - .route({ - method: 'GET', - tags: ['Simulation'], - path: '/simulation/{simulation_id}/steps', - summary: 'Get all simulation steps with topic and screenshot link as JSON', - description: - 'Returns each step with step_number, topic, screenshot_url, and optional title/url for the simulation', - }) - .input(BySimulationIdSchema) - .output(listOf(SimulationStepSummarySchema)), - - simulationMindmap: oc - .route({ - method: 'GET', - tags: ['Simulation'], - path: '/simulation/{simulation_id}/mindmap', - summary: 'Get simulation mindmap by ID', - description: 'Returns the mindmap knowledge graph for the specified simulation', - }) - .input(BySimulationIdSchema) - .output(MindMapSchema), - - simulationStop: oc - .route({ - method: 'POST', - tags: ['Simulation'], - path: '/simulation/{simulation_id}/stop', - summary: 'Stop running simulation', - description: 'Terminates a running simulation and closes its browser session.', - }) - .input(BySimulationIdSchema) - .output(SuccessWithMessageSchema), - - simulationAnswer: oc - .route({ - method: 'POST', - tags: ['Simulation'], - path: '/simulation/{simulation_id}/answer', - summary: 'Submit answer to simulation question', - description: 'Submits an answer to a pending question from the simulation agent', - }) - .input(SimulationAnswerSchema.extend({ simulation_id: z.coerce.number() })) - .output(SuccessWithMessageSchema), - - simulationRetryMindmap: oc - .route({ - method: 'POST', - tags: ['Simulation'], - path: '/simulation/{simulation_id}/retry-mindmap', - summary: 'Retry mindmap generation', - description: 'Restarts mindmap generation from stored history for a simulation.', - }) - .input(z.object({ simulation_id: z.coerce.number() })) - .output(SuccessWithMessageSchema), - - simulationDelete: oc - .route({ - method: 'DELETE', - tags: ['Simulation'], - path: '/simulation/{simulation_id}', - summary: 'Delete simulation', - description: - 'Permanently deletes a simulation and all associated data. Only works on simulations in terminal state.', - }) - .input(BySimulationIdSchema) - .output(SuccessWithMessageSchema), - - notificationListPending: oc - .route({ - method: 'GET', - tags: ['Notification'], - path: '/notification/pending', - summary: 'List pending notifications for the current user', - }) - .output(listOf(NotificationEntitySchema)), - - notificationMarkRead: oc - .route({ - method: 'POST', - tags: ['Notification'], - path: '/notification/{id}/read', - summary: 'Mark a notification as read', - }) - .input(z.object({ id: z.coerce.number() })) - .output(SuccessSchema), - - notificationDismiss: oc - .route({ - method: 'POST', - tags: ['Notification'], - path: '/notification/{id}/dismiss', - summary: 'Dismiss a pending notification', - }) - .input(z.object({ id: z.coerce.number() })) - .output(SuccessSchema), - - notificationGetPreferences: oc - .route({ - method: 'GET', - tags: ['Notification'], - path: '/notification/preferences', - summary: 'Get notification preferences for the current (user, workspace)', - }) - .output(NotificationChannelsSchema), - - notificationUpdatePreferences: oc - .route({ - method: 'PUT', - tags: ['Notification'], - path: '/notification/preferences', - summary: 'Update notification preferences for the current (user, workspace)', - }) - .input(NotificationChannelsSchema) - .output(NotificationChannelsSchema), - - notificationGetVapidPublicKey: oc - .route({ - method: 'GET', - tags: ['Notification'], - path: '/notification/vapid-public-key', - summary: 'Public VAPID key for browser pushManager.subscribe', - }) - .output(VapidPublicKeyResponseSchema), - - notificationRegisterPushSubscription: oc - .route({ - method: 'POST', - tags: ['Notification'], - path: '/notification/push-subscription', - summary: 'Register a Web Push subscription for the current user', - }) - .input(PushSubscriptionRegisterSchema) - .output(SuccessSchema), - - notificationUnregisterPushSubscription: oc - .route({ - method: 'DELETE', - tags: ['Notification'], - path: '/notification/push-subscription', - summary: 'Unregister a Web Push subscription for the current user', - }) - .input(PushSubscriptionUnregisterSchema) - .output(SuccessSchema), - - notificationSendTestSlack: oc - .route({ - method: 'POST', - tags: ['Notification'], - path: '/notification/slack-test', - summary: 'Send a test message to a Slack incoming-webhook URL', - }) - .input(NotificationSlackTestSchema) - .output(SuccessWithMessageSchema), - - sessionSearch: oc - .route({ - method: 'GET', - tags: ['Session'], - path: '/sessions', - summary: 'Get all sessions', - description: 'Retrieves all sessions ordered by creation date, optionally filtered by application and date range', - }) - .input( - z - .object({ - application_id: z.coerce.number().optional(), - start_date: z.string().optional(), - end_date: z.string().optional(), - }) - .extend(PaginationSchema.shape), - ) - .output(paginatedListOf(SessionEntitySchema)), - - sessionStats: oc - .route({ - method: 'GET', - tags: ['Session'], - path: '/sessions/stats', - summary: 'Get session activity stats', - description: - 'Returns daily aggregated session metrics for charting. Each entry is one calendar day with session count and total event count.', - }) - .input( - z.object({ - application_id: z.coerce.number(), - start_date: z.string().describe('YYYY-MM-DD'), - end_date: z.string().describe('YYYY-MM-DD'), - }), - ) - .output(SessionStatsResponseSchema), - - sessionEvents: oc - .route({ - method: 'GET', - tags: ['Session'], - path: '/sessions/{session_id}/events', - summary: 'Get session events (paginated by batch)', - description: 'Fetches batches from storage in pages. Use batch_offset/batch_limit to paginate.', - }) - .input( - z.object({ - session_id: z.string().min(1), - batch_offset: z.coerce.number().int().nonnegative().default(0), - batch_limit: z.coerce.number().int().positive().max(50).default(20), - }), - ) - .output( - z.object({ - items: z.array( - z.object({ - type: z.number(), - timestamp: z.number(), - data: z.unknown().describe('rrweb event payload — shape defined by rrweb library'), - }), - ), - count: z.number(), - total_batches: z.number(), - batch_offset: z.number(), - batch_limit: z.number(), - }), - ), - - browserSessionCreate: oc - .route({ - method: 'POST', - tags: ['Browser Session'], - path: '/browser-session/create', - summary: 'Create browser session for application', - description: 'Creates a browser session and navigates to the application URL', - }) - .input( - z.object({ - application_id: z.number(), - agent_id: z.number(), - url: z.string().nullish(), - }), - ) - .output( - z.object({ - browser_session_id: z.string(), - live_view_url: z.string(), - }), - ), - - browserSessionStop: oc - .route({ - method: 'POST', - tags: ['Browser Session'], - path: '/browser-session/{browser_session_id}/stop', - summary: 'Stop browser session', - description: 'Stops all running tasks and closes the browser session. Set stop_tasks=false to skip task cleanup.', - }) - .input( - z.object({ - browser_session_id: z.string(), - stop_tasks: z.boolean().optional().default(true), - }), - ) - .output(SuccessWithMessageSchema), - - knowledgeSearch: oc - .route({ - method: 'GET', - tags: ['Knowledge'], - path: '/knowledge', - summary: 'Search and filter knowledge base documents', - description: - 'Returns list of knowledge documents matching search parameters. Supports filtering by workspace_id, type, and application_id.', - }) - .input( - z - .object({ - workspace_id: z.coerce.number().optional(), - type: KnowledgeTypeSchema.optional(), - application_id: z.coerce.number().optional(), - include: z.array(z.enum(['agents', 'application'])).optional(), - }) - .extend(PaginationSchema.shape), - ) - .output(paginatedListOf(KnowledgeEntitySchema)), - - knowledgeCreate: oc - .route({ - method: 'POST', - tags: ['Knowledge'], - path: '/knowledge', - summary: 'Upload new knowledge base document', - description: 'Upload a file, document URL, or video URL to create a knowledge entry for AI training', - }) - .input( - z.object({ - file: z.file().optional(), - application_id: z.coerce.number(), - document_url: z.string().url().optional(), - document_name: z.string().optional(), - video_url: z.string().url().optional(), - video_name: z.string().optional(), - }), - ) - .output(KnowledgeEntitySchema), - - knowledgeGet: oc - .route({ - method: 'GET', - tags: ['Knowledge'], - path: '/knowledge/{id}', - summary: 'Get specific knowledge document by ID', - description: 'Returns complete knowledge document information and content', - }) - .input(ByIdSchema) - .output(KnowledgeEntitySchema), - - knowledgeDelete: oc - .route({ - method: 'DELETE', - tags: ['Knowledge'], - path: '/knowledge/{id}', - summary: 'Delete knowledge document', - description: 'Permanently deletes a knowledge document from the database. This action cannot be undone.', - }) - .input(ByIdSchema) - .output(SuccessSchema), - - knowledgeRefresh: oc - .route({ - method: 'POST', - tags: ['Knowledge'], - path: '/knowledge/{id}/refresh', - summary: 'Refresh knowledge document', - description: 'Re-fetches HTML content from source URL and updates the document', - }) - .input(ByIdSchema) - .output(KnowledgeEntitySchema), - - knowledgeAssignAgents: oc - .route({ - method: 'PUT', - tags: ['Knowledge'], - path: '/knowledge/{knowledge_id}/agents', - summary: 'Assign agents to a knowledge item', - description: 'Sets which agents use this knowledge item. Triggers learning on affected agents.', - }) - .input( - z.object({ - knowledge_id: z.coerce.number(), - agent_ids: z.array(z.number()), - }), - ) - .output(KnowledgeEntitySchema), - - qaFlowCreate: oc - .route({ - method: 'POST', - tags: ['QA'], - path: '/qa/document', - summary: 'Upload QA flow (file or text)', - description: - 'Uploads a PDF/text file or text content for QA test generation. Also accepts multipart/form-data for file uploads (handled by raw Express route).', - }) - .input(QAFlowCreateSchema) - .output(QAFlowEntitySchema), - - qaFlowProcess: oc - .route({ - method: 'POST', - tags: ['QA'], - path: '/qa/document/{id}/process', - summary: 'Process QA flow and generate test cases', - description: - 'Starts async processing (returns 202). Poll GET /qa/document/:id for status and processing_step until completed/failed.', - }) - .input(ByIdSchema) - .output(z.object({ document: QAFlowEntitySchema })), - - qaFlowProcessStream: oc - .route({ - method: 'POST', - tags: ['QA'], - path: '/qa/document/{id}/process/stream', - summary: 'Process QA flow with SSE streaming', - description: - 'Streams processing events as Server-Sent Events. Events: response.created, response.progress, response.clear, response.completed, error.', - }) - .input(ByIdSchema) - .output( - eventIterator( - z.object({ - event: z - .string() - .describe( - 'SSE event type: response.created | response.progress | response.clear | response.completed | error', - ), - data: z.unknown().describe('Event payload object — shape varies by event type'), - }), - ), - ), - - qaFlowRefineStream: oc - .route({ - method: 'POST', - tags: ['QA'], - path: '/qa/document/{id}/refine/stream', - summary: 'Refine QA flow test cases with SSE streaming', - description: 'Streams refinement events as Server-Sent Events. Requires a refinement prompt (min 5 chars).', - }) - .input( - ByIdSchema.extend({ - refinementPrompt: z.string().min(5), - }), - ) - .output( - eventIterator( - z.object({ - event: z - .string() - .describe( - 'SSE event type: response.created | response.progress | response.clear | response.completed | error', - ), - data: z.unknown().describe('Event payload object — shape varies by event type'), - }), - ), - ), - - qaFlowGet: oc - .route({ - method: 'GET', - tags: ['QA'], - path: '/qa/document/{id}', - summary: 'Get QA flow by ID', - description: 'Retrieves a QA flow by its ID', - }) - .input(ByIdSchema) - .output(QAFlowEntitySchema), - - qaFlowSearch: oc - .route({ - method: 'GET', - tags: ['QA'], - path: '/qa/document', - summary: 'List QA flows', - description: - 'Returns all QA flows with aggregated metrics (run_count, pass_rate, total_failed) and the last run summary. Supports filtering by application_id.', - }) - .input( - z - .object({ - application_id: z.coerce.number().optional(), - }) - .extend(PaginationSchema.shape), - ) - .output( - paginatedListOf( - QAFlowEntitySchema.extend({ - run_count: z.number(), - display_title: z.string(), - total_failed: z.number(), - pass_rate: z.number().nullable(), - test_case_count: z.number().default(0), - last_run: z - .object({ - id: z.number(), - status: z.string(), - total_tests: z.number(), - passed_tests: z.number(), - failed_tests: z.number(), - created_at: z.coerce.date().nullable(), - }) - .nullable(), - }), - ).extend({ - metrics: z.object({ - total_flows: z.number(), - total_runs: z.number(), - avg_pass_rate: z.number().nullable(), - total_failed: z.number(), - total_passed: z.number(), - }), - }), - ), - - qaFlowDelete: oc - .route({ - method: 'DELETE', - tags: ['QA'], - path: '/qa/document/{id}', - summary: 'Delete QA flow', - description: 'Permanently deletes a QA flow and all its runs and test cases. This action cannot be undone.', - }) - .input(ByIdSchema) - .output(SuccessSchema), - - qaFlowUpdate: oc - .route({ - method: 'PUT', - tags: ['QA'], - path: '/qa/document/{id}', - summary: 'Update QA flow', - description: 'Updates status and/or pinned state of a QA flow', - }) - .input( - ByIdSchema.extend({ - status: QAFlowStatusSchema.optional(), - pinned: z.boolean().optional(), - }), - ) - .output(QAFlowEntitySchema), - - qaFlowRuns: oc - .route({ - method: 'GET', - tags: ['QA'], - path: '/qa/document/{id}/runs', - summary: 'List runs for a QA flow', - description: 'Returns all runs for a document with total/passed/failed counts', - }) - .input(ByIdSchema.extend(PaginationSchema.shape)) - .output( - paginatedListOf( - QARunEntitySchema.extend({ - status: z.string(), - total_tests: z.number(), - passed: z.number(), - failed: z.number(), - }), - ), - ), - - qaRunGet: oc - .route({ - method: 'GET', - tags: ['QA'], - path: '/qa/run/{id}', - summary: 'Get a QA run by ID', - description: 'Returns run details with test cases', - }) - .input(ByIdSchema) - .output( - QARunEntitySchema.extend({ - test_cases: z.array(QATestCaseEntitySchema), - }), - ), - - qaRunVerdicts: oc - .route({ - method: 'GET', - tags: ['QA'], - path: '/qa/run/{run_id}/verdicts', - summary: 'Get evaluator verdicts for a QA run', - description: 'Returns all qa_run_task rows for a run, keyed by test case', - }) - .input(z.object({ run_id: z.coerce.number() })) - .output(z.array(QARunTaskEntitySchema)), - - qaRunStop: oc - .route({ - method: 'POST', - tags: ['QA'], - path: '/qa/run/{id}/stop', - summary: 'Stop a running QA run', - description: - 'Marks every still-running/queued task in the linked simulation as stopped, sets the simulation to stopped, and fires the agent stop signal. No-op on terminal runs.', - }) - .input(ByIdSchema) - .output(SuccessSchema), - - qaRunDelete: oc - .route({ - method: 'DELETE', - tags: ['QA'], - path: '/qa/run/{id}', - summary: 'Delete a QA run', - description: - 'Permanently deletes a QA run and all its test case results. Refuses while in flight — call qaRunStop first.', - }) - .input(ByIdSchema) - .output(SuccessSchema), - - qaRunReportGenerate: oc - .route({ - method: 'POST', - tags: ['QA'], - path: '/qa/run/{id}/report', - summary: 'Schedule background generation of a QA run PDF report', - description: - 'Kicks off PDF rendering in the background to avoid nginx timeouts on long runs. Listen for the qa-run/report-ready (or qa-run/report-failed) SSE event for the resulting URL.', - }) - .input(ByIdSchema) - .output(z.object({ status: z.literal('pending') })), - - qaFlowRun: oc - .route({ - method: 'POST', - tags: ['QA'], - path: '/qa/document/{id}/run', - summary: 'Create and start a new QA run', - description: 'Creates a new run and starts execution. Returns 202; poll test-cases for execution status.', - }) - .input( - ByIdSchema.extend({ - auto_heal: z.boolean().default(false), - auto_accept: z.boolean().default(false), - }), - ) - .output( - z.object({ - run: QARunEntitySchema, - }), - ), - - qaTestCaseList: oc - .route({ - method: 'GET', - tags: ['QA'], - path: '/qa/document/{id}/test-cases', - summary: 'Get test cases for a QA flow', - description: 'Retrieves test cases; optional run_id scopes to that run', - }) - .input(ByIdSchema.extend({ run_id: z.coerce.number().optional() })) - .output(listOf(QATestCaseEntitySchema)), - - qaTestCaseUpdate: oc - .route({ - method: 'PUT', - tags: ['QA'], - path: '/qa/test-case/{id}', - summary: 'Update test case definition fields', - description: 'Updates a test case definition (title, objective, steps, etc.)', - }) - .input( - ByIdSchema.extend({ - test_title: z.string().optional(), - test_objective: z.string().optional(), - test_steps: z.array(z.string()).optional(), - expected_outcome: z.string().optional(), - priority: z.enum(['Low', 'Medium', 'High']).optional(), - is_active: z.boolean().optional(), - }), - ) - .output(QATestCaseEntitySchema), - - // Execute all tests for a document (creates a run and executes; returns run + simulations) - qaFlowExecuteTests: oc - .route({ - method: 'POST', - tags: ['QA'], - path: '/qa/document/{id}/execute', - summary: 'Create a run and execute all pending tests for a QA flow as simulations', - description: - 'Creates a new QA run, then starts a simulation for every active test case. Returns the run and simulation entities.', - }) - .input(ByIdSchema) - .output( - z.object({ - run: QARunEntitySchema, - simulations: z.array(SimulationEntitySchema), - }), - ), - - // Execute a single test - qaTestCaseExecute: oc - .route({ - method: 'POST', - tags: ['QA'], - path: '/qa/test-case/{id}/execute', - summary: 'Execute a single test case as a simulation', - description: 'Starts a simulation for the specified test case and returns the simulation entity.', - }) - .input(ByIdSchema) - .output( - z.object({ - simulation: SimulationEntitySchema, - }), - ), - - // Get simulation linked to test case - qaTestCaseSimulation: oc - .route({ - method: 'GET', - tags: ['QA'], - path: '/qa/test-case/{id}/simulation', - summary: 'Get the simulation linked to a test case', - description: - 'Returns the simulation entity associated with the test case for a specific run, or null if no simulation exists.', - }) - .input(ByIdSchema.extend({ run_id: z.coerce.number().int().positive().optional() })) - .output( - z.object({ - simulation: SimulationEntitySchema.nullable(), - }), - ), - - // QA Test Case Version routes (reads from qa_test_case.version_history JSON) - qaTestCaseVersionList: oc - .route({ - method: 'GET', - tags: ['QA'], - path: '/qa/test-case/{id}/versions', - summary: 'Get version history for a test case', - description: 'Returns all version entries from the test case version_history JSON', - }) - .input(ByIdSchema) - .output(listOf(QAVersionHistoryEntrySchema)), - - qaTestCaseVersionGet: oc - .route({ - method: 'GET', - tags: ['QA'], - path: '/qa/test-case/{id}/versions/{version}', - summary: 'Get specific test case version', - description: 'Returns a specific version entry from the test case version_history JSON', - }) - .input(ByIdSchema.extend({ version: z.coerce.number() })) - .output(QAVersionHistoryEntrySchema), - - qaTestCaseVersionRollback: oc - .route({ - method: 'POST', - tags: ['QA'], - path: '/qa/test-case/{id}/versions/{version}/rollback', - summary: 'Rollback to specific version', - description: 'Restores test case to a previous version and appends new version entry', - }) - .input(ByIdSchema.extend({ version: z.coerce.number() })) - .output(QAVersionHistoryEntrySchema), - - qaTestCaseVersionCompare: oc - .route({ - method: 'GET', - tags: ['QA'], - path: '/qa/test-case/{id}/versions/compare', - summary: 'Compare two versions', - description: 'Returns differences between two test case versions', - }) - .input(ByIdSchema.extend({ version1: z.coerce.number(), version2: z.coerce.number() })) - .output( - z.object({ - version1: QAVersionHistoryEntrySchema, - version2: QAVersionHistoryEntrySchema, - diff: z.array( - z.object({ - field: z.string(), - oldValue: DiffValueSchema, - newValue: DiffValueSchema, - }), - ), - }), - ), - - qaTestCaseVersionDelete: oc - .route({ - method: 'DELETE', - tags: ['QA'], - path: '/qa/test-case/{id}/versions/{version}', - summary: 'Delete a specific version', - description: 'Removes a version from history and reverts test case to previous version if it was the latest', - }) - .input(ByIdSchema.extend({ version: z.coerce.number() })) - .output(SuccessSchema.extend({ current_version: z.number() })), - - qaTestCaseVersionAccept: oc - .route({ - method: 'POST', - tags: ['QA'], - path: '/qa/test-case/{id}/versions/{version}/accept', - summary: 'Accept a specific version', - description: 'Marks a version as accepted by the user', - }) - .input(ByIdSchema.extend({ version: z.coerce.number() })) - .output(SuccessSchema), - - // QA Self-Healing routes (reads from qa_test_case.healing_attempts JSON) - qaHealingAttemptList: oc - .route({ - method: 'GET', - tags: ['QA'], - path: '/qa/test-case/{id}/healing-attempts', - summary: 'Get healing attempts for a test case', - description: 'Returns all healing attempt entries from the test case healing_attempts JSON', - }) - .input(ByIdSchema) - .output(listOf(QAHealingAttemptEntrySchema)), - - qaHealingAttemptApprove: oc - .route({ - method: 'POST', - tags: ['QA'], - path: '/qa/test-case/{id}/healing-attempts/{attemptIndex}/approve', - summary: 'Approve a healing attempt', - description: 'Applies the repair from a validated healing attempt by array index', - }) - .input(ByIdSchema.extend({ attemptIndex: z.coerce.number() })) - .output(SuccessSchema), - - qaHealingAttemptReject: oc - .route({ - method: 'POST', - tags: ['QA'], - path: '/qa/test-case/{id}/healing-attempts/{attemptIndex}/reject', - summary: 'Reject a healing attempt', - description: 'Marks a healing attempt as rejected by array index', - }) - .input(ByIdSchema.extend({ attemptIndex: z.coerce.number() })) - .output(SuccessSchema), - - qaHealingTrigger: oc - .route({ - method: 'POST', - tags: ['QA'], - path: '/qa/test-case/{id}/heal', - summary: 'Trigger self-healing for a failed test case', - description: 'Initiates the self-healing workflow for a test case', - }) - .input( - ByIdSchema.extend({ - simulation_id: z.number(), - config: z - .object({ - enabled: z.boolean().optional(), - autoApply: z.boolean().optional(), - minConfidenceThreshold: z.number().min(0).max(1).optional(), - maxHealingAttempts: z.number().int().min(1).max(5).optional(), - }) - .optional(), - }), - ) - .output( - z.object({ - healed: z.boolean(), - attemptIndex: z.number().nullable(), - analysis: FailureAnalysisSchema.nullable(), - confidence: z.number().nullable(), - }), - ), - - // QA Cross-Browser routes - qaCrossBrowserRun: oc - .route({ - method: 'POST', - tags: ['QA'], - path: '/qa/document/{id}/run-cross-browser', - summary: 'Execute cross-browser tests', - description: 'Runs tests across multiple browsers (chromium, firefox, webkit)', - }) - .input(BrowserConfigSchema.extend({ id: z.coerce.number() })) - .output( - z.object({ - runs: z.array( - z.object({ - browserType: BrowserTypeSchema, - runId: z.number(), - status: z.enum(['completed', 'failed', 'skipped']), - passedTests: z.number(), - failedTests: z.number(), - totalTests: z.number(), - }), - ), - summary: z.object({ - totalRuns: z.number(), - completedRuns: z.number(), - failedRuns: z.number(), - overallPassed: z.number(), - overallFailed: z.number(), - }), - }), - ), - - qaBrowserConfigUpdate: oc - .route({ - method: 'PUT', - tags: ['QA'], - path: '/qa/document/{id}/browser-config', - summary: 'Update browser configuration', - description: 'Updates the default browser configuration for a QA flow', - }) - .input(BrowserConfigSchema.extend({ id: z.coerce.number() })) - .output(SuccessSchema), - - stripeCreateTrial: oc - .route({ - method: 'POST', - tags: ['Stripe'], - path: '/stripe/trial', - summary: 'Create a trial subscription', - description: - 'Creates a trial subscription for the authenticated workspace. The trial period is 30 days by default.', - }) - .input(StripeTrialSchema) - .output(TrialSubscriptionSchema), - - stripeCreateCheckout: oc - .route({ - method: 'POST', - tags: ['Stripe'], - path: '/stripe/checkout', - summary: 'Create a checkout session for subscription purchase', - description: - 'Creates a Stripe checkout session for purchasing a subscription. Returns a session URL for redirect.', - }) - .input(StripeCheckoutSchema) - .output(CheckoutSessionSchema), - - stripeCreatePortal: oc - .route({ - method: 'POST', - tags: ['Stripe'], - path: '/stripe/portal', - summary: 'Create a customer portal session', - description: 'Creates a Stripe billing portal session for managing subscription, payment methods, and invoices.', - }) - .input(StripePortalSchema) - .output(PortalSessionSchema), - - stripeConfirmDowngrade: oc - .route({ - method: 'POST', - tags: ['Stripe'], - path: '/stripe/downgrade', - summary: 'Confirm and process a subscription downgrade', - description: 'Processes a confirmed subscription downgrade to a lower tier plan. No payment required.', - }) - .input(StripeDowngradeSchema) - .output(StripeDowngradeResponseSchema), - - stripeCancelSubscription: oc - .route({ - method: 'POST', - tags: ['Stripe'], - path: '/stripe/cancel', - summary: 'Cancel subscription at period end', - description: 'Cancels the subscription at the end of the current billing period.', - }) - .input(z.object({ subscriptionId: z.string().min(1) })) - .output(z.object({ subscriptionId: z.string(), cancelAtPeriodEnd: z.boolean() })), - - stripeGetCatalog: oc - .route({ - method: 'GET', - tags: ['Stripe'], - path: '/stripe/catalog', - summary: 'Get public Stripe catalog (pricing, plans, config)', - description: - 'Returns pricing from Stripe, plan catalog metadata, and Stripe frontend configuration in a single call.', - }) - .output(z.object({ pricing: StripePricingSchema, plans: PlanCatalogSchema, config: StripeConfigSchema })), - - stripeGetSubscription: oc - .route({ - method: 'GET', - tags: ['Stripe'], - path: '/stripe/subscription', - summary: 'Get current subscription plan and usage', - description: - 'Returns current subscription plan information and actual usage statistics for the authenticated workspace.', - }) - .output(z.object({ plan: PlanInfoSchema, usage: SubscriptionUsageSchema })), - - stripeSyncPlan: oc - .route({ - method: 'POST', - tags: ['Stripe'], - path: '/stripe/sync', - summary: 'Reconcile workspace plan with Stripe', - description: - 'Force-syncs the workspace_plan row with live Stripe subscription state. Called by the app after checkout success to recover from delayed or missed webhooks.', - }) - .output(z.object({ plan: PlanInfoSchema })), - - // stripeWebhook is handled as a raw Express route — not part of the oRPC contract. - - widgetStream: oc - .route({ - method: 'GET', - tags: ['Widget'], - path: '/widget/stream', - summary: 'SSE stream for real-time widget events', - description: - 'Typed event stream delivering tool calls, task status updates, chat responses, and registration confirmation.', - }) - .input( - z.object({ - chat_id: z.string(), - tab_id: z.string().optional(), - marketrix_id: z.string().optional(), - marketrix_key: z.string().optional(), - agent_id: z.coerce.number().optional(), - application_id: z.coerce.number().optional(), - }), - ) - .output(eventIterator(WidgetEventSchema)), - - widgetMessage: oc - .route({ - method: 'POST', - tags: ['Widget'], - path: '/widget/message', - summary: 'Send a typed command from widget to server', - description: 'Receives chat commands, tool responses, and keepalive pings from the widget.', - }) - .input( - z.object({ - chat_id: z.string(), - command: WidgetCommandSchema, - }), - ) - .output(z.object({ ok: z.boolean() })), - - appEvents: oc - .route({ - method: 'GET', - tags: ['App'], - path: '/app/events', - summary: 'SSE stream for real-time app dashboard events', - description: - 'Typed event stream delivering simulation, agent, QA, and user updates filtered by scope and application.', - }) - .input( - z.object({ - scopes: z.array(AppEventScopeSchema).min(1), - application_id: z.coerce.number().optional(), - }), - ) - .output(eventIterator(AppEventSchema)), - - /** @docs-only — raw Express handler; requires webhook signature verification */ - workosWebhook: oc - .route({ - method: 'POST', - tags: ['Internal'], - path: '/workos/webhook', - summary: 'WorkOS webhook receiver', - description: - 'Receives webhook events from WorkOS (user management, SSO). Verified via workos-signature header. Not for external use.', - }) - .input(z.object({ event: z.string(), data: z.record(z.string(), z.unknown()) })) - .output(z.object({ received: z.boolean() })), - - /** @docs-only — raw Express handler; requires webhook signature verification */ - stripeWebhook: oc - .route({ - method: 'POST', - tags: ['Internal'], - path: '/stripe/webhook', - summary: 'Stripe webhook receiver', - description: - 'Receives webhook events from Stripe (payments, subscriptions). Verified via stripe-signature header. Not for external use.', - }) - .input( - z - .object({ - id: z.string(), - object: z.string(), - type: z.string(), - data: z.record(z.string(), z.unknown()), - }) - .passthrough() - .describe('Stripe event payload'), - ) - .output(z.object({ received: z.boolean() })), - - // Personas - insightPersonasGet: oc - .route({ - method: 'GET', - tags: ['Insight'], - path: '/insights/personas', - summary: 'Get personas overview (connectors + segments + personas)', - }) - .input(ByApplicationIdSchema) - .output(InsightPersonasResponseSchema), - - insightPersonaDelete: oc - .route({ method: 'DELETE', tags: ['Insight'], path: '/insights/personas/{id}', summary: 'Delete a persona' }) - .input(z.object({ id: z.coerce.number() })) - .output(SuccessSchema), - - insightPersonasDomainSuggest: oc - .route({ - method: 'GET', - tags: ['Insight'], - path: '/insights/personas/domain-suggest', - summary: 'Get domain-specific persona suggestions (accessible during onboarding)', - }) - .input(z.object({ domain: z.string() })) - .output(DomainPersonaSuggestResponseSchema), - - insightPersonasSaveDomain: oc - .route({ - method: 'POST', - tags: ['Insight'], - path: '/insights/personas/save-domain', - summary: 'Save domain-suggested personas to the workspace (accessible during onboarding)', - }) - .input(InsightPersonasSaveDomainInputSchema) - .output(z.object({ saved: z.number() })), - - insightPersonaUpdate: oc - .route({ - method: 'PATCH', - tags: ['Insight'], - path: '/insights/personas/{id}', - summary: 'Update persona content (name, description, profile fields, tags, traits)', - }) - .input(InsightPersonaUpdateInputSchema) - .output(SuccessSchema), - - insightPersonaPin: oc - .route({ - method: 'PATCH', - tags: ['Insight'], - path: '/insights/personas/{id}/pin', - summary: 'Pin or unpin a persona (pinned personas sort to the top of the grid)', - }) - .input(InsightPersonaPinInputSchema) - .output(SuccessSchema), - - insightPersonasRegenerate: oc - .route({ - method: 'POST', - tags: ['Insight'], - path: '/insights/personas/regenerate', - summary: 'Start segment + persona regeneration for an application (fire-and-forget)', - }) - .input(z.object({ application_id: z.number() })) - .output(z.object({ job_id: z.string() })), - - insightPersonaKeyFeatures: oc - .route({ - method: 'POST', - tags: ['Insight'], - path: '/insights/personas/{id}/key-features', - summary: 'Generate AI key product feature insights for a specific persona', - }) - .input(z.object({ id: z.coerce.number() })) - .output(z.object({ features: z.array(z.string()) })), - - insightResearchRun: oc - .route({ - method: 'POST', - tags: ['Insight'], - path: '/insights/{application_id}/research/run', - summary: 'Kick off Background Research for an application', - description: - "Fire-and-forget: dispatches Background Research to the agent, returns a job_id the client can subscribe to via appEvents. On completion, research output is persisted as knowledge rows with source='research' and a knowledge_embed is dispatched per row.", - }) - .input(z.object({ application_id: z.coerce.number() })) - .output(z.object({ job_id: z.string() })), - - insightResearchStatus: oc - .route({ - method: 'GET', - tags: ['Insight'], - path: '/insights/{application_id}/research/status', - summary: 'Check whether Background Research is currently running', - description: - 'Returns the current Background Research dispatch state for an application. Used by the client to restore the "Researching…" banner across page refreshes — if `status="in_progress"`, the client subscribes to appEvents and waits for `job/completed`. Per-pod tracking; not persistent across api restarts.', - }) - .input(z.object({ application_id: z.coerce.number() })) - .output(z.object({ status: z.enum(['idle', 'in_progress']) })), - - // Personas — Findings (Personas redesign v1.0) - - insightPersonasOverview: oc - .route({ - method: 'GET', - tags: ['Insight'], - path: '/insights/personas/overview', - summary: 'Personas list-page stats bar: personas, simulations, issues, uncovered traffic, audience', - }) - .input(ByApplicationIdSchema) - .output(InsightPersonasOverviewSchema), - - insightPersonaStatsList: oc - .route({ - method: 'GET', - tags: ['Insight'], - path: '/insights/personas/stats', - summary: 'Per-persona outcome stats (simulations count, issues count, severity mix, last run)', - }) - .input(ByApplicationIdSchema) - .output(z.object({ items: z.array(InsightPersonaStatsSchema) })), - - insightAudienceCoverage: oc - .route({ - method: 'GET', - tags: ['Insight'], - path: '/insights/personas/audience-coverage', - summary: 'Audience coverage: segments, traits, near-duplicate persona groups, segment gaps', - }) - .input(ByApplicationIdSchema) - .output(InsightAudienceCoverageSchema), - - insightPersonaFindings: oc - .route({ - method: 'GET', - tags: ['Insight'], - path: '/insights/personas/{persona_id}/findings', - summary: 'List findings surfaced by a persona, with severity + status breakdowns', - }) - .input(InsightFindingsListInputSchema) - .output(InsightFindingsListResponseSchema), - - insightSimulationTopPages: oc - .route({ - method: 'GET', - tags: ['Insight'], - path: '/insights/simulations/{simulation_id}/top-pages', - summary: 'Top pages or flows surfaced by a single simulation', - description: - 'Buckets non-dismissed findings for this simulation by page_or_flow and returns the 5 heaviest. Drives the TOP PAGES chips on the simulation detail page below PersonasPanel.', - }) - .input(InsightSimulationTopPagesInputSchema) - .output(InsightSimulationTopPagesResponseSchema), - - insightFindingSummarize: oc - .route({ - method: 'POST', - tags: ['Insight'], - path: '/insights/personas/{persona_id}/findings/summarize', - summary: "Three-sentence AI synthesis of a persona's findings (clusters, patterns, recommendation)", - }) - .input(InsightFindingSummarizeInputSchema) - .output(InsightFindingSummarizeResponseSchema), - - insightPersonaCompare: oc - .route({ - method: 'POST', - tags: ['Insight'], - path: '/insights/personas/compare', - summary: 'Side-by-side comparison of 2-3 personas: stats, top pages, shared findings, AI synthesis', - }) - .input(InsightPersonaCompareInputSchema) - .output(InsightPersonaCompareResponseSchema), - - // Heatmaps - insightHeatmapPages: oc - .route({ - method: 'GET', - tags: ['Insight'], - path: '/insights/heatmaps/pages', - summary: 'Get all tracked heatmap pages', - }) - .input(ByApplicationIdSchema) - .output(z.object({ items: z.array(HeatmapPageEntitySchema) })), - - insightHeatmapSnapshot: oc - .route({ - method: 'GET', - tags: ['Insight'], - path: '/insights/heatmaps/snapshot', - summary: 'Get heatmap snapshot for a page/variation/type', - }) - .input(z.object({ page_id: z.coerce.number(), variation: HeatmapVariationSchema, type: HeatmapTypeSchema })) - .output(HeatmapSnapshotEntitySchema.nullable()), - - insightHeatmapsGenerate: oc - .route({ - method: 'POST', - tags: ['Insight'], - path: '/insights/heatmaps/generate', - summary: 'Enqueue a heatmap aggregation job over RRWeb session batches', - }) - .input(ByApplicationIdSchema) - .output(z.object({ job_id: z.string(), status: z.enum(['queued', 'active']) })), - - insightHeatmapsStatus: oc - .route({ - method: 'GET', - tags: ['Insight'], - path: '/insights/heatmaps/status', - summary: 'Get the current heatmap aggregation job status for an application', - }) - .input(ByApplicationIdSchema) - .output(HeatmapJobStatusSchema), - - insightHeatmapCandidates: oc - .route({ - method: 'POST', - tags: ['Insight'], - path: '/insights/heatmaps/candidates', - summary: 'Live-scan contributing sessions for a (page, variation) snapshot', - }) - .input( - z.object({ - page_id: z.number(), - variation: HeatmapVariationSchema, - limit: z.number().int().min(1).max(50).default(15), - offset: z.number().int().min(0).default(0), - }), - ) - .output( - z.object({ - candidates: z.array(HeatmapCandidateSchema), - total: z.number(), - }), - ), - - insightHeatmapSetBackdrop: oc - .route({ - method: 'POST', - tags: ['Insight'], - path: '/insights/heatmaps/backdrop', - summary: 'Pin a session as the backdrop override for a (page, variation) snapshot, or clear it with null', - }) - .input( - z.object({ - page_id: z.number(), - variation: HeatmapVariationSchema, - session_id: z.string().nullable(), - }), - ) - .output(z.object({ success: z.literal(true) })), - - // Reactions - insightReactionsGet: oc - .route({ - method: 'GET', - tags: ['Insight'], - path: '/insights/reactions', - summary: 'Get all reactions with runs and results', - }) - .input(ByApplicationIdSchema) - .output(z.object({ reactions: z.array(ReactionEntitySchema), personas: z.array(InsightPersonaEntitySchema) })), - - insightReactionCreate: oc - .route({ - method: 'POST', - tags: ['Insight'], - path: '/insights/reactions', - summary: 'Create a new reaction (multi-question study)', - description: - 'Pass either {description} (AI-draft path; returns drafted_questions, no DB persistence yet) or {questions} (direct-paste path; persists immediately). Exactly one is required.', - }) - .input( - z - .object({ - application_id: z.coerce.number(), - description: z.string().optional(), - questions: z.array(z.string().min(1).max(1000)).optional(), - }) - .refine(v => Boolean(v.description?.trim()) !== Boolean(v.questions?.length), { - message: 'exactly one of description or questions required', - }), - ) - .output(ReactionEntitySchema), - - insightReactionTemplateConfirm: oc - .route({ - method: 'POST', - tags: ['Insight'], - path: '/insights/reactions/{reaction_id}/template/confirm', - summary: 'Confirm (persist) the question template for a reaction', - description: - 'Replaces the entire question list. 409 if any reaction_run already exists for this reaction (templates are immutable post-run).', - }) - .input( - z.object({ - reaction_id: z.coerce.number(), - questions: z.array(z.string().min(1).max(1000)).min(1), - }), - ) - .output(ReactionEntitySchema), - - insightReactionDelete: oc - .route({ - method: 'DELETE', - tags: ['Insight'], - path: '/insights/reactions/{id}', - summary: 'Delete a reaction and all its runs', - }) - .input(z.object({ id: z.coerce.number() })) - .output(SuccessSchema), - - insightReactionContext: oc - .route({ - method: 'POST', - tags: ['Insight'], - path: '/insights/reactions/context', - summary: 'Get context for a reaction template', - }) - .input( - z.object({ - application_id: z.coerce.number(), - questions: z.array(z.string().min(1)).min(1), - }), - ) - .output(ChatContextResponseSchema), - - insightReactionRun: oc - .route({ - method: 'POST', - tags: ['Insight'], - path: '/insights/reactions/{reaction_id}/run', - summary: 'Run reaction scoring for selected personas', - }) - .input( - z.object({ - reaction_id: z.coerce.number(), - persona_ids: z.array(z.coerce.number()).min(1).max(50), - context_refs: z.array(ContextRefSchema), - simulations: z.array(SuggestedSimulationSchema.pick({ description: true, selected: true })), - }), - ) - .output(ReactionRunEntitySchema), - - insightReactionReportGenerate: oc - .route({ - method: 'POST', - tags: ['Insight'], - path: '/insights/reactions/run/{id}/report', - summary: 'Schedule background generation of a reaction run PDF report', - description: - 'Kicks off PDF rendering in the background to avoid nginx timeouts. Listen for reaction-run/report-ready (or reaction-run/report-failed) SSE for the resulting URL. Pass force=true to bypass the cached URL and always render a fresh PDF.', - }) - .input(z.object({ id: z.coerce.number(), force: z.boolean().optional() })) - .output(z.object({ status: z.literal('pending') })), -}; - -export { contract }; diff --git a/src/sdk/schema.ts b/src/sdk/schema.ts deleted file mode 100644 index 9ee02de6..00000000 --- a/src/sdk/schema.ts +++ /dev/null @@ -1,3052 +0,0 @@ -import { z } from 'zod'; - -export const UserPlanSchema = z.enum(['free', 'startup', 'growth', 'enterprise']); -export const EntityStatusSchema = z.enum(['created', 'active', 'suspended', 'pending_approval']); -export const WorkspacePackageSchema = z.enum(['free', 'startup', 'growth', 'enterprise']); -export const AgentTypeSchema = z.enum(['human', 'ai']); -export const AgentVoiceSchema = z.enum(['male', 'female']); -export const AgentStatusSchema = z.enum(['active', 'learning', 'error']); -// Learning progress uses nullable boolean: -// - null: callback not yet received -// - true: callback received with success -// - false: callback received with failure -export const LearningProgressSchema = z.object({ - graph_index_created: z.boolean().nullable(), -}); -export const KnowledgeTypeSchema = z.enum(['document', 'video']); -export const KnowledgeSourceSchema = z.enum(['user', 'research']); -export const QAFlowStatusSchema = z.enum(['pending', 'processing', 'waiting_review', 'completed', 'failed']); -/** - * Simulation parent status — canonical wire vocabulary matching the - * `simulation_status` Postgres ENUM and the `SimulationStatus` proto enum. - * Wave-14 added `has_question` (parent reflects a task awaiting answer); - * V56 migration adds the corresponding PG enum value. - */ -export const SimulationStatusSchema = z.enum([ - 'queued', - 'running', - 'creating_knowledge', - 'has_question', - 'completed', - 'failed', - 'stopped', -]); -/** - * Per-task status within a simulation. Matches the `simulation_task_status` - * Postgres ENUM and the SimulationTaskStatus proto enum. - */ -export const SimulationTaskStatusSchema = z.enum([ - 'queued', - 'running', - 'completed', - 'failed', - 'has_question', - 'stopped', -]); -/** - * Mindmap generation lifecycle status on a simulation row. Values written by - * `mindmapDispatch.ts` and `simulationHooks.ts`; relayed on the - * `simulation/mindmap-updated` app event. - */ -export const MindmapStatusSchema = z.enum(['pending', 'generating', 'completed', 'failed']); -/** - * Status carried on the `agent/updated` app event. The event has two emitter - * lineages: (1) `agentService` + `agentLearningHooks` emit an agent-entity - * status (`AgentStatusSchema`), and (2) the agentTask flow's `eventMapping` - * emits a task-status (`SimulationTaskStatusSchema`). The union captures both - * vocabularies so downstream consumers get exhaustiveness rather than `string`. - */ -export const AgentUpdatedStatusSchema = z.enum([ - // Agent entity statuses (AgentStatusSchema) - 'active', - 'learning', - 'error', - // Task-level statuses surfaced via agentTask flow transitions - 'queued', - 'running', - 'completed', - 'failed', - 'has_question', - 'stopped', -]); -export const ChatRoleSchema = z.enum(['user', 'agent']); -export const ChatSourceSchema = z.enum(['widget', 'app']); -export const InstructionTypeSchema = z.enum(['tell', 'show', 'do']); -export const ApplicationTypeSchema = z.enum(['app', 'website']); -export const WidgetTypeSchema = z.enum(['widget']); -export const ConnectorTypeSchema = z.enum(['timer', 'github', 'slack', 'teams', 'jira', 'mcp']); -export const ActionLogTypeSchema = z.enum([ - 'user_login', - 'url_visit', - 'update_workspace', - 'create_user', - 'update_user', - 'delete_user', - 'create_agent', - 'update_agent', - 'delete_agent', - 'create_application', - 'update_application', - 'delete_application', - 'create_widget', - 'update_widget', - 'delete_widget', - 'create_knowledge', - 'update_knowledge', - 'delete_knowledge', - 'approve_user', - 'deny_user', - 'request_workspace', - 'widget_question', - 'qa_run_started', - 'start_simulation', - 'create_automation', - 'update_automation', - 'delete_automation', - 'toggle_automation', - 'slack_command', -]); - -/** - * Base entity schema with common fields for all database entities - */ -export const BaseEntitySchema = z.object({ - id: z.number().optional(), - created_at: z.coerce.date().optional(), - updated_at: z.coerce.date().optional(), -}); - -/** Convert an Express multipart file to a Web File. */ -export function fromMulterFile(f: { buffer: Uint8Array; originalname: string; mimetype: string }): File { - return new File([new Uint8Array(f.buffer)], f.originalname, { type: f.mimetype }); -} - -export const FileSchema = z.object({ - file: z.instanceof(File), - application_id: z.coerce.number().optional(), -}); - -// ── Shared input helpers ── -export const ByIdSchema = z.object({ id: z.coerce.number() }); -export const BySlugSchema = z.object({ slug: z.string() }); -export const ByAgentIdSchema = z.object({ agent_id: z.coerce.number() }); -export const ByWidgetIdSchema = z.object({ widget_id: z.coerce.number() }); -export const BySimulationIdSchema = z.object({ simulation_id: z.coerce.number() }); -export const ByApplicationIdSchema = z.object({ application_id: z.coerce.number() }); -export const ByUserIdSchema = z.object({ user_id: z.coerce.number() }); - -// ── Shared output helpers ── -export const SuccessSchema = z.object({ success: z.literal(true) }); -export const SuccessWithMessageSchema = SuccessSchema.extend({ message: z.string() }); - -// ── Shared filter fragments ── -export const WorkspaceFilterSchema = z.object({ - workspace_id: z.coerce.number().optional(), -}); -export const ApplicationFilterSchema = z.object({ - application_id: z.coerce.number().optional(), -}); -export const PaginationSchema = z.object({ - limit: z.coerce.number().optional().default(50), - offset: z.coerce.number().optional().default(0), -}); - -// ── List wrappers ── - -/** Paginated list for unbounded queries — includes total/limit/offset for pagination */ -export const paginatedListOf = (schema: T) => - z.object({ - items: z.array(schema), - total: z.number(), - limit: z.number(), - offset: z.number(), - }); - -/** Simple list for bounded results (scoped to parent entity) — includes count */ -export const listOf = (schema: T) => - z.object({ - items: z.array(schema), - count: z.number(), - }); - -export const HealthResponseSchema = z.object({ - status: z.string(), - timestamp: z.string(), - service: z.string(), - version: z.string(), - build: z.string(), -}); - -export const IndexResponseSchema = z.object({ - name: z.string(), - version: z.string(), - build: z.string(), - status: z.string(), - timestamp: z.string(), -}); - -/** - * Authentication method schema - * password: Email/password authentication - * oauth: Social login (Google, Microsoft, Apple, etc.) or SSO - */ -export const AuthMethodSchema = z.enum(['password', 'oauth']); - -/** - * Complete user entity schema - * Note: Users don't have plans - plans belong to workspaces (via workspace_plan table) - */ -export const UserEntitySchema = BaseEntitySchema.extend({ - is_super: z.boolean(), - status: EntityStatusSchema, - email: z.string().email(), - external_id: z.string().nullish(), - first_name: z.string().nullish(), - last_name: z.string().nullish(), - password: z.string().nullish(), - image_url: z.string().nullish(), - prompt_limit: z.number().nullish(), - last_login_at: z.coerce.date().nullish(), - auth_method: AuthMethodSchema.nullish(), -}); - -export const UserCreateSchema = UserEntitySchema.partial().extend({ - email: z.string().email(), - password: z.string(), -}); - -export const UserUpdateSchema = UserEntitySchema.partial(); - -export const BatchUserCreateSchema = z.object({ - users: z.array(UserCreateSchema), -}); - -export const BatchUserCreateResultSchema = z.object({ - users: z.array(UserEntitySchema.partial()), -}); - -export const TokenSchema = z.object({ - token: z.string(), -}); - -/** - * Complete workspace entity schema - * Note: package and ending_date come from workspace_plan table (joined when fetching workspace data). - * They are NOT stored on the workspace table itself. - */ -export const WorkspaceEntitySchema = BaseEntitySchema.extend({ - name: z.string(), - slug: z.string(), - status: EntityStatusSchema, - package: WorkspacePackageSchema, - ending_date: z.coerce.date().nullish(), - external_workspace_id: z.string().nullish(), - // Read-only flag derived from `slack_webhook_url`'s presence. The URL itself - // is a secret and is never returned to clients — only this boolean is. - slack_webhook_configured: z.boolean().optional(), - notify_all_members_on_question: z.boolean().optional(), -}); - -export const WorkspaceCreateSchema = WorkspaceEntitySchema.partial().extend({ - name: z.string().min(1), -}); - -/** - * Slack incoming-webhook URL validator — shared by the workspace update path - * and the slack-test endpoint so both surface a 400 BAD_REQUEST for the same - * malformed input. - */ -export const SlackWebhookUrlSchema = z - .string() - .url() - .refine(u => /^https:\/\/hooks\.slack\.com\//.test(u), { - message: 'Slack webhook URL must start with https://hooks.slack.com/', - }); - -/** - * Workspace update schema. `slack_webhook_url` is write-only — it never - * appears in the entity response (only `slack_webhook_configured` does). - * Pass `null` to clear the stored webhook. - */ -export const WorkspaceUpdateSchema = WorkspaceEntitySchema.omit({ slack_webhook_configured: true }).partial().extend({ - slack_webhook_url: SlackWebhookUrlSchema.nullable().optional(), -}); - -export const WorkspacePlanStatusSchema = z.enum([ - 'active', - 'past_due', - 'canceled', - 'unpaid', - 'trialing', - 'incomplete', - 'incomplete_expired', - 'paused', -]); - -export type WorkspacePlanStatus = z.infer; - -/** - * Workspace plan entity schema - tracks plan/subscription for each workspace - */ -export const WorkspacePlanEntitySchema = BaseEntitySchema.extend({ - workspace_id: z.number(), - package: WorkspacePackageSchema, - ending_date: z.coerce.date(), - stripe_subscription_id: z.string().nullable(), - stripe_price_id: z.string().nullable(), - status: WorkspacePlanStatusSchema.default('active').optional(), -}); - -export const WorkspacePlanCreateSchema = WorkspacePlanEntitySchema.partial().extend({ - workspace_id: z.number(), - package: WorkspacePackageSchema, -}); - -export const WorkspacePlanUpdateSchema = WorkspacePlanEntitySchema.partial(); - -export const WorkspaceMemberRoleSchema = z.enum(['owner', 'member']); - -export const WorkspaceMemberEntitySchema = BaseEntitySchema.extend({ - workspace_id: z.number(), - user_id: z.number(), - role: WorkspaceMemberRoleSchema, -}); - -export type WorkspaceMemberData = z.infer; -export type WorkspaceMemberRole = z.infer; - -/** - * Lightweight agent badge for embedding in other entities - */ -export const AgentBadgeSchema = z.object({ - id: z.number(), - agent_name: z.string(), - image_url: z.string().nullish(), -}); - -export type AgentBadgeData = z.infer; - -/** - * Knowledge base document schema - */ -export const KnowledgeEntitySchema = BaseEntitySchema.extend({ - workspace_id: z.number(), - application_id: z.number().optional(), - file_name: z.string().min(1), - file_size: z.coerce.number(), - file_type: KnowledgeTypeSchema, - file_url: z.string(), - source_url: z.string().nullish(), // Original URL for URL-based documents - source: KnowledgeSourceSchema.default('user').optional(), - agents: z.array(AgentBadgeSchema).optional(), -}); - -export const BrowserTypeSchema = z.enum(['chromium', 'firefox', 'webkit']); - -export const BrowserConfigSchema = z.object({ - browsers: z.array(BrowserTypeSchema).default(['chromium']), - parallel: z.boolean().default(false), - fail_fast: z.boolean().default(false), - timeout_per_browser: z.number().int().min(60).max(3600).optional(), -}); - -export type BrowserType = z.infer; -export type BrowserConfig = z.infer; - -/** - * QA document entity schema - */ -export const QAFlowEntitySchema = BaseEntitySchema.extend({ - workspace_id: z.number(), - user_id: z.number(), - application_id: z.number(), - file_name: z.string().min(1), - file_size: z.coerce.number(), - file_type: z.string(), - file_url: z.string(), - file_path: z.string().nullable(), - text_content: z.string().nullish(), - additional_instructions: z.string().max(1000).nullish(), - status: QAFlowStatusSchema, - processing_step: z.string().nullish(), - processing_started_at: z.coerce.date().nullish(), - ultimate_goal: z.string().nullish(), - browser_config: BrowserConfigSchema.nullish(), - persona_ids: z.array(z.coerce.number()).optional().default([]), - pinned: z.boolean().optional().default(false), -}); - -export const QAFlowCreateSchema = z.object({ - application_id: z.coerce.number(), - file: z.instanceof(File).optional(), - text_content: z.string().optional(), - file_name: z.string().optional(), - additional_instructions: z.string().max(1000).optional(), - persona_ids: z.array(z.coerce.number()).optional(), -}); - -export const QARunEntitySchema = BaseEntitySchema.extend({ - qa_flow_id: z.number(), - workspace_id: z.number(), - triggered_by: z.number(), - browser_type: BrowserTypeSchema, - browser_config: BrowserConfigSchema.nullish(), - simulation_id: z.number(), - source: z.enum(['manual', 'automation', 'github_pr', 'slack_command']).nullish(), - source_metadata: z.record(z.string(), z.unknown()).nullish(), - auto_heal: z.boolean().default(false), - auto_accept: z.boolean().default(false), - report_pdf_url: z.string().nullish(), - // Derived flag: true if any test case in the run is paused waiting on user - // input. Parent run status itself is computed from terminal task states and - // does not hold a "question" value. - has_question: z.boolean().optional(), -}); - -/** - * JSON entry schemas for qa_test_case embedded arrays - */ -export const QAVersionHistoryEntrySchema = z.object({ - version: z.number().int().positive(), - test_title: z.string(), - test_objective: z.string(), - test_steps: z.array(z.string()), - expected_outcome: z.string(), - priority: z.enum(['Low', 'Medium', 'High']), - change_type: z.enum(['created', 'modified', 'self_healed', 'refined', 'deleted']), - change_reason: z.string().nullish(), - changed_by: z.number().nullish(), - created_at: z.string(), - accepted: z.boolean().optional(), -}); - -export const QAHealingAttemptEntrySchema = z.object({ - failure_type: z.enum(['locator', 'assertion', 'timeout', 'flow_change', 'environment']), - failure_message: z.string().nullish(), - failure_context: z.record(z.string(), z.unknown()).nullish(), - repair_strategy: z.string().nullish(), - repair_details: z.record(z.string(), z.unknown()).nullish(), - confidence_score: z.number().min(0).max(1).nullish(), - validation_status: z.enum(['pending', 'validated', 'failed', 'rejected']), - simulation_id: z.number().nullish(), - validation_simulation_id: z.number().nullish(), - healed_version: z.number().nullish(), - created_at: z.string(), -}); - -/** - * QA verdict entity schemas — LLM evaluator output per (qa_run, qa_test_case). - * Independent of simulation task status (which keeps raw execution semantics). - */ -export const QAVerdictSchema = z.enum(['passed', 'needs_healing', 'failed']); -export type QAVerdict = z.infer; - -export const QAEvaluationSchema = z.object({ - outcome_reached: z.boolean(), - steps_aligned: z.boolean(), - healing_recommended: z.boolean(), - is_actual_bug: z.boolean(), - confidence: z.number().min(0).max(1), - reasoning: z.string(), - evaluated_at: z.string(), - screenshot_url: z.string().nullish(), -}); -export type QAEvaluation = z.infer; - -/** - * QA task status — first-class outcome of a QA run task. - * Mirrors the `qa_task_status` Postgres ENUM and proto QATaskStatus. - */ -export const QATaskStatusSchema = z.enum(['queued', 'running', 'passed', 'failed', 'needs_healing', 'stopped']); -export type QATaskStatusValue = z.infer; - -/** - * QA run task entity schema. - * - * Composite PK (qa_run_id, qa_test_case_id) — no surrogate `id`. The - * pass/fail/needs_healing outcome lives on the typed `status` enum; - * `verdict` is a nullable human-readable summary set by the post-run - * evaluator. - * - * `created_at` / `updated_at` are populated by Sequelize, so they are - * optional on the input shape (a fresh upsert payload can omit them). - */ -export const QARunTaskEntitySchema = z.object({ - qa_run_id: z.number(), - qa_test_case_id: z.number(), - simulation_task_id: z.string().nullable().optional(), - status: QATaskStatusSchema, - verdict: z.string().nullable().optional(), - proposed_steps: z.array(z.string()).default([]), - executed_steps: z.array(z.string()).nullable().optional(), - evaluation: QAEvaluationSchema.nullable().optional(), - started_at: z.coerce.date().nullable().optional(), - ended_at: z.coerce.date().nullable().optional(), - created_at: z.coerce.date().optional(), - updated_at: z.coerce.date().optional(), -}); -export type QARunTaskData = z.infer; - -/** QA test case entity schema. */ -export const QATestCaseEntitySchema = BaseEntitySchema.extend({ - qa_flow_id: z.number(), - workspace_id: z.number(), - order_index: z.number().int().nonnegative(), - test_title: z.string(), - test_objective: z.string(), - test_steps: z.array(z.string()), - expected_outcome: z.string(), - priority: z.enum(['Low', 'Medium', 'High']), - is_active: z.preprocess(v => (typeof v === 'number' ? v !== 0 : v), z.boolean()), - current_version: z.number().int().positive(), - version_history: z.array(QAVersionHistoryEntrySchema).nullable(), - healing_attempts: z.array(QAHealingAttemptEntrySchema).nullable(), - healing_metadata: z.record(z.string(), z.unknown()).nullish(), - last_healed_at: z.coerce.date().nullish(), - blocked_by: z - .array(z.object({ index: z.number().int().nonnegative(), condition: z.enum(['pass']).optional() })) - .default([]), -}); - -export const QATestCaseCreateSchema = z.object({ - qa_flow_id: z.number(), - test_title: z.string(), - test_objective: z.string(), - test_steps: z.array(z.string()), - expected_outcome: z.string(), - priority: z.enum(['Low', 'Medium', 'High']).optional(), -}); - -// ── QA Insight Response (completion payload for process/refine streams) ── -export const QAInsightResponseSchema = z.object({ - ultimate_goal: z.string(), - test_cases: z.array( - z.object({ - test_title: z.string(), - test_objective: z.string(), - test_steps: z.array(z.string()), - expected_outcome: z.string(), - priority: z.enum(['Low', 'Medium', 'High']), - }), - ), - summary: z.object({ - total_tests: z.number(), - high_priority: z.number(), - medium_priority: z.number(), - low_priority: z.number(), - estimated_time_minutes: z.number(), - }), -}); - -// ── SSE stream events for QA processing ── -export const SSEEventSchema = z.discriminatedUnion('event', [ - z.object({ event: z.literal('response.created'), data: z.object({ message: z.string() }) }), - z.object({ event: z.literal('response.progress'), data: z.object({ message: z.string() }) }), - z.object({ event: z.literal('response.clear'), data: z.object({}).optional() }), - z.object({ event: z.literal('response.completed'), data: QAInsightResponseSchema }), - z.object({ event: z.literal('error'), data: z.object({ detail: z.string() }) }), -]); - -/** Value types in QA test case version diffs */ -export const DiffValueSchema = z.union([z.string(), z.number(), z.boolean(), z.array(z.string()), z.null()]); - -export const QATestVersionChangeTypeSchema = z.enum(['created', 'modified', 'self_healed', 'refined', 'deleted']); -export const QAFailureTypeSchema = z.enum(['locator', 'assertion', 'timeout', 'flow_change', 'environment']); -export const QAValidationStatusSchema = z.enum(['pending', 'validated', 'failed', 'rejected']); - -export const QAHealingMetadataSchema = z.object({ - healing_enabled: z.boolean().default(true), - auto_apply: z.boolean().default(false), - min_confidence_threshold: z.number().min(0).max(1).default(0.85), - max_healing_attempts: z.number().int().min(1).max(5).default(2), - last_healing_attempt: z.string().datetime().nullable(), - total_healing_attempts: z.number().int().default(0), - successful_heals: z.number().int().default(0), -}); - -export const FailureAnalysisSchema = z.object({ - failure_type: QAFailureTypeSchema, - is_healable: z.boolean(), - is_actual_bug: z.boolean(), - failure_message: z.string(), - failure_context: z.record(z.string(), z.unknown()), - suggested_repair: z - .object({ - type: z.enum(['update_locator', 'update_assertion', 'update_steps', 'skip_test']), - confidence: z.number().min(0).max(1), - details: z.record(z.string(), z.unknown()), - }) - .nullable(), -}); - -export type QATestVersionChangeType = z.infer; -export type QAFailureType = z.infer; -export type QAValidationStatus = z.infer; -export type QAHealingMetadata = z.infer; -export type FailureAnalysis = z.infer; -export type QAVersionHistoryEntry = z.infer; -export type QAHealingAttemptEntry = z.infer; - -export const GoToUrlActionSchema = z.object({ - go_to_url: z.object({ - url: z.string(), - new_tab: z.boolean(), - }), -}); - -export const ClickElementByIndexActionSchema = z.object({ - click_element_by_index: z.object({ - index: z.number().int().min(1), // Must be >= 1 (data-id value from browser_state) - }), -}); - -export const InputTextActionSchema = z.object({ - input_text: z.object({ - index: z.number(), - text: z.string(), - clear_existing: z.boolean(), - }), -}); - -export const WriteFileActionSchema = z.object({ - write_file: z.object({ - file_name: z.string(), - content: z.string(), - append: z.boolean(), - trailing_newline: z.boolean(), - leading_newline: z.boolean(), - }), -}); - -export const GetOtpActionSchema = z.object({ - get_otp: z.object({ - question: z.string(), - }), -}); - -export const DoneActionSchema = z.object({ - done: z.object({ - success: z.boolean(), - message: z.string().optional(), - }), -}); - -export const WaitActionSchema = z.object({ - wait: z.object({ - seconds: z.number(), - }), -}); - -export const ReloadPageActionSchema = z.object({ - reload_page: z.object({}), -}); - -export const OpenNewTabActionSchema = z.object({ - open_new_tab: z.object({ - url: z.string(), - }), -}); - -export const NavigateToUrlActionSchema = z.object({ - navigate_to_url: z.object({ - url: z.string(), - }), -}); - -/** - * One tool_call recorded inside a SimulationStep — the underlying browser_op - * invocation that contributed to the step's action. - */ -export const ToolCallRecordSchema = z.object({ - name: z.string().min(1), - params: z.record(z.string(), z.unknown()).default({}), - result: z.record(z.string(), z.unknown()).default({}), -}); -export type ToolCallRecord = z.infer; - -/** - * Flat step shape written by the BrowserRuntime. UI renders `action_text` as - * the headline and `tool_calls[]` as the expandable detail. - */ -export const SimulationStepSchema = z - .object({ - action_text: z.string().min(1), - tool_calls: z.array(ToolCallRecordSchema).default([]), - skill_used: z.string().nullable().default(null), - screenshot_path: z.string().nullable().default(null), - status: z.enum(['running', 'completed', 'failed', 'has_question']).default('completed'), - started_at: z.string(), - ended_at: z.string(), - error_message: z.string().nullable().default(null), - url: z.string().nullable().default(null), - title: z.string().nullable().default(null), - }) - .passthrough(); -export type SimulationStepData = z.infer; - -/** - * Summary of one simulation step for JSON listing (topic + screenshot link) - */ -export const SimulationStepSummarySchema = z.object({ - step_number: z.number().int().positive(), - topic: z.string(), - screenshot_url: z.string().nullable(), - title: z.string().optional().nullable(), - url: z.string().optional().nullable(), -}); - -/** - * One row in the simulation_progress table. - */ -export const SimulationProgressEntrySchema = z.object({ - status: SimulationTaskStatusSchema, - status_message: z.string().nullable(), - task_id: z.string().nullish(), - created_at: z.coerce.date(), -}); -export type SimulationProgressEntry = z.infer; - -export const TaskDependencySchema = z.object({ - task_id: z.string(), - condition: z.enum(['pass']).optional(), -}); -export type TaskDependency = z.infer; - -/** - * A single task within a simulation. Direct simulations have 1 task (the prompt). - * QA simulations have N tasks (one per test case). - */ -export const SimulationTaskEntrySchema = z.object({ - task_id: z.string(), - title: z.string(), - instructions: z.string(), - status: z.enum(['pending', 'running', 'has_question', 'passed', 'failed', 'skipped', 'stopped']), - error_message: z.string().nullish(), - started_at: z.string().nullish(), - completed_at: z.string().nullish(), - order_index: z.number().int().nonnegative().default(0), - tab_id: z.string().nullish(), - step_count: z.number().int().nonnegative().default(0), - blocked_by: z.array(TaskDependencySchema).default([]), -}); -export type SimulationTaskEntry = z.infer; - -export const SimulationEntitySchema = BaseEntitySchema.extend({ - application_id: z.number(), - agent_id: z.number(), - job_id: z.string(), - browser_session_id: z.string().nullish(), - status: SimulationStatusSchema, - status_message: z.string().nullish(), - path: z.string().nullish(), - instructions: z.string().nullish(), - pinned: z.boolean().optional(), - source: z.enum(['direct', 'qa']).optional(), - agent_name: z.string().nullish(), - graph_index_id: z.string().nullish(), - source_metadata: z.record(z.string(), z.unknown()).nullish(), - tasks: z.array(SimulationTaskEntrySchema).optional(), - agents: z.array(AgentBadgeSchema).optional(), - mindmap_status: MindmapStatusSchema.optional(), - mindmap_steps_processed: z.number().int().nonnegative().optional(), - mindmap_steps_total: z.number().int().nonnegative().optional(), - mindmap_error: z.string().nullish(), - created_by_user_id: z.number().nullish(), - // Persona selected for this run in the Generate Simulation modal. Nullable - // for "Generic" runs and for reaction-flow runs (which use - // reaction_result.persona_id for many-personas-per-run). See migration V54. - persona_id: z.number().nullish(), - // Derived flag: true if any per-task status is `has_question`. The parent - // `status` itself never holds `has_question` — that's a per-task state. The - // flag drives the "Question" UI pill on the simulation header. - has_question: z.boolean().optional(), -}); - -/** - * Simulation row shape returned by list endpoints (e.g. simulationSearch). - * Omits per-row fields that are heavy and unused in the list view: `tasks` - * (full SimulationTaskEntry array) and `source_metadata` (free-form bag). - */ -export const SimulationListEntitySchema = SimulationEntitySchema.omit({ - tasks: true, - source_metadata: true, -}); -export type SimulationListData = z.infer; - -/** - * Simulation logging user - user who has started at least one simulation (for GET /simulation/logging-users) - */ -export const SimulationLoggingUserSchema = z.object({ - user_id: z.number(), - email: z.string(), - first_name: z.string().optional().nullable(), - last_name: z.string().optional().nullable(), - last_simulation_at: z.string().optional(), // ISO date of most recent simulation start -}); - -// Optional input on simulationStart to invoke a specific skill -export const SkillInvocationRequestSchema = z.object({ - skill_id: z.number().int(), - params: z.record(z.string(), z.string()), -}); -export type SkillInvocationRequest = z.infer; - -export const SimulationCreateSchema = SimulationEntitySchema.omit({ tasks: true }) - .partial() - .extend({ - application_id: z.number(), - agent_id: z.number(), - instructions: z.string(), - max_steps: z.number().int().positive().max(1000).optional(), - timeout: z.number().positive().max(360).optional(), // Max 6 hours in minutes - // Skill-registry experience level (1=junior … 5=senior). Default 5 so - // legacy callers without the field get the most-capable agent. - experience_level: z.number().int().min(1).max(5).optional().default(5), - // Optional skill invocation: when present, the simulation is launched to - // execute a specific skill with the given params. The skill's description - // is templated (`{{token}}` → `params[token]`) and used as the task text. - skill_invocation: SkillInvocationRequestSchema.optional(), - }); - -export const SimulationUpdateSchema = z.object({ - job_id: z.string().optional(), - status: SimulationStatusSchema.optional(), - status_message: z.string().optional(), - pinned: z.boolean().optional(), - graph_index_id: z.string().optional(), -}); - -export const SimulationAnswerSchema = z.object({ - answer: z.string().min(1), -}); - -// --- Skill registry --- - -export const SkillRunKindSchema = z.enum(['simulation', 'qa', 'reaction']); -export type SkillRunKind = z.infer; - -export const SkillEntitySchema = z.object({ - id: z.number().int(), - workspace_id: z.number().int().nullable(), - application_id: z.number().int().nullable(), - name: z.string().min(1), - version: z.number().int().positive(), - description: z.string().min(1), - script: z.string().nullable(), // NULL for browser_ops; NON-NULL for learned skills (enforced at write-time in service) - deprecated_at: z.coerce.date().nullable(), - created_at: z.coerce.date(), -}); -export type SkillData = z.infer; - -// List-view rows omit the script body for list density -export const SkillListRowSchema = SkillEntitySchema.omit({ script: true }); -export type SkillListRow = z.infer; - -// Detail-view: skill + full lineage (all versions of the same scope+name, ordered ASC) -export const SkillDetailSchema = z.object({ - skill: SkillEntitySchema, - lineage: z.array(SkillEntitySchema), -}); -export type SkillDetail = z.infer; - -// Candidate emitted by the agent's distillation pass. Persisted by -// skillEvolutionService.persistDistilledCandidates with version bump. -export const SkillCandidateSchema = z.object({ - name: z.string().min(1), - description: z.string().min(1), - script: z.string().min(1), -}); -export type SkillCandidate = z.infer; - -export const SubmitSkillCandidatesPayloadSchema = z.object({ - workspace_id: z.number().int().positive(), - application_id: z.number().int().positive(), - candidates: z.array(SkillCandidateSchema).min(1), -}); -export type SubmitSkillCandidatesPayload = z.infer; - -/** - * Complete RRWeb session entity schema - */ -export const SessionEntitySchema = BaseEntitySchema.extend({ - session_id: z.string(), - chat_id: z.string(), - application_id: z.coerce.number(), - blob_url: z.string().nullable(), - event_count: z.coerce.number().int().nonnegative(), - started_at: z.coerce.date(), - ended_at: z.coerce.date().nullable(), - is_active: z.preprocess(v => (typeof v === 'number' ? v !== 0 : v), z.boolean()), - metadata: z - .object({ - userAgent: z.string().optional(), - url: z.string().optional(), - viewport: z.object({ width: z.number(), height: z.number() }).optional(), - }) - .nullable(), - last_batch_index: z.coerce.number().int().nullable(), - last_event_timestamp: z.coerce.number().int().nullable(), - last_upload_time: z.coerce.date().nullable(), -}); - -/** - * RRWeb session upsert schema (for create/update) - */ -export const SessionUpsertSchema = z.object({ - session_id: z.string().min(1), - chat_id: z.string().min(1), - application_id: z.number().int().nullish(), - blob_url: z.string().nullish(), - event_count: z.number().int().nonnegative().optional(), - started_at: z.coerce.date().optional(), - ended_at: z.coerce.date().nullish(), - is_active: z.preprocess(v => (typeof v === 'number' ? v !== 0 : v), z.boolean()).optional(), - metadata: z - .object({ - userAgent: z.string().optional(), - url: z.string().optional(), - viewport: z.object({ width: z.number(), height: z.number() }).optional(), - }) - .nullable() - .optional(), - last_batch_index: z.number().int().nonnegative().nullish(), - last_event_timestamp: z.number().int().nullish(), - last_upload_time: z.coerce.date().nullish(), -}); - -/** - * API response schema for the simulationProgress endpoint. - */ -export const SimulationProgressEntitySchema = z.object({ - id: z.number(), - simulation_id: z.number(), - status: SimulationTaskStatusSchema, - status_message: z.string().nullable(), - skill: z.string().nullable().optional(), - screenshot_path: z.string().nullable().optional(), - tool_calls: z.array(ToolCallRecordSchema).default([]), - created_at: z.coerce.date(), -}); - -/** - * Mindmap edge schema - transition from one node to another via an action - */ -export const MindMapEdgeSchema = z - .object({ - start: z.string(), - end: z.string(), - action: z.string(), - }) - .passthrough(); - -/** - * Mindmap section schema - functional UI section within a page node - */ -export const MindMapSectionSchema = z - .object({ - id: z.string(), - label: z.string(), - purpose: z.string(), - elements: z.array(z.record(z.string(), z.unknown())).default([]), - bbox: z.record(z.string(), z.unknown()).default({}), - screenshot: z.string().default(''), - embedding: z.array(z.number()).nullish(), - }) - .passthrough(); - -/** - * Mindmap node schema - unique page state observed during simulation. - * Matches agent's PageNode model (perception/graph.py). - * Uses passthrough() because the agent model may evolve faster than the schema. - */ -export const MindMapNodeSchema = z - .object({ - id: z.string(), - title: z.string(), - url: z.string(), - summary: z.string().default(''), - screenshot: z.string().default(''), - sections: z.array(MindMapSectionSchema).default([]), - sequence_ids: z.array(z.number()).default([]), - embedding: z.array(z.number()).nullish(), - }) - .passthrough(); - -export const MindMapSchema = z.object({ - nodes: z.array(MindMapNodeSchema), - edges: z.array(MindMapEdgeSchema), -}); - -export const AgentEntitySchema = BaseEntitySchema.extend({ - workspace_id: z.number(), - user_id: z.number().nullish(), - application_id: z.number(), - agent_name: z.string(), - agent_type: AgentTypeSchema, - agent_voice: AgentVoiceSchema, - agent_description: z.string().nullish(), - instructions: z.string().nullish(), - image_url: z.string().nullish(), - graph_index_id: z.string().nullish(), - status: AgentStatusSchema, - status_message: z.string().nullish(), - learning_progress: LearningProgressSchema.nullish(), - learning_started_at: z.coerce.date().nullish(), - workspace: WorkspaceEntitySchema.optional(), - user: UserEntitySchema.optional(), - knowledge: z.array(KnowledgeEntitySchema).optional(), - simulations: z.array(SimulationEntitySchema).optional(), - simulation_count: z.number().int().nonnegative().optional(), - knowledge_count: z.number().int().nonnegative().optional(), -}); - -const parseIds = (val: unknown): number[] => { - if (Array.isArray(val)) return val.map(v => Number(v)); - if (typeof val === 'string') { - const parsed: unknown = JSON.parse(val); - return Array.isArray(parsed) ? (parsed as unknown[]).map(v => Number(v)) : []; - } - return []; -}; - -const KnowledgeIdsSchema = z.union([z.array(z.number()), z.string()]).transform(parseIds); -const SimulationIdsSchema = z.union([z.array(z.number()), z.string()]).transform(parseIds); - -export const AgentCreateSchema = AgentEntitySchema.partial().extend({ - application_id: z.coerce.number(), - agent_name: z.string(), - agent_type: AgentTypeSchema, - agent_voice: AgentVoiceSchema, - agent_description: z.string(), - instructions: z.string(), - file: z.instanceof(File).optional(), - image_url: z.string().optional(), - knowledge_ids: KnowledgeIdsSchema, - simulation_ids: SimulationIdsSchema, -}); - -export const AgentUpdateSchema = AgentEntitySchema.partial().extend({ - file: z.instanceof(File).optional(), - knowledge_ids: KnowledgeIdsSchema, - simulation_ids: SimulationIdsSchema, -}); - -/** - * Agent task start response schema - * Response from agent server when starting a task - */ -export const AgentTaskStartResponseSchema = z.object({ - text: z.string(), - task_id: z.string().optional(), -}); - -/** - * Agent task stop response schema - * Response from agent server when stopping a task - */ -export const AgentTaskStopResponseSchema = z.object({ - status: z.string(), - message: z.string().optional(), -}); - -/** - * Agent task status response schema - * Response from agent server for task status queries - */ -export const AgentTaskStatusResponseSchema = z.object({ - task_id: z.string().optional(), - status: z.string().optional(), - current_step: z.string().optional(), - error: z.string().optional(), - message: z.string().optional(), -}); - -/** - * Simulation status response schema - * Response for simulation status queries from agent server - */ -export const SimulationStatusResponseSchema = z.object({ - workflow_id: z.string(), - workflow_type: z.string(), - status: z.string(), - current_step: z.string(), - error: z.string().nullable(), - created_at: z.string(), - updated_at: z.string(), -}); - -/** - * Browser session response schema - * Response for browser session operations from agent server - */ -export const BrowserSessionResponseSchema = z.object({ - browser_session_id: z.string().optional(), - live_view_url: z.string().nullish(), - status: z.string().optional(), - message: z.string().nullish(), - success: z.boolean().optional(), -}); - -export const AgentSearchConfigSchema = z.object({ - contentType: z.enum(['document', 'video', 'automation_log', 'screenshot', 'all']).optional(), - context: z.string().optional(), - previousActions: z.array(z.string()).optional(), - top: z.coerce.number().min(1).max(100).optional(), - minConfidence: z.coerce.number().min(0).max(1).optional(), - entities: z.array(z.string()).optional(), - useVectorSearch: z.boolean().optional(), - vectorThreshold: z.coerce.number().min(0).max(1).optional(), -}); - -/** - * Search document schema (matches Azure Search document structure) - */ -export const SearchDocumentSchema = z.object({ - id: z.string(), - content: z.string(), - contentType: z.enum(['document', 'video', 'automation_log', 'screenshot']), - sourceFile: z.string(), - sourceType: z.string(), - metadata: z.string(), // JSON stringified - confidence: z.number(), - keyPhrases: z.array(z.string()), - entities: z.array(z.string()), // Format: "text:category:confidence" - sentiment: z.string(), // JSON stringified - vectorContent: z.array(z.number()), -}); - -/** - * Search result schema (Azure Search result wrapper) - */ -export const SearchResultSchema = z.object({ - document: SearchDocumentSchema, - score: z.number(), - highlights: z.record(z.string(), z.array(z.string())).optional(), -}); - -export const AgentSimulationIndexRequestSchema = z.object({ - simulation_id: z.coerce.number().positive('Simulation ID must be a positive number'), -}); - -export const AgentSimulationIndexResponseSchema = z.object({ - agent: AgentEntitySchema, - simulation_id: z.number(), - knowledge_id: z.number(), - message: z.string(), -}); - -export const ApplicationEntitySchema = BaseEntitySchema.extend({ - workspace_id: z.number(), - name: z.string(), - slug: z.string(), - type: ApplicationTypeSchema, - url: z.string().nullish(), - username: z.string().nullish(), - password: z.string().nullish(), - allowed_domains: z.array(z.string()).nullish().default([]), -}); - -/** - * Application read schema — entity minus password. Used for all API - * responses; password is write-only and never returned to clients. - */ -export const ApplicationReadSchema = ApplicationEntitySchema.omit({ password: true }); - -export const ApplicationCreateSchema = ApplicationEntitySchema.partial().extend({ - type: ApplicationTypeSchema, - name: z.string().min(1), - url: z.string(), - allowed_domains: z.array(z.string()).optional().default([]), -}); - -export const ApplicationUpdateSchema = ApplicationEntitySchema.partial().omit({ workspace_id: true, slug: true }); - -export const WidgetChipSchema = z.object({ - chip_mode: z.enum(['show', 'tell', 'do']), - chip_text: z.string(), -}); - -export const WidgetSettingsDataSchema = z.object({ - widget_enabled: z.boolean(), - widget_appearance: z.enum(['default', 'compact', 'full']), - widget_position: z.enum(['bottom_left', 'bottom_right', 'top_left', 'top_right']), - widget_device: z.enum(['desktop', 'mobile', 'desktop_mobile']), - widget_header: z.string(), - widget_body: z.string(), - widget_greeting: z.string(), - widget_feature_tell: z.boolean(), - widget_feature_show: z.boolean(), - widget_feature_do: z.boolean(), - widget_feature_human: z.boolean(), - widget_background_color: z.string(), - widget_text_color: z.string(), - widget_border_color: z.string(), - widget_accent_color: z.string(), - widget_secondary_color: z.string(), - widget_border_radius: z.string(), - widget_font_size: z.string(), - widget_width: z.string(), - widget_height: z.string(), - widget_shadow: z.string(), - widget_animation_duration: z.string(), - widget_fade_duration: z.string(), - widget_bounce_effect: z.boolean(), - widget_chips: z.array(WidgetChipSchema), -}); - -export const WidgetEntitySchema = BaseEntitySchema.extend({ - application_id: z.number(), - agent_id: z.number(), - type: WidgetTypeSchema, - settings: WidgetSettingsDataSchema, - status: EntityStatusSchema, - marketrix_id: z.string(), - marketrix_key: z.string(), - snippet: z.string().nullish(), -}); - -export const WidgetInfoSchema = WidgetEntitySchema.extend({ - application: ApplicationReadSchema.partial(), - workspace: WorkspaceEntitySchema.partial(), - user: UserEntitySchema.partial(), - agent: AgentEntitySchema.partial(), -}); - -/** - * Widget search result schema - includes optional eager-loaded agent - */ -export const WidgetWithAgentSchema = WidgetEntitySchema.extend({ - agent: AgentEntitySchema.partial().optional(), -}); - -/** - * Application with widgets schema - matches API response structure - */ -export const ApplicationWithWidgetsSchema = ApplicationReadSchema.extend({ - widgets: z.array(WidgetEntitySchema).optional(), - agents: z.array(AgentEntitySchema).optional(), -}); - -export const WidgetCreateSchema = WidgetEntitySchema.partial().extend({ - application_id: z.number().positive(), - agent_id: z.number().positive(), - type: WidgetTypeSchema, - settings: WidgetSettingsDataSchema.optional(), -}); - -export const WidgetUpdateSchema = WidgetEntitySchema.partial(); - -/** - * State Trigger entity schema - stores URL patterns and messages to show in widget - * `message` is one or more chip texts shown by the widget when the URL pattern matches. - */ -export const StateTriggerEntitySchema = BaseEntitySchema.extend({ - widget_id: z.number(), - url_pattern: z.string(), - message: z.array(z.string()), - description: z.string().optional(), -}); - -export const StateTriggerCreateSchema = StateTriggerEntitySchema.partial().extend({ - widget_id: z.number().positive(), - url_pattern: z.string().min(1), - message: z.array(z.string().min(1)).min(1), -}); - -export const StateTriggerUpdateSchema = StateTriggerEntitySchema.partial(); - -/** - * Chat request schema - * Requires either: marketrix_id + marketrix_key OR agent_id + application_id - */ -export const ChatRequestSchema = z - .object({ - marketrix_id: z.string().optional(), - marketrix_key: z.string().optional(), - agent_id: z.number().positive().optional(), - application_id: z.number().positive().optional(), - chat_id: z.string().optional(), // Optional since it comes from path params in some routes - content: z.string(), - }) - .refine(data => (data.marketrix_id && data.marketrix_key) ?? (data.agent_id && data.application_id), { - message: 'Either marketrix_id + marketrix_key or both agent_id + application_id must be provided', - }); - -export const ChatResponseSchema = z.object({ - text: z.string(), - task_id: z.string().optional(), -}); - -/** Server → Widget event (discriminated union on `type`) */ -export const WidgetEventSchema = z.discriminatedUnion('type', [ - z.object({ type: z.literal('registered'), chat_id: z.string(), application_id: z.number().optional() }), - z.object({ type: z.literal('pong') }), - z.object({ type: z.literal('heartbeat') }), - z.object({ - type: z.literal('chat/response'), - request_id: z.string(), - text: z.string(), - task_id: z.string().optional(), - }), - z.object({ - type: z.literal('chat/error'), - request_id: z.string(), - error: z.string(), - }), - z.object({ - type: z.literal('task/status'), - // Matches SimulationTaskStatus / QATaskStatus on the agent side. - status: z.enum(['running', 'completed', 'failed', 'stopped', 'has_question']), - message: z.string().optional(), - task_id: z.string().optional(), - timestamp: z.number().optional(), - }), - z.object({ - type: z.literal('tool/call'), - call_id: z.string(), - tool: z.string(), - args: z.record(z.string(), z.unknown()), - mode: z.enum(['show', 'do']).optional(), - explanation: z.string().optional(), - state_version: z.number().optional(), - }), -]); - -/** Widget → Server command (discriminated union on `type`) */ -export const WidgetCommandSchema = z.discriminatedUnion('type', [ - z.object({ type: z.literal('chat/tell'), request_id: z.string(), content: z.string() }), - z.object({ type: z.literal('chat/show'), request_id: z.string(), content: z.string() }), - z.object({ type: z.literal('chat/do'), request_id: z.string(), content: z.string() }), - z.object({ type: z.literal('chat/stop'), task_id: z.string().optional() }), - z.object({ - type: z.literal('tool/response'), - call_id: z.string(), - success: z.boolean(), - data: z.string().optional(), - error: z.string().optional(), - state_version: z.number().optional(), - }), - z.object({ type: z.literal('ping') }), - z.object({ - type: z.literal('rrweb/metadata'), - session_id: z.string(), - chat_id: z.string(), - application_id: z.number(), - url: z.string().optional(), - user_agent: z.string().optional(), - timestamp: z.number().optional(), - viewport: z - .object({ - width: z.number(), - height: z.number(), - }) - .optional(), - }), - z.object({ - type: z.literal('rrweb/events'), - session_id: z.string(), - events: z.array(z.unknown()), - }), -]); - -export type WidgetEvent = z.infer; -export type WidgetCommand = z.infer; - -export const AppEventScopeSchema = z.enum([ - 'simulations', - 'agents', - 'qa', - 'user', - 'jobs', - 'triggers', - 'automations', - 'notifications', -]); - -export type SimulationStatus = z.infer; -export type SimulationTaskStatus = z.infer; - -/** - * QA run derived status — set by `deriveQARunStats` based on aggregated - * task states. Emitted by the API on `qa-run/updated` events and returned - * in QA run oRPC payloads. - * - * Canonical wire vocabulary; `deriveQARunStats` returns `'running'` directly. - * `'pending'` is the empty-task initial state. - */ -export const QARunDerivedStatusSchema = z.enum(['pending', 'running', 'completed', 'failed', 'stopped']); -export type QARunDerivedStatus = z.infer; - -export const AppEventSchema = z.discriminatedUnion('type', [ - // Simulation events - z.object({ - type: z.literal('simulation/updated'), - simulation_id: z.number(), - application_id: z.number(), - status: SimulationStatusSchema, - // Mirrors the entity-level derived flag: true if any task is currently - // paused on a `has_question`. The parent `status` itself doesn't carry - // that value any more. - has_question: z.boolean().optional(), - step_label: z.string().optional(), - step_pending: z.boolean().optional(), - task_id: z.string().nullish(), - // Tool calls about to run for a pending step. Lets the live UI render - // the same "N tool call" expandable for the in-flight step that the - // history view shows for completed ones. - tool_calls: z.array(ToolCallRecordSchema).optional(), - }), - z.object({ - type: z.literal('simulation/created'), - simulation_id: z.number(), - application_id: z.number(), - }), - z.object({ - type: z.literal('simulation/deleted'), - simulation_id: z.number(), - application_id: z.number(), - }), - - // Flow-driven simulation lifecycle events - z.object({ - type: z.literal('simulation/queued'), - simulation_id: z.number(), - job_id: z.string(), - }), - z.object({ - type: z.literal('simulation/started'), - simulation_id: z.number(), - job_id: z.string(), - }), - z.object({ - type: z.literal('simulation/step'), - simulation_id: z.number(), - job_id: z.string(), - step_count: z.number().optional(), - message: z.string().optional(), - }), - z.object({ - type: z.literal('simulation/question'), - simulation_id: z.number(), - job_id: z.string(), - question: z.string().optional(), - }), - z.object({ - type: z.literal('simulation/completed'), - simulation_id: z.number(), - job_id: z.string(), - step_count: z.number().optional(), - }), - z.object({ - type: z.literal('simulation/failed'), - simulation_id: z.number(), - job_id: z.string(), - error: z.string().optional(), - }), - z.object({ - type: z.literal('simulation/stopped'), - simulation_id: z.number(), - job_id: z.string(), - }), - - // Mindmap generation progress - z.object({ - type: z.literal('simulation/mindmap-updated'), - simulation_id: z.number(), - application_id: z.number(), - mindmap_status: MindmapStatusSchema, - steps_processed: z.number(), - steps_total: z.number(), - }), - - // Flow-driven qa-run terminal event - z.object({ - type: z.literal('qa-run/completed'), - run_id: z.number(), - status: QARunDerivedStatusSchema, - }), - - // Agent events - z.object({ - type: z.literal('agent/updated'), - agent_id: z.number().optional(), - context_id: z.string().optional(), - task_id: z.string().optional(), - application_id: z.number().optional(), - status: AgentUpdatedStatusSchema, - error: z.string().optional(), - }), - z.object({ - type: z.literal('agent/created'), - agent_id: z.number(), - application_id: z.number(), - }), - z.object({ - type: z.literal('agent/deleted'), - agent_id: z.number(), - application_id: z.number(), - }), - - // QA events - z.object({ - type: z.literal('qa-document/updated'), - document_id: z.number(), - application_id: z.number(), - status: QAFlowStatusSchema, - step_label: z.string().optional(), - }), - z.object({ - type: z.literal('qa-run/updated'), - run_id: z.number(), - document_id: z.number(), - application_id: z.number(), - status: QARunDerivedStatusSchema, - // True if any test case in the run is paused on a `has_question`. The - // parent run status doesn't carry this value. - has_question: z.boolean().optional(), - simulation_id: z.number().optional(), - }), - z.object({ - type: z.literal('qa-test/updated'), - test_id: z.number(), - run_id: z.number(), - document_id: z.number(), - application_id: z.number(), - status: QAVerdictSchema, - }), - z.object({ - type: z.literal('qa-run/report-ready'), - run_id: z.number(), - url: z.string(), - }), - z.object({ - type: z.literal('qa-run/report-failed'), - run_id: z.number(), - error: z.string(), - }), - z.object({ - type: z.literal('reaction-run/report-ready'), - run_id: z.number(), - url: z.string(), - }), - z.object({ - type: z.literal('reaction-run/report-failed'), - run_id: z.number(), - error: z.string(), - }), - - // User events - z.object({ - type: z.literal('user/updated'), - user_id: z.number(), - workspace_id: z.number(), - status: EntityStatusSchema, - }), - - // Job lifecycle events (for long-running dashboard operations) - z.object({ - type: z.literal('job/progress'), - job_id: z.string(), - application_id: z.number(), - // `status` is heterogeneous: insight services emit `'in_progress'`, the - // GitHub connector forwards raw `workflow_run.status` from the GitHub API - // (`queued`/`in_progress`/`completed`/`waiting`) plus webhook actions - // (`received`/``). No registry to constrain against. - status: z.string(), - message: z.string().optional(), - // Phase 4: sub-phase indicator for chained jobs (e.g. research -> segments -> personas). - phase: z.enum(['research', 'segments', 'personas']).optional(), - }), - z.object({ - type: z.literal('job/completed'), - job_id: z.string(), - application_id: z.number(), - result: z.unknown().optional(), - }), - z.object({ - type: z.literal('job/failed'), - job_id: z.string(), - application_id: z.number(), - error: z.string(), - }), - - // Trigger events - z.object({ - type: z.literal('trigger/fired'), - trigger_id: z.number(), - workspace_id: z.number(), - provider: z.string(), - name: z.string(), - payload: z.unknown().optional(), - timestamp: z.string(), - }), - - // Automation run events - z.object({ - type: z.literal('automation-run/completed'), - automation_id: z.number(), - run_id: z.number(), - status: z.literal('completed'), - }), - z.object({ - type: z.literal('automation-run/failed'), - automation_id: z.number(), - run_id: z.number(), - status: z.literal('failed'), - error: z.string().optional(), - }), - - // Reaction simulation events - z.object({ - type: z.literal('reaction/completed'), - reaction_id: z.number(), - run_id: z.number(), - application_id: z.number(), - }), - z.object({ - type: z.literal('reaction/failed'), - reaction_id: z.number(), - run_id: z.number(), - application_id: z.number(), - error: z.string().optional(), - }), - z.object({ - type: z.literal('reaction/progress'), - reaction_id: z.number(), - run_id: z.number(), - application_id: z.number(), - result_id: z.number(), - persona_id: z.number().nullable(), - completed_personas: z.number(), - total_personas: z.number(), - failed_personas: z.number(), - }), - - // Notification events - z.object({ - type: z.literal('notification/created'), - notification_id: z.number(), - workspace_id: z.number(), - recipient_user_id: z.number(), - simulation_id: z.number(), - application_id: z.number(), - task_id: z.string().nullable(), - url: z.string(), - summary: z.string(), - // Server-side only; safe inside the authenticated app surface. Stripped - // before email/Slack/push fan-out by those channel services. - question_text: z.string().nullable(), - }), - z.object({ - type: z.literal('notification/resolved'), - notification_id: z.number(), - workspace_id: z.number(), - simulation_id: z.number(), - application_id: z.number(), - reason: z.enum(['answered', 'dismissed', 'cancelled']), - }), -]); - -export type AppEvent = z.infer; -export type AppEventScope = z.infer; - -export const NotificationTypeSchema = z.enum(['simulation_question']); -export const NotificationResolvedReasonSchema = z.enum(['answered', 'dismissed', 'cancelled']); - -export const NotificationEntitySchema = z.object({ - id: z.number(), - workspace_id: z.number(), - recipient_user_id: z.number(), - type: NotificationTypeSchema, - simulation_id: z.number().nullable(), - task_id: z.string().nullable(), - question_text: z.string().nullable(), - // Server-canonical deep link. Null for notification types that don't have - // one (none today; future-proofing). - url: z.string().nullable(), - // Human-readable one-line summary. Server-computed so every channel (in-app - // toast, email subject, Slack ping, web push body) sees the same text. - summary: z.string(), - email_sent_at: z.coerce.date().nullable(), - read_at: z.coerce.date().nullable(), - resolved_at: z.coerce.date().nullable(), - resolved_reason: NotificationResolvedReasonSchema.nullable(), - resolved_by_user_id: z.number().nullable(), - created_at: z.coerce.date(), -}); -export type NotificationEntity = z.infer; - -export const NotificationChannelsSchema = z.object({ - push: z.object({ enabled: z.boolean() }), - email: z.object({ - enabled: z.boolean(), - delay_minutes: z.coerce.number().int().min(1).max(120), - }), - slack: z.object({ enabled: z.boolean() }), -}); -export type NotificationChannelsInput = z.infer; - -export const PushSubscriptionRegisterSchema = z.object({ - endpoint: z.string().url(), - p256dh: z.string().min(1), - auth: z.string().min(1), - user_agent: z.string().optional(), -}); - -export const PushSubscriptionUnregisterSchema = z.object({ - endpoint: z.string().url(), -}); - -export const NotificationSlackTestSchema = z.object({ - webhook_url: SlackWebhookUrlSchema, -}); - -export const VapidPublicKeyResponseSchema = z.object({ - public_key: z.string().nullable(), -}); - -/** - * Action log metadata schema - * Captures common metadata fields used across different action log types - */ -export const ActionLogMetadataSchema = z - .object({ - details: z.string().optional(), - id: z.number().optional(), - type: z.string().optional(), - name: z.string().optional(), - target_user_id: z.number().optional(), - target_user_email: z.string().optional(), - reason: z.string().optional(), - assigned_role: z.string().optional(), - new_role: z.string().optional(), - previous_role: z.string().optional(), - workspace_name: z.string().optional(), - workspace_slug: z.string().optional(), - ip_address: z.string().optional(), - user_agent: z.string().optional(), - widget_type: z.string().optional(), - created_by: z.number().optional(), - }) - .passthrough(); // Allow additional fields for flexibility (e.g., updatedData, previousData, createdData) - -export const ActionLogEntitySchema = BaseEntitySchema.extend({ - workspace_id: z.number(), - user_id: z.number(), - type: ActionLogTypeSchema, - metadata: ActionLogMetadataSchema.optional(), -}); - -export const ActionLogCreateSchema = ActionLogEntitySchema.partial().extend({ - type: ActionLogTypeSchema, -}); - -export const ConversationTypeSchema = z.enum(['widget_chat', 'app_chat', 'guide_preview', 'slack']); -export const ConversationMessageRoleSchema = z.enum(['user', 'assistant', 'system', 'tool']); - -export const ConversationEntitySchema = BaseEntitySchema.extend({ - context_id: z.string(), - workspace_id: z.number(), - application_id: z.number().nullish(), - agent_id: z.number().nullish(), - user_id: z.number().nullish(), - simulation_id: z.number().nullish(), - session_id: z.number().nullish(), - persona_id: z.number().nullish(), - type: ConversationTypeSchema, - channel_id: z.string().nullish(), - preview_video_url: z.string().nullish(), - metadata: z.record(z.string(), z.unknown()).nullish(), -}); - -export const ConversationMessageEntitySchema = BaseEntitySchema.extend({ - conversation_id: z.number(), - role: ConversationMessageRoleSchema, - content: z.string(), - tool_call_id: z.string().nullish(), -}); - -/** - * Connector entity schema (per-workspace provider credentials/endpoints) - */ -export const ConnectorEntitySchema = BaseEntitySchema.extend({ - workspace_id: z.number(), - application_id: z.number().nullish(), - provider: ConnectorTypeSchema, - name: z.string().min(1).max(120), - identifier: z.string().max(120).nullish(), - api_endpoint: z.string().url().nullish(), - api_token: z.string().nullish(), - metadata: z.record(z.string(), z.unknown()).nullish(), - is_active: z.boolean().default(true), - status: EntityStatusSchema.default('active'), -}); - -export const ConnectorUpsertSchema = z.object({ - id: z.coerce.number().optional(), - application_id: z.number().nullish(), - provider: ConnectorTypeSchema, - name: z.string().min(1).max(120), - identifier: z.string().max(120).nullish(), - api_endpoint: z.string().url().nullish(), - api_token: z.string().nullish(), - metadata: z.record(z.string(), z.unknown()).nullish(), - is_active: z.boolean().optional(), - status: EntityStatusSchema.optional(), -}); - -export const ConnectorSearchSchema = z.object({ - application_id: z.number().nullish(), - provider: ConnectorTypeSchema.optional(), - status: EntityStatusSchema.optional(), - is_active: z.coerce.boolean().optional(), - limit: z.coerce.number().int().positive().max(100).optional(), - offset: z.coerce.number().int().nonnegative().optional(), -}); - -/** - * MCP activation status — subset of connector fields exposed to the dashboard - */ -export const McpStatusSchema = z.object({ - id: z.number(), - application_id: z.number(), - identifier: z.string(), - api_token: z.string(), - is_active: z.boolean(), - created_at: z.coerce.date().optional(), -}); - -export const McpToolSchema = z.object({ - name: z.string(), - category: z.string(), - description: z.string(), - inputSchema: z.record(z.string(), z.unknown()), - enabled: z.boolean(), -}); - -export const AutomationRunStatusSchema = z.enum(['pending', 'running', 'completed', 'failed', 'stopped']); - -export const AutomationNodeKindSchema = z.enum(['connector', 'condition']); - -const ConnectorNodeSchema = z.object({ - kind: z.literal('connector'), - connector_type: z.string().min(1), - connector_id: z.number().nullable().optional(), - trigger_id: z.number().nullable().optional(), - action_id: z.number().nullable().optional(), - capability: z.string().min(1), - role: z.enum(['trigger', 'callback']), - config: z.record(z.string(), z.unknown()).default({}), -}); - -const ConditionNodeSchema = z.object({ - kind: z.literal('condition'), - config: z.object({ - field: z.string().min(1), - operator: z.enum(['equals', 'not_equals', 'contains', 'gt', 'lt']), - value: z.unknown(), - }), -}); - -export const AutomationNodeSchema = z.discriminatedUnion('kind', [ConnectorNodeSchema, ConditionNodeSchema]); - -export const AutomationEdgeSchema = z.object({ - from: z.string().min(1), - to: z.string().min(1), - when: z.enum(['true', 'false']).optional(), -}); - -export const AutomationGraphSchema = z.object({ - nodes: z.record(z.string(), AutomationNodeSchema), - edges: z.array(AutomationEdgeSchema), -}); - -export const AutomationConcurrencySchema = z.enum(['skip', 'queue', 'replace']).default('skip'); - -export const AutomationEntitySchema = BaseEntitySchema.extend({ - workspace_id: z.number(), - created_by_user_id: z.number().nullable(), - name: z.string(), - graph: AutomationGraphSchema, - concurrency: AutomationConcurrencySchema, - enabled: z.boolean(), - last_run_at: z.coerce.date().nullable(), - next_run_at: z.coerce.date().nullable(), -}); - -export const AutomationCreateSchema = z.object({ - name: z.string().min(1).max(255), - graph: AutomationGraphSchema, - concurrency: AutomationConcurrencySchema.optional(), - enabled: z.boolean().optional(), -}); - -export const AutomationUpdateSchema = z.object({ - id: z.coerce.number(), - name: z.string().min(1).max(255).optional(), - graph: AutomationGraphSchema.optional(), - enabled: z.boolean().optional(), -}); - -export const AutomationSearchSchema = z.object({ - enabled: z.coerce.boolean().optional(), - limit: z.coerce.number().int().positive().max(100).optional(), - offset: z.coerce.number().int().nonnegative().optional(), -}); - -export const AutomationNodeResultSchema = z.object({ - status: z.enum(['in_progress', 'completed', 'failed', 'skipped', 'stopped']), - output: z.record(z.string(), z.unknown()).nullable().optional(), - error: z.string().nullable().optional(), - duration_ms: z.number().optional(), -}); - -export const AutomationRunEntitySchema = z.object({ - id: z.number().optional(), - automation_id: z.number(), - status: AutomationRunStatusSchema, - trigger_node_id: z.string(), - trigger_data: z.record(z.string(), z.unknown()).nullable(), - node_results: z.record(z.string(), AutomationNodeResultSchema), - started_at: z.coerce.date(), - completed_at: z.coerce.date().nullable(), -}); - -export const AutomationRunSearchSchema = z.object({ - status: AutomationRunStatusSchema.optional(), - limit: z.coerce.number().int().positive().max(100).optional(), - offset: z.coerce.number().int().nonnegative().optional(), -}); - -export const ConnectorCapabilitySchema = z.object({ - connector_type: z.string(), - built_in: z.boolean(), - triggers: z.array( - z.object({ - capability: z.string(), - description: z.string(), - config_schema: z.record(z.string(), z.unknown()), - }), - ), - callbacks: z.array( - z.object({ - capability: z.string(), - description: z.string(), - config_schema: z.record(z.string(), z.unknown()), - }), - ), -}); - -export const GithubPRFileItemSchema = z.object({ - filename: z.string(), - // External GitHub API field (added/modified/removed/renamed/copied/...). - // Left as `z.string()` so GitHub can introduce new values without breaking us. - status: z.string(), - additions: z.number(), - deletions: z.number(), - patch: z.string().optional(), -}); - -export const GithubPRTriggerDataSchema = z.object({ - event: z.string(), - action: z.string(), - pull_request: z.object({ - number: z.number(), - title: z.string(), - html_url: z.string(), - head: z.object({ sha: z.string(), ref: z.string() }), - base: z.object({ ref: z.string() }), - }), - repository: z.object({ - full_name: z.string(), - name: z.string(), - owner: z.object({ login: z.string() }), - }), - sender: z.object({ login: z.string() }), - files: z.array(GithubPRFileItemSchema).optional(), -}); - -export const GithubCheckRunInputSchema = z.object({ - name: z.string(), - head_sha: z.string(), - status: z.enum(['queued', 'in_progress', 'completed']).optional(), - conclusion: z.enum(['success', 'failure', 'neutral', 'cancelled', 'timed_out', 'action_required']).optional(), - started_at: z.string().optional(), - completed_at: z.string().optional(), - output: z - .object({ - title: z.string(), - summary: z.string(), - text: z.string().optional(), - }) - .optional(), -}); - -export const FileUploadResponseSchema = z.object({ - url: z.string(), -}); - -export const UploadUserLogoDataSchema = z.object({ - user_id: z.number(), - file: z.instanceof(File), -}); - -export const UserPromptDataSchema = z.object({ - user_id: z.number(), - application_id: z.number(), - source: ChatSourceSchema, - prompt: z.string(), - status: z.string(), -}); - -export const UserQuotaSchema = z.object({ - user_id: z.number(), - limit: z.number(), - used: z.number(), - remaining: z.number(), -}); - -export const UpdateChatCountDataSchema = z.object({ - user_id: z.number(), - workspace_id: z.number(), - chat_count: z.number(), - prompt_text: z.string(), - metadata: z.record(z.string(), z.string()).optional(), -}); - -/** - * AI agent status email data schema - */ - -export const MailOptionsDataSchema = z.object({ - to: z.string(), - subject: z.string(), - template: z.string(), - context: z.record(z.string(), z.string()), -}); - -/** - * Initial prompt limit for new users - */ -export const INITIAL_PROMPT_LIMIT = 50; - -/** - * Stripe webhook event schema - * Matches the structure of Stripe.Event from the Stripe SDK - */ -export const StripeWebhookEventSchema = z.object({ - id: z.string(), - object: z.literal('event'), - api_version: z.string().nullish(), - created: z.number(), - data: z.object({ - object: z.record(z.string(), z.unknown()), // The actual event object varies by event type - previous_attributes: z.record(z.string(), z.unknown()).optional(), - }), - livemode: z.boolean(), - pending_webhooks: z.number(), - request: z - .object({ - id: z.string().nullable(), - idempotency_key: z.string().nullable(), - }) - .nullable() - .optional(), - type: z.string(), // Event type (e.g., 'customer.subscription.created') -}); - -export const StripeCheckoutSchema = z.object({ - priceId: z.string().min(1, 'Price ID is required'), - successUrl: z.string().url('Success URL must be a valid URL'), - cancelUrl: z.string().url('Cancel URL must be a valid URL'), -}); - -export const StripePortalSchema = z.object({ - returnUrl: z.string().url('Return URL must be a valid URL'), -}); - -export const StripeTrialSchema = z.object({ - plan: z.enum(['startup', 'growth']).default('startup'), - interval: z.enum(['month', 'year']).default('month'), -}); - -export const StripeDowngradeSchema = z.object({ - subscriptionId: z.string().min(1, 'Subscription ID is required'), - priceId: z.string().min(1, 'Price ID is required'), -}); - -export const StripeDowngradeResponseSchema = z.object({ - success: z.boolean(), - message: z.string(), -}); - -export const PlanInfoSchema = z.object({ - subscriptionId: z.string().nullable(), - customerId: z.string().nullable(), - status: z - .enum(['active', 'past_due', 'canceled', 'unpaid', 'trialing', 'incomplete', 'incomplete_expired', 'paused']) - .nullable(), - planTier: z.enum(['free', 'startup', 'growth', 'enterprise']).nullable(), - billingInterval: z.enum(['month', 'year']).nullable(), - priceId: z.string().nullable(), - trialEndDate: z.coerce.date().nullable(), - currentPeriodStart: z.coerce.date().nullable(), - currentPeriodEnd: z.coerce.date().nullable(), - cancelAtPeriodEnd: z.boolean(), - isTrialing: z.boolean(), - daysRemainingInTrial: z.number().nullable(), - trial_provisioned: z.boolean(), -}); - -export const CheckoutSessionSchema = z.object({ - sessionId: z.string(), - url: z.string().url(), -}); - -export const PortalSessionSchema = z.object({ - url: z.string().url(), -}); - -export const TrialSubscriptionSchema = z.object({ - subscriptionId: z.string(), - customerId: z.string(), - status: z.literal('trialing'), - trialStartDate: z.coerce.date(), - trialEndDate: z.coerce.date(), - planTier: z.enum(['startup', 'growth']), - daysRemainingInTrial: z.number(), -}); - -export const UsageMetricSchema = z.object({ - used: z.number().int().min(0), - limit: z.number().int(), -}); - -/** - * Subscription usage statistics schema (workspace-wide resource tracking) - * Used for billing and subscription management - */ -export const SubscriptionUsageSchema = z.object({ - applications: UsageMetricSchema, - simulationsPerMonth: UsageMetricSchema, - userPrompts: UsageMetricSchema, -}); - -export const PriceAmountSchema = z.object({ - amount: z.number().int().min(0), - currency: z.string().default('usd'), - formatted: z.string(), -}); - -export const PlanPricingSchema = z.object({ - planId: z.enum(['free', 'startup', 'growth', 'enterprise']), - monthly: PriceAmountSchema.nullable(), - annual: PriceAmountSchema.nullable(), - priceIds: z.object({ - monthly: z.string().nullable(), - annual: z.string().nullable(), - }), -}); - -export const StripePricingSchema = z.object({ - plans: z.array(PlanPricingSchema), - lastUpdated: z.string().datetime(), -}); - -export const PlanCatalogEntrySchema = z.object({ - id: z.enum(['free', 'startup', 'growth', 'enterprise']), - name: z.string(), - description: z.string(), - features: z.array(z.string()), - cta: z.string(), - isPopular: z.boolean(), - customPriceDisplay: z.string().optional(), - priceSubtext: z.string().optional(), - borderColor: z.string(), - buttonColor: z.string(), - outlineColor: z.string(), - checkColor: z.string(), -}); - -/** Inferred type for a single plan catalog entry — mirrored to the app SDK so - * consumers don't need to redeclare the shape locally. */ -export type PlanCatalogEntry = z.infer; - -export const PlanCatalogSchema = z.object({ - plans: z.array(PlanCatalogEntrySchema), -}); - -/** Inferred type for the plan catalog response. */ -export type PlanCatalog = z.infer; - -export const StripeConfigSchema = z.object({ - publishableKey: z.string(), - priceIds: z.object({ - startup: z.object({ - monthly: z.string().nullable(), - annual: z.string().nullable(), - }), - growth: z.object({ - monthly: z.string().nullable(), - annual: z.string().nullable(), - }), - }), - trialDays: z.number().int().min(0), - calendlyUrl: z.string(), -}); - -/** - * Public client config (no auth). Values are kept in backend env only. - */ -export const PublicConfigSchema = z.object({ - widgetKey: z.string(), - widgetId: z.string(), - widgetUrl: z.string(), -}); - -/** - * Core enum types - */ -export type UserPlan = z.infer; -export type EntityStatus = z.infer; -export type WorkspacePackage = z.infer; -export type AgentType = z.infer; -export type AgentVoice = z.infer; -export type AgentStatus = z.infer; -export type KnowledgeType = z.infer; -export type KnowledgeSource = z.infer; -export type ChatStatus = z.infer; -export type ChatSource = z.infer; -export type InstructionType = z.infer; -export type ApplicationType = z.infer; -export type WidgetType = z.infer; -export type ConnectorType = z.infer; -export type ActionLogType = z.infer; - -export type FileData = z.infer; - -/** - * Service and data transfer types - */ -export type ActionLogData = z.infer; -export type ActionLogMetadataData = z.infer; -export type UpdateChatCountData = z.infer; -export type TokenData = z.infer; -export type UserCreateData = z.infer; -export type UserUpdateData = z.infer; -export type BatchUserCreateData = z.infer; -export type BatchUserCreateResult = z.infer; -export type UploadUserLogoData = z.infer; -export type WorkspaceReadData = z.infer; -export type WorkspaceUpdateData = z.infer; -export type WidgetInfoData = z.infer; -export type AgentCreationData = z.infer; -export type AgentUpdateData = z.infer; -export type AgentSearchConfig = z.infer; -export type SearchDocument = z.infer; -export type SearchResult = z.infer; -export type AgentSimulationIndexRequest = z.infer; -export type AgentSimulationIndexResponse = z.infer; -export type AgentTaskStartResponseData = z.infer; -export type AgentTaskStopResponseData = z.infer; -export type AgentTaskStatusResponseData = z.infer; -export type SimulationStatusResponseData = z.infer; -export type BrowserSessionResponseData = z.infer; -export type FileUploadResponse = z.infer; -export type ChatRequest = z.infer; -export type ChatResponseData = z.infer; -export type MailOptionsData = z.infer; -/** - * Model attribute types for Sequelize models - */ -export type UserData = z.infer; -export type WorkspaceData = z.infer; -export type WorkspacePlanData = z.infer; -export type AgentData = z.infer; -export type KnowledgeData = z.infer; -export type ApplicationData = z.infer; -export type ApplicationReadData = z.infer; -export type ApplicationCreateData = z.infer; -export type ApplicationUpdateData = z.infer; -export type ApplicationWithWidgetsData = z.infer; -export type WidgetData = z.infer; -export type WidgetWithAgentData = z.infer; -export type WidgetCreateData = z.infer; -export type WidgetUpdateData = z.infer; -export type StateTriggerData = z.infer; -export type StateTriggerCreateData = z.infer; -export type StateTriggerUpdateData = z.infer; -export type WidgetSettingsData = z.infer; -export type WidgetSettingsKey = keyof z.infer; -export type WidgetChip = z.infer; -export type SimulationData = z.infer; -export type SimulationLoggingUserData = z.infer; -export type SimulationStepSummaryData = z.infer; -export type ConversationType = z.infer; -export type ConversationMessageRole = z.infer; -export type ConversationData = z.infer; -export type ConversationMessageData = z.infer; -export type ConnectorData = z.infer; -export type ConnectorUpsertData = z.infer; -export type ConnectorSearchData = z.infer; -export type McpStatusData = z.infer; -export type McpToolData = z.infer; -export type AutomationData = z.infer; -export type AutomationCreateData = z.infer; -export type AutomationUpdateData = z.infer; -export type AutomationSearchData = z.infer; -export type AutomationRunData = z.infer; -export type AutomationRunSearchData = z.infer; -export type AutomationGraph = z.infer; -export type AutomationNode = z.infer; -export type AutomationNodeResult = z.infer; -export type UserQuotaData = z.infer; -export type SubscriptionUsageData = z.infer; -export type SimulationProgressData = z.infer; -export type MindMapEdgeData = z.infer; -export type MindMapNodeData = z.infer; -export type MindMapData = z.infer; -export const SessionStatsEntrySchema = z.object({ - date: z.string().describe('YYYY-MM-DD'), - sessions: z.number().int().nonnegative(), - events: z.number().int().nonnegative(), -}); - -export const SessionStatsResponseSchema = z.object({ - items: z.array(SessionStatsEntrySchema), -}); - -export type SessionData = z.infer; -export type SessionUpsertData = z.infer; -export type SessionStatsEntry = z.infer; -export type SessionStatsResponse = z.infer; -export type StripeCheckoutData = z.infer; -export type StripePortalData = z.infer; -export type StripeTrialData = z.infer; -export type StripeDowngradeData = z.infer; -export type StripeDowngradeResponseData = z.infer; -export type PlanInfoData = z.infer; -export type CheckoutSessionData = z.infer; -export type PortalSessionData = z.infer; -export type TrialSubscriptionData = z.infer; -export type PriceAmountData = z.infer; -export type PlanPricingData = z.infer; -export type StripePricingData = z.infer; -export type StripeConfigData = z.infer; -export type PublicConfigData = z.infer; -export type QAFlowData = z.infer; -export type QAFlowLastRunData = { - id: number; - status: string; - total_tests: number; - passed_tests: number; - failed_tests: number; - created_at: Date | null; -}; -export type QAFlowListItemData = QAFlowData & { - run_count: number; - display_title: string; - total_failed: number; - pass_rate: number | null; - test_case_count: number; - last_run: QAFlowLastRunData | null; -}; -export type QARunData = z.infer; -export type QATestCaseData = z.infer; - -// --------------------------------------------------------------------------- -// Preview Video Chat -// --------------------------------------------------------------------------- - -export const PreviewVideoChatMessageSchema = z.object({ - role: z.enum(['user', 'assistant', 'system']), - content: z.string(), - timestamp: z.string().optional(), -}); - -export const PreviewVideoChatEntitySchema = BaseEntitySchema.extend({ - workspace_id: z.number(), - application_id: z.number().nullish(), - agent_id: z.number().nullish(), - simulation_id: z.number().nullish(), - chat_id: z.string(), - chat_content: z.string().nullish(), - chat_history: z.array(PreviewVideoChatMessageSchema).nullish(), - chat_output: z.string().nullish(), - preview_video_url: z.string().nullish(), - metadata: z.record(z.string(), z.unknown()).nullish(), -}); - -export const PreviewVideoChatUpsertSchema = z.object({ - application_id: z.number().nullish(), - agent_id: z.number().nullish(), - simulation_id: z.number().nullish(), - chat_id: z.string(), - chat_content: z.string().nullish(), - chat_history: z.array(PreviewVideoChatMessageSchema).nullish(), - chat_output: z.string().nullish(), - preview_video_url: z.string().nullish(), - metadata: z.record(z.string(), z.unknown()).nullish(), -}); - -export const PreviewVideoChatSearchSchema = z.object({ - chat_id: z.string().optional(), - agent_id: z.number().optional(), - simulation_id: z.number().optional(), - limit: z.number().optional(), - offset: z.number().optional(), -}); - -export type PreviewVideoChatMessageData = z.infer; -export type PreviewVideoChatData = z.infer; -export type PreviewVideoChatUpsertData = z.infer; -export type PreviewVideoChatSearchData = z.infer; - -// Subset of providers we have first-party OAuth integrations for. -// TriggerProviderSchema and ActionProviderSchema extend this with extra values -// (`timer`, `mcp`, `internal`) that don't have a corresponding provider row. -export const ProviderNameSchema = z.enum(['github', 'slack', 'teams', 'jira']); -export const TriggerProviderSchema = z.enum(['github', 'slack', 'teams', 'jira', 'timer', 'mcp']); -export const ActionProviderSchema = z.enum(['github', 'slack', 'teams', 'jira', 'internal']); -export const ProviderStatusSchema = z.enum(['connected', 'disconnected']); - -// --- Provider --- -// Keyed by (workspace_id, provider). No surrogate id — every consumer already -// looks up by that pair, and the trigger/action linkage flows through their -// own (workspace_id, provider) columns rather than a FK. -export const ProviderEntitySchema = z.object({ - workspace_id: z.number(), - provider: ProviderNameSchema, - status: ProviderStatusSchema.default('disconnected'), - credentials: z.record(z.string(), z.unknown()).nullish(), - provider_data: z.record(z.string(), z.unknown()).nullish(), - connected_at: z.coerce.date().nullish(), - created_at: z.coerce.date().optional(), - updated_at: z.coerce.date().optional(), -}); - -// --- Trigger --- -export const TriggerEntitySchema = BaseEntitySchema.extend({ - workspace_id: z.number(), - connector_id: z.number().nullish(), - provider: TriggerProviderSchema, - name: z.string().min(1).max(255), - source_config: z.record(z.string(), z.unknown()).default({}), - webhook_id: z.string(), - webhook_secret: z.string(), - enabled: z.boolean().default(true), - last_triggered_at: z.coerce.date().nullish(), -}); - -export const TriggerCreateSchema = z.object({ - connector_id: z.number().nullish(), - provider: TriggerProviderSchema, - name: z.string().min(1).max(255), - source_config: z.record(z.string(), z.unknown()).default({}), -}); - -export const TriggerUpdateSchema = z.object({ - trigger_id: z.coerce.number(), - name: z.string().min(1).max(255).optional(), - source_config: z.record(z.string(), z.unknown()).optional(), - enabled: z.boolean().optional(), -}); - -export const TriggerSearchSchema = z - .object({ - provider: TriggerProviderSchema.optional(), - connector_id: z.coerce.number().optional(), - }) - .extend(PaginationSchema.shape); - -// --- Action --- -export const ActionEntitySchema = BaseEntitySchema.extend({ - workspace_id: z.number(), - provider: ActionProviderSchema, - name: z.string().min(1).max(255), - type: z.string().min(1).max(60), - target_config: z.record(z.string(), z.unknown()).default({}), - is_default: z.boolean().default(false), - enabled: z.boolean().default(true), - last_executed_at: z.coerce.date().nullish(), -}); - -export const ActionCreateSchema = z.object({ - provider: ActionProviderSchema, - name: z.string().min(1).max(255), - type: z.string().min(1).max(60), - target_config: z.record(z.string(), z.unknown()).default({}), -}); - -export const ActionUpdateSchema = z.object({ - action_id: z.coerce.number(), - name: z.string().min(1).max(255).optional(), - target_config: z.record(z.string(), z.unknown()).optional(), - enabled: z.boolean().optional(), -}); - -export const ActionSearchSchema = z - .object({ - provider: ActionProviderSchema.optional(), - type: z.string().optional(), - }) - .extend(PaginationSchema.shape); - -export const InsightConnectorStatusSchema = z.enum(['connected', 'not_connected']); - -export const InsightConnectorSchema = z.object({ - key: z.string(), - label: z.string(), - color: z.string(), - status: InsightConnectorStatusSchema, - session_count: z.number().nullable(), -}); - -export const InsightSegmentEntitySchema = z.object({ - id: z.number(), - application_id: z.number(), - name: z.string(), - percentage: z.number(), - description: z.string(), - avg_sessions_per_week: z.number(), - mobile_percentage: z.number(), - created_at: z.coerce.date().optional(), - updated_at: z.coerce.date().optional(), -}); - -export const InsightPersonaEntitySchema = z.object({ - id: z.number(), - application_id: z.number(), - segment_id: z.number().nullable(), - segment_name: z.string().optional(), - is_selected: z.boolean().default(false), - is_top: z.boolean().default(false), - is_pinned: z.boolean().default(false), - name: z.string(), - initials: z.string(), - description: z.string(), - traits: z.array(z.string()), - tags: z.array(z.string()).optional().default([]), - source: z.enum(['generated', 'domain', 'manual']).optional().default('generated'), - age_range: z.string().optional().default(''), - goals: z.string().optional().default(''), - behavior: z.string().optional().default(''), - pain_points: z.string().optional().default(''), - triggers: z.string().optional().default(''), - openness: z.number().nullable().optional(), - conscientiousness: z.number().nullable().optional(), - extraversion: z.number().nullable().optional(), - agreeableness: z.number().nullable().optional(), - neuroticism: z.number().nullable().optional(), - mbti_type: z.string().optional().default(''), - mbti_rationale: z.string().optional().default(''), - key_features: z.array(z.string()).optional().default([]), - bhvr_proficiency: z.number().nullable().optional(), - bhvr_preciseness: z.number().nullable().optional(), - bhvr_sensitivity: z.number().nullable().optional(), - bhvr_efficiency: z.number().nullable().optional(), - created_at: z.coerce.date().optional(), - updated_at: z.coerce.date().optional(), -}); - -export const InsightPersonasResponseSchema = z.object({ - connectors: z.array(InsightConnectorSchema), - segments: z.array(InsightSegmentEntitySchema), - personas: z.array(InsightPersonaEntitySchema), -}); - -export const DomainPersonaSuggestionSchema = z.object({ - name: z.string(), - initials: z.string(), - description: z.string(), - traits: z.array(z.string()), - tags: z.array(z.string()).optional().default([]), - is_top: z.boolean().default(false), -}); - -export const InsightPersonasSaveDomainInputSchema = z.object({ - personas: z.array( - DomainPersonaSuggestionSchema.extend({ - is_selected: z.boolean(), - }), - ), -}); - -export const InsightPersonaUpdateInputSchema = z.object({ - id: z.coerce.number(), - name: z.string().optional(), - description: z.string().optional(), - age_range: z.string().optional(), - goals: z.string().optional(), - behavior: z.string().optional(), - pain_points: z.string().optional(), - triggers: z.string().optional(), - tags: z.array(z.string()).optional(), - traits: z.array(z.string()).optional(), - key_features: z.array(z.string()).optional(), -}); - -export const DomainPersonaSuggestResponseSchema = z - .object({ - label: z.string(), - industry: z.string(), - personas: z.array(DomainPersonaSuggestionSchema), - }) - .nullable(); - -// Findings (Personas redesign v1.0) -// A Finding is an issue surfaced by a persona during a simulation. Severity -// and status are closed enums defined in PDF §03 (design tokens & taxonomies). - -export const InsightFindingSeveritySchema = z.enum(['critical', 'high', 'medium', 'low']); -export const InsightFindingStatusSchema = z.enum(['open', 'triaged', 'fixed', 'dismissed']); - -export const InsightFindingEntitySchema = z.object({ - id: z.number(), - application_id: z.number(), - persona_id: z.number(), - simulation_id: z.number().nullable(), - title: z.string(), - severity: InsightFindingSeveritySchema, - status: InsightFindingStatusSchema, - page_or_flow: z.string().nullable(), - description: z.string().nullable(), - fingerprint: z.string().nullable(), - first_seen_at: z.coerce.date().nullable(), - created_at: z.coerce.date().optional(), - updated_at: z.coerce.date().optional(), -}); - -export const InsightFindingSeverityBreakdownSchema = z.object({ - critical: z.number(), - high: z.number(), - medium: z.number(), - low: z.number(), -}); - -export const InsightFindingStatusBreakdownSchema = z.object({ - open: z.number(), - triaged: z.number(), - fixed: z.number(), - dismissed: z.number(), -}); - -// Hover quick-preview surface — top open critical/high finding (advanced -// persona-card feature). Lets the card teaser the worst-known issue without -// requiring a click into the detail page. -export const InsightPersonaTopFindingSchema = z.object({ - id: z.number(), - title: z.string(), - severity: InsightFindingSeveritySchema, - page_or_flow: z.string().nullable(), -}); - -// One bucket on the sparkline (advanced persona-card feature). `at` is the -// simulation's surfaced-at timestamp; `issues` is the count of findings -// surfaced in that simulation. -export const InsightPersonaTrendPointSchema = z.object({ - sim_id: z.number().nullable(), - issues: z.number(), - at: z.coerce.date().nullable(), -}); - -// Top pages bucket — page_or_flow with the count of findings surfaced there. -// Rendered as chips under TOP PAGES on the persona card (PDF §06 image). -export const InsightPersonaTopPageSchema = z.object({ - page: z.string(), - count: z.number(), -}); - -// Simulation outcome split for a persona. `running` rolls up queued, running, -// and creating_knowledge — anything not yet terminal. `failed` includes the -// 'stopped' status (user-cancelled runs read as failure for the persona since -// no findings will surface). Drives the small status pills next to the Sims -// count on the persona card. -export const InsightPersonaSimulationsBreakdownSchema = z.object({ - completed: z.number(), - failed: z.number(), - running: z.number(), -}); - -// Aggregated stats per persona, used by the redesigned persona card. -// `velocity_*` and `trend` power the velocity badge + sparkline on the card. -// `top_finding` powers the hover quick-preview AND the SIGNATURE FINDING row. -// `top_pages` powers the TOP PAGES chips row on the card. -export const InsightPersonaStatsSchema = z.object({ - persona_id: z.number(), - simulations_count: z.number(), - issues_count: z.number(), - severity_breakdown: InsightFindingSeverityBreakdownSchema, - status_breakdown: InsightFindingStatusBreakdownSchema, - last_run_at: z.coerce.date().nullable(), - velocity_7d: z.number().default(0), - velocity_delta: z.number().default(0), - trend: z.array(InsightPersonaTrendPointSchema).default([]), - top_finding: InsightPersonaTopFindingSchema.nullable().default(null), - top_pages: z.array(InsightPersonaTopPageSchema).default([]), - simulations_breakdown: InsightPersonaSimulationsBreakdownSchema.default({ - completed: 0, - failed: 0, - running: 0, - }), -}); - -export const InsightPersonaPinInputSchema = z.object({ - id: z.coerce.number(), - is_pinned: z.boolean(), -}); - -// Stats bar tiles on the list page header. -export const InsightPersonasOverviewSchema = z.object({ - personas_count: z.number(), - simulations_total: z.number(), - issues_total: z.number(), - uncovered_traffic_pct: z.number(), - audience: z.object({ - segments_count: z.number(), - traits_count: z.number(), - gaps_count: z.number(), - }), -}); - -export const InsightFindingsListInputSchema = z.object({ - persona_id: z.coerce.number(), -}); - -// A simulation that this persona ran against — derived from `reaction_result`, -// the source-of-truth join table for persona ↔ simulation. Surfaced -// independently of findings so the detail page can show "this persona ran -// against sim #X" even when X produced no findings yet. -export const InsightPersonaLinkedSimulationSchema = z.object({ - simulation_id: z.number(), - ran_at: z.coerce.date().nullable(), -}); - -export const InsightFindingsListResponseSchema = z.object({ - findings: z.array(InsightFindingEntitySchema), - severity_breakdown: InsightFindingSeverityBreakdownSchema, - status_breakdown: InsightFindingStatusBreakdownSchema, - simulations_count: z.number(), - simulations_breakdown: InsightPersonaSimulationsBreakdownSchema.default({ - completed: 0, - failed: 0, - running: 0, - }), - linked_simulations: z.array(InsightPersonaLinkedSimulationSchema).default([]), -}); - -// Top pages surfaced by a single simulation — rendered in the simulation -// detail page (below PersonasPanel) so the operator sees at-a-glance which -// pages or flows this specific run hit hardest. -export const InsightSimulationTopPagesInputSchema = z.object({ - simulation_id: z.coerce.number(), -}); - -export const InsightSimulationTopPagesResponseSchema = z.object({ - top_pages: z.array(InsightPersonaTopPageSchema), - total_findings: z.number(), -}); - -export const InsightFindingSummarizeInputSchema = z.object({ - persona_id: z.coerce.number(), -}); - -export const InsightFindingSummarizeResponseSchema = z.object({ - summary: z.string(), -}); - -// Compare view (PDF §06). Two or three personas side by side. - -export const InsightPersonaCompareCardSchema = z.object({ - persona: InsightPersonaEntitySchema, - stats: InsightPersonaStatsSchema, - top_pages: z.array(z.object({ page: z.string(), count: z.number() })), - signature_finding: InsightFindingEntitySchema.nullable(), -}); - -export const InsightPersonaCompareInputSchema = z.object({ - persona_ids: z.array(z.coerce.number()).min(2).max(3), -}); - -export const InsightPersonaCompareResponseSchema = z.object({ - cards: z.array(InsightPersonaCompareCardSchema), - shared_findings: z.array( - z.object({ - title: z.string(), - page_or_flow: z.string().nullable(), - persona_ids: z.array(z.number()), - }), - ), - synthesis: z.string(), -}); - -// Duplicate detection + audience gaps (PDF §04 — P3). - -export const InsightPersonaDuplicateGroupSchema = z.object({ - reason: z.string(), - persona_ids: z.array(z.number()), -}); - -export const InsightAudienceGapSchema = z.object({ - label: z.string(), - description: z.string(), -}); - -export const InsightAudienceCoverageSchema = z.object({ - segments_count: z.number(), - traits_count: z.number(), - duplicates: z.array(InsightPersonaDuplicateGroupSchema), - gaps: z.array(InsightAudienceGapSchema), -}); - -// Heatmaps - -export const HeatmapTypeSchema = z.enum(['clicks', 'scroll', 'attention']); -export const HeatmapVariationSchema = z.enum(['desktop', 'tablet', 'mobile']); - -export const HeatSpotSchema = z.object({ - x: z.number(), - y: z.number(), - radius: z.number(), - intensity: z.number(), -}); - -export const HeatmapStatsSchema = z.object({ - total_interactions: z.number(), - unique_users: z.number(), - hottest_zone: z.string(), - dead_zones: z.number(), -}); - -export const HeatmapElementCountSchema = z.object({ - selector: z.string(), - count: z.number(), - dead_clicks: z.number(), - rage_clicks: z.number(), -}); - -export const HeatmapPageEntitySchema = z.object({ - id: z.number(), - application_id: z.number(), - path: z.string(), - session_count: z.number(), - variation_count: z.number(), - variations: z.array(HeatmapVariationSchema).default([]), - visitor_count: z.number().optional(), - last_aggregated_at: z.coerce.date().nullable().optional(), - representative_session_id: z.string().nullable().optional(), -}); - -export const HeatmapSnapshotEntitySchema = z.object({ - id: z.number(), - heatmap_page_id: z.number(), - variation: HeatmapVariationSchema, - type: HeatmapTypeSchema, - spots: z.array(HeatSpotSchema), - stats: HeatmapStatsSchema, - page_width: z.number().nullable().optional(), - page_height: z.number().nullable().optional(), - element_counts: z.array(HeatmapElementCountSchema).optional(), - representative_session_override: z.string().nullable().optional(), -}); - -export const HeatmapJobStatusSchema = z.object({ - job_id: z.string().nullable(), - status: z.enum(['idle', 'queued', 'active', 'completed', 'failed']), - last_aggregated_at: z.coerce.date().nullable(), -}); - -/** - * Candidate session returned by `insightHeatmapCandidates`. Represents a - * session that contributed to a (page, variation) heatmap and could be - * pinned as that snapshot's DOM backdrop via `insightHeatmapSetBackdrop`. - */ -export const HeatmapCandidateSchema = z.object({ - session_id: z.string(), - width: z.number(), - height: z.number(), - event_count: z.number(), - started_at: z.coerce.date(), - has_full_snapshot: z.boolean(), -}); - -export type HeatmapCandidate = z.infer; - -// Reactions - -export const ReactionScoreSchema = z.object({ - score: z.number().min(1).max(10), - justification: z.string(), -}); - -export const ContextRefSchema = z.object({ - type: z.enum(['doc', 'sim', 'session']), - id: z.string(), - label: z.string(), -}); - -export const ReactionReplayEvidenceSchema = z.object({ - simulations: z - .array( - z.object({ - simulation_id: z.number(), - task_id: z.string().nullable().optional(), - step_count: z.number(), - summary: z.string(), - }), - ) - .default([]), - moments: z - .array( - z.object({ - sim_index: z.number(), - step_index: z.number(), - label: z.string(), - }), - ) - .max(6) - .default([]), - context_refs: z.array(ContextRefSchema).default([]), -}); - -export const PersonaSnapshotSchema = z.object({ - name: z.string(), - initials: z.string().optional(), - segment_name: z.string().optional(), - traits: z.array(z.string()).optional(), - age_range: z.string().optional(), -}); - -export const ReactionAnswerMomentSchema = z.object({ - label: z.string(), - description: z.string().optional(), - simulation_id: z.number().optional(), - task_id: z.string().optional(), - sim_index: z.number().optional(), - step_index: z.number().optional(), -}); - -export const ReactionAnswerEntitySchema = z.object({ - id: z.number(), - result_id: z.number(), - question_id: z.number(), - answer_text: z.string(), - dimension_scores: z.record(z.string(), ReactionScoreSchema), - overall_reactions: z.record(z.string(), ReactionScoreSchema), - moments: z.array(ReactionAnswerMomentSchema).optional(), -}); - -export const ReactionQuestionEntitySchema = z.object({ - id: z.number(), - reaction_id: z.number(), - order_index: z.number(), - text: z.string(), - question_type: z.string(), - created_at: z.coerce.date().optional(), -}); - -export const ReactionResultEntitySchema = z.object({ - id: z.number(), - run_id: z.number(), - persona_id: z.number().nullable(), - persona_name: z.string().optional(), - persona_initials: z.string().optional(), - ad_hoc_persona: z - .object({ name: z.string(), description: z.string(), traits: z.array(z.string()) }) - .nullable() - .optional(), - overall_reactions: z.record(z.string(), ReactionScoreSchema), - dimension_scores: z.record(z.string(), ReactionScoreSchema), - simulation_id: z.number().nullable().optional(), - task_id: z.string().nullable().optional(), - status: z.enum(['pending', 'completed', 'failed']).optional(), - replay_evidence: ReactionReplayEvidenceSchema.nullable().optional(), - error: z.string().nullable().optional(), - persona_snapshot: PersonaSnapshotSchema.nullable().optional(), - answers: z.array(ReactionAnswerEntitySchema).optional(), - created_at: z.coerce.date().optional(), -}); - -export const SuggestedSimulationSchema = z.object({ - description: z.string(), - selected: z.boolean(), - simulation_id: z.number().nullable().optional(), - task_id: z.string().nullable().optional(), - status: SimulationStatusSchema.nullable().optional(), -}); - -export const ReactionRunEntitySchema = z.object({ - id: z.number(), - reaction_id: z.number(), - run_number: z.number().optional(), - context_refs: z.array(ContextRefSchema), - simulations: z.array(SuggestedSimulationSchema), - persona_ids: z.array(z.number()).optional(), - results: z.array(ReactionResultEntitySchema).optional(), - status: z.enum(['running', 'completed', 'failed']).nullable().optional(), - processing_started_at: z.coerce.date().nullable().optional(), - completed_at: z.coerce.date().nullable().optional(), - failed_at: z.coerce.date().nullable().optional(), - error: z.string().nullable().optional(), - report_pdf_url: z.string().nullish(), - created_at: z.coerce.date().optional(), -}); - -export const ReactionEntitySchema = z.object({ - id: z.number(), - application_id: z.number(), - questions: z.array(ReactionQuestionEntitySchema).default([]), - run_count: z.number().optional(), - last_run_at: z.coerce.date().optional(), - runs: z.array(ReactionRunEntitySchema).optional(), - drafted_questions: z.array(z.string()).optional(), - created_at: z.coerce.date().optional(), -}); - -export const ChatContextResponseSchema = z.object({ - contextRefs: z.array(ContextRefSchema), - suggestedSimulations: z.array(SuggestedSimulationSchema), -}); - -export const SlackCommandLogStatusSchema = z.enum(['received', 'classifying', 'dispatched', 'completed', 'failed']); - -export const SlackCommandLogEntitySchema = z.object({ - id: z.number(), - workspace_id: z.number(), - slack_user_id: z.string(), - slack_channel_id: z.string().nullable(), - raw_text: z.string(), - detected_intent: z.string(), - extracted_params: z.record(z.string(), z.unknown()), - status: SlackCommandLogStatusSchema, - response_text: z.string().nullable(), - error_message: z.string().nullable(), - duration_ms: z.number().nullable(), - created_at: z.coerce.date().optional(), - updated_at: z.coerce.date().optional(), -}); - -export const SlackCommandLogSearchSchema = z.object({ - intent: z.string().optional(), - limit: z.coerce.number().optional().default(20), - offset: z.coerce.number().optional().default(0), -}); - -export const SlackCapabilitySchema = z.object({ - intent: z.string(), - name: z.string(), - description: z.string(), - example: z.string(), - execution_count: z.number(), - last_used: z.string().nullable(), -}); - -// Type aliases -export type ProviderData = z.infer; -export type TriggerData = z.infer; -export type ActionData = z.infer; -export type SlackCommandLogData = z.infer; -export type SlackCapabilityData = z.infer; -export type GithubPRFileItem = z.infer; -export type GithubPRTriggerData = z.infer; -export type GithubCheckRunInput = z.infer; diff --git a/src/services/ApiService.ts b/src/services/ApiService.ts index 612f061a..6d9568c7 100644 --- a/src/services/ApiService.ts +++ b/src/services/ApiService.ts @@ -1,5 +1,4 @@ -import { sdk } from '../sdk'; -import type { WidgetCommand } from '../sdk/schema'; +import { sdk, type WidgetCommand } from '../sdk'; import type { MarketrixConfig, SendMessageRequest, SendMessageResponse } from '../types'; import { sessionManager } from './SessionManager'; import { StreamClient } from './StreamClient'; diff --git a/src/services/StreamClient.ts b/src/services/StreamClient.ts index f014ed4f..b2ffe730 100644 --- a/src/services/StreamClient.ts +++ b/src/services/StreamClient.ts @@ -1,5 +1,4 @@ -import { sdk } from '../sdk'; -import type { WidgetCommand, WidgetEvent } from '../sdk/schema'; +import { sdk, type WidgetCommand, type WidgetEvent } from '../sdk'; export type StreamStatus = 'disconnected' | 'connecting' | 'connected' | 'registered' | 'error'; diff --git a/src/services/__tests__/sse-event-handling.test.ts b/src/services/__tests__/sse-event-handling.test.ts index a1d4b7bd..56b52632 100644 --- a/src/services/__tests__/sse-event-handling.test.ts +++ b/src/services/__tests__/sse-event-handling.test.ts @@ -6,7 +6,7 @@ import { describe, expect, it } from 'vitest'; -import { type WidgetEvent, WidgetEventSchema } from '@/sdk/schema'; +import { type WidgetEvent, WidgetEventSchema } from '@/sdk'; import type { ChatMessage } from '@/types'; import { hasThinkingMarker, updateThinkingMarker } from '@/utils/chat'; diff --git a/src/services/__tests__/widget-status-mapping.test.ts b/src/services/__tests__/widget-status-mapping.test.ts index aeeb7dfb..4e1a79cd 100644 --- a/src/services/__tests__/widget-status-mapping.test.ts +++ b/src/services/__tests__/widget-status-mapping.test.ts @@ -4,7 +4,7 @@ */ import { describe, expect, it } from 'vitest'; -import { WidgetEventSchema } from '@/sdk/schema'; +import { WidgetEventSchema } from '@/sdk'; describe('Widget task status contract — Wave 14 (C1 cutover)', () => { const SIM_TASK_STATUSES = ['queued', 'running', 'completed', 'failed', 'has_question', 'stopped'] as const;