From a6df1a5cd985d06f03fe21183905c995be190064 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 3 Jul 2026 18:41:44 +0000 Subject: [PATCH 01/13] feat: add local lead scout extension shell Registers "local-lead-scout" in the extension catalog and sidebar icon map so it can be installed/enabled per workspace like any other extension. Feature logic lands in follow-up commits. --- apps/server/src/extensions.ts | 9 +++++++++ apps/web/src/components/app-sidebar.tsx | 2 ++ 2 files changed, 11 insertions(+) diff --git a/apps/server/src/extensions.ts b/apps/server/src/extensions.ts index a36a299..ea4d3a0 100644 --- a/apps/server/src/extensions.ts +++ b/apps/server/src/extensions.ts @@ -47,6 +47,15 @@ export const EXTENSION_CATALOG: ExtensionCatalogEntry[] = [ icon: "Film", route: "video-studio", }, + { + key: "local-lead-scout", + name: "Local Lead Scout", + description: + "Finds local businesses with no website via compliant sources (CSV import, OSM Overpass, official Places API), then drafts a prototype and outreach email for you to review and approve before anything is sent.", + category: "Growth", + icon: "MapPin", + route: "local-lead-scout", + }, ]; export function getExtensionCatalogEntry( diff --git a/apps/web/src/components/app-sidebar.tsx b/apps/web/src/components/app-sidebar.tsx index 5475ec3..b038cae 100644 --- a/apps/web/src/components/app-sidebar.tsx +++ b/apps/web/src/components/app-sidebar.tsx @@ -15,6 +15,7 @@ import { Images, LayoutDashboard, Library, + MapPin, MessageSquare, Package, Plug, @@ -56,6 +57,7 @@ import { useInstallation } from "@/lib/use-installation"; const EXTENSION_ICON_MAP: Record = { TrendingUp, Film, + MapPin, }; export function AppSidebar() { From b833bf642a80606fb4a85d73079619d30d29055e Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 3 Jul 2026 18:44:27 +0000 Subject: [PATCH 02/13] feat: add lead scout campaign and lead storage MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds the DB tables backing Local Lead Scout: campaigns, per-provider source config, scan runs, leads, prototypes, outreach drafts, and a suppression list, for both the Postgres and SQLite dialects. Schema only — repo layer and feature logic land in follow-up commits. --- .../drizzle/pg/0045_strange_sally_floyd.sql | 170 + .../db/drizzle/pg/meta/0045_snapshot.json | 6199 +++++++++++++++++ packages/db/drizzle/pg/meta/_journal.json | 7 + .../drizzle/sqlite/0041_luxuriant_jubilee.sql | 161 + .../db/drizzle/sqlite/meta/0041_snapshot.json | 6014 ++++++++++++++++ packages/db/drizzle/sqlite/meta/_journal.json | 7 + packages/db/src/schema/pg/app.ts | 295 + packages/db/src/schema/sqlite/app.ts | 248 + 8 files changed, 13101 insertions(+) create mode 100644 packages/db/drizzle/pg/0045_strange_sally_floyd.sql create mode 100644 packages/db/drizzle/pg/meta/0045_snapshot.json create mode 100644 packages/db/drizzle/sqlite/0041_luxuriant_jubilee.sql create mode 100644 packages/db/drizzle/sqlite/meta/0041_snapshot.json diff --git a/packages/db/drizzle/pg/0045_strange_sally_floyd.sql b/packages/db/drizzle/pg/0045_strange_sally_floyd.sql new file mode 100644 index 0000000..9791571 --- /dev/null +++ b/packages/db/drizzle/pg/0045_strange_sally_floyd.sql @@ -0,0 +1,170 @@ +CREATE TYPE "public"."lead_scout_draft_status" AS ENUM('draft', 'approved', 'rejected', 'sending', 'sent', 'failed');--> statement-breakpoint +CREATE TYPE "public"."lead_scout_email_provider" AS ENUM('smtp', 'resend', 'mailgun', 'custom');--> statement-breakpoint +CREATE TYPE "public"."lead_scout_lead_status" AS ENUM('new', 'reviewed', 'prototype_requested', 'prototype_ready', 'email_drafted', 'approved_to_send', 'sending', 'sent', 'rejected', 'suppressed');--> statement-breakpoint +CREATE TYPE "public"."lead_scout_outreach_mode" AS ENUM('draft_only', 'review_and_send');--> statement-breakpoint +CREATE TYPE "public"."lead_scout_prototype_status" AS ENUM('pending', 'ready', 'failed');--> statement-breakpoint +CREATE TYPE "public"."lead_scout_provider" AS ENUM('manual_csv', 'google_places_api', 'osm_overpass', 'custom_api');--> statement-breakpoint +CREATE TYPE "public"."lead_scout_scan_run_status" AS ENUM('running', 'completed', 'failed');--> statement-breakpoint +CREATE TYPE "public"."lead_scout_website_status" AS ENUM('unknown', 'has_website', 'missing_website', 'invalid_website');--> statement-breakpoint +CREATE TABLE "lead_scout_campaign" ( + "id" text PRIMARY KEY NOT NULL, + "workspace_id" text NOT NULL, + "extension_id" text NOT NULL, + "name" text NOT NULL, + "postal_code" text NOT NULL, + "country" text DEFAULT 'US' NOT NULL, + "radius_km" double precision DEFAULT 10 NOT NULL, + "niches" jsonb DEFAULT '[]'::jsonb NOT NULL, + "max_results_per_run" integer DEFAULT 25 NOT NULL, + "provider" "lead_scout_provider" NOT NULL, + "min_confidence" integer DEFAULT 50 NOT NULL, + "outreach_mode" "lead_scout_outreach_mode" DEFAULT 'draft_only' NOT NULL, + "schedule_enabled" boolean DEFAULT false NOT NULL, + "schedule_cron_expression" text, + "next_scan_at" timestamp, + "last_scan_at" timestamp, + "auto_generate_prototype" boolean DEFAULT false NOT NULL, + "auto_draft_email" boolean DEFAULT false NOT NULL, + "auto_send_after_approval" boolean DEFAULT false NOT NULL, + "require_approval_before_prototype" boolean DEFAULT true NOT NULL, + "require_approval_before_email_send" boolean DEFAULT true NOT NULL, + "prototype_agent_id" text, + "created_at" timestamp DEFAULT now() NOT NULL, + "updated_at" timestamp DEFAULT now() NOT NULL +); +--> statement-breakpoint +CREATE TABLE "lead_scout_email_settings" ( + "id" text PRIMARY KEY NOT NULL, + "workspace_id" text NOT NULL, + "provider" "lead_scout_email_provider" DEFAULT 'smtp' NOT NULL, + "from_name" text NOT NULL, + "from_email" text NOT NULL, + "reply_to" text, + "credentials" text, + "daily_send_limit" integer DEFAULT 20 NOT NULL, + "per_campaign_send_limit" integer DEFAULT 10 NOT NULL, + "dry_run_mode" boolean DEFAULT true NOT NULL, + "legal_footer" text, + "unsubscribe_text" text DEFAULT 'Reply STOP to opt out.' NOT NULL, + "send_count_today" integer DEFAULT 0 NOT NULL, + "send_count_date" text, + "created_at" timestamp DEFAULT now() NOT NULL, + "updated_at" timestamp DEFAULT now() NOT NULL, + CONSTRAINT "lead_scout_email_settings_workspace_id_unique" UNIQUE("workspace_id") +); +--> statement-breakpoint +CREATE TABLE "lead_scout_lead" ( + "id" text PRIMARY KEY NOT NULL, + "workspace_id" text NOT NULL, + "campaign_id" text NOT NULL, + "scan_run_id" text, + "source_provider" "lead_scout_provider" NOT NULL, + "source_id" text NOT NULL, + "business_name" text NOT NULL, + "category" text, + "niche" text, + "formatted_address" text, + "postal_code" text, + "city" text, + "phone" text, + "email" text, + "website" text, + "website_status" "lead_scout_website_status" DEFAULT 'unknown' NOT NULL, + "confidence" integer DEFAULT 0 NOT NULL, + "evidence_summary" text, + "missing_reason" text, + "status" "lead_scout_lead_status" DEFAULT 'new' NOT NULL, + "created_at" timestamp DEFAULT now() NOT NULL, + "updated_at" timestamp DEFAULT now() NOT NULL +); +--> statement-breakpoint +CREATE TABLE "lead_scout_outreach_draft" ( + "id" text PRIMARY KEY NOT NULL, + "workspace_id" text NOT NULL, + "lead_id" text NOT NULL, + "prototype_id" text, + "task_id" text, + "subject" text, + "body_text" text, + "body_html" text, + "status" "lead_scout_draft_status" DEFAULT 'draft' NOT NULL, + "error_message" text, + "approved_at" timestamp, + "sent_at" timestamp, + "created_at" timestamp DEFAULT now() NOT NULL, + "updated_at" timestamp DEFAULT now() NOT NULL +); +--> statement-breakpoint +CREATE TABLE "lead_scout_prototype" ( + "id" text PRIMARY KEY NOT NULL, + "workspace_id" text NOT NULL, + "lead_id" text NOT NULL, + "task_id" text, + "status" "lead_scout_prototype_status" DEFAULT 'pending' NOT NULL, + "concept" text, + "hero_copy" text, + "sections" jsonb DEFAULT '[]'::jsonb NOT NULL, + "call_to_action" text, + "style_direction" text, + "artifact_markdown" text, + "approved" boolean DEFAULT false NOT NULL, + "error_message" text, + "created_at" timestamp DEFAULT now() NOT NULL, + "updated_at" timestamp DEFAULT now() NOT NULL +); +--> statement-breakpoint +CREATE TABLE "lead_scout_scan_run" ( + "id" text PRIMARY KEY NOT NULL, + "campaign_id" text NOT NULL, + "workspace_id" text NOT NULL, + "provider" "lead_scout_provider" NOT NULL, + "status" "lead_scout_scan_run_status" DEFAULT 'running' NOT NULL, + "result_count" integer DEFAULT 0 NOT NULL, + "new_lead_count" integer DEFAULT 0 NOT NULL, + "missing_website_count" integer DEFAULT 0 NOT NULL, + "summary" text, + "error_message" text, + "started_at" timestamp DEFAULT now() NOT NULL, + "completed_at" timestamp +); +--> statement-breakpoint +CREATE TABLE "lead_scout_source_config" ( + "id" text PRIMARY KEY NOT NULL, + "workspace_id" text NOT NULL, + "provider" "lead_scout_provider" NOT NULL, + "config" jsonb DEFAULT '{}'::jsonb NOT NULL, + "api_key" text, + "enabled" boolean DEFAULT true NOT NULL, + "created_at" timestamp DEFAULT now() NOT NULL, + "updated_at" timestamp DEFAULT now() NOT NULL +); +--> statement-breakpoint +CREATE TABLE "lead_scout_suppression" ( + "id" text PRIMARY KEY NOT NULL, + "workspace_id" text NOT NULL, + "email" text, + "domain" text, + "reason" text NOT NULL, + "created_at" timestamp DEFAULT now() NOT NULL +); +--> statement-breakpoint +ALTER TABLE "lead_scout_campaign" ADD CONSTRAINT "lead_scout_campaign_workspace_id_workspace_id_fk" FOREIGN KEY ("workspace_id") REFERENCES "public"."workspace"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "lead_scout_campaign" ADD CONSTRAINT "lead_scout_campaign_extension_id_extension_id_fk" FOREIGN KEY ("extension_id") REFERENCES "public"."extension"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "lead_scout_campaign" ADD CONSTRAINT "lead_scout_campaign_prototype_agent_id_agent_id_fk" FOREIGN KEY ("prototype_agent_id") REFERENCES "public"."agent"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "lead_scout_email_settings" ADD CONSTRAINT "lead_scout_email_settings_workspace_id_workspace_id_fk" FOREIGN KEY ("workspace_id") REFERENCES "public"."workspace"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "lead_scout_lead" ADD CONSTRAINT "lead_scout_lead_workspace_id_workspace_id_fk" FOREIGN KEY ("workspace_id") REFERENCES "public"."workspace"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "lead_scout_lead" ADD CONSTRAINT "lead_scout_lead_campaign_id_lead_scout_campaign_id_fk" FOREIGN KEY ("campaign_id") REFERENCES "public"."lead_scout_campaign"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "lead_scout_lead" ADD CONSTRAINT "lead_scout_lead_scan_run_id_lead_scout_scan_run_id_fk" FOREIGN KEY ("scan_run_id") REFERENCES "public"."lead_scout_scan_run"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "lead_scout_outreach_draft" ADD CONSTRAINT "lead_scout_outreach_draft_workspace_id_workspace_id_fk" FOREIGN KEY ("workspace_id") REFERENCES "public"."workspace"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "lead_scout_outreach_draft" ADD CONSTRAINT "lead_scout_outreach_draft_lead_id_lead_scout_lead_id_fk" FOREIGN KEY ("lead_id") REFERENCES "public"."lead_scout_lead"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "lead_scout_outreach_draft" ADD CONSTRAINT "lead_scout_outreach_draft_prototype_id_lead_scout_prototype_id_fk" FOREIGN KEY ("prototype_id") REFERENCES "public"."lead_scout_prototype"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "lead_scout_outreach_draft" ADD CONSTRAINT "lead_scout_outreach_draft_task_id_task_id_fk" FOREIGN KEY ("task_id") REFERENCES "public"."task"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "lead_scout_prototype" ADD CONSTRAINT "lead_scout_prototype_workspace_id_workspace_id_fk" FOREIGN KEY ("workspace_id") REFERENCES "public"."workspace"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "lead_scout_prototype" ADD CONSTRAINT "lead_scout_prototype_lead_id_lead_scout_lead_id_fk" FOREIGN KEY ("lead_id") REFERENCES "public"."lead_scout_lead"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "lead_scout_prototype" ADD CONSTRAINT "lead_scout_prototype_task_id_task_id_fk" FOREIGN KEY ("task_id") REFERENCES "public"."task"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "lead_scout_scan_run" ADD CONSTRAINT "lead_scout_scan_run_campaign_id_lead_scout_campaign_id_fk" FOREIGN KEY ("campaign_id") REFERENCES "public"."lead_scout_campaign"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "lead_scout_scan_run" ADD CONSTRAINT "lead_scout_scan_run_workspace_id_workspace_id_fk" FOREIGN KEY ("workspace_id") REFERENCES "public"."workspace"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "lead_scout_source_config" ADD CONSTRAINT "lead_scout_source_config_workspace_id_workspace_id_fk" FOREIGN KEY ("workspace_id") REFERENCES "public"."workspace"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "lead_scout_suppression" ADD CONSTRAINT "lead_scout_suppression_workspace_id_workspace_id_fk" FOREIGN KEY ("workspace_id") REFERENCES "public"."workspace"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +CREATE UNIQUE INDEX "lead_scout_lead_campaign_source_idx" ON "lead_scout_lead" USING btree ("campaign_id","source_provider","source_id");--> statement-breakpoint +CREATE UNIQUE INDEX "lead_scout_source_config_workspace_provider_idx" ON "lead_scout_source_config" USING btree ("workspace_id","provider"); \ No newline at end of file diff --git a/packages/db/drizzle/pg/meta/0045_snapshot.json b/packages/db/drizzle/pg/meta/0045_snapshot.json new file mode 100644 index 0000000..55cef0f --- /dev/null +++ b/packages/db/drizzle/pg/meta/0045_snapshot.json @@ -0,0 +1,6199 @@ +{ + "id": "c6f62dfb-71e5-401c-a7f0-f45a02f17aa8", + "prevId": "f5a73027-3ea8-461f-bf6c-796064a0a407", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.agent": { + "name": "agent", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "system_prompt": { + "name": "system_prompt", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "goal_template": { + "name": "goal_template", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "model_id": { + "name": "model_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "autonomy_level": { + "name": "autonomy_level", + "type": "agent_autonomy_level", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'chat'" + }, + "mcp_server_ids": { + "name": "mcp_server_ids", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "tool_ids": { + "name": "tool_ids", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "skill_ids": { + "name": "skill_ids", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "mcp_tool_filter": { + "name": "mcp_tool_filter", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "delegate_agent_ids": { + "name": "delegate_agent_ids", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "autonomy_budget": { + "name": "autonomy_budget", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "agent_workspace_id_workspace_id_fk": { + "name": "agent_workspace_id_workspace_id_fk", + "tableFrom": "agent", + "tableTo": "workspace", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.agent_run": { + "name": "agent_run", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "task_id": { + "name": "task_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "agent_id": { + "name": "agent_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "chat_id": { + "name": "chat_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "automation_id": { + "name": "automation_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "trigger": { + "name": "trigger", + "type": "agent_run_trigger", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "model_id": { + "name": "model_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "step_count": { + "name": "step_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "status": { + "name": "status", + "type": "agent_run_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "final_output": { + "name": "final_output", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "started_at": { + "name": "started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "worker_id": { + "name": "worker_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "heartbeat_at": { + "name": "heartbeat_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "lease_until": { + "name": "lease_until", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "cancel_requested_at": { + "name": "cancel_requested_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "retry_count": { + "name": "retry_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "max_retries": { + "name": "max_retries", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 3 + }, + "next_retry_at": { + "name": "next_retry_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "agent_run_workspace_id_workspace_id_fk": { + "name": "agent_run_workspace_id_workspace_id_fk", + "tableFrom": "agent_run", + "tableTo": "workspace", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "agent_run_task_id_task_id_fk": { + "name": "agent_run_task_id_task_id_fk", + "tableFrom": "agent_run", + "tableTo": "task", + "columnsFrom": [ + "task_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "agent_run_agent_id_agent_id_fk": { + "name": "agent_run_agent_id_agent_id_fk", + "tableFrom": "agent_run", + "tableTo": "agent", + "columnsFrom": [ + "agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "agent_run_chat_id_chat_id_fk": { + "name": "agent_run_chat_id_chat_id_fk", + "tableFrom": "agent_run", + "tableTo": "chat", + "columnsFrom": [ + "chat_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "agent_run_automation_id_automation_id_fk": { + "name": "agent_run_automation_id_automation_id_fk", + "tableFrom": "agent_run", + "tableTo": "automation", + "columnsFrom": [ + "automation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.approval_request": { + "name": "approval_request", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "agent_id": { + "name": "agent_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "chat_id": { + "name": "chat_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "automation_id": { + "name": "automation_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "task_id": { + "name": "task_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "agent_run_id": { + "name": "agent_run_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "kind": { + "name": "kind", + "type": "approval_kind", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "skill_id": { + "name": "skill_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tool_id": { + "name": "tool_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "mcp_server_id": { + "name": "mcp_server_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "mcp_tool_name": { + "name": "mcp_tool_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tool_label": { + "name": "tool_label", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "input": { + "name": "input", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "approval_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "result_output": { + "name": "result_output", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "resolved_at": { + "name": "resolved_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "risk_level": { + "name": "risk_level", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "affected_resources": { + "name": "affected_resources", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "diff_preview": { + "name": "diff_preview", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "approval_request_workspace_id_workspace_id_fk": { + "name": "approval_request_workspace_id_workspace_id_fk", + "tableFrom": "approval_request", + "tableTo": "workspace", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "approval_request_agent_id_agent_id_fk": { + "name": "approval_request_agent_id_agent_id_fk", + "tableFrom": "approval_request", + "tableTo": "agent", + "columnsFrom": [ + "agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "approval_request_chat_id_chat_id_fk": { + "name": "approval_request_chat_id_chat_id_fk", + "tableFrom": "approval_request", + "tableTo": "chat", + "columnsFrom": [ + "chat_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "approval_request_automation_id_automation_id_fk": { + "name": "approval_request_automation_id_automation_id_fk", + "tableFrom": "approval_request", + "tableTo": "automation", + "columnsFrom": [ + "automation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "approval_request_task_id_task_id_fk": { + "name": "approval_request_task_id_task_id_fk", + "tableFrom": "approval_request", + "tableTo": "task", + "columnsFrom": [ + "task_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "approval_request_agent_run_id_agent_run_id_fk": { + "name": "approval_request_agent_run_id_agent_run_id_fk", + "tableFrom": "approval_request", + "tableTo": "agent_run", + "columnsFrom": [ + "agent_run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.artifact": { + "name": "artifact", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "artifact_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "task_id": { + "name": "task_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "agent_run_id": { + "name": "agent_run_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "agent_id": { + "name": "agent_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "artifact_workspace_id_workspace_id_fk": { + "name": "artifact_workspace_id_workspace_id_fk", + "tableFrom": "artifact", + "tableTo": "workspace", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "artifact_task_id_task_id_fk": { + "name": "artifact_task_id_task_id_fk", + "tableFrom": "artifact", + "tableTo": "task", + "columnsFrom": [ + "task_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "artifact_agent_run_id_agent_run_id_fk": { + "name": "artifact_agent_run_id_agent_run_id_fk", + "tableFrom": "artifact", + "tableTo": "agent_run", + "columnsFrom": [ + "agent_run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "artifact_agent_id_agent_id_fk": { + "name": "artifact_agent_id_agent_id_fk", + "tableFrom": "artifact", + "tableTo": "agent", + "columnsFrom": [ + "agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.audit_log": { + "name": "audit_log", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "agent_id": { + "name": "agent_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "chat_id": { + "name": "chat_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "automation_id": { + "name": "automation_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "actor": { + "name": "actor", + "type": "audit_actor", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "tool_label": { + "name": "tool_label", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "input": { + "name": "input", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "output": { + "name": "output", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "audit_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "input_hash": { + "name": "input_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "permission_snapshot": { + "name": "permission_snapshot", + "type": "jsonb", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "audit_log_workspace_id_workspace_id_fk": { + "name": "audit_log_workspace_id_workspace_id_fk", + "tableFrom": "audit_log", + "tableTo": "workspace", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "audit_log_agent_id_agent_id_fk": { + "name": "audit_log_agent_id_agent_id_fk", + "tableFrom": "audit_log", + "tableTo": "agent", + "columnsFrom": [ + "agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "audit_log_chat_id_chat_id_fk": { + "name": "audit_log_chat_id_chat_id_fk", + "tableFrom": "audit_log", + "tableTo": "chat", + "columnsFrom": [ + "chat_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "audit_log_automation_id_automation_id_fk": { + "name": "audit_log_automation_id_automation_id_fk", + "tableFrom": "audit_log", + "tableTo": "automation", + "columnsFrom": [ + "automation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.automation": { + "name": "automation", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "agent_id": { + "name": "agent_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "target_kind": { + "name": "target_kind", + "type": "automation_target_kind", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'agent'" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "trigger_type": { + "name": "trigger_type", + "type": "automation_trigger_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'cron'" + }, + "cron_expression": { + "name": "cron_expression", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "watch_path": { + "name": "watch_path", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "watch_glob": { + "name": "watch_glob", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_watch_check_at": { + "name": "last_watch_check_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "prompt": { + "name": "prompt", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "last_run_at": { + "name": "last_run_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "next_run_at": { + "name": "next_run_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_run_status": { + "name": "last_run_status", + "type": "automation_run_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "last_error_message": { + "name": "last_error_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "automation_workspace_id_workspace_id_fk": { + "name": "automation_workspace_id_workspace_id_fk", + "tableFrom": "automation", + "tableTo": "workspace", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "automation_agent_id_agent_id_fk": { + "name": "automation_agent_id_agent_id_fk", + "tableFrom": "automation", + "tableTo": "agent", + "columnsFrom": [ + "agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "automation_workflow_id_workflow_id_fk": { + "name": "automation_workflow_id_workflow_id_fk", + "tableFrom": "automation", + "tableTo": "workflow", + "columnsFrom": [ + "workflow_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.chat": { + "name": "chat", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "working_directory": { + "name": "working_directory", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "agent_id": { + "name": "agent_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'New chat'" + }, + "model_id": { + "name": "model_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "pinned_at": { + "name": "pinned_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "share_id": { + "name": "share_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "shared_at": { + "name": "shared_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "tool_mode": { + "name": "tool_mode", + "type": "chat_tool_mode", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'default'" + }, + "tool_policy": { + "name": "tool_policy", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{\"mode\":\"default\",\"approveFileWrites\":true,\"approveFileDeletes\":true,\"approveCustomCode\":true,\"approveMcpTools\":true}'::jsonb" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "chat_workspace_id_workspace_id_fk": { + "name": "chat_workspace_id_workspace_id_fk", + "tableFrom": "chat", + "tableTo": "workspace", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "chat_agent_id_agent_id_fk": { + "name": "chat_agent_id_agent_id_fk", + "tableFrom": "chat", + "tableTo": "agent", + "columnsFrom": [ + "agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "chat_project_id_project_id_fk": { + "name": "chat_project_id_project_id_fk", + "tableFrom": "chat", + "tableTo": "project", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "chat_share_id_unique": { + "name": "chat_share_id_unique", + "nullsNotDistinct": false, + "columns": [ + "share_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.extension": { + "name": "extension", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "installed_at": { + "name": "installed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "extension_workspace_id_workspace_id_fk": { + "name": "extension_workspace_id_workspace_id_fk", + "tableFrom": "extension", + "tableTo": "workspace", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.goal": { + "name": "goal", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "goal_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "priority": { + "name": "priority", + "type": "task_priority", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'normal'" + }, + "default_agent_id": { + "name": "default_agent_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "success_criteria": { + "name": "success_criteria", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "orchestration_enabled": { + "name": "orchestration_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "next_review_at": { + "name": "next_review_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_reviewed_at": { + "name": "last_reviewed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "blocked_reason": { + "name": "blocked_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "plan_generated_at": { + "name": "plan_generated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "goal_workspace_id_workspace_id_fk": { + "name": "goal_workspace_id_workspace_id_fk", + "tableFrom": "goal", + "tableTo": "workspace", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "goal_default_agent_id_agent_id_fk": { + "name": "goal_default_agent_id_agent_id_fk", + "tableFrom": "goal", + "tableTo": "agent", + "columnsFrom": [ + "default_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.goal_milestone": { + "name": "goal_milestone", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "goal_id": { + "name": "goal_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "goal_milestone_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "order": { + "name": "order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "goal_milestone_goal_id_goal_id_fk": { + "name": "goal_milestone_goal_id_goal_id_fk", + "tableFrom": "goal_milestone", + "tableTo": "goal", + "columnsFrom": [ + "goal_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "goal_milestone_workspace_id_workspace_id_fk": { + "name": "goal_milestone_workspace_id_workspace_id_fk", + "tableFrom": "goal_milestone", + "tableTo": "workspace", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.goal_progress_event": { + "name": "goal_progress_event", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "goal_id": { + "name": "goal_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "goal_event_kind", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "message": { + "name": "message", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "payload": { + "name": "payload", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "goal_progress_event_goal_id_goal_id_fk": { + "name": "goal_progress_event_goal_id_goal_id_fk", + "tableFrom": "goal_progress_event", + "tableTo": "goal", + "columnsFrom": [ + "goal_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "goal_progress_event_workspace_id_workspace_id_fk": { + "name": "goal_progress_event_workspace_id_workspace_id_fk", + "tableFrom": "goal_progress_event", + "tableTo": "workspace", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.installation": { + "name": "installation", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "mode": { + "name": "mode", + "type": "installation_mode", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "owner_user_id": { + "name": "owner_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "primary_workspace_id": { + "name": "primary_workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "app_url": { + "name": "app_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "installation_owner_user_id_user_id_fk": { + "name": "installation_owner_user_id_user_id_fk", + "tableFrom": "installation", + "tableTo": "user", + "columnsFrom": [ + "owner_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.knowledge_base_config": { + "name": "knowledge_base_config", + "schema": "", + "columns": { + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "vault_path": { + "name": "vault_path", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'knowledge-base'" + }, + "obsidian_rest_url": { + "name": "obsidian_rest_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "obsidian_api_key": { + "name": "obsidian_api_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "docs_agent_enabled": { + "name": "docs_agent_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "inject_into_prompts": { + "name": "inject_into_prompts", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "last_docs_sync_at": { + "name": "last_docs_sync_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_docs_sync_error": { + "name": "last_docs_sync_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "knowledge_base_config_workspace_id_workspace_id_fk": { + "name": "knowledge_base_config_workspace_id_workspace_id_fk", + "tableFrom": "knowledge_base_config", + "tableTo": "workspace", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.lead_scout_campaign": { + "name": "lead_scout_campaign", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "extension_id": { + "name": "extension_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "postal_code": { + "name": "postal_code", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "country": { + "name": "country", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'US'" + }, + "radius_km": { + "name": "radius_km", + "type": "double precision", + "primaryKey": false, + "notNull": true, + "default": 10 + }, + "niches": { + "name": "niches", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "max_results_per_run": { + "name": "max_results_per_run", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 25 + }, + "provider": { + "name": "provider", + "type": "lead_scout_provider", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "min_confidence": { + "name": "min_confidence", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 50 + }, + "outreach_mode": { + "name": "outreach_mode", + "type": "lead_scout_outreach_mode", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'draft_only'" + }, + "schedule_enabled": { + "name": "schedule_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "schedule_cron_expression": { + "name": "schedule_cron_expression", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "next_scan_at": { + "name": "next_scan_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_scan_at": { + "name": "last_scan_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "auto_generate_prototype": { + "name": "auto_generate_prototype", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "auto_draft_email": { + "name": "auto_draft_email", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "auto_send_after_approval": { + "name": "auto_send_after_approval", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "require_approval_before_prototype": { + "name": "require_approval_before_prototype", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "require_approval_before_email_send": { + "name": "require_approval_before_email_send", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "prototype_agent_id": { + "name": "prototype_agent_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "lead_scout_campaign_workspace_id_workspace_id_fk": { + "name": "lead_scout_campaign_workspace_id_workspace_id_fk", + "tableFrom": "lead_scout_campaign", + "tableTo": "workspace", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "lead_scout_campaign_extension_id_extension_id_fk": { + "name": "lead_scout_campaign_extension_id_extension_id_fk", + "tableFrom": "lead_scout_campaign", + "tableTo": "extension", + "columnsFrom": [ + "extension_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "lead_scout_campaign_prototype_agent_id_agent_id_fk": { + "name": "lead_scout_campaign_prototype_agent_id_agent_id_fk", + "tableFrom": "lead_scout_campaign", + "tableTo": "agent", + "columnsFrom": [ + "prototype_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.lead_scout_email_settings": { + "name": "lead_scout_email_settings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "lead_scout_email_provider", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'smtp'" + }, + "from_name": { + "name": "from_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "from_email": { + "name": "from_email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "reply_to": { + "name": "reply_to", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "credentials": { + "name": "credentials", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "daily_send_limit": { + "name": "daily_send_limit", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 20 + }, + "per_campaign_send_limit": { + "name": "per_campaign_send_limit", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 10 + }, + "dry_run_mode": { + "name": "dry_run_mode", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "legal_footer": { + "name": "legal_footer", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "unsubscribe_text": { + "name": "unsubscribe_text", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'Reply STOP to opt out.'" + }, + "send_count_today": { + "name": "send_count_today", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "send_count_date": { + "name": "send_count_date", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "lead_scout_email_settings_workspace_id_workspace_id_fk": { + "name": "lead_scout_email_settings_workspace_id_workspace_id_fk", + "tableFrom": "lead_scout_email_settings", + "tableTo": "workspace", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "lead_scout_email_settings_workspace_id_unique": { + "name": "lead_scout_email_settings_workspace_id_unique", + "nullsNotDistinct": false, + "columns": [ + "workspace_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.lead_scout_lead": { + "name": "lead_scout_lead", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "campaign_id": { + "name": "campaign_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scan_run_id": { + "name": "scan_run_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source_provider": { + "name": "source_provider", + "type": "lead_scout_provider", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "source_id": { + "name": "source_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "business_name": { + "name": "business_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "niche": { + "name": "niche", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "formatted_address": { + "name": "formatted_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "postal_code": { + "name": "postal_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "city": { + "name": "city", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "phone": { + "name": "phone", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "website": { + "name": "website", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "website_status": { + "name": "website_status", + "type": "lead_scout_website_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'unknown'" + }, + "confidence": { + "name": "confidence", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "evidence_summary": { + "name": "evidence_summary", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "missing_reason": { + "name": "missing_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "lead_scout_lead_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'new'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "lead_scout_lead_campaign_source_idx": { + "name": "lead_scout_lead_campaign_source_idx", + "columns": [ + { + "expression": "campaign_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source_provider", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "lead_scout_lead_workspace_id_workspace_id_fk": { + "name": "lead_scout_lead_workspace_id_workspace_id_fk", + "tableFrom": "lead_scout_lead", + "tableTo": "workspace", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "lead_scout_lead_campaign_id_lead_scout_campaign_id_fk": { + "name": "lead_scout_lead_campaign_id_lead_scout_campaign_id_fk", + "tableFrom": "lead_scout_lead", + "tableTo": "lead_scout_campaign", + "columnsFrom": [ + "campaign_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "lead_scout_lead_scan_run_id_lead_scout_scan_run_id_fk": { + "name": "lead_scout_lead_scan_run_id_lead_scout_scan_run_id_fk", + "tableFrom": "lead_scout_lead", + "tableTo": "lead_scout_scan_run", + "columnsFrom": [ + "scan_run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.lead_scout_outreach_draft": { + "name": "lead_scout_outreach_draft", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "lead_id": { + "name": "lead_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "prototype_id": { + "name": "prototype_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "task_id": { + "name": "task_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "subject": { + "name": "subject", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "body_text": { + "name": "body_text", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "body_html": { + "name": "body_html", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "lead_scout_draft_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'draft'" + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "approved_at": { + "name": "approved_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "sent_at": { + "name": "sent_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "lead_scout_outreach_draft_workspace_id_workspace_id_fk": { + "name": "lead_scout_outreach_draft_workspace_id_workspace_id_fk", + "tableFrom": "lead_scout_outreach_draft", + "tableTo": "workspace", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "lead_scout_outreach_draft_lead_id_lead_scout_lead_id_fk": { + "name": "lead_scout_outreach_draft_lead_id_lead_scout_lead_id_fk", + "tableFrom": "lead_scout_outreach_draft", + "tableTo": "lead_scout_lead", + "columnsFrom": [ + "lead_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "lead_scout_outreach_draft_prototype_id_lead_scout_prototype_id_fk": { + "name": "lead_scout_outreach_draft_prototype_id_lead_scout_prototype_id_fk", + "tableFrom": "lead_scout_outreach_draft", + "tableTo": "lead_scout_prototype", + "columnsFrom": [ + "prototype_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "lead_scout_outreach_draft_task_id_task_id_fk": { + "name": "lead_scout_outreach_draft_task_id_task_id_fk", + "tableFrom": "lead_scout_outreach_draft", + "tableTo": "task", + "columnsFrom": [ + "task_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.lead_scout_prototype": { + "name": "lead_scout_prototype", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "lead_id": { + "name": "lead_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "task_id": { + "name": "task_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "lead_scout_prototype_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "concept": { + "name": "concept", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "hero_copy": { + "name": "hero_copy", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sections": { + "name": "sections", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "call_to_action": { + "name": "call_to_action", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "style_direction": { + "name": "style_direction", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "artifact_markdown": { + "name": "artifact_markdown", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "approved": { + "name": "approved", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "lead_scout_prototype_workspace_id_workspace_id_fk": { + "name": "lead_scout_prototype_workspace_id_workspace_id_fk", + "tableFrom": "lead_scout_prototype", + "tableTo": "workspace", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "lead_scout_prototype_lead_id_lead_scout_lead_id_fk": { + "name": "lead_scout_prototype_lead_id_lead_scout_lead_id_fk", + "tableFrom": "lead_scout_prototype", + "tableTo": "lead_scout_lead", + "columnsFrom": [ + "lead_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "lead_scout_prototype_task_id_task_id_fk": { + "name": "lead_scout_prototype_task_id_task_id_fk", + "tableFrom": "lead_scout_prototype", + "tableTo": "task", + "columnsFrom": [ + "task_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.lead_scout_scan_run": { + "name": "lead_scout_scan_run", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "campaign_id": { + "name": "campaign_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "lead_scout_provider", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "lead_scout_scan_run_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'running'" + }, + "result_count": { + "name": "result_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "new_lead_count": { + "name": "new_lead_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "missing_website_count": { + "name": "missing_website_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "summary": { + "name": "summary", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "started_at": { + "name": "started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "lead_scout_scan_run_campaign_id_lead_scout_campaign_id_fk": { + "name": "lead_scout_scan_run_campaign_id_lead_scout_campaign_id_fk", + "tableFrom": "lead_scout_scan_run", + "tableTo": "lead_scout_campaign", + "columnsFrom": [ + "campaign_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "lead_scout_scan_run_workspace_id_workspace_id_fk": { + "name": "lead_scout_scan_run_workspace_id_workspace_id_fk", + "tableFrom": "lead_scout_scan_run", + "tableTo": "workspace", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.lead_scout_source_config": { + "name": "lead_scout_source_config", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "lead_scout_provider", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "api_key": { + "name": "api_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "lead_scout_source_config_workspace_provider_idx": { + "name": "lead_scout_source_config_workspace_provider_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "lead_scout_source_config_workspace_id_workspace_id_fk": { + "name": "lead_scout_source_config_workspace_id_workspace_id_fk", + "tableFrom": "lead_scout_source_config", + "tableTo": "workspace", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.lead_scout_suppression": { + "name": "lead_scout_suppression", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "domain": { + "name": "domain", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "reason": { + "name": "reason", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "lead_scout_suppression_workspace_id_workspace_id_fk": { + "name": "lead_scout_suppression_workspace_id_workspace_id_fk", + "tableFrom": "lead_scout_suppression", + "tableTo": "workspace", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.library_file": { + "name": "library_file", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "folder_id": { + "name": "folder_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "mime_type": { + "name": "mime_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "size_bytes": { + "name": "size_bytes", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "library_item_kind", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'other'" + }, + "storage_key": { + "name": "storage_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "library_file_workspace_id_workspace_id_fk": { + "name": "library_file_workspace_id_workspace_id_fk", + "tableFrom": "library_file", + "tableTo": "workspace", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "library_file_folder_id_library_folder_id_fk": { + "name": "library_file_folder_id_library_folder_id_fk", + "tableFrom": "library_file", + "tableTo": "library_folder", + "columnsFrom": [ + "folder_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.library_folder": { + "name": "library_folder", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "parent_id": { + "name": "parent_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "library_folder_workspace_id_workspace_id_fk": { + "name": "library_folder_workspace_id_workspace_id_fk", + "tableFrom": "library_folder", + "tableTo": "workspace", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mcp_server": { + "name": "mcp_server", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "transport": { + "name": "transport", + "type": "mcp_transport", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "command": { + "name": "command", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "args": { + "name": "args", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "env": { + "name": "env", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "oauth_state": { + "name": "oauth_state", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "mcp_server_workspace_id_workspace_id_fk": { + "name": "mcp_server_workspace_id_workspace_id_fk", + "tableFrom": "mcp_server", + "tableTo": "workspace", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.memory_entry": { + "name": "memory_entry", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "memory_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source": { + "name": "source", + "type": "memory_source", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "confidence": { + "name": "confidence", + "type": "double precision", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "created_by_agent_id": { + "name": "created_by_agent_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "memory_entry_workspace_id_workspace_id_fk": { + "name": "memory_entry_workspace_id_workspace_id_fk", + "tableFrom": "memory_entry", + "tableTo": "workspace", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "memory_entry_created_by_agent_id_agent_id_fk": { + "name": "memory_entry_created_by_agent_id_agent_id_fk", + "tableFrom": "memory_entry", + "tableTo": "agent", + "columnsFrom": [ + "created_by_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.message": { + "name": "message", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "chat_id": { + "name": "chat_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "message_role", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "model_id": { + "name": "model_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "input_tokens": { + "name": "input_tokens", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "output_tokens": { + "name": "output_tokens", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "reasoning_tokens": { + "name": "reasoning_tokens", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "cache_read_tokens": { + "name": "cache_read_tokens", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "total_tokens": { + "name": "total_tokens", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "cost_micros": { + "name": "cost_micros", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "duration_ms": { + "name": "duration_ms", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "thinking_ms": { + "name": "thinking_ms", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "line_count": { + "name": "line_count", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "code_line_count": { + "name": "code_line_count", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "code_block_count": { + "name": "code_block_count", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "message_chat_id_chat_id_fk": { + "name": "message_chat_id_chat_id_fk", + "tableFrom": "message", + "tableTo": "chat", + "columnsFrom": [ + "chat_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.model_installation": { + "name": "model_installation", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_kind": { + "name": "provider_kind", + "type": "model_provider_kind", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "base_url": { + "name": "base_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "api_key": { + "name": "api_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "model_ids": { + "name": "model_ids", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "disabled_model_ids": { + "name": "disabled_model_ids", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "model_installation_workspace_id_workspace_id_fk": { + "name": "model_installation_workspace_id_workspace_id_fk", + "tableFrom": "model_installation", + "tableTo": "workspace", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.model_parameter": { + "name": "model_parameter", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "model_id": { + "name": "model_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "custom_name": { + "name": "custom_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "custom_instructions": { + "name": "custom_instructions", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "max_output_tokens": { + "name": "max_output_tokens", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "temperature": { + "name": "temperature", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "top_p": { + "name": "top_p", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "frequency_penalty": { + "name": "frequency_penalty", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "presence_penalty": { + "name": "presence_penalty", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "stop_sequences": { + "name": "stop_sequences", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "reasoning_effort": { + "name": "reasoning_effort", + "type": "model_reasoning_effort", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "model_parameter_workspace_model_idx": { + "name": "model_parameter_workspace_model_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "model_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "model_parameter_workspace_id_workspace_id_fk": { + "name": "model_parameter_workspace_id_workspace_id_fk", + "tableFrom": "model_parameter", + "tableTo": "workspace", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.nyxel_event": { + "name": "nyxel_event", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "nyxel_event_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "entity_type": { + "name": "entity_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "entity_id": { + "name": "entity_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "payload": { + "name": "payload", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "nyxel_event_workspace_id_workspace_id_fk": { + "name": "nyxel_event_workspace_id_workspace_id_fk", + "tableFrom": "nyxel_event", + "tableTo": "workspace", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.plugin": { + "name": "plugin", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "version": { + "name": "version", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "author": { + "name": "author", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "homepage": { + "name": "homepage", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "repo_url": { + "name": "repo_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "manifest": { + "name": "manifest", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "skill_slugs": { + "name": "skill_slugs", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "agent_defs": { + "name": "agent_defs", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "file_count": { + "name": "file_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "install_dir": { + "name": "install_dir", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "ref": { + "name": "ref", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "resolved_sha": { + "name": "resolved_sha", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ref_pinned": { + "name": "ref_pinned", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "risk_findings": { + "name": "risk_findings", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "plugin_workspace_id_workspace_id_fk": { + "name": "plugin_workspace_id_workspace_id_fk", + "tableFrom": "plugin", + "tableTo": "workspace", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.project": { + "name": "project", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'gray'" + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'folder'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "project_workspace_id_workspace_id_fk": { + "name": "project_workspace_id_workspace_id_fk", + "tableFrom": "project", + "tableTo": "workspace", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.push_subscription": { + "name": "push_subscription", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "endpoint": { + "name": "endpoint", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "p256dh": { + "name": "p256dh", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "auth": { + "name": "auth", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "push_subscription_user_id_user_id_fk": { + "name": "push_subscription_user_id_user_id_fk", + "tableFrom": "push_subscription", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "push_subscription_endpoint_unique": { + "name": "push_subscription_endpoint_unique", + "nullsNotDistinct": false, + "columns": [ + "endpoint" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.seo_analysis_run": { + "name": "seo_analysis_run", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "seo_project_id": { + "name": "seo_project_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "seo_analysis_run_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'running'" + }, + "score": { + "name": "score", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "pages_scanned": { + "name": "pages_scanned", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "summary": { + "name": "summary", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "started_at": { + "name": "started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "seo_analysis_run_seo_project_id_seo_project_id_fk": { + "name": "seo_analysis_run_seo_project_id_seo_project_id_fk", + "tableFrom": "seo_analysis_run", + "tableTo": "seo_project", + "columnsFrom": [ + "seo_project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "seo_analysis_run_workspace_id_workspace_id_fk": { + "name": "seo_analysis_run_workspace_id_workspace_id_fk", + "tableFrom": "seo_analysis_run", + "tableTo": "workspace", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.seo_blog_post": { + "name": "seo_blog_post", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "seo_project_id": { + "name": "seo_project_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "keyword": { + "name": "keyword", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "file_path": { + "name": "file_path", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "seo_blog_post_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'suggested'" + }, + "task_id": { + "name": "task_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "seo_blog_post_seo_project_id_seo_project_id_fk": { + "name": "seo_blog_post_seo_project_id_seo_project_id_fk", + "tableFrom": "seo_blog_post", + "tableTo": "seo_project", + "columnsFrom": [ + "seo_project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "seo_blog_post_workspace_id_workspace_id_fk": { + "name": "seo_blog_post_workspace_id_workspace_id_fk", + "tableFrom": "seo_blog_post", + "tableTo": "workspace", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "seo_blog_post_task_id_task_id_fk": { + "name": "seo_blog_post_task_id_task_id_fk", + "tableFrom": "seo_blog_post", + "tableTo": "task", + "columnsFrom": [ + "task_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.seo_finding": { + "name": "seo_finding", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "run_id": { + "name": "run_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "seo_project_id": { + "name": "seo_project_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "category": { + "name": "category", + "type": "seo_finding_category", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "severity": { + "name": "severity", + "type": "seo_finding_severity", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "recommendation": { + "name": "recommendation", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "location": { + "name": "location", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "resolved": { + "name": "resolved", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "seo_finding_run_id_seo_analysis_run_id_fk": { + "name": "seo_finding_run_id_seo_analysis_run_id_fk", + "tableFrom": "seo_finding", + "tableTo": "seo_analysis_run", + "columnsFrom": [ + "run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "seo_finding_seo_project_id_seo_project_id_fk": { + "name": "seo_finding_seo_project_id_seo_project_id_fk", + "tableFrom": "seo_finding", + "tableTo": "seo_project", + "columnsFrom": [ + "seo_project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.seo_project": { + "name": "seo_project", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "extension_id": { + "name": "extension_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "domain": { + "name": "domain", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "repo_path": { + "name": "repo_path", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "blog_config": { + "name": "blog_config", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "fixer_agent_id": { + "name": "fixer_agent_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "reanalyze_cron_expression": { + "name": "reanalyze_cron_expression", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "next_reanalyze_at": { + "name": "next_reanalyze_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_reanalyze_at": { + "name": "last_reanalyze_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "seo_project_workspace_id_workspace_id_fk": { + "name": "seo_project_workspace_id_workspace_id_fk", + "tableFrom": "seo_project", + "tableTo": "workspace", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "seo_project_extension_id_extension_id_fk": { + "name": "seo_project_extension_id_extension_id_fk", + "tableFrom": "seo_project", + "tableTo": "extension", + "columnsFrom": [ + "extension_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "seo_project_fixer_agent_id_agent_id_fk": { + "name": "seo_project_fixer_agent_id_agent_id_fk", + "tableFrom": "seo_project", + "tableTo": "agent", + "columnsFrom": [ + "fixer_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.task": { + "name": "task", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "parent_task_id": { + "name": "parent_task_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source_chat_id": { + "name": "source_chat_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by_agent_id": { + "name": "created_by_agent_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "assigned_agent_id": { + "name": "assigned_agent_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "goal_id": { + "name": "goal_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "goal_milestone_id": { + "name": "goal_milestone_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "instruction": { + "name": "instruction", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "model_id": { + "name": "model_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "task_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "priority": { + "name": "priority", + "type": "task_priority", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'normal'" + }, + "requires_approval": { + "name": "requires_approval", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "input": { + "name": "input", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "plan": { + "name": "plan", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "handoff": { + "name": "handoff", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "result_summary": { + "name": "result_summary", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "started_at": { + "name": "started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "task_workspace_id_workspace_id_fk": { + "name": "task_workspace_id_workspace_id_fk", + "tableFrom": "task", + "tableTo": "workspace", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "task_source_chat_id_chat_id_fk": { + "name": "task_source_chat_id_chat_id_fk", + "tableFrom": "task", + "tableTo": "chat", + "columnsFrom": [ + "source_chat_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "task_created_by_agent_id_agent_id_fk": { + "name": "task_created_by_agent_id_agent_id_fk", + "tableFrom": "task", + "tableTo": "agent", + "columnsFrom": [ + "created_by_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "task_assigned_agent_id_agent_id_fk": { + "name": "task_assigned_agent_id_agent_id_fk", + "tableFrom": "task", + "tableTo": "agent", + "columnsFrom": [ + "assigned_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "task_goal_id_goal_id_fk": { + "name": "task_goal_id_goal_id_fk", + "tableFrom": "task", + "tableTo": "goal", + "columnsFrom": [ + "goal_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "task_goal_milestone_id_goal_milestone_id_fk": { + "name": "task_goal_milestone_id_goal_milestone_id_fk", + "tableFrom": "task", + "tableTo": "goal_milestone", + "columnsFrom": [ + "goal_milestone_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.task_event": { + "name": "task_event", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "task_id": { + "name": "task_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "agent_run_id": { + "name": "agent_run_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "agent_id": { + "name": "agent_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "kind": { + "name": "kind", + "type": "task_event_kind", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "message": { + "name": "message", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "payload": { + "name": "payload", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "task_event_task_id_task_id_fk": { + "name": "task_event_task_id_task_id_fk", + "tableFrom": "task_event", + "tableTo": "task", + "columnsFrom": [ + "task_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "task_event_workspace_id_workspace_id_fk": { + "name": "task_event_workspace_id_workspace_id_fk", + "tableFrom": "task_event", + "tableTo": "workspace", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "task_event_agent_run_id_agent_run_id_fk": { + "name": "task_event_agent_run_id_agent_run_id_fk", + "tableFrom": "task_event", + "tableTo": "agent_run", + "columnsFrom": [ + "agent_run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "task_event_agent_id_agent_id_fk": { + "name": "task_event_agent_id_agent_id_fk", + "tableFrom": "task_event", + "tableTo": "agent", + "columnsFrom": [ + "agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.tool": { + "name": "tool", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "sensitive": { + "name": "sensitive", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "builtin": { + "name": "builtin", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "tool_workspace_id_workspace_id_fk": { + "name": "tool_workspace_id_workspace_id_fk", + "tableFrom": "tool", + "tableTo": "workspace", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.video_generation_job": { + "name": "video_generation_job", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "chat_id": { + "name": "chat_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "prompt": { + "name": "prompt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "video_generation_job_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'queued'" + }, + "progress": { + "name": "progress", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "size": { + "name": "size", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "seconds": { + "name": "seconds", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "auto": { + "name": "auto", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "external_job_id": { + "name": "external_job_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "library_file_id": { + "name": "library_file_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "poster_library_file_id": { + "name": "poster_library_file_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "video_generation_job_workspace_id_workspace_id_fk": { + "name": "video_generation_job_workspace_id_workspace_id_fk", + "tableFrom": "video_generation_job", + "tableTo": "workspace", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "video_generation_job_library_file_id_library_file_id_fk": { + "name": "video_generation_job_library_file_id_library_file_id_fk", + "tableFrom": "video_generation_job", + "tableTo": "library_file", + "columnsFrom": [ + "library_file_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "video_generation_job_poster_library_file_id_library_file_id_fk": { + "name": "video_generation_job_poster_library_file_id_library_file_id_fk", + "tableFrom": "video_generation_job", + "tableTo": "library_file", + "columnsFrom": [ + "poster_library_file_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow": { + "name": "workflow", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "definition": { + "name": "definition", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "workflow_workspace_id_workspace_id_fk": { + "name": "workflow_workspace_id_workspace_id_fk", + "tableFrom": "workflow", + "tableTo": "workspace", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_run": { + "name": "workflow_run", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "workflow_run_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'queued'" + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "started_at": { + "name": "started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "workflow_run_workflow_id_workflow_id_fk": { + "name": "workflow_run_workflow_id_workflow_id_fk", + "tableFrom": "workflow_run", + "tableTo": "workflow", + "columnsFrom": [ + "workflow_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_run_workspace_id_workspace_id_fk": { + "name": "workflow_run_workspace_id_workspace_id_fk", + "tableFrom": "workflow_run", + "tableTo": "workspace", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_run_node": { + "name": "workflow_run_node", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "run_id": { + "name": "run_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "node_id": { + "name": "node_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "workflow_run_node_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'queued'" + }, + "progress": { + "name": "progress", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "library_file_id": { + "name": "library_file_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "started_at": { + "name": "started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "workflow_run_node_run_id_workflow_run_id_fk": { + "name": "workflow_run_node_run_id_workflow_run_id_fk", + "tableFrom": "workflow_run_node", + "tableTo": "workflow_run", + "columnsFrom": [ + "run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_run_node_library_file_id_library_file_id_fk": { + "name": "workflow_run_node_library_file_id_library_file_id_fk", + "tableFrom": "workflow_run_node", + "tableTo": "library_file", + "columnsFrom": [ + "library_file_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace": { + "name": "workspace", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "custom_instructions": { + "name": "custom_instructions", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "default_model_id": { + "name": "default_model_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "default_autonomy_level": { + "name": "default_autonomy_level", + "type": "agent_autonomy_level", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'assisted'" + }, + "default_tool_policy": { + "name": "default_tool_policy", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "workspace_user_id_user_id_fk": { + "name": "workspace_user_id_user_id_fk", + "tableFrom": "workspace", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.account": { + "name": "account", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "account_id": { + "name": "account_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "id_token": { + "name": "id_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "access_token_expires_at": { + "name": "access_token_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "refresh_token_expires_at": { + "name": "refresh_token_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "account_user_id_user_id_fk": { + "name": "account_user_id_user_id_fk", + "tableFrom": "account", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.session": { + "name": "session", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "session_user_id_user_id_fk": { + "name": "session_user_id_user_id_fk", + "tableFrom": "session", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "session_token_unique": { + "name": "session_token_unique", + "nullsNotDistinct": false, + "columns": [ + "token" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user": { + "name": "user", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email_verified": { + "name": "email_verified", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_email_unique": { + "name": "user_email_unique", + "nullsNotDistinct": false, + "columns": [ + "email" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.verification": { + "name": "verification", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": { + "public.agent_autonomy_level": { + "name": "agent_autonomy_level", + "schema": "public", + "values": [ + "chat", + "assisted", + "autonomous", + "super_agent" + ] + }, + "public.agent_run_status": { + "name": "agent_run_status", + "schema": "public", + "values": [ + "pending", + "running", + "waiting_approval", + "completed", + "failed", + "cancelled", + "dead_letter" + ] + }, + "public.agent_run_trigger": { + "name": "agent_run_trigger", + "schema": "public", + "values": [ + "chat", + "task", + "automation", + "delegate", + "extension" + ] + }, + "public.approval_kind": { + "name": "approval_kind", + "schema": "public", + "values": [ + "skill", + "tool", + "mcp" + ] + }, + "public.approval_status": { + "name": "approval_status", + "schema": "public", + "values": [ + "pending", + "approved", + "rejected" + ] + }, + "public.artifact_type": { + "name": "artifact_type", + "schema": "public", + "values": [ + "text", + "markdown", + "code_patch", + "diff", + "file", + "report", + "json", + "image_reference", + "task_result", + "command_output" + ] + }, + "public.audit_actor": { + "name": "audit_actor", + "schema": "public", + "values": [ + "chat", + "automation", + "approval", + "delegate", + "extension", + "goal_orchestrator" + ] + }, + "public.audit_status": { + "name": "audit_status", + "schema": "public", + "values": [ + "success", + "error", + "pending_approval", + "rejected" + ] + }, + "public.automation_run_status": { + "name": "automation_run_status", + "schema": "public", + "values": [ + "success", + "error", + "pending_approval" + ] + }, + "public.automation_target_kind": { + "name": "automation_target_kind", + "schema": "public", + "values": [ + "agent", + "workflow" + ] + }, + "public.automation_trigger_type": { + "name": "automation_trigger_type", + "schema": "public", + "values": [ + "cron", + "file_watch" + ] + }, + "public.chat_tool_mode": { + "name": "chat_tool_mode", + "schema": "public", + "values": [ + "default", + "automatic", + "auto" + ] + }, + "public.goal_event_kind": { + "name": "goal_event_kind", + "schema": "public", + "values": [ + "created", + "status_changed", + "milestone_added", + "milestone_status_changed", + "plan_created", + "task_created", + "task_status_changed", + "review" + ] + }, + "public.goal_milestone_status": { + "name": "goal_milestone_status", + "schema": "public", + "values": [ + "pending", + "completed" + ] + }, + "public.goal_status": { + "name": "goal_status", + "schema": "public", + "values": [ + "active", + "paused", + "blocked", + "completed", + "archived" + ] + }, + "public.installation_mode": { + "name": "installation_mode", + "schema": "public", + "values": [ + "pc", + "server" + ] + }, + "public.lead_scout_draft_status": { + "name": "lead_scout_draft_status", + "schema": "public", + "values": [ + "draft", + "approved", + "rejected", + "sending", + "sent", + "failed" + ] + }, + "public.lead_scout_email_provider": { + "name": "lead_scout_email_provider", + "schema": "public", + "values": [ + "smtp", + "resend", + "mailgun", + "custom" + ] + }, + "public.lead_scout_lead_status": { + "name": "lead_scout_lead_status", + "schema": "public", + "values": [ + "new", + "reviewed", + "prototype_requested", + "prototype_ready", + "email_drafted", + "approved_to_send", + "sending", + "sent", + "rejected", + "suppressed" + ] + }, + "public.lead_scout_outreach_mode": { + "name": "lead_scout_outreach_mode", + "schema": "public", + "values": [ + "draft_only", + "review_and_send" + ] + }, + "public.lead_scout_prototype_status": { + "name": "lead_scout_prototype_status", + "schema": "public", + "values": [ + "pending", + "ready", + "failed" + ] + }, + "public.lead_scout_provider": { + "name": "lead_scout_provider", + "schema": "public", + "values": [ + "manual_csv", + "google_places_api", + "osm_overpass", + "custom_api" + ] + }, + "public.lead_scout_scan_run_status": { + "name": "lead_scout_scan_run_status", + "schema": "public", + "values": [ + "running", + "completed", + "failed" + ] + }, + "public.lead_scout_website_status": { + "name": "lead_scout_website_status", + "schema": "public", + "values": [ + "unknown", + "has_website", + "missing_website", + "invalid_website" + ] + }, + "public.library_item_kind": { + "name": "library_item_kind", + "schema": "public", + "values": [ + "image", + "document", + "video", + "other" + ] + }, + "public.mcp_transport": { + "name": "mcp_transport", + "schema": "public", + "values": [ + "stdio", + "http" + ] + }, + "public.memory_source": { + "name": "memory_source", + "schema": "public", + "values": [ + "user", + "agent", + "automation", + "system" + ] + }, + "public.memory_type": { + "name": "memory_type", + "schema": "public", + "values": [ + "user_preference", + "workspace_fact", + "project_decision", + "agent_observation", + "task_summary", + "file_summary", + "repo_summary", + "long_term_note" + ] + }, + "public.message_role": { + "name": "message_role", + "schema": "public", + "values": [ + "user", + "assistant", + "system", + "tool" + ] + }, + "public.model_provider_kind": { + "name": "model_provider_kind", + "schema": "public", + "values": [ + "anthropic", + "openai", + "openrouter", + "openai_compatible", + "claude_cli", + "codex_cli" + ] + }, + "public.model_reasoning_effort": { + "name": "model_reasoning_effort", + "schema": "public", + "values": [ + "low", + "medium", + "high" + ] + }, + "public.nyxel_event_type": { + "name": "nyxel_event_type", + "schema": "public", + "values": [ + "agent.run.started", + "agent.run.completed", + "agent.run.failed", + "approval.created", + "approval.resolved", + "workflow.completed", + "task.failed", + "library.file.created", + "automation.triggered" + ] + }, + "public.seo_analysis_run_status": { + "name": "seo_analysis_run_status", + "schema": "public", + "values": [ + "running", + "completed", + "failed" + ] + }, + "public.seo_blog_post_status": { + "name": "seo_blog_post_status", + "schema": "public", + "values": [ + "suggested", + "generating", + "written", + "failed" + ] + }, + "public.seo_finding_category": { + "name": "seo_finding_category", + "schema": "public", + "values": [ + "seo", + "geo", + "aeo" + ] + }, + "public.seo_finding_severity": { + "name": "seo_finding_severity", + "schema": "public", + "values": [ + "info", + "warning", + "critical" + ] + }, + "public.task_event_kind": { + "name": "task_event_kind", + "schema": "public", + "values": [ + "created", + "planned", + "status_changed", + "assigned", + "delegated", + "tool_called", + "approval_waiting", + "approval_resolved", + "run_started", + "run_finished", + "comment", + "question", + "question_answered", + "completed", + "failed" + ] + }, + "public.task_priority": { + "name": "task_priority", + "schema": "public", + "values": [ + "low", + "normal", + "high", + "urgent" + ] + }, + "public.task_status": { + "name": "task_status", + "schema": "public", + "values": [ + "pending", + "planning", + "ready", + "running", + "blocked", + "waiting_approval", + "completed", + "failed", + "cancelled" + ] + }, + "public.video_generation_job_status": { + "name": "video_generation_job_status", + "schema": "public", + "values": [ + "queued", + "in_progress", + "completed", + "failed" + ] + }, + "public.workflow_run_node_status": { + "name": "workflow_run_node_status", + "schema": "public", + "values": [ + "queued", + "running", + "completed", + "failed", + "skipped" + ] + }, + "public.workflow_run_status": { + "name": "workflow_run_status", + "schema": "public", + "values": [ + "queued", + "running", + "completed", + "failed", + "partial" + ] + } + }, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/packages/db/drizzle/pg/meta/_journal.json b/packages/db/drizzle/pg/meta/_journal.json index 29fe767..42c1a90 100644 --- a/packages/db/drizzle/pg/meta/_journal.json +++ b/packages/db/drizzle/pg/meta/_journal.json @@ -316,6 +316,13 @@ "when": 1783095974277, "tag": "0044_ambitious_hulk", "breakpoints": true + }, + { + "idx": 45, + "version": "7", + "when": 1783104248173, + "tag": "0045_strange_sally_floyd", + "breakpoints": true } ] } \ No newline at end of file diff --git a/packages/db/drizzle/sqlite/0041_luxuriant_jubilee.sql b/packages/db/drizzle/sqlite/0041_luxuriant_jubilee.sql new file mode 100644 index 0000000..f1e3229 --- /dev/null +++ b/packages/db/drizzle/sqlite/0041_luxuriant_jubilee.sql @@ -0,0 +1,161 @@ +CREATE TABLE `lead_scout_campaign` ( + `id` text PRIMARY KEY NOT NULL, + `workspace_id` text NOT NULL, + `extension_id` text NOT NULL, + `name` text NOT NULL, + `postal_code` text NOT NULL, + `country` text DEFAULT 'US' NOT NULL, + `radius_km` real DEFAULT 10 NOT NULL, + `niches` text DEFAULT '[]' NOT NULL, + `max_results_per_run` integer DEFAULT 25 NOT NULL, + `provider` text NOT NULL, + `min_confidence` integer DEFAULT 50 NOT NULL, + `outreach_mode` text DEFAULT 'draft_only' NOT NULL, + `schedule_enabled` integer DEFAULT false NOT NULL, + `schedule_cron_expression` text, + `next_scan_at` integer, + `last_scan_at` integer, + `auto_generate_prototype` integer DEFAULT false NOT NULL, + `auto_draft_email` integer DEFAULT false NOT NULL, + `auto_send_after_approval` integer DEFAULT false NOT NULL, + `require_approval_before_prototype` integer DEFAULT true NOT NULL, + `require_approval_before_email_send` integer DEFAULT true NOT NULL, + `prototype_agent_id` text, + `created_at` integer NOT NULL, + `updated_at` integer NOT NULL, + FOREIGN KEY (`workspace_id`) REFERENCES `workspace`(`id`) ON UPDATE no action ON DELETE cascade, + FOREIGN KEY (`extension_id`) REFERENCES `extension`(`id`) ON UPDATE no action ON DELETE cascade, + FOREIGN KEY (`prototype_agent_id`) REFERENCES `agent`(`id`) ON UPDATE no action ON DELETE set null +); +--> statement-breakpoint +CREATE TABLE `lead_scout_email_settings` ( + `id` text PRIMARY KEY NOT NULL, + `workspace_id` text NOT NULL, + `provider` text DEFAULT 'smtp' NOT NULL, + `from_name` text NOT NULL, + `from_email` text NOT NULL, + `reply_to` text, + `credentials` text, + `daily_send_limit` integer DEFAULT 20 NOT NULL, + `per_campaign_send_limit` integer DEFAULT 10 NOT NULL, + `dry_run_mode` integer DEFAULT true NOT NULL, + `legal_footer` text, + `unsubscribe_text` text DEFAULT 'Reply STOP to opt out.' NOT NULL, + `send_count_today` integer DEFAULT 0 NOT NULL, + `send_count_date` text, + `created_at` integer NOT NULL, + `updated_at` integer NOT NULL, + FOREIGN KEY (`workspace_id`) REFERENCES `workspace`(`id`) ON UPDATE no action ON DELETE cascade +); +--> statement-breakpoint +CREATE UNIQUE INDEX `lead_scout_email_settings_workspace_id_unique` ON `lead_scout_email_settings` (`workspace_id`);--> statement-breakpoint +CREATE TABLE `lead_scout_lead` ( + `id` text PRIMARY KEY NOT NULL, + `workspace_id` text NOT NULL, + `campaign_id` text NOT NULL, + `scan_run_id` text, + `source_provider` text NOT NULL, + `source_id` text NOT NULL, + `business_name` text NOT NULL, + `category` text, + `niche` text, + `formatted_address` text, + `postal_code` text, + `city` text, + `phone` text, + `email` text, + `website` text, + `website_status` text DEFAULT 'unknown' NOT NULL, + `confidence` integer DEFAULT 0 NOT NULL, + `evidence_summary` text, + `missing_reason` text, + `status` text DEFAULT 'new' NOT NULL, + `created_at` integer NOT NULL, + `updated_at` integer NOT NULL, + FOREIGN KEY (`workspace_id`) REFERENCES `workspace`(`id`) ON UPDATE no action ON DELETE cascade, + FOREIGN KEY (`campaign_id`) REFERENCES `lead_scout_campaign`(`id`) ON UPDATE no action ON DELETE cascade, + FOREIGN KEY (`scan_run_id`) REFERENCES `lead_scout_scan_run`(`id`) ON UPDATE no action ON DELETE set null +); +--> statement-breakpoint +CREATE UNIQUE INDEX `lead_scout_lead_campaign_source_idx` ON `lead_scout_lead` (`campaign_id`,`source_provider`,`source_id`);--> statement-breakpoint +CREATE TABLE `lead_scout_outreach_draft` ( + `id` text PRIMARY KEY NOT NULL, + `workspace_id` text NOT NULL, + `lead_id` text NOT NULL, + `prototype_id` text, + `task_id` text, + `subject` text, + `body_text` text, + `body_html` text, + `status` text DEFAULT 'draft' NOT NULL, + `error_message` text, + `approved_at` integer, + `sent_at` integer, + `created_at` integer NOT NULL, + `updated_at` integer NOT NULL, + FOREIGN KEY (`workspace_id`) REFERENCES `workspace`(`id`) ON UPDATE no action ON DELETE cascade, + FOREIGN KEY (`lead_id`) REFERENCES `lead_scout_lead`(`id`) ON UPDATE no action ON DELETE cascade, + FOREIGN KEY (`prototype_id`) REFERENCES `lead_scout_prototype`(`id`) ON UPDATE no action ON DELETE set null, + FOREIGN KEY (`task_id`) REFERENCES `task`(`id`) ON UPDATE no action ON DELETE set null +); +--> statement-breakpoint +CREATE TABLE `lead_scout_prototype` ( + `id` text PRIMARY KEY NOT NULL, + `workspace_id` text NOT NULL, + `lead_id` text NOT NULL, + `task_id` text, + `status` text DEFAULT 'pending' NOT NULL, + `concept` text, + `hero_copy` text, + `sections` text DEFAULT '[]' NOT NULL, + `call_to_action` text, + `style_direction` text, + `artifact_markdown` text, + `approved` integer DEFAULT false NOT NULL, + `error_message` text, + `created_at` integer NOT NULL, + `updated_at` integer NOT NULL, + FOREIGN KEY (`workspace_id`) REFERENCES `workspace`(`id`) ON UPDATE no action ON DELETE cascade, + FOREIGN KEY (`lead_id`) REFERENCES `lead_scout_lead`(`id`) ON UPDATE no action ON DELETE cascade, + FOREIGN KEY (`task_id`) REFERENCES `task`(`id`) ON UPDATE no action ON DELETE set null +); +--> statement-breakpoint +CREATE TABLE `lead_scout_scan_run` ( + `id` text PRIMARY KEY NOT NULL, + `campaign_id` text NOT NULL, + `workspace_id` text NOT NULL, + `provider` text NOT NULL, + `status` text DEFAULT 'running' NOT NULL, + `result_count` integer DEFAULT 0 NOT NULL, + `new_lead_count` integer DEFAULT 0 NOT NULL, + `missing_website_count` integer DEFAULT 0 NOT NULL, + `summary` text, + `error_message` text, + `started_at` integer NOT NULL, + `completed_at` integer, + FOREIGN KEY (`campaign_id`) REFERENCES `lead_scout_campaign`(`id`) ON UPDATE no action ON DELETE cascade, + FOREIGN KEY (`workspace_id`) REFERENCES `workspace`(`id`) ON UPDATE no action ON DELETE cascade +); +--> statement-breakpoint +CREATE TABLE `lead_scout_source_config` ( + `id` text PRIMARY KEY NOT NULL, + `workspace_id` text NOT NULL, + `provider` text NOT NULL, + `config` text DEFAULT '{}' NOT NULL, + `api_key` text, + `enabled` integer DEFAULT true NOT NULL, + `created_at` integer NOT NULL, + `updated_at` integer NOT NULL, + FOREIGN KEY (`workspace_id`) REFERENCES `workspace`(`id`) ON UPDATE no action ON DELETE cascade +); +--> statement-breakpoint +CREATE UNIQUE INDEX `lead_scout_source_config_workspace_provider_idx` ON `lead_scout_source_config` (`workspace_id`,`provider`);--> statement-breakpoint +CREATE TABLE `lead_scout_suppression` ( + `id` text PRIMARY KEY NOT NULL, + `workspace_id` text NOT NULL, + `email` text, + `domain` text, + `reason` text NOT NULL, + `created_at` integer NOT NULL, + FOREIGN KEY (`workspace_id`) REFERENCES `workspace`(`id`) ON UPDATE no action ON DELETE cascade +); diff --git a/packages/db/drizzle/sqlite/meta/0041_snapshot.json b/packages/db/drizzle/sqlite/meta/0041_snapshot.json new file mode 100644 index 0000000..e51390b --- /dev/null +++ b/packages/db/drizzle/sqlite/meta/0041_snapshot.json @@ -0,0 +1,6014 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "76f8d3f7-1104-41bf-bdde-efb72a3694fc", + "prevId": "e85f1648-0d13-46ef-9a85-a82fc13c9c9d", + "tables": { + "agent": { + "name": "agent", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "system_prompt": { + "name": "system_prompt", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "goal_template": { + "name": "goal_template", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "model_id": { + "name": "model_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "autonomy_level": { + "name": "autonomy_level", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'chat'" + }, + "mcp_server_ids": { + "name": "mcp_server_ids", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'[]'" + }, + "tool_ids": { + "name": "tool_ids", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'[]'" + }, + "skill_ids": { + "name": "skill_ids", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'[]'" + }, + "mcp_tool_filter": { + "name": "mcp_tool_filter", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "delegate_agent_ids": { + "name": "delegate_agent_ids", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'[]'" + }, + "autonomy_budget": { + "name": "autonomy_budget", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "agent_workspace_id_workspace_id_fk": { + "name": "agent_workspace_id_workspace_id_fk", + "tableFrom": "agent", + "tableTo": "workspace", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "agent_run": { + "name": "agent_run", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "task_id": { + "name": "task_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "agent_id": { + "name": "agent_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "chat_id": { + "name": "chat_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "automation_id": { + "name": "automation_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "trigger": { + "name": "trigger", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "model_id": { + "name": "model_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "step_count": { + "name": "step_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'pending'" + }, + "final_output": { + "name": "final_output", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "started_at": { + "name": "started_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "completed_at": { + "name": "completed_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "worker_id": { + "name": "worker_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "heartbeat_at": { + "name": "heartbeat_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "lease_until": { + "name": "lease_until", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cancel_requested_at": { + "name": "cancel_requested_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "retry_count": { + "name": "retry_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "max_retries": { + "name": "max_retries", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 3 + }, + "next_retry_at": { + "name": "next_retry_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "agent_run_workspace_id_workspace_id_fk": { + "name": "agent_run_workspace_id_workspace_id_fk", + "tableFrom": "agent_run", + "tableTo": "workspace", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "agent_run_task_id_task_id_fk": { + "name": "agent_run_task_id_task_id_fk", + "tableFrom": "agent_run", + "tableTo": "task", + "columnsFrom": [ + "task_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "agent_run_agent_id_agent_id_fk": { + "name": "agent_run_agent_id_agent_id_fk", + "tableFrom": "agent_run", + "tableTo": "agent", + "columnsFrom": [ + "agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "agent_run_chat_id_chat_id_fk": { + "name": "agent_run_chat_id_chat_id_fk", + "tableFrom": "agent_run", + "tableTo": "chat", + "columnsFrom": [ + "chat_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "agent_run_automation_id_automation_id_fk": { + "name": "agent_run_automation_id_automation_id_fk", + "tableFrom": "agent_run", + "tableTo": "automation", + "columnsFrom": [ + "automation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "approval_request": { + "name": "approval_request", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "agent_id": { + "name": "agent_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "chat_id": { + "name": "chat_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "automation_id": { + "name": "automation_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "task_id": { + "name": "task_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "agent_run_id": { + "name": "agent_run_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "skill_id": { + "name": "skill_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "tool_id": { + "name": "tool_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "mcp_server_id": { + "name": "mcp_server_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "mcp_tool_name": { + "name": "mcp_tool_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "tool_label": { + "name": "tool_label", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "input": { + "name": "input", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'pending'" + }, + "result_output": { + "name": "result_output", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "resolved_at": { + "name": "resolved_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "risk_level": { + "name": "risk_level", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "affected_resources": { + "name": "affected_resources", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "diff_preview": { + "name": "diff_preview", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "approval_request_workspace_id_workspace_id_fk": { + "name": "approval_request_workspace_id_workspace_id_fk", + "tableFrom": "approval_request", + "tableTo": "workspace", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "approval_request_agent_id_agent_id_fk": { + "name": "approval_request_agent_id_agent_id_fk", + "tableFrom": "approval_request", + "tableTo": "agent", + "columnsFrom": [ + "agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "approval_request_chat_id_chat_id_fk": { + "name": "approval_request_chat_id_chat_id_fk", + "tableFrom": "approval_request", + "tableTo": "chat", + "columnsFrom": [ + "chat_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "approval_request_automation_id_automation_id_fk": { + "name": "approval_request_automation_id_automation_id_fk", + "tableFrom": "approval_request", + "tableTo": "automation", + "columnsFrom": [ + "automation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "approval_request_task_id_task_id_fk": { + "name": "approval_request_task_id_task_id_fk", + "tableFrom": "approval_request", + "tableTo": "task", + "columnsFrom": [ + "task_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "approval_request_agent_run_id_agent_run_id_fk": { + "name": "approval_request_agent_run_id_agent_run_id_fk", + "tableFrom": "approval_request", + "tableTo": "agent_run", + "columnsFrom": [ + "agent_run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "artifact": { + "name": "artifact", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "task_id": { + "name": "task_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "agent_run_id": { + "name": "agent_run_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "agent_id": { + "name": "agent_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "artifact_workspace_id_workspace_id_fk": { + "name": "artifact_workspace_id_workspace_id_fk", + "tableFrom": "artifact", + "tableTo": "workspace", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "artifact_task_id_task_id_fk": { + "name": "artifact_task_id_task_id_fk", + "tableFrom": "artifact", + "tableTo": "task", + "columnsFrom": [ + "task_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "artifact_agent_run_id_agent_run_id_fk": { + "name": "artifact_agent_run_id_agent_run_id_fk", + "tableFrom": "artifact", + "tableTo": "agent_run", + "columnsFrom": [ + "agent_run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "artifact_agent_id_agent_id_fk": { + "name": "artifact_agent_id_agent_id_fk", + "tableFrom": "artifact", + "tableTo": "agent", + "columnsFrom": [ + "agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "audit_log": { + "name": "audit_log", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "agent_id": { + "name": "agent_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "chat_id": { + "name": "chat_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "automation_id": { + "name": "automation_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "actor": { + "name": "actor", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tool_label": { + "name": "tool_label", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "input": { + "name": "input", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "output": { + "name": "output", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "input_hash": { + "name": "input_hash", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "permission_snapshot": { + "name": "permission_snapshot", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "audit_log_workspace_id_workspace_id_fk": { + "name": "audit_log_workspace_id_workspace_id_fk", + "tableFrom": "audit_log", + "tableTo": "workspace", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "audit_log_agent_id_agent_id_fk": { + "name": "audit_log_agent_id_agent_id_fk", + "tableFrom": "audit_log", + "tableTo": "agent", + "columnsFrom": [ + "agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "audit_log_chat_id_chat_id_fk": { + "name": "audit_log_chat_id_chat_id_fk", + "tableFrom": "audit_log", + "tableTo": "chat", + "columnsFrom": [ + "chat_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "audit_log_automation_id_automation_id_fk": { + "name": "audit_log_automation_id_automation_id_fk", + "tableFrom": "audit_log", + "tableTo": "automation", + "columnsFrom": [ + "automation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "automation": { + "name": "automation", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "agent_id": { + "name": "agent_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "target_kind": { + "name": "target_kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'agent'" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "trigger_type": { + "name": "trigger_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'cron'" + }, + "cron_expression": { + "name": "cron_expression", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "watch_path": { + "name": "watch_path", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "watch_glob": { + "name": "watch_glob", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_watch_check_at": { + "name": "last_watch_check_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "prompt": { + "name": "prompt", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "enabled": { + "name": "enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "last_run_at": { + "name": "last_run_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "next_run_at": { + "name": "next_run_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_run_status": { + "name": "last_run_status", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_error_message": { + "name": "last_error_message", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "automation_workspace_id_workspace_id_fk": { + "name": "automation_workspace_id_workspace_id_fk", + "tableFrom": "automation", + "tableTo": "workspace", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "automation_agent_id_agent_id_fk": { + "name": "automation_agent_id_agent_id_fk", + "tableFrom": "automation", + "tableTo": "agent", + "columnsFrom": [ + "agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "automation_workflow_id_workflow_id_fk": { + "name": "automation_workflow_id_workflow_id_fk", + "tableFrom": "automation", + "tableTo": "workflow", + "columnsFrom": [ + "workflow_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "chat": { + "name": "chat", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "working_directory": { + "name": "working_directory", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "agent_id": { + "name": "agent_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'New chat'" + }, + "model_id": { + "name": "model_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "archived_at": { + "name": "archived_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "pinned_at": { + "name": "pinned_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "share_id": { + "name": "share_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "shared_at": { + "name": "shared_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "tool_mode": { + "name": "tool_mode", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'default'" + }, + "tool_policy": { + "name": "tool_policy", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'{\"mode\":\"default\",\"approveFileWrites\":true,\"approveFileDeletes\":true,\"approveCustomCode\":true,\"approveMcpTools\":true}'" + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "chat_share_id_unique": { + "name": "chat_share_id_unique", + "columns": [ + "share_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "chat_workspace_id_workspace_id_fk": { + "name": "chat_workspace_id_workspace_id_fk", + "tableFrom": "chat", + "tableTo": "workspace", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "chat_agent_id_agent_id_fk": { + "name": "chat_agent_id_agent_id_fk", + "tableFrom": "chat", + "tableTo": "agent", + "columnsFrom": [ + "agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "chat_project_id_project_id_fk": { + "name": "chat_project_id_project_id_fk", + "tableFrom": "chat", + "tableTo": "project", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "extension": { + "name": "extension", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "config": { + "name": "config", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'{}'" + }, + "installed_at": { + "name": "installed_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "extension_workspace_id_workspace_id_fk": { + "name": "extension_workspace_id_workspace_id_fk", + "tableFrom": "extension", + "tableTo": "workspace", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "goal": { + "name": "goal", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'active'" + }, + "priority": { + "name": "priority", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'normal'" + }, + "default_agent_id": { + "name": "default_agent_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "success_criteria": { + "name": "success_criteria", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "orchestration_enabled": { + "name": "orchestration_enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "next_review_at": { + "name": "next_review_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_reviewed_at": { + "name": "last_reviewed_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "blocked_reason": { + "name": "blocked_reason", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "plan_generated_at": { + "name": "plan_generated_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "goal_workspace_id_workspace_id_fk": { + "name": "goal_workspace_id_workspace_id_fk", + "tableFrom": "goal", + "tableTo": "workspace", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "goal_default_agent_id_agent_id_fk": { + "name": "goal_default_agent_id_agent_id_fk", + "tableFrom": "goal", + "tableTo": "agent", + "columnsFrom": [ + "default_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "goal_milestone": { + "name": "goal_milestone", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "goal_id": { + "name": "goal_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'pending'" + }, + "order": { + "name": "order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "goal_milestone_goal_id_goal_id_fk": { + "name": "goal_milestone_goal_id_goal_id_fk", + "tableFrom": "goal_milestone", + "tableTo": "goal", + "columnsFrom": [ + "goal_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "goal_milestone_workspace_id_workspace_id_fk": { + "name": "goal_milestone_workspace_id_workspace_id_fk", + "tableFrom": "goal_milestone", + "tableTo": "workspace", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "goal_progress_event": { + "name": "goal_progress_event", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "goal_id": { + "name": "goal_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "message": { + "name": "message", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "payload": { + "name": "payload", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "goal_progress_event_goal_id_goal_id_fk": { + "name": "goal_progress_event_goal_id_goal_id_fk", + "tableFrom": "goal_progress_event", + "tableTo": "goal", + "columnsFrom": [ + "goal_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "goal_progress_event_workspace_id_workspace_id_fk": { + "name": "goal_progress_event_workspace_id_workspace_id_fk", + "tableFrom": "goal_progress_event", + "tableTo": "workspace", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "installation": { + "name": "installation", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "mode": { + "name": "mode", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "owner_user_id": { + "name": "owner_user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "primary_workspace_id": { + "name": "primary_workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "app_url": { + "name": "app_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "installation_owner_user_id_user_id_fk": { + "name": "installation_owner_user_id_user_id_fk", + "tableFrom": "installation", + "tableTo": "user", + "columnsFrom": [ + "owner_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "knowledge_base_config": { + "name": "knowledge_base_config", + "columns": { + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "vault_path": { + "name": "vault_path", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'knowledge-base'" + }, + "obsidian_rest_url": { + "name": "obsidian_rest_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "obsidian_api_key": { + "name": "obsidian_api_key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "docs_agent_enabled": { + "name": "docs_agent_enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "inject_into_prompts": { + "name": "inject_into_prompts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "last_docs_sync_at": { + "name": "last_docs_sync_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_docs_sync_error": { + "name": "last_docs_sync_error", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "knowledge_base_config_workspace_id_workspace_id_fk": { + "name": "knowledge_base_config_workspace_id_workspace_id_fk", + "tableFrom": "knowledge_base_config", + "tableTo": "workspace", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "lead_scout_campaign": { + "name": "lead_scout_campaign", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "extension_id": { + "name": "extension_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "postal_code": { + "name": "postal_code", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "country": { + "name": "country", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'US'" + }, + "radius_km": { + "name": "radius_km", + "type": "real", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 10 + }, + "niches": { + "name": "niches", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'[]'" + }, + "max_results_per_run": { + "name": "max_results_per_run", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 25 + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "min_confidence": { + "name": "min_confidence", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 50 + }, + "outreach_mode": { + "name": "outreach_mode", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'draft_only'" + }, + "schedule_enabled": { + "name": "schedule_enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "schedule_cron_expression": { + "name": "schedule_cron_expression", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "next_scan_at": { + "name": "next_scan_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_scan_at": { + "name": "last_scan_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "auto_generate_prototype": { + "name": "auto_generate_prototype", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "auto_draft_email": { + "name": "auto_draft_email", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "auto_send_after_approval": { + "name": "auto_send_after_approval", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "require_approval_before_prototype": { + "name": "require_approval_before_prototype", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "require_approval_before_email_send": { + "name": "require_approval_before_email_send", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "prototype_agent_id": { + "name": "prototype_agent_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "lead_scout_campaign_workspace_id_workspace_id_fk": { + "name": "lead_scout_campaign_workspace_id_workspace_id_fk", + "tableFrom": "lead_scout_campaign", + "tableTo": "workspace", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "lead_scout_campaign_extension_id_extension_id_fk": { + "name": "lead_scout_campaign_extension_id_extension_id_fk", + "tableFrom": "lead_scout_campaign", + "tableTo": "extension", + "columnsFrom": [ + "extension_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "lead_scout_campaign_prototype_agent_id_agent_id_fk": { + "name": "lead_scout_campaign_prototype_agent_id_agent_id_fk", + "tableFrom": "lead_scout_campaign", + "tableTo": "agent", + "columnsFrom": [ + "prototype_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "lead_scout_email_settings": { + "name": "lead_scout_email_settings", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'smtp'" + }, + "from_name": { + "name": "from_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "from_email": { + "name": "from_email", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "reply_to": { + "name": "reply_to", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "credentials": { + "name": "credentials", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "daily_send_limit": { + "name": "daily_send_limit", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 20 + }, + "per_campaign_send_limit": { + "name": "per_campaign_send_limit", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 10 + }, + "dry_run_mode": { + "name": "dry_run_mode", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "legal_footer": { + "name": "legal_footer", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "unsubscribe_text": { + "name": "unsubscribe_text", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'Reply STOP to opt out.'" + }, + "send_count_today": { + "name": "send_count_today", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "send_count_date": { + "name": "send_count_date", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "lead_scout_email_settings_workspace_id_unique": { + "name": "lead_scout_email_settings_workspace_id_unique", + "columns": [ + "workspace_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "lead_scout_email_settings_workspace_id_workspace_id_fk": { + "name": "lead_scout_email_settings_workspace_id_workspace_id_fk", + "tableFrom": "lead_scout_email_settings", + "tableTo": "workspace", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "lead_scout_lead": { + "name": "lead_scout_lead", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "campaign_id": { + "name": "campaign_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "scan_run_id": { + "name": "scan_run_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "source_provider": { + "name": "source_provider", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source_id": { + "name": "source_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "business_name": { + "name": "business_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "niche": { + "name": "niche", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "formatted_address": { + "name": "formatted_address", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "postal_code": { + "name": "postal_code", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "city": { + "name": "city", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "phone": { + "name": "phone", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "website": { + "name": "website", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "website_status": { + "name": "website_status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'unknown'" + }, + "confidence": { + "name": "confidence", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "evidence_summary": { + "name": "evidence_summary", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "missing_reason": { + "name": "missing_reason", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'new'" + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "lead_scout_lead_campaign_source_idx": { + "name": "lead_scout_lead_campaign_source_idx", + "columns": [ + "campaign_id", + "source_provider", + "source_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "lead_scout_lead_workspace_id_workspace_id_fk": { + "name": "lead_scout_lead_workspace_id_workspace_id_fk", + "tableFrom": "lead_scout_lead", + "tableTo": "workspace", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "lead_scout_lead_campaign_id_lead_scout_campaign_id_fk": { + "name": "lead_scout_lead_campaign_id_lead_scout_campaign_id_fk", + "tableFrom": "lead_scout_lead", + "tableTo": "lead_scout_campaign", + "columnsFrom": [ + "campaign_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "lead_scout_lead_scan_run_id_lead_scout_scan_run_id_fk": { + "name": "lead_scout_lead_scan_run_id_lead_scout_scan_run_id_fk", + "tableFrom": "lead_scout_lead", + "tableTo": "lead_scout_scan_run", + "columnsFrom": [ + "scan_run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "lead_scout_outreach_draft": { + "name": "lead_scout_outreach_draft", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "lead_id": { + "name": "lead_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "prototype_id": { + "name": "prototype_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "task_id": { + "name": "task_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "subject": { + "name": "subject", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "body_text": { + "name": "body_text", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "body_html": { + "name": "body_html", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'draft'" + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "approved_at": { + "name": "approved_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sent_at": { + "name": "sent_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "lead_scout_outreach_draft_workspace_id_workspace_id_fk": { + "name": "lead_scout_outreach_draft_workspace_id_workspace_id_fk", + "tableFrom": "lead_scout_outreach_draft", + "tableTo": "workspace", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "lead_scout_outreach_draft_lead_id_lead_scout_lead_id_fk": { + "name": "lead_scout_outreach_draft_lead_id_lead_scout_lead_id_fk", + "tableFrom": "lead_scout_outreach_draft", + "tableTo": "lead_scout_lead", + "columnsFrom": [ + "lead_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "lead_scout_outreach_draft_prototype_id_lead_scout_prototype_id_fk": { + "name": "lead_scout_outreach_draft_prototype_id_lead_scout_prototype_id_fk", + "tableFrom": "lead_scout_outreach_draft", + "tableTo": "lead_scout_prototype", + "columnsFrom": [ + "prototype_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "lead_scout_outreach_draft_task_id_task_id_fk": { + "name": "lead_scout_outreach_draft_task_id_task_id_fk", + "tableFrom": "lead_scout_outreach_draft", + "tableTo": "task", + "columnsFrom": [ + "task_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "lead_scout_prototype": { + "name": "lead_scout_prototype", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "lead_id": { + "name": "lead_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "task_id": { + "name": "task_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'pending'" + }, + "concept": { + "name": "concept", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "hero_copy": { + "name": "hero_copy", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sections": { + "name": "sections", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'[]'" + }, + "call_to_action": { + "name": "call_to_action", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "style_direction": { + "name": "style_direction", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "artifact_markdown": { + "name": "artifact_markdown", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "approved": { + "name": "approved", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "lead_scout_prototype_workspace_id_workspace_id_fk": { + "name": "lead_scout_prototype_workspace_id_workspace_id_fk", + "tableFrom": "lead_scout_prototype", + "tableTo": "workspace", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "lead_scout_prototype_lead_id_lead_scout_lead_id_fk": { + "name": "lead_scout_prototype_lead_id_lead_scout_lead_id_fk", + "tableFrom": "lead_scout_prototype", + "tableTo": "lead_scout_lead", + "columnsFrom": [ + "lead_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "lead_scout_prototype_task_id_task_id_fk": { + "name": "lead_scout_prototype_task_id_task_id_fk", + "tableFrom": "lead_scout_prototype", + "tableTo": "task", + "columnsFrom": [ + "task_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "lead_scout_scan_run": { + "name": "lead_scout_scan_run", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "campaign_id": { + "name": "campaign_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'running'" + }, + "result_count": { + "name": "result_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "new_lead_count": { + "name": "new_lead_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "missing_website_count": { + "name": "missing_website_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "summary": { + "name": "summary", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "started_at": { + "name": "started_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "completed_at": { + "name": "completed_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "lead_scout_scan_run_campaign_id_lead_scout_campaign_id_fk": { + "name": "lead_scout_scan_run_campaign_id_lead_scout_campaign_id_fk", + "tableFrom": "lead_scout_scan_run", + "tableTo": "lead_scout_campaign", + "columnsFrom": [ + "campaign_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "lead_scout_scan_run_workspace_id_workspace_id_fk": { + "name": "lead_scout_scan_run_workspace_id_workspace_id_fk", + "tableFrom": "lead_scout_scan_run", + "tableTo": "workspace", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "lead_scout_source_config": { + "name": "lead_scout_source_config", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "config": { + "name": "config", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'{}'" + }, + "api_key": { + "name": "api_key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "lead_scout_source_config_workspace_provider_idx": { + "name": "lead_scout_source_config_workspace_provider_idx", + "columns": [ + "workspace_id", + "provider" + ], + "isUnique": true + } + }, + "foreignKeys": { + "lead_scout_source_config_workspace_id_workspace_id_fk": { + "name": "lead_scout_source_config_workspace_id_workspace_id_fk", + "tableFrom": "lead_scout_source_config", + "tableTo": "workspace", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "lead_scout_suppression": { + "name": "lead_scout_suppression", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "domain": { + "name": "domain", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "reason": { + "name": "reason", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "lead_scout_suppression_workspace_id_workspace_id_fk": { + "name": "lead_scout_suppression_workspace_id_workspace_id_fk", + "tableFrom": "lead_scout_suppression", + "tableTo": "workspace", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "library_file": { + "name": "library_file", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "folder_id": { + "name": "folder_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "mime_type": { + "name": "mime_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "size_bytes": { + "name": "size_bytes", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'other'" + }, + "storage_key": { + "name": "storage_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "library_file_workspace_id_workspace_id_fk": { + "name": "library_file_workspace_id_workspace_id_fk", + "tableFrom": "library_file", + "tableTo": "workspace", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "library_file_folder_id_library_folder_id_fk": { + "name": "library_file_folder_id_library_folder_id_fk", + "tableFrom": "library_file", + "tableTo": "library_folder", + "columnsFrom": [ + "folder_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "library_folder": { + "name": "library_folder", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "parent_id": { + "name": "parent_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "library_folder_workspace_id_workspace_id_fk": { + "name": "library_folder_workspace_id_workspace_id_fk", + "tableFrom": "library_folder", + "tableTo": "workspace", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "mcp_server": { + "name": "mcp_server", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "transport": { + "name": "transport", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "command": { + "name": "command", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "args": { + "name": "args", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "env": { + "name": "env", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "oauth_state": { + "name": "oauth_state", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "mcp_server_workspace_id_workspace_id_fk": { + "name": "mcp_server_workspace_id_workspace_id_fk", + "tableFrom": "mcp_server", + "tableTo": "workspace", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "memory_entry": { + "name": "memory_entry", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "confidence": { + "name": "confidence", + "type": "real", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 1 + }, + "created_by_agent_id": { + "name": "created_by_agent_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "memory_entry_workspace_id_workspace_id_fk": { + "name": "memory_entry_workspace_id_workspace_id_fk", + "tableFrom": "memory_entry", + "tableTo": "workspace", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "memory_entry_created_by_agent_id_agent_id_fk": { + "name": "memory_entry_created_by_agent_id_agent_id_fk", + "tableFrom": "memory_entry", + "tableTo": "agent", + "columnsFrom": [ + "created_by_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "message": { + "name": "message", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "chat_id": { + "name": "chat_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "model_id": { + "name": "model_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "input_tokens": { + "name": "input_tokens", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "output_tokens": { + "name": "output_tokens", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "reasoning_tokens": { + "name": "reasoning_tokens", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cache_read_tokens": { + "name": "cache_read_tokens", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "total_tokens": { + "name": "total_tokens", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cost_micros": { + "name": "cost_micros", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "duration_ms": { + "name": "duration_ms", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "thinking_ms": { + "name": "thinking_ms", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "line_count": { + "name": "line_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "code_line_count": { + "name": "code_line_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "code_block_count": { + "name": "code_block_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "message_chat_id_chat_id_fk": { + "name": "message_chat_id_chat_id_fk", + "tableFrom": "message", + "tableTo": "chat", + "columnsFrom": [ + "chat_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "model_installation": { + "name": "model_installation", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "provider_kind": { + "name": "provider_kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "base_url": { + "name": "base_url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "api_key": { + "name": "api_key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "model_ids": { + "name": "model_ids", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'[]'" + }, + "disabled_model_ids": { + "name": "disabled_model_ids", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'[]'" + }, + "enabled": { + "name": "enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "model_installation_workspace_id_workspace_id_fk": { + "name": "model_installation_workspace_id_workspace_id_fk", + "tableFrom": "model_installation", + "tableTo": "workspace", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "model_parameter": { + "name": "model_parameter", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "model_id": { + "name": "model_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "custom_name": { + "name": "custom_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "custom_instructions": { + "name": "custom_instructions", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "max_output_tokens": { + "name": "max_output_tokens", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "temperature": { + "name": "temperature", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "top_p": { + "name": "top_p", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "frequency_penalty": { + "name": "frequency_penalty", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "presence_penalty": { + "name": "presence_penalty", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "stop_sequences": { + "name": "stop_sequences", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'[]'" + }, + "reasoning_effort": { + "name": "reasoning_effort", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "model_parameter_workspace_model_idx": { + "name": "model_parameter_workspace_model_idx", + "columns": [ + "workspace_id", + "model_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "model_parameter_workspace_id_workspace_id_fk": { + "name": "model_parameter_workspace_id_workspace_id_fk", + "tableFrom": "model_parameter", + "tableTo": "workspace", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "nyxel_event": { + "name": "nyxel_event", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "entity_type": { + "name": "entity_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "entity_id": { + "name": "entity_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "payload": { + "name": "payload", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "nyxel_event_workspace_id_workspace_id_fk": { + "name": "nyxel_event_workspace_id_workspace_id_fk", + "tableFrom": "nyxel_event", + "tableTo": "workspace", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "plugin": { + "name": "plugin", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "version": { + "name": "version", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "author": { + "name": "author", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "homepage": { + "name": "homepage", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "repo_url": { + "name": "repo_url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "manifest": { + "name": "manifest", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'{}'" + }, + "skill_slugs": { + "name": "skill_slugs", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'[]'" + }, + "agent_defs": { + "name": "agent_defs", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'[]'" + }, + "file_count": { + "name": "file_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "install_dir": { + "name": "install_dir", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "ref": { + "name": "ref", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "resolved_sha": { + "name": "resolved_sha", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ref_pinned": { + "name": "ref_pinned", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "risk_findings": { + "name": "risk_findings", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'[]'" + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "plugin_workspace_id_workspace_id_fk": { + "name": "plugin_workspace_id_workspace_id_fk", + "tableFrom": "plugin", + "tableTo": "workspace", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "project": { + "name": "project", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'gray'" + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'folder'" + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "project_workspace_id_workspace_id_fk": { + "name": "project_workspace_id_workspace_id_fk", + "tableFrom": "project", + "tableTo": "workspace", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "push_subscription": { + "name": "push_subscription", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "endpoint": { + "name": "endpoint", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "p256dh": { + "name": "p256dh", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "auth": { + "name": "auth", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "push_subscription_endpoint_unique": { + "name": "push_subscription_endpoint_unique", + "columns": [ + "endpoint" + ], + "isUnique": true + } + }, + "foreignKeys": { + "push_subscription_user_id_user_id_fk": { + "name": "push_subscription_user_id_user_id_fk", + "tableFrom": "push_subscription", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "seo_analysis_run": { + "name": "seo_analysis_run", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "seo_project_id": { + "name": "seo_project_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'running'" + }, + "score": { + "name": "score", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "pages_scanned": { + "name": "pages_scanned", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "summary": { + "name": "summary", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "started_at": { + "name": "started_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "completed_at": { + "name": "completed_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "seo_analysis_run_seo_project_id_seo_project_id_fk": { + "name": "seo_analysis_run_seo_project_id_seo_project_id_fk", + "tableFrom": "seo_analysis_run", + "tableTo": "seo_project", + "columnsFrom": [ + "seo_project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "seo_analysis_run_workspace_id_workspace_id_fk": { + "name": "seo_analysis_run_workspace_id_workspace_id_fk", + "tableFrom": "seo_analysis_run", + "tableTo": "workspace", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "seo_blog_post": { + "name": "seo_blog_post", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "seo_project_id": { + "name": "seo_project_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "keyword": { + "name": "keyword", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "file_path": { + "name": "file_path", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'suggested'" + }, + "task_id": { + "name": "task_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "seo_blog_post_seo_project_id_seo_project_id_fk": { + "name": "seo_blog_post_seo_project_id_seo_project_id_fk", + "tableFrom": "seo_blog_post", + "tableTo": "seo_project", + "columnsFrom": [ + "seo_project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "seo_blog_post_workspace_id_workspace_id_fk": { + "name": "seo_blog_post_workspace_id_workspace_id_fk", + "tableFrom": "seo_blog_post", + "tableTo": "workspace", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "seo_blog_post_task_id_task_id_fk": { + "name": "seo_blog_post_task_id_task_id_fk", + "tableFrom": "seo_blog_post", + "tableTo": "task", + "columnsFrom": [ + "task_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "seo_finding": { + "name": "seo_finding", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "run_id": { + "name": "run_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "seo_project_id": { + "name": "seo_project_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "severity": { + "name": "severity", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "recommendation": { + "name": "recommendation", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "location": { + "name": "location", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "resolved": { + "name": "resolved", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "seo_finding_run_id_seo_analysis_run_id_fk": { + "name": "seo_finding_run_id_seo_analysis_run_id_fk", + "tableFrom": "seo_finding", + "tableTo": "seo_analysis_run", + "columnsFrom": [ + "run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "seo_finding_seo_project_id_seo_project_id_fk": { + "name": "seo_finding_seo_project_id_seo_project_id_fk", + "tableFrom": "seo_finding", + "tableTo": "seo_project", + "columnsFrom": [ + "seo_project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "seo_project": { + "name": "seo_project", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "extension_id": { + "name": "extension_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "domain": { + "name": "domain", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "repo_path": { + "name": "repo_path", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "blog_config": { + "name": "blog_config", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "fixer_agent_id": { + "name": "fixer_agent_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "reanalyze_cron_expression": { + "name": "reanalyze_cron_expression", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "next_reanalyze_at": { + "name": "next_reanalyze_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_reanalyze_at": { + "name": "last_reanalyze_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "seo_project_workspace_id_workspace_id_fk": { + "name": "seo_project_workspace_id_workspace_id_fk", + "tableFrom": "seo_project", + "tableTo": "workspace", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "seo_project_extension_id_extension_id_fk": { + "name": "seo_project_extension_id_extension_id_fk", + "tableFrom": "seo_project", + "tableTo": "extension", + "columnsFrom": [ + "extension_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "seo_project_fixer_agent_id_agent_id_fk": { + "name": "seo_project_fixer_agent_id_agent_id_fk", + "tableFrom": "seo_project", + "tableTo": "agent", + "columnsFrom": [ + "fixer_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "task": { + "name": "task", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "parent_task_id": { + "name": "parent_task_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "source_chat_id": { + "name": "source_chat_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_by_agent_id": { + "name": "created_by_agent_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "assigned_agent_id": { + "name": "assigned_agent_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "goal_id": { + "name": "goal_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "goal_milestone_id": { + "name": "goal_milestone_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "instruction": { + "name": "instruction", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "model_id": { + "name": "model_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'pending'" + }, + "priority": { + "name": "priority", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'normal'" + }, + "requires_approval": { + "name": "requires_approval", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "input": { + "name": "input", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'{}'" + }, + "plan": { + "name": "plan", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "handoff": { + "name": "handoff", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "result_summary": { + "name": "result_summary", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "started_at": { + "name": "started_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "completed_at": { + "name": "completed_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "task_workspace_id_workspace_id_fk": { + "name": "task_workspace_id_workspace_id_fk", + "tableFrom": "task", + "tableTo": "workspace", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "task_source_chat_id_chat_id_fk": { + "name": "task_source_chat_id_chat_id_fk", + "tableFrom": "task", + "tableTo": "chat", + "columnsFrom": [ + "source_chat_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "task_created_by_agent_id_agent_id_fk": { + "name": "task_created_by_agent_id_agent_id_fk", + "tableFrom": "task", + "tableTo": "agent", + "columnsFrom": [ + "created_by_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "task_assigned_agent_id_agent_id_fk": { + "name": "task_assigned_agent_id_agent_id_fk", + "tableFrom": "task", + "tableTo": "agent", + "columnsFrom": [ + "assigned_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "task_goal_id_goal_id_fk": { + "name": "task_goal_id_goal_id_fk", + "tableFrom": "task", + "tableTo": "goal", + "columnsFrom": [ + "goal_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "task_goal_milestone_id_goal_milestone_id_fk": { + "name": "task_goal_milestone_id_goal_milestone_id_fk", + "tableFrom": "task", + "tableTo": "goal_milestone", + "columnsFrom": [ + "goal_milestone_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "task_event": { + "name": "task_event", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "task_id": { + "name": "task_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "agent_run_id": { + "name": "agent_run_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "agent_id": { + "name": "agent_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "message": { + "name": "message", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "payload": { + "name": "payload", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "task_event_task_id_task_id_fk": { + "name": "task_event_task_id_task_id_fk", + "tableFrom": "task_event", + "tableTo": "task", + "columnsFrom": [ + "task_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "task_event_workspace_id_workspace_id_fk": { + "name": "task_event_workspace_id_workspace_id_fk", + "tableFrom": "task_event", + "tableTo": "workspace", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "task_event_agent_run_id_agent_run_id_fk": { + "name": "task_event_agent_run_id_agent_run_id_fk", + "tableFrom": "task_event", + "tableTo": "agent_run", + "columnsFrom": [ + "agent_run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "task_event_agent_id_agent_id_fk": { + "name": "task_event_agent_id_agent_id_fk", + "tableFrom": "task_event", + "tableTo": "agent", + "columnsFrom": [ + "agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "tool": { + "name": "tool", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "config": { + "name": "config", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'{}'" + }, + "sensitive": { + "name": "sensitive", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "enabled": { + "name": "enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "builtin": { + "name": "builtin", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "tool_workspace_id_workspace_id_fk": { + "name": "tool_workspace_id_workspace_id_fk", + "tableFrom": "tool", + "tableTo": "workspace", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "video_generation_job": { + "name": "video_generation_job", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "chat_id": { + "name": "chat_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "prompt": { + "name": "prompt", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'queued'" + }, + "progress": { + "name": "progress", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "size": { + "name": "size", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "seconds": { + "name": "seconds", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "auto": { + "name": "auto", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "external_job_id": { + "name": "external_job_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "library_file_id": { + "name": "library_file_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "poster_library_file_id": { + "name": "poster_library_file_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "video_generation_job_workspace_id_workspace_id_fk": { + "name": "video_generation_job_workspace_id_workspace_id_fk", + "tableFrom": "video_generation_job", + "tableTo": "workspace", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "video_generation_job_library_file_id_library_file_id_fk": { + "name": "video_generation_job_library_file_id_library_file_id_fk", + "tableFrom": "video_generation_job", + "tableTo": "library_file", + "columnsFrom": [ + "library_file_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "video_generation_job_poster_library_file_id_library_file_id_fk": { + "name": "video_generation_job_poster_library_file_id_library_file_id_fk", + "tableFrom": "video_generation_job", + "tableTo": "library_file", + "columnsFrom": [ + "poster_library_file_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "workflow": { + "name": "workflow", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "definition": { + "name": "definition", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "workflow_workspace_id_workspace_id_fk": { + "name": "workflow_workspace_id_workspace_id_fk", + "tableFrom": "workflow", + "tableTo": "workspace", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "workflow_run": { + "name": "workflow_run", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'queued'" + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "started_at": { + "name": "started_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "completed_at": { + "name": "completed_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "workflow_run_workflow_id_workflow_id_fk": { + "name": "workflow_run_workflow_id_workflow_id_fk", + "tableFrom": "workflow_run", + "tableTo": "workflow", + "columnsFrom": [ + "workflow_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_run_workspace_id_workspace_id_fk": { + "name": "workflow_run_workspace_id_workspace_id_fk", + "tableFrom": "workflow_run", + "tableTo": "workspace", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "workflow_run_node": { + "name": "workflow_run_node", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "run_id": { + "name": "run_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "node_id": { + "name": "node_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'queued'" + }, + "progress": { + "name": "progress", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "library_file_id": { + "name": "library_file_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "started_at": { + "name": "started_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "completed_at": { + "name": "completed_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "workflow_run_node_run_id_workflow_run_id_fk": { + "name": "workflow_run_node_run_id_workflow_run_id_fk", + "tableFrom": "workflow_run_node", + "tableTo": "workflow_run", + "columnsFrom": [ + "run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_run_node_library_file_id_library_file_id_fk": { + "name": "workflow_run_node_library_file_id_library_file_id_fk", + "tableFrom": "workflow_run_node", + "tableTo": "library_file", + "columnsFrom": [ + "library_file_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "workspace": { + "name": "workspace", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "custom_instructions": { + "name": "custom_instructions", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "default_model_id": { + "name": "default_model_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "default_autonomy_level": { + "name": "default_autonomy_level", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'assisted'" + }, + "default_tool_policy": { + "name": "default_tool_policy", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "workspace_user_id_user_id_fk": { + "name": "workspace_user_id_user_id_fk", + "tableFrom": "workspace", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "account": { + "name": "account", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "account_id": { + "name": "account_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "id_token": { + "name": "id_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "access_token_expires_at": { + "name": "access_token_expires_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "refresh_token_expires_at": { + "name": "refresh_token_expires_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "account_user_id_user_id_fk": { + "name": "account_user_id_user_id_fk", + "tableFrom": "account", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "session": { + "name": "session", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "session_token_unique": { + "name": "session_token_unique", + "columns": [ + "token" + ], + "isUnique": true + } + }, + "foreignKeys": { + "session_user_id_user_id_fk": { + "name": "session_user_id_user_id_fk", + "tableFrom": "session", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "user": { + "name": "user", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "email_verified": { + "name": "email_verified", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "user_email_unique": { + "name": "user_email_unique", + "columns": [ + "email" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "verification": { + "name": "verification", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + } + }, + "views": {}, + "enums": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "indexes": {} + } +} \ No newline at end of file diff --git a/packages/db/drizzle/sqlite/meta/_journal.json b/packages/db/drizzle/sqlite/meta/_journal.json index a024597..6f62b99 100644 --- a/packages/db/drizzle/sqlite/meta/_journal.json +++ b/packages/db/drizzle/sqlite/meta/_journal.json @@ -288,6 +288,13 @@ "when": 1783095971059, "tag": "0040_tricky_sprite", "breakpoints": true + }, + { + "idx": 41, + "version": "6", + "when": 1783104248898, + "tag": "0041_luxuriant_jubilee", + "breakpoints": true } ] } \ No newline at end of file diff --git a/packages/db/src/schema/pg/app.ts b/packages/db/src/schema/pg/app.ts index 2cff81e..c026018 100644 --- a/packages/db/src/schema/pg/app.ts +++ b/packages/db/src/schema/pg/app.ts @@ -128,6 +128,66 @@ export const seoBlogPostStatus = pgEnum("seo_blog_post_status", [ "written", "failed", ]); + +// Local Lead Scout (see apps/server/src/lead-scout*.ts). `custom_api` is a +// minimal placeholder adapter; `manual_csv` and `osm_overpass` need no API +// key, `google_places_api` is disabled until one is configured. +export const leadScoutProvider = pgEnum("lead_scout_provider", [ + "manual_csv", + "google_places_api", + "osm_overpass", + "custom_api", +]); +// draft_only never offers a send action past the drafted email; review_and_send +// allows the approve -> send flow. Campaigns default to draft_only (compliance +// requirement: no cold outreach without explicit review). +export const leadScoutOutreachMode = pgEnum("lead_scout_outreach_mode", [ + "draft_only", + "review_and_send", +]); +export const leadScoutWebsiteStatus = pgEnum("lead_scout_website_status", [ + "unknown", + "has_website", + "missing_website", + "invalid_website", +]); +export const leadScoutLeadStatus = pgEnum("lead_scout_lead_status", [ + "new", + "reviewed", + "prototype_requested", + "prototype_ready", + "email_drafted", + "approved_to_send", + "sending", + "sent", + "rejected", + "suppressed", +]); +export const leadScoutScanRunStatus = pgEnum("lead_scout_scan_run_status", [ + "running", + "completed", + "failed", +]); +export const leadScoutPrototypeStatus = pgEnum("lead_scout_prototype_status", [ + "pending", + "ready", + "failed", +]); +export const leadScoutDraftStatus = pgEnum("lead_scout_draft_status", [ + "draft", + "approved", + "rejected", + "sending", + "sent", + "failed", +]); +export const leadScoutEmailProvider = pgEnum("lead_scout_email_provider", [ + "smtp", + "resend", + "mailgun", + "custom", +]); + export const agentRunStatus = pgEnum("agent_run_status", [ "pending", "running", @@ -1017,6 +1077,241 @@ export const seoBlogPost = pgTable("seo_blog_post", { updatedAt: timestamp("updated_at").notNull().defaultNow(), }); +/** A Local Lead Scout campaign — the workspace-scoped config for one region/ + * niche scan (postal code + radius + niches), mirroring seoProject's + * "extensionId FK + own recurring schedule" shape. `prototypeAgentId` is the + * existing NyxelOS agent used to draft prototypes/emails (ADR: reuse the + * agent/task system rather than a bespoke LLM pipeline) — left null until + * the user picks one or a prototype/draft is first requested, at which point + * lead-scout.ts lazily provisions a workspace-default agent, same idea as + * seo-analyzer's configureSeoFixerAgent. */ +export const leadScoutCampaign = pgTable("lead_scout_campaign", { + id: text("id").primaryKey(), + workspaceId: text("workspace_id") + .notNull() + .references(() => workspace.id, { onDelete: "cascade" }), + extensionId: text("extension_id") + .notNull() + .references(() => extension.id, { onDelete: "cascade" }), + name: text("name").notNull(), + postalCode: text("postal_code").notNull(), + country: text("country").notNull().default("US"), + radiusKm: doublePrecision("radius_km").notNull().default(10), + niches: jsonb("niches").notNull().default([]).$type(), + maxResultsPerRun: integer("max_results_per_run").notNull().default(25), + provider: leadScoutProvider("provider").notNull(), + minConfidence: integer("min_confidence").notNull().default(50), + outreachMode: leadScoutOutreachMode("outreach_mode").notNull().default("draft_only"), + scheduleEnabled: boolean("schedule_enabled").notNull().default(false), + scheduleCronExpression: text("schedule_cron_expression"), + nextScanAt: timestamp("next_scan_at"), + lastScanAt: timestamp("last_scan_at"), + autoGeneratePrototype: boolean("auto_generate_prototype").notNull().default(false), + autoDraftEmail: boolean("auto_draft_email").notNull().default(false), + autoSendAfterApproval: boolean("auto_send_after_approval").notNull().default(false), + requireApprovalBeforePrototype: boolean("require_approval_before_prototype") + .notNull() + .default(true), + requireApprovalBeforeEmailSend: boolean("require_approval_before_email_send") + .notNull() + .default(true), + prototypeAgentId: text("prototype_agent_id").references(() => agent.id, { + onDelete: "set null", + }), + createdAt: timestamp("created_at").notNull().defaultNow(), + updatedAt: timestamp("updated_at").notNull().defaultNow(), +}); + +/** Per-workspace, per-provider settings — separate from the campaign row so + * an encrypted API key (google_places_api/custom_api) is configured once per + * workspace and reused across every campaign that picks that provider, + * rather than duplicated per campaign. `apiKey` is encrypted/decrypted at + * the repo layer, same convention as modelInstallation.apiKey — never + * stored or returned raw. */ +export const leadScoutSourceConfig = pgTable( + "lead_scout_source_config", + { + id: text("id").primaryKey(), + workspaceId: text("workspace_id") + .notNull() + .references(() => workspace.id, { onDelete: "cascade" }), + provider: leadScoutProvider("provider").notNull(), + config: jsonb("config").notNull().default({}).$type>(), + apiKey: text("api_key"), + enabled: boolean("enabled").notNull().default(true), + createdAt: timestamp("created_at").notNull().defaultNow(), + updatedAt: timestamp("updated_at").notNull().defaultNow(), + }, + (table) => [uniqueIndex("lead_scout_source_config_workspace_provider_idx").on( + table.workspaceId, + table.provider, + )], +); + +export const leadScoutScanRun = pgTable("lead_scout_scan_run", { + id: text("id").primaryKey(), + campaignId: text("campaign_id") + .notNull() + .references(() => leadScoutCampaign.id, { onDelete: "cascade" }), + workspaceId: text("workspace_id") + .notNull() + .references(() => workspace.id, { onDelete: "cascade" }), + provider: leadScoutProvider("provider").notNull(), + status: leadScoutScanRunStatus("status").notNull().default("running"), + resultCount: integer("result_count").notNull().default(0), + newLeadCount: integer("new_lead_count").notNull().default(0), + missingWebsiteCount: integer("missing_website_count").notNull().default(0), + summary: text("summary"), + errorMessage: text("error_message"), + startedAt: timestamp("started_at").notNull().defaultNow(), + completedAt: timestamp("completed_at"), +}); + +/** A discovered business. `sourceId` (the provider's own id — CSV row hash, + * OSM node/way id, Google Place id) is unique per campaign+provider so + * re-running a scan reconciles instead of re-inserting the same business — + * same "don't bulk-store more than needed" rule the provider docs require. + * `websiteStatus`/`confidence`/`evidenceSummary` are exactly what a reviewer + * needs to judge the lead; no reviews/photos/ratings are ever stored. */ +export const leadScoutLead = pgTable( + "lead_scout_lead", + { + id: text("id").primaryKey(), + workspaceId: text("workspace_id") + .notNull() + .references(() => workspace.id, { onDelete: "cascade" }), + campaignId: text("campaign_id") + .notNull() + .references(() => leadScoutCampaign.id, { onDelete: "cascade" }), + scanRunId: text("scan_run_id").references(() => leadScoutScanRun.id, { + onDelete: "set null", + }), + sourceProvider: leadScoutProvider("source_provider").notNull(), + sourceId: text("source_id").notNull(), + businessName: text("business_name").notNull(), + category: text("category"), + niche: text("niche"), + formattedAddress: text("formatted_address"), + postalCode: text("postal_code"), + city: text("city"), + phone: text("phone"), + email: text("email"), + website: text("website"), + websiteStatus: leadScoutWebsiteStatus("website_status").notNull().default("unknown"), + confidence: integer("confidence").notNull().default(0), + evidenceSummary: text("evidence_summary"), + missingReason: text("missing_reason"), + status: leadScoutLeadStatus("status").notNull().default("new"), + createdAt: timestamp("created_at").notNull().defaultNow(), + updatedAt: timestamp("updated_at").notNull().defaultNow(), + }, + (table) => [uniqueIndex("lead_scout_lead_campaign_source_idx").on( + table.campaignId, + table.sourceProvider, + table.sourceId, + )], +); + +/** One AI-generated website concept for a lead, produced by dispatching a + * task to the campaign's prototypeAgentId (see dispatchLeadScoutPrototype in + * lead-scout.ts) — never a bespoke LLM pipeline. `approved` gates its use in + * an outreach draft (compliance requirement: review before it goes near an + * email). */ +export const leadScoutPrototype = pgTable("lead_scout_prototype", { + id: text("id").primaryKey(), + workspaceId: text("workspace_id") + .notNull() + .references(() => workspace.id, { onDelete: "cascade" }), + leadId: text("lead_id") + .notNull() + .references(() => leadScoutLead.id, { onDelete: "cascade" }), + taskId: text("task_id").references(() => task.id, { onDelete: "set null" }), + status: leadScoutPrototypeStatus("status").notNull().default("pending"), + concept: text("concept"), + heroCopy: text("hero_copy"), + sections: jsonb("sections").notNull().default([]).$type(), + callToAction: text("call_to_action"), + styleDirection: text("style_direction"), + artifactMarkdown: text("artifact_markdown"), + approved: boolean("approved").notNull().default(false), + errorMessage: text("error_message"), + createdAt: timestamp("created_at").notNull().defaultNow(), + updatedAt: timestamp("updated_at").notNull().defaultNow(), +}); + +/** A drafted outreach email for a lead, tied to the prototype it references. + * `status` carries the entire approval-gated send lifecycle as an explicit + * state machine (see resolveLeadScoutSendApproval / claimLeadForSend in + * lead-scout.ts, which use conditional UPDATEs on this column instead of + * check-then-write) rather than a separate approval-request row — there is + * exactly one irreversible action per draft (send), so the draft's own + * status is the approval record. */ +export const leadScoutOutreachDraft = pgTable("lead_scout_outreach_draft", { + id: text("id").primaryKey(), + workspaceId: text("workspace_id") + .notNull() + .references(() => workspace.id, { onDelete: "cascade" }), + leadId: text("lead_id") + .notNull() + .references(() => leadScoutLead.id, { onDelete: "cascade" }), + prototypeId: text("prototype_id").references(() => leadScoutPrototype.id, { + onDelete: "set null", + }), + taskId: text("task_id").references(() => task.id, { onDelete: "set null" }), + subject: text("subject"), + bodyText: text("body_text"), + bodyHtml: text("body_html"), + status: leadScoutDraftStatus("status").notNull().default("draft"), + errorMessage: text("error_message"), + approvedAt: timestamp("approved_at"), + sentAt: timestamp("sent_at"), + createdAt: timestamp("created_at").notNull().defaultNow(), + updatedAt: timestamp("updated_at").notNull().defaultNow(), +}); + +/** Per-workspace suppression list (opt-outs, bounces, manual blocks) checked + * before every send — at least one of email/domain is always set (enforced + * in lead-scout.ts, not at the DB level). */ +export const leadScoutSuppression = pgTable("lead_scout_suppression", { + id: text("id").primaryKey(), + workspaceId: text("workspace_id") + .notNull() + .references(() => workspace.id, { onDelete: "cascade" }), + email: text("email"), + domain: text("domain"), + reason: text("reason").notNull(), + createdAt: timestamp("created_at").notNull().defaultNow(), +}); + +/** Workspace-level secure outreach email settings — one row per workspace. + * `credentials` is a single encrypted JSON blob (shape depends on + * `provider`: SMTP host/port/username/password, or an API key for + * resend/mailgun) via encryptJsonNullable, encrypted/decrypted at the repo + * layer and never returned raw to the client (see + * toClientSafeLeadScoutEmailSettings in router.ts). Defaults to + * dryRunMode=true so installing the extension can never itself cause a send. */ +export const leadScoutEmailSettings = pgTable("lead_scout_email_settings", { + id: text("id").primaryKey(), + workspaceId: text("workspace_id") + .notNull() + .unique() + .references(() => workspace.id, { onDelete: "cascade" }), + provider: leadScoutEmailProvider("provider").notNull().default("smtp"), + fromName: text("from_name").notNull(), + fromEmail: text("from_email").notNull(), + replyTo: text("reply_to"), + credentials: text("credentials"), + dailySendLimit: integer("daily_send_limit").notNull().default(20), + perCampaignSendLimit: integer("per_campaign_send_limit").notNull().default(10), + dryRunMode: boolean("dry_run_mode").notNull().default(true), + legalFooter: text("legal_footer"), + unsubscribeText: text("unsubscribe_text").notNull().default("Reply STOP to opt out."), + sendCountToday: integer("send_count_today").notNull().default(0), + sendCountDate: text("send_count_date"), + createdAt: timestamp("created_at").notNull().defaultNow(), + updatedAt: timestamp("updated_at").notNull().defaultNow(), +}); + /** A folder in the workspace's document/image library — purely an * organizational grouping, not tied to disk paths directly (see * libraryFile.storageKey for where bytes actually live). `parentId` is a diff --git a/packages/db/src/schema/sqlite/app.ts b/packages/db/src/schema/sqlite/app.ts index 7bfd4df..7b93f8d 100644 --- a/packages/db/src/schema/sqlite/app.ts +++ b/packages/db/src/schema/sqlite/app.ts @@ -87,6 +87,36 @@ export type SeoAnalysisRunStatus = "running" | "completed" | "failed"; export type SeoFindingCategory = "seo" | "geo" | "aeo"; export type SeoFindingSeverity = "info" | "warning" | "critical"; export type SeoBlogPostStatus = "suggested" | "generating" | "written" | "failed"; + +// Local Lead Scout (see ../pg/app.ts for the full rationale on each type). +export type LeadScoutProvider = "manual_csv" | "google_places_api" | "osm_overpass" | "custom_api"; +export type LeadScoutOutreachMode = "draft_only" | "review_and_send"; +export type LeadScoutWebsiteStatus = + | "unknown" + | "has_website" + | "missing_website" + | "invalid_website"; +export type LeadScoutLeadStatus = + | "new" + | "reviewed" + | "prototype_requested" + | "prototype_ready" + | "email_drafted" + | "approved_to_send" + | "sending" + | "sent" + | "rejected" + | "suppressed"; +export type LeadScoutScanRunStatus = "running" | "completed" | "failed"; +export type LeadScoutPrototypeStatus = "pending" | "ready" | "failed"; +export type LeadScoutDraftStatus = + | "draft" + | "approved" + | "rejected" + | "sending" + | "sent" + | "failed"; +export type LeadScoutEmailProvider = "smtp" | "resend" | "mailgun" | "custom"; export type AgentRunStatus = | "pending" | "running" @@ -929,6 +959,224 @@ export const seoBlogPost = sqliteTable("seo_blog_post", { updatedAt: integer("updated_at", { mode: "timestamp" }).notNull(), }); +/** See ../pg/app.ts. */ +export const leadScoutCampaign = sqliteTable("lead_scout_campaign", { + id: text("id").primaryKey(), + workspaceId: text("workspace_id") + .notNull() + .references(() => workspace.id, { onDelete: "cascade" }), + extensionId: text("extension_id") + .notNull() + .references(() => extension.id, { onDelete: "cascade" }), + name: text("name").notNull(), + postalCode: text("postal_code").notNull(), + country: text("country").notNull().default("US"), + radiusKm: real("radius_km").notNull().default(10), + niches: text("niches", { mode: "json" }).notNull().default([]).$type(), + maxResultsPerRun: integer("max_results_per_run").notNull().default(25), + provider: text("provider").notNull().$type(), + minConfidence: integer("min_confidence").notNull().default(50), + outreachMode: text("outreach_mode") + .notNull() + .default("draft_only") + .$type(), + scheduleEnabled: integer("schedule_enabled", { mode: "boolean" }).notNull().default(false), + scheduleCronExpression: text("schedule_cron_expression"), + nextScanAt: integer("next_scan_at", { mode: "timestamp" }), + lastScanAt: integer("last_scan_at", { mode: "timestamp" }), + autoGeneratePrototype: integer("auto_generate_prototype", { mode: "boolean" }) + .notNull() + .default(false), + autoDraftEmail: integer("auto_draft_email", { mode: "boolean" }).notNull().default(false), + autoSendAfterApproval: integer("auto_send_after_approval", { mode: "boolean" }) + .notNull() + .default(false), + requireApprovalBeforePrototype: integer("require_approval_before_prototype", { + mode: "boolean", + }) + .notNull() + .default(true), + requireApprovalBeforeEmailSend: integer("require_approval_before_email_send", { + mode: "boolean", + }) + .notNull() + .default(true), + prototypeAgentId: text("prototype_agent_id").references(() => agent.id, { + onDelete: "set null", + }), + createdAt: integer("created_at", { mode: "timestamp" }).notNull(), + updatedAt: integer("updated_at", { mode: "timestamp" }).notNull(), +}); + +/** See ../pg/app.ts. */ +export const leadScoutSourceConfig = sqliteTable( + "lead_scout_source_config", + { + id: text("id").primaryKey(), + workspaceId: text("workspace_id") + .notNull() + .references(() => workspace.id, { onDelete: "cascade" }), + provider: text("provider").notNull().$type(), + config: text("config", { mode: "json" }) + .notNull() + .default({}) + .$type>(), + apiKey: text("api_key"), + enabled: integer("enabled", { mode: "boolean" }).notNull().default(true), + createdAt: integer("created_at", { mode: "timestamp" }).notNull(), + updatedAt: integer("updated_at", { mode: "timestamp" }).notNull(), + }, + (table) => [uniqueIndex("lead_scout_source_config_workspace_provider_idx").on( + table.workspaceId, + table.provider, + )], +); + +/** See ../pg/app.ts. */ +export const leadScoutScanRun = sqliteTable("lead_scout_scan_run", { + id: text("id").primaryKey(), + campaignId: text("campaign_id") + .notNull() + .references(() => leadScoutCampaign.id, { onDelete: "cascade" }), + workspaceId: text("workspace_id") + .notNull() + .references(() => workspace.id, { onDelete: "cascade" }), + provider: text("provider").notNull().$type(), + status: text("status").notNull().default("running").$type(), + resultCount: integer("result_count").notNull().default(0), + newLeadCount: integer("new_lead_count").notNull().default(0), + missingWebsiteCount: integer("missing_website_count").notNull().default(0), + summary: text("summary"), + errorMessage: text("error_message"), + startedAt: integer("started_at", { mode: "timestamp" }).notNull(), + completedAt: integer("completed_at", { mode: "timestamp" }), +}); + +/** See ../pg/app.ts. */ +export const leadScoutLead = sqliteTable( + "lead_scout_lead", + { + id: text("id").primaryKey(), + workspaceId: text("workspace_id") + .notNull() + .references(() => workspace.id, { onDelete: "cascade" }), + campaignId: text("campaign_id") + .notNull() + .references(() => leadScoutCampaign.id, { onDelete: "cascade" }), + scanRunId: text("scan_run_id").references(() => leadScoutScanRun.id, { + onDelete: "set null", + }), + sourceProvider: text("source_provider").notNull().$type(), + sourceId: text("source_id").notNull(), + businessName: text("business_name").notNull(), + category: text("category"), + niche: text("niche"), + formattedAddress: text("formatted_address"), + postalCode: text("postal_code"), + city: text("city"), + phone: text("phone"), + email: text("email"), + website: text("website"), + websiteStatus: text("website_status") + .notNull() + .default("unknown") + .$type(), + confidence: integer("confidence").notNull().default(0), + evidenceSummary: text("evidence_summary"), + missingReason: text("missing_reason"), + status: text("status").notNull().default("new").$type(), + createdAt: integer("created_at", { mode: "timestamp" }).notNull(), + updatedAt: integer("updated_at", { mode: "timestamp" }).notNull(), + }, + (table) => [uniqueIndex("lead_scout_lead_campaign_source_idx").on( + table.campaignId, + table.sourceProvider, + table.sourceId, + )], +); + +/** See ../pg/app.ts. */ +export const leadScoutPrototype = sqliteTable("lead_scout_prototype", { + id: text("id").primaryKey(), + workspaceId: text("workspace_id") + .notNull() + .references(() => workspace.id, { onDelete: "cascade" }), + leadId: text("lead_id") + .notNull() + .references(() => leadScoutLead.id, { onDelete: "cascade" }), + taskId: text("task_id").references(() => task.id, { onDelete: "set null" }), + status: text("status").notNull().default("pending").$type(), + concept: text("concept"), + heroCopy: text("hero_copy"), + sections: text("sections", { mode: "json" }).notNull().default([]).$type(), + callToAction: text("call_to_action"), + styleDirection: text("style_direction"), + artifactMarkdown: text("artifact_markdown"), + approved: integer("approved", { mode: "boolean" }).notNull().default(false), + errorMessage: text("error_message"), + createdAt: integer("created_at", { mode: "timestamp" }).notNull(), + updatedAt: integer("updated_at", { mode: "timestamp" }).notNull(), +}); + +/** See ../pg/app.ts. */ +export const leadScoutOutreachDraft = sqliteTable("lead_scout_outreach_draft", { + id: text("id").primaryKey(), + workspaceId: text("workspace_id") + .notNull() + .references(() => workspace.id, { onDelete: "cascade" }), + leadId: text("lead_id") + .notNull() + .references(() => leadScoutLead.id, { onDelete: "cascade" }), + prototypeId: text("prototype_id").references(() => leadScoutPrototype.id, { + onDelete: "set null", + }), + taskId: text("task_id").references(() => task.id, { onDelete: "set null" }), + subject: text("subject"), + bodyText: text("body_text"), + bodyHtml: text("body_html"), + status: text("status").notNull().default("draft").$type(), + errorMessage: text("error_message"), + approvedAt: integer("approved_at", { mode: "timestamp" }), + sentAt: integer("sent_at", { mode: "timestamp" }), + createdAt: integer("created_at", { mode: "timestamp" }).notNull(), + updatedAt: integer("updated_at", { mode: "timestamp" }).notNull(), +}); + +/** See ../pg/app.ts. */ +export const leadScoutSuppression = sqliteTable("lead_scout_suppression", { + id: text("id").primaryKey(), + workspaceId: text("workspace_id") + .notNull() + .references(() => workspace.id, { onDelete: "cascade" }), + email: text("email"), + domain: text("domain"), + reason: text("reason").notNull(), + createdAt: integer("created_at", { mode: "timestamp" }).notNull(), +}); + +/** See ../pg/app.ts. */ +export const leadScoutEmailSettings = sqliteTable("lead_scout_email_settings", { + id: text("id").primaryKey(), + workspaceId: text("workspace_id") + .notNull() + .unique() + .references(() => workspace.id, { onDelete: "cascade" }), + provider: text("provider").notNull().default("smtp").$type(), + fromName: text("from_name").notNull(), + fromEmail: text("from_email").notNull(), + replyTo: text("reply_to"), + credentials: text("credentials"), + dailySendLimit: integer("daily_send_limit").notNull().default(20), + perCampaignSendLimit: integer("per_campaign_send_limit").notNull().default(10), + dryRunMode: integer("dry_run_mode", { mode: "boolean" }).notNull().default(true), + legalFooter: text("legal_footer"), + unsubscribeText: text("unsubscribe_text").notNull().default("Reply STOP to opt out."), + sendCountToday: integer("send_count_today").notNull().default(0), + sendCountDate: text("send_count_date"), + createdAt: integer("created_at", { mode: "timestamp" }).notNull(), + updatedAt: integer("updated_at", { mode: "timestamp" }).notNull(), +}); + /** See ../pg/app.ts. */ export const libraryFolder = sqliteTable("library_folder", { id: text("id").primaryKey(), From 01b17e0fd2c5cebae985ccba6b82006093f9fc6a Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 3 Jul 2026 18:53:53 +0000 Subject: [PATCH 03/13] feat: add lead scout db repository layer MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds typed record interfaces and dialect-agnostic CRUD methods for campaigns, source configs, scan runs, leads, prototypes, outreach drafts, suppressions, and encrypted email settings — implemented for both Postgres and SQLite. Lead/outreach-draft status transitions use atomic compare-and-swap updates (claimLeadScoutLeadStatus, claimLeadScoutOutreachDraftStatus) so approval/send can never race. --- packages/db/src/index.ts | 17 + packages/db/src/repo/pg.repo.ts | 461 ++++++++++++++++++++++++ packages/db/src/repo/sqlite.repo.ts | 539 ++++++++++++++++++++++++++++ packages/db/src/repo/types.ts | 416 +++++++++++++++++++++ 4 files changed, 1433 insertions(+) diff --git a/packages/db/src/index.ts b/packages/db/src/index.ts index f87b545..599ffb7 100644 --- a/packages/db/src/index.ts +++ b/packages/db/src/index.ts @@ -32,6 +32,23 @@ export type { InstallationMode, InstallationRecord, KnowledgeBaseConfigRecord, + LeadScoutCampaignRecord, + LeadScoutDraftStatus, + LeadScoutEmailCredentials, + LeadScoutEmailProvider, + LeadScoutEmailSettingsRecord, + LeadScoutLeadRecord, + LeadScoutLeadStatus, + LeadScoutOutreachDraftRecord, + LeadScoutOutreachMode, + LeadScoutPrototypeRecord, + LeadScoutPrototypeStatus, + LeadScoutProvider, + LeadScoutScanRunRecord, + LeadScoutScanRunStatus, + LeadScoutSourceConfigRecord, + LeadScoutSuppressionRecord, + LeadScoutWebsiteStatus, LibraryFileRecord, LibraryFolderRecord, LibraryItemKind, diff --git a/packages/db/src/repo/pg.repo.ts b/packages/db/src/repo/pg.repo.ts index 4d1904e..188f33b 100644 --- a/packages/db/src/repo/pg.repo.ts +++ b/packages/db/src/repo/pg.repo.ts @@ -2551,5 +2551,466 @@ export function createPgRepository(connectionString: string): DbRepository { if (!row) throw new Error(`SEO blog post not found: ${id}`); return row; }, + + async createLeadScoutCampaign({ + workspaceId, + extensionId, + name, + postalCode, + country, + radiusKm, + niches, + maxResultsPerRun, + provider, + minConfidence, + outreachMode, + }) { + const [row] = await db + .insert(schema.leadScoutCampaign) + .values({ + id: randomUUID(), + workspaceId, + extensionId, + name, + postalCode, + ...(country !== undefined ? { country } : {}), + ...(radiusKm !== undefined ? { radiusKm } : {}), + ...(niches !== undefined ? { niches } : {}), + ...(maxResultsPerRun !== undefined ? { maxResultsPerRun } : {}), + provider, + ...(minConfidence !== undefined ? { minConfidence } : {}), + ...(outreachMode !== undefined ? { outreachMode } : {}), + }) + .returning(); + if (!row) throw new Error("Failed to create lead scout campaign"); + return row; + }, + + async listLeadScoutCampaignsByWorkspace(workspaceId) { + return db + .select() + .from(schema.leadScoutCampaign) + .where(eq(schema.leadScoutCampaign.workspaceId, workspaceId)) + .orderBy(desc(schema.leadScoutCampaign.createdAt)); + }, + + async getLeadScoutCampaign(id) { + const row = await db.query.leadScoutCampaign.findFirst({ + where: eq(schema.leadScoutCampaign.id, id), + }); + return row ?? null; + }, + + async updateLeadScoutCampaign(id, patch) { + const [row] = await db + .update(schema.leadScoutCampaign) + .set({ ...patch, updatedAt: new Date() }) + .where(eq(schema.leadScoutCampaign.id, id)) + .returning(); + if (!row) throw new Error(`Lead scout campaign not found: ${id}`); + return row; + }, + + async deleteLeadScoutCampaign(id) { + await db.delete(schema.leadScoutCampaign).where(eq(schema.leadScoutCampaign.id, id)); + }, + + async listDueLeadScoutCampaigns(now) { + return db + .select() + .from(schema.leadScoutCampaign) + .where( + and( + eq(schema.leadScoutCampaign.scheduleEnabled, true), + isNotNull(schema.leadScoutCampaign.nextScanAt), + lte(schema.leadScoutCampaign.nextScanAt, now), + ), + ); + }, + + async upsertLeadScoutSourceConfig({ workspaceId, provider, config, apiKey, enabled }) { + const existing = await db.query.leadScoutSourceConfig.findFirst({ + where: and( + eq(schema.leadScoutSourceConfig.workspaceId, workspaceId), + eq(schema.leadScoutSourceConfig.provider, provider), + ), + }); + + if (existing) { + const [row] = await db + .update(schema.leadScoutSourceConfig) + .set({ + ...(config !== undefined ? { config } : {}), + ...(apiKey !== undefined ? { apiKey: encryptNullable(apiKey) } : {}), + ...(enabled !== undefined ? { enabled } : {}), + updatedAt: new Date(), + }) + .where(eq(schema.leadScoutSourceConfig.id, existing.id)) + .returning(); + if (!row) throw new Error(`Lead scout source config not found: ${existing.id}`); + return { ...row, apiKey: decryptNullable(row.apiKey) }; + } + + const [row] = await db + .insert(schema.leadScoutSourceConfig) + .values({ + id: randomUUID(), + workspaceId, + provider, + config: config ?? {}, + apiKey: encryptNullable(apiKey ?? null), + enabled: enabled ?? true, + }) + .returning(); + if (!row) throw new Error("Failed to create lead scout source config"); + return { ...row, apiKey: decryptNullable(row.apiKey) }; + }, + + async listLeadScoutSourceConfigsByWorkspace(workspaceId) { + const rows = await db + .select() + .from(schema.leadScoutSourceConfig) + .where(eq(schema.leadScoutSourceConfig.workspaceId, workspaceId)); + return rows.map((row) => ({ ...row, apiKey: decryptNullable(row.apiKey) })); + }, + + async getLeadScoutSourceConfig(workspaceId, provider) { + const row = await db.query.leadScoutSourceConfig.findFirst({ + where: and( + eq(schema.leadScoutSourceConfig.workspaceId, workspaceId), + eq(schema.leadScoutSourceConfig.provider, provider), + ), + }); + return row ? { ...row, apiKey: decryptNullable(row.apiKey) } : null; + }, + + async createLeadScoutScanRun({ campaignId, workspaceId, provider }) { + const [row] = await db + .insert(schema.leadScoutScanRun) + .values({ id: randomUUID(), campaignId, workspaceId, provider }) + .returning(); + if (!row) throw new Error("Failed to create lead scout scan run"); + return row; + }, + + async getLeadScoutScanRun(id) { + const row = await db.query.leadScoutScanRun.findFirst({ + where: eq(schema.leadScoutScanRun.id, id), + }); + return row ?? null; + }, + + async listLeadScoutScanRunsByCampaign(campaignId) { + return db + .select() + .from(schema.leadScoutScanRun) + .where(eq(schema.leadScoutScanRun.campaignId, campaignId)) + .orderBy(desc(schema.leadScoutScanRun.startedAt)); + }, + + async updateLeadScoutScanRun(id, patch) { + const [row] = await db + .update(schema.leadScoutScanRun) + .set(patch) + .where(eq(schema.leadScoutScanRun.id, id)) + .returning(); + if (!row) throw new Error(`Lead scout scan run not found: ${id}`); + return row; + }, + + async createLeadScoutLead({ + workspaceId, + campaignId, + scanRunId, + sourceProvider, + sourceId, + businessName, + category, + niche, + formattedAddress, + postalCode, + city, + phone, + email, + website, + websiteStatus, + confidence, + evidenceSummary, + missingReason, + }) { + const [row] = await db + .insert(schema.leadScoutLead) + .values({ + id: randomUUID(), + workspaceId, + campaignId, + scanRunId: scanRunId ?? null, + sourceProvider, + sourceId, + businessName, + category: category ?? null, + niche: niche ?? null, + formattedAddress: formattedAddress ?? null, + postalCode: postalCode ?? null, + city: city ?? null, + phone: phone ?? null, + email: email ?? null, + website: website ?? null, + ...(websiteStatus !== undefined ? { websiteStatus } : {}), + ...(confidence !== undefined ? { confidence } : {}), + evidenceSummary: evidenceSummary ?? null, + missingReason: missingReason ?? null, + }) + .returning(); + if (!row) throw new Error("Failed to create lead scout lead"); + return row; + }, + + async getLeadScoutLeadBySource(campaignId, sourceProvider, sourceId) { + const row = await db.query.leadScoutLead.findFirst({ + where: and( + eq(schema.leadScoutLead.campaignId, campaignId), + eq(schema.leadScoutLead.sourceProvider, sourceProvider), + eq(schema.leadScoutLead.sourceId, sourceId), + ), + }); + return row ?? null; + }, + + async listLeadScoutLeadsByCampaign(campaignId) { + return db + .select() + .from(schema.leadScoutLead) + .where(eq(schema.leadScoutLead.campaignId, campaignId)) + .orderBy(desc(schema.leadScoutLead.createdAt)); + }, + + async getLeadScoutLead(id) { + const row = await db.query.leadScoutLead.findFirst({ + where: eq(schema.leadScoutLead.id, id), + }); + return row ?? null; + }, + + async updateLeadScoutLead(id, patch) { + const [row] = await db + .update(schema.leadScoutLead) + .set({ ...patch, updatedAt: new Date() }) + .where(eq(schema.leadScoutLead.id, id)) + .returning(); + if (!row) throw new Error(`Lead scout lead not found: ${id}`); + return row; + }, + + async claimLeadScoutLeadStatus({ id, fromStatus, toStatus }) { + const [row] = await db + .update(schema.leadScoutLead) + .set({ status: toStatus, updatedAt: new Date() }) + .where(and(eq(schema.leadScoutLead.id, id), eq(schema.leadScoutLead.status, fromStatus))) + .returning(); + return row ?? null; + }, + + async createLeadScoutPrototype({ workspaceId, leadId, taskId }) { + const [row] = await db + .insert(schema.leadScoutPrototype) + .values({ id: randomUUID(), workspaceId, leadId, taskId: taskId ?? null }) + .returning(); + if (!row) throw new Error("Failed to create lead scout prototype"); + return row; + }, + + async getLeadScoutPrototype(id) { + const row = await db.query.leadScoutPrototype.findFirst({ + where: eq(schema.leadScoutPrototype.id, id), + }); + return row ?? null; + }, + + async listLeadScoutPrototypesByLead(leadId) { + return db + .select() + .from(schema.leadScoutPrototype) + .where(eq(schema.leadScoutPrototype.leadId, leadId)) + .orderBy(desc(schema.leadScoutPrototype.createdAt)); + }, + + async updateLeadScoutPrototype(id, patch) { + const [row] = await db + .update(schema.leadScoutPrototype) + .set({ ...patch, updatedAt: new Date() }) + .where(eq(schema.leadScoutPrototype.id, id)) + .returning(); + if (!row) throw new Error(`Lead scout prototype not found: ${id}`); + return row; + }, + + async createLeadScoutOutreachDraft({ workspaceId, leadId, prototypeId, taskId }) { + const [row] = await db + .insert(schema.leadScoutOutreachDraft) + .values({ + id: randomUUID(), + workspaceId, + leadId, + prototypeId: prototypeId ?? null, + taskId: taskId ?? null, + }) + .returning(); + if (!row) throw new Error("Failed to create lead scout outreach draft"); + return row; + }, + + async getLeadScoutOutreachDraft(id) { + const row = await db.query.leadScoutOutreachDraft.findFirst({ + where: eq(schema.leadScoutOutreachDraft.id, id), + }); + return row ?? null; + }, + + async listLeadScoutOutreachDraftsByLead(leadId) { + return db + .select() + .from(schema.leadScoutOutreachDraft) + .where(eq(schema.leadScoutOutreachDraft.leadId, leadId)) + .orderBy(desc(schema.leadScoutOutreachDraft.createdAt)); + }, + + async updateLeadScoutOutreachDraft(id, patch) { + const [row] = await db + .update(schema.leadScoutOutreachDraft) + .set({ ...patch, updatedAt: new Date() }) + .where(eq(schema.leadScoutOutreachDraft.id, id)) + .returning(); + if (!row) throw new Error(`Lead scout outreach draft not found: ${id}`); + return row; + }, + + async claimLeadScoutOutreachDraftStatus({ id, fromStatus, toStatus }) { + const [row] = await db + .update(schema.leadScoutOutreachDraft) + .set({ status: toStatus, updatedAt: new Date() }) + .where( + and( + eq(schema.leadScoutOutreachDraft.id, id), + eq(schema.leadScoutOutreachDraft.status, fromStatus), + ), + ) + .returning(); + return row ?? null; + }, + + async createLeadScoutSuppression({ workspaceId, email, domain, reason }) { + const [row] = await db + .insert(schema.leadScoutSuppression) + .values({ id: randomUUID(), workspaceId, email: email ?? null, domain: domain ?? null, reason }) + .returning(); + if (!row) throw new Error("Failed to create lead scout suppression"); + return row; + }, + + async listLeadScoutSuppressionsByWorkspace(workspaceId) { + return db + .select() + .from(schema.leadScoutSuppression) + .where(eq(schema.leadScoutSuppression.workspaceId, workspaceId)) + .orderBy(desc(schema.leadScoutSuppression.createdAt)); + }, + + async getLeadScoutSuppressionMatch(workspaceId, email, domain) { + const matches: ReturnType[] = []; + if (email) matches.push(eq(schema.leadScoutSuppression.email, email)); + if (domain) matches.push(eq(schema.leadScoutSuppression.domain, domain)); + if (matches.length === 0) return null; + const row = await db.query.leadScoutSuppression.findFirst({ + where: and(eq(schema.leadScoutSuppression.workspaceId, workspaceId), or(...matches)), + }); + return row ?? null; + }, + + async getLeadScoutEmailSettings(workspaceId) { + const row = await db.query.leadScoutEmailSettings.findFirst({ + where: eq(schema.leadScoutEmailSettings.workspaceId, workspaceId), + }); + return row + ? { ...row, credentials: decryptJsonNullable>(row.credentials) } + : null; + }, + + async upsertLeadScoutEmailSettings({ + workspaceId, + provider, + fromName, + fromEmail, + replyTo, + credentials, + dailySendLimit, + perCampaignSendLimit, + dryRunMode, + legalFooter, + unsubscribeText, + }) { + const existing = await db.query.leadScoutEmailSettings.findFirst({ + where: eq(schema.leadScoutEmailSettings.workspaceId, workspaceId), + }); + + if (existing) { + const [row] = await db + .update(schema.leadScoutEmailSettings) + .set({ + ...(provider !== undefined ? { provider } : {}), + fromName, + fromEmail, + ...(replyTo !== undefined ? { replyTo } : {}), + ...(credentials !== undefined ? { credentials: encryptJsonNullable(credentials) } : {}), + ...(dailySendLimit !== undefined ? { dailySendLimit } : {}), + ...(perCampaignSendLimit !== undefined ? { perCampaignSendLimit } : {}), + ...(dryRunMode !== undefined ? { dryRunMode } : {}), + ...(legalFooter !== undefined ? { legalFooter } : {}), + ...(unsubscribeText !== undefined ? { unsubscribeText } : {}), + updatedAt: new Date(), + }) + .where(eq(schema.leadScoutEmailSettings.id, existing.id)) + .returning(); + if (!row) throw new Error(`Lead scout email settings not found: ${existing.id}`); + return { ...row, credentials: decryptJsonNullable>(row.credentials) }; + } + + const [row] = await db + .insert(schema.leadScoutEmailSettings) + .values({ + id: randomUUID(), + workspaceId, + ...(provider !== undefined ? { provider } : {}), + fromName, + fromEmail, + replyTo: replyTo ?? null, + credentials: encryptJsonNullable(credentials ?? null), + ...(dailySendLimit !== undefined ? { dailySendLimit } : {}), + ...(perCampaignSendLimit !== undefined ? { perCampaignSendLimit } : {}), + ...(dryRunMode !== undefined ? { dryRunMode } : {}), + legalFooter: legalFooter ?? null, + ...(unsubscribeText !== undefined ? { unsubscribeText } : {}), + }) + .returning(); + if (!row) throw new Error("Failed to create lead scout email settings"); + return { ...row, credentials: decryptJsonNullable>(row.credentials) }; + }, + + async incrementLeadScoutEmailSendCount(workspaceId, today) { + const existing = await db.query.leadScoutEmailSettings.findFirst({ + where: eq(schema.leadScoutEmailSettings.workspaceId, workspaceId), + }); + if (!existing) { + throw new Error(`Lead scout email settings not configured for workspace: ${workspaceId}`); + } + const nextCount = existing.sendCountDate === today ? existing.sendCountToday + 1 : 1; + const [row] = await db + .update(schema.leadScoutEmailSettings) + .set({ sendCountToday: nextCount, sendCountDate: today, updatedAt: new Date() }) + .where(eq(schema.leadScoutEmailSettings.id, existing.id)) + .returning(); + if (!row) throw new Error(`Lead scout email settings not found: ${existing.id}`); + return { ...row, credentials: decryptJsonNullable>(row.credentials) }; + }, }; } diff --git a/packages/db/src/repo/sqlite.repo.ts b/packages/db/src/repo/sqlite.repo.ts index 38f8c2a..42dddf3 100644 --- a/packages/db/src/repo/sqlite.repo.ts +++ b/packages/db/src/repo/sqlite.repo.ts @@ -2772,5 +2772,544 @@ export function createSqliteRepository(filePath: string): DbRepository { if (!row) throw new Error(`SEO blog post not found: ${id}`); return row; }, + + async createLeadScoutCampaign({ + workspaceId, + extensionId, + name, + postalCode, + country, + radiusKm, + niches, + maxResultsPerRun, + provider, + minConfidence, + outreachMode, + }) { + const now = new Date(); + const row = db + .insert(schema.leadScoutCampaign) + .values({ + id: randomUUID(), + workspaceId, + extensionId, + name, + postalCode, + ...(country !== undefined ? { country } : {}), + ...(radiusKm !== undefined ? { radiusKm } : {}), + ...(niches !== undefined ? { niches } : {}), + ...(maxResultsPerRun !== undefined ? { maxResultsPerRun } : {}), + provider, + ...(minConfidence !== undefined ? { minConfidence } : {}), + ...(outreachMode !== undefined ? { outreachMode } : {}), + createdAt: now, + updatedAt: now, + }) + .returning() + .get(); + return row; + }, + + async listLeadScoutCampaignsByWorkspace(workspaceId) { + return db + .select() + .from(schema.leadScoutCampaign) + .where(eq(schema.leadScoutCampaign.workspaceId, workspaceId)) + .orderBy(desc(schema.leadScoutCampaign.createdAt)) + .all(); + }, + + async getLeadScoutCampaign(id) { + const row = db + .select() + .from(schema.leadScoutCampaign) + .where(eq(schema.leadScoutCampaign.id, id)) + .get(); + return row ?? null; + }, + + async updateLeadScoutCampaign(id, patch) { + const row = db + .update(schema.leadScoutCampaign) + .set({ ...patch, updatedAt: new Date() }) + .where(eq(schema.leadScoutCampaign.id, id)) + .returning() + .get(); + if (!row) throw new Error(`Lead scout campaign not found: ${id}`); + return row; + }, + + async deleteLeadScoutCampaign(id) { + db.delete(schema.leadScoutCampaign).where(eq(schema.leadScoutCampaign.id, id)).run(); + }, + + async listDueLeadScoutCampaigns(now) { + return db + .select() + .from(schema.leadScoutCampaign) + .where( + and( + eq(schema.leadScoutCampaign.scheduleEnabled, true), + isNotNull(schema.leadScoutCampaign.nextScanAt), + lte(schema.leadScoutCampaign.nextScanAt, now), + ), + ) + .all(); + }, + + async upsertLeadScoutSourceConfig({ workspaceId, provider, config, apiKey, enabled }) { + const existing = db + .select() + .from(schema.leadScoutSourceConfig) + .where( + and( + eq(schema.leadScoutSourceConfig.workspaceId, workspaceId), + eq(schema.leadScoutSourceConfig.provider, provider), + ), + ) + .get(); + + if (existing) { + const row = db + .update(schema.leadScoutSourceConfig) + .set({ + ...(config !== undefined ? { config } : {}), + ...(apiKey !== undefined ? { apiKey: encryptNullable(apiKey) } : {}), + ...(enabled !== undefined ? { enabled } : {}), + updatedAt: new Date(), + }) + .where(eq(schema.leadScoutSourceConfig.id, existing.id)) + .returning() + .get(); + if (!row) throw new Error(`Lead scout source config not found: ${existing.id}`); + return { ...row, apiKey: decryptNullable(row.apiKey) }; + } + + const now = new Date(); + const row = db + .insert(schema.leadScoutSourceConfig) + .values({ + id: randomUUID(), + workspaceId, + provider, + config: config ?? {}, + apiKey: encryptNullable(apiKey ?? null), + enabled: enabled ?? true, + createdAt: now, + updatedAt: now, + }) + .returning() + .get(); + return { ...row, apiKey: decryptNullable(row.apiKey) }; + }, + + async listLeadScoutSourceConfigsByWorkspace(workspaceId) { + const rows = db + .select() + .from(schema.leadScoutSourceConfig) + .where(eq(schema.leadScoutSourceConfig.workspaceId, workspaceId)) + .all(); + return rows.map((row) => ({ ...row, apiKey: decryptNullable(row.apiKey) })); + }, + + async getLeadScoutSourceConfig(workspaceId, provider) { + const row = db + .select() + .from(schema.leadScoutSourceConfig) + .where( + and( + eq(schema.leadScoutSourceConfig.workspaceId, workspaceId), + eq(schema.leadScoutSourceConfig.provider, provider), + ), + ) + .get(); + return row ? { ...row, apiKey: decryptNullable(row.apiKey) } : null; + }, + + async createLeadScoutScanRun({ campaignId, workspaceId, provider }) { + const row = db + .insert(schema.leadScoutScanRun) + .values({ + id: randomUUID(), + campaignId, + workspaceId, + provider, + startedAt: new Date(), + }) + .returning() + .get(); + return row; + }, + + async getLeadScoutScanRun(id) { + const row = db + .select() + .from(schema.leadScoutScanRun) + .where(eq(schema.leadScoutScanRun.id, id)) + .get(); + return row ?? null; + }, + + async listLeadScoutScanRunsByCampaign(campaignId) { + return db + .select() + .from(schema.leadScoutScanRun) + .where(eq(schema.leadScoutScanRun.campaignId, campaignId)) + .orderBy(desc(schema.leadScoutScanRun.startedAt)) + .all(); + }, + + async updateLeadScoutScanRun(id, patch) { + const row = db + .update(schema.leadScoutScanRun) + .set(patch) + .where(eq(schema.leadScoutScanRun.id, id)) + .returning() + .get(); + if (!row) throw new Error(`Lead scout scan run not found: ${id}`); + return row; + }, + + async createLeadScoutLead({ + workspaceId, + campaignId, + scanRunId, + sourceProvider, + sourceId, + businessName, + category, + niche, + formattedAddress, + postalCode, + city, + phone, + email, + website, + websiteStatus, + confidence, + evidenceSummary, + missingReason, + }) { + const now = new Date(); + const row = db + .insert(schema.leadScoutLead) + .values({ + id: randomUUID(), + workspaceId, + campaignId, + scanRunId: scanRunId ?? null, + sourceProvider, + sourceId, + businessName, + category: category ?? null, + niche: niche ?? null, + formattedAddress: formattedAddress ?? null, + postalCode: postalCode ?? null, + city: city ?? null, + phone: phone ?? null, + email: email ?? null, + website: website ?? null, + ...(websiteStatus !== undefined ? { websiteStatus } : {}), + ...(confidence !== undefined ? { confidence } : {}), + evidenceSummary: evidenceSummary ?? null, + missingReason: missingReason ?? null, + createdAt: now, + updatedAt: now, + }) + .returning() + .get(); + return row; + }, + + async getLeadScoutLeadBySource(campaignId, sourceProvider, sourceId) { + const row = db + .select() + .from(schema.leadScoutLead) + .where( + and( + eq(schema.leadScoutLead.campaignId, campaignId), + eq(schema.leadScoutLead.sourceProvider, sourceProvider), + eq(schema.leadScoutLead.sourceId, sourceId), + ), + ) + .get(); + return row ?? null; + }, + + async listLeadScoutLeadsByCampaign(campaignId) { + return db + .select() + .from(schema.leadScoutLead) + .where(eq(schema.leadScoutLead.campaignId, campaignId)) + .orderBy(desc(schema.leadScoutLead.createdAt)) + .all(); + }, + + async getLeadScoutLead(id) { + const row = db.select().from(schema.leadScoutLead).where(eq(schema.leadScoutLead.id, id)).get(); + return row ?? null; + }, + + async updateLeadScoutLead(id, patch) { + const row = db + .update(schema.leadScoutLead) + .set({ ...patch, updatedAt: new Date() }) + .where(eq(schema.leadScoutLead.id, id)) + .returning() + .get(); + if (!row) throw new Error(`Lead scout lead not found: ${id}`); + return row; + }, + + async claimLeadScoutLeadStatus({ id, fromStatus, toStatus }) { + const row = db + .update(schema.leadScoutLead) + .set({ status: toStatus, updatedAt: new Date() }) + .where(and(eq(schema.leadScoutLead.id, id), eq(schema.leadScoutLead.status, fromStatus))) + .returning() + .get(); + return row ?? null; + }, + + async createLeadScoutPrototype({ workspaceId, leadId, taskId }) { + const now = new Date(); + const row = db + .insert(schema.leadScoutPrototype) + .values({ + id: randomUUID(), + workspaceId, + leadId, + taskId: taskId ?? null, + createdAt: now, + updatedAt: now, + }) + .returning() + .get(); + return row; + }, + + async getLeadScoutPrototype(id) { + const row = db + .select() + .from(schema.leadScoutPrototype) + .where(eq(schema.leadScoutPrototype.id, id)) + .get(); + return row ?? null; + }, + + async listLeadScoutPrototypesByLead(leadId) { + return db + .select() + .from(schema.leadScoutPrototype) + .where(eq(schema.leadScoutPrototype.leadId, leadId)) + .orderBy(desc(schema.leadScoutPrototype.createdAt)) + .all(); + }, + + async updateLeadScoutPrototype(id, patch) { + const row = db + .update(schema.leadScoutPrototype) + .set({ ...patch, updatedAt: new Date() }) + .where(eq(schema.leadScoutPrototype.id, id)) + .returning() + .get(); + if (!row) throw new Error(`Lead scout prototype not found: ${id}`); + return row; + }, + + async createLeadScoutOutreachDraft({ workspaceId, leadId, prototypeId, taskId }) { + const now = new Date(); + const row = db + .insert(schema.leadScoutOutreachDraft) + .values({ + id: randomUUID(), + workspaceId, + leadId, + prototypeId: prototypeId ?? null, + taskId: taskId ?? null, + createdAt: now, + updatedAt: now, + }) + .returning() + .get(); + return row; + }, + + async getLeadScoutOutreachDraft(id) { + const row = db + .select() + .from(schema.leadScoutOutreachDraft) + .where(eq(schema.leadScoutOutreachDraft.id, id)) + .get(); + return row ?? null; + }, + + async listLeadScoutOutreachDraftsByLead(leadId) { + return db + .select() + .from(schema.leadScoutOutreachDraft) + .where(eq(schema.leadScoutOutreachDraft.leadId, leadId)) + .orderBy(desc(schema.leadScoutOutreachDraft.createdAt)) + .all(); + }, + + async updateLeadScoutOutreachDraft(id, patch) { + const row = db + .update(schema.leadScoutOutreachDraft) + .set({ ...patch, updatedAt: new Date() }) + .where(eq(schema.leadScoutOutreachDraft.id, id)) + .returning() + .get(); + if (!row) throw new Error(`Lead scout outreach draft not found: ${id}`); + return row; + }, + + async claimLeadScoutOutreachDraftStatus({ id, fromStatus, toStatus }) { + const row = db + .update(schema.leadScoutOutreachDraft) + .set({ status: toStatus, updatedAt: new Date() }) + .where( + and( + eq(schema.leadScoutOutreachDraft.id, id), + eq(schema.leadScoutOutreachDraft.status, fromStatus), + ), + ) + .returning() + .get(); + return row ?? null; + }, + + async createLeadScoutSuppression({ workspaceId, email, domain, reason }) { + const row = db + .insert(schema.leadScoutSuppression) + .values({ + id: randomUUID(), + workspaceId, + email: email ?? null, + domain: domain ?? null, + reason, + createdAt: new Date(), + }) + .returning() + .get(); + return row; + }, + + async listLeadScoutSuppressionsByWorkspace(workspaceId) { + return db + .select() + .from(schema.leadScoutSuppression) + .where(eq(schema.leadScoutSuppression.workspaceId, workspaceId)) + .orderBy(desc(schema.leadScoutSuppression.createdAt)) + .all(); + }, + + async getLeadScoutSuppressionMatch(workspaceId, email, domain) { + const matches: ReturnType[] = []; + if (email) matches.push(eq(schema.leadScoutSuppression.email, email)); + if (domain) matches.push(eq(schema.leadScoutSuppression.domain, domain)); + if (matches.length === 0) return null; + const row = db + .select() + .from(schema.leadScoutSuppression) + .where(and(eq(schema.leadScoutSuppression.workspaceId, workspaceId), or(...matches))) + .get(); + return row ?? null; + }, + + async getLeadScoutEmailSettings(workspaceId) { + const row = db + .select() + .from(schema.leadScoutEmailSettings) + .where(eq(schema.leadScoutEmailSettings.workspaceId, workspaceId)) + .get(); + return row ? { ...row, credentials: decryptJsonNullable>(row.credentials) } : null; + }, + + async upsertLeadScoutEmailSettings({ + workspaceId, + provider, + fromName, + fromEmail, + replyTo, + credentials, + dailySendLimit, + perCampaignSendLimit, + dryRunMode, + legalFooter, + unsubscribeText, + }) { + const existing = db + .select() + .from(schema.leadScoutEmailSettings) + .where(eq(schema.leadScoutEmailSettings.workspaceId, workspaceId)) + .get(); + + if (existing) { + const row = db + .update(schema.leadScoutEmailSettings) + .set({ + ...(provider !== undefined ? { provider } : {}), + fromName, + fromEmail, + ...(replyTo !== undefined ? { replyTo } : {}), + ...(credentials !== undefined ? { credentials: encryptJsonNullable(credentials) } : {}), + ...(dailySendLimit !== undefined ? { dailySendLimit } : {}), + ...(perCampaignSendLimit !== undefined ? { perCampaignSendLimit } : {}), + ...(dryRunMode !== undefined ? { dryRunMode } : {}), + ...(legalFooter !== undefined ? { legalFooter } : {}), + ...(unsubscribeText !== undefined ? { unsubscribeText } : {}), + updatedAt: new Date(), + }) + .where(eq(schema.leadScoutEmailSettings.id, existing.id)) + .returning() + .get(); + if (!row) throw new Error(`Lead scout email settings not found: ${existing.id}`); + return { ...row, credentials: decryptJsonNullable>(row.credentials) }; + } + + const now = new Date(); + const row = db + .insert(schema.leadScoutEmailSettings) + .values({ + id: randomUUID(), + workspaceId, + ...(provider !== undefined ? { provider } : {}), + fromName, + fromEmail, + replyTo: replyTo ?? null, + credentials: encryptJsonNullable(credentials ?? null), + ...(dailySendLimit !== undefined ? { dailySendLimit } : {}), + ...(perCampaignSendLimit !== undefined ? { perCampaignSendLimit } : {}), + ...(dryRunMode !== undefined ? { dryRunMode } : {}), + legalFooter: legalFooter ?? null, + ...(unsubscribeText !== undefined ? { unsubscribeText } : {}), + createdAt: now, + updatedAt: now, + }) + .returning() + .get(); + return { ...row, credentials: decryptJsonNullable>(row.credentials) }; + }, + + async incrementLeadScoutEmailSendCount(workspaceId, today) { + const existing = db + .select() + .from(schema.leadScoutEmailSettings) + .where(eq(schema.leadScoutEmailSettings.workspaceId, workspaceId)) + .get(); + if (!existing) { + throw new Error(`Lead scout email settings not configured for workspace: ${workspaceId}`); + } + const nextCount = existing.sendCountDate === today ? existing.sendCountToday + 1 : 1; + const row = db + .update(schema.leadScoutEmailSettings) + .set({ sendCountToday: nextCount, sendCountDate: today, updatedAt: new Date() }) + .where(eq(schema.leadScoutEmailSettings.id, existing.id)) + .returning() + .get(); + if (!row) throw new Error(`Lead scout email settings not found: ${existing.id}`); + return { ...row, credentials: decryptJsonNullable>(row.credentials) }; + }, }; } diff --git a/packages/db/src/repo/types.ts b/packages/db/src/repo/types.ts index 4771d1f..ab9ed7a 100644 --- a/packages/db/src/repo/types.ts +++ b/packages/db/src/repo/types.ts @@ -78,6 +78,35 @@ export type SeoAnalysisRunStatus = "running" | "completed" | "failed"; export type SeoFindingCategory = "seo" | "geo" | "aeo"; export type SeoFindingSeverity = "info" | "warning" | "critical"; export type SeoBlogPostStatus = "suggested" | "generating" | "written" | "failed"; + +export type LeadScoutProvider = "manual_csv" | "google_places_api" | "osm_overpass" | "custom_api"; +export type LeadScoutOutreachMode = "draft_only" | "review_and_send"; +export type LeadScoutWebsiteStatus = + | "unknown" + | "has_website" + | "missing_website" + | "invalid_website"; +export type LeadScoutLeadStatus = + | "new" + | "reviewed" + | "prototype_requested" + | "prototype_ready" + | "email_drafted" + | "approved_to_send" + | "sending" + | "sent" + | "rejected" + | "suppressed"; +export type LeadScoutScanRunStatus = "running" | "completed" | "failed"; +export type LeadScoutPrototypeStatus = "pending" | "ready" | "failed"; +export type LeadScoutDraftStatus = + | "draft" + | "approved" + | "rejected" + | "sending" + | "sent" + | "failed"; +export type LeadScoutEmailProvider = "smtp" | "resend" | "mailgun" | "custom"; export type AgentRunStatus = | "pending" | "running" @@ -855,6 +884,158 @@ export interface SeoBlogPostRecord { updatedAt: Date; } +export interface LeadScoutCampaignRecord { + id: string; + workspaceId: string; + extensionId: string; + name: string; + postalCode: string; + country: string; + radiusKm: number; + niches: string[]; + maxResultsPerRun: number; + provider: LeadScoutProvider; + minConfidence: number; + outreachMode: LeadScoutOutreachMode; + scheduleEnabled: boolean; + scheduleCronExpression: string | null; + nextScanAt: Date | null; + lastScanAt: Date | null; + autoGeneratePrototype: boolean; + autoDraftEmail: boolean; + autoSendAfterApproval: boolean; + requireApprovalBeforePrototype: boolean; + requireApprovalBeforeEmailSend: boolean; + prototypeAgentId: string | null; + createdAt: Date; + updatedAt: Date; +} + +export interface LeadScoutSourceConfigRecord { + id: string; + workspaceId: string; + provider: LeadScoutProvider; + config: Record; + /** Decrypted at the repo layer (see modelInstallation.apiKey) — callers + * must redact this before it ever reaches the browser. */ + apiKey: string | null; + enabled: boolean; + createdAt: Date; + updatedAt: Date; +} + +export interface LeadScoutScanRunRecord { + id: string; + campaignId: string; + workspaceId: string; + provider: LeadScoutProvider; + status: LeadScoutScanRunStatus; + resultCount: number; + newLeadCount: number; + missingWebsiteCount: number; + summary: string | null; + errorMessage: string | null; + startedAt: Date; + completedAt: Date | null; +} + +export interface LeadScoutLeadRecord { + id: string; + workspaceId: string; + campaignId: string; + scanRunId: string | null; + sourceProvider: LeadScoutProvider; + sourceId: string; + businessName: string; + category: string | null; + niche: string | null; + formattedAddress: string | null; + postalCode: string | null; + city: string | null; + phone: string | null; + email: string | null; + website: string | null; + websiteStatus: LeadScoutWebsiteStatus; + confidence: number; + evidenceSummary: string | null; + missingReason: string | null; + status: LeadScoutLeadStatus; + createdAt: Date; + updatedAt: Date; +} + +export interface LeadScoutPrototypeRecord { + id: string; + workspaceId: string; + leadId: string; + taskId: string | null; + status: LeadScoutPrototypeStatus; + concept: string | null; + heroCopy: string | null; + sections: string[]; + callToAction: string | null; + styleDirection: string | null; + artifactMarkdown: string | null; + approved: boolean; + errorMessage: string | null; + createdAt: Date; + updatedAt: Date; +} + +export interface LeadScoutOutreachDraftRecord { + id: string; + workspaceId: string; + leadId: string; + prototypeId: string | null; + taskId: string | null; + subject: string | null; + bodyText: string | null; + bodyHtml: string | null; + status: LeadScoutDraftStatus; + errorMessage: string | null; + approvedAt: Date | null; + sentAt: Date | null; + createdAt: Date; + updatedAt: Date; +} + +export interface LeadScoutSuppressionRecord { + id: string; + workspaceId: string; + email: string | null; + domain: string | null; + reason: string; + createdAt: Date; +} + +/** Provider-shaped secret bag: SMTP holds host/port/username/password; + * resend/mailgun hold apiKey; custom holds whatever a compliant adapter + * needs (e.g. webhookUrl/secret). Kept as a flat string map (like + * mcpServer.env) rather than a per-provider type since only the email + * module (lead-scout-email.ts) ever reads specific keys out of it. */ +export type LeadScoutEmailCredentials = Record; + +export interface LeadScoutEmailSettingsRecord { + id: string; + workspaceId: string; + provider: LeadScoutEmailProvider; + fromName: string; + fromEmail: string; + replyTo: string | null; + /** Decrypted at the repo layer (see mcpServer.env) — callers must redact + * this before it ever reaches the browser. */ + credentials: LeadScoutEmailCredentials | null; + dailySendLimit: number; + perCampaignSendLimit: number; + dryRunMode: boolean; + legalFooter: string | null; + unsubscribeText: string; + sendCountToday: number; + sendCountDate: string | null; + createdAt: Date; + updatedAt: Date; +} + /** * A dialect-agnostic data access interface. `packages/db` ships one * implementation per SQL dialect (pg.repo.ts, sqlite.repo.ts) so the rest of @@ -1669,4 +1850,239 @@ export interface DbRepository { errorMessage?: string | null; }, ): Promise; + + createLeadScoutCampaign(input: { + workspaceId: string; + extensionId: string; + name: string; + postalCode: string; + country?: string; + radiusKm?: number; + niches?: string[]; + maxResultsPerRun?: number; + provider: LeadScoutProvider; + minConfidence?: number; + outreachMode?: LeadScoutOutreachMode; + }): Promise; + listLeadScoutCampaignsByWorkspace(workspaceId: string): Promise; + getLeadScoutCampaign(id: string): Promise; + updateLeadScoutCampaign( + id: string, + patch: Partial<{ + name: string; + postalCode: string; + country: string; + radiusKm: number; + niches: string[]; + maxResultsPerRun: number; + provider: LeadScoutProvider; + minConfidence: number; + outreachMode: LeadScoutOutreachMode; + scheduleEnabled: boolean; + scheduleCronExpression: string | null; + nextScanAt: Date | null; + lastScanAt: Date | null; + autoGeneratePrototype: boolean; + autoDraftEmail: boolean; + autoSendAfterApproval: boolean; + requireApprovalBeforePrototype: boolean; + requireApprovalBeforeEmailSend: boolean; + prototypeAgentId: string | null; + }>, + ): Promise; + deleteLeadScoutCampaign(id: string): Promise; + /** Enabled recurring scans whose nextScanAt has passed — polled by the + * scheduler's tick alongside due automations/SEO projects. */ + listDueLeadScoutCampaigns(now: Date): Promise; + + /** One row per workspace+provider (unique constraint) — `upsert` since a + * user editing provider settings should update in place, not accumulate + * duplicate rows. */ + upsertLeadScoutSourceConfig(input: { + workspaceId: string; + provider: LeadScoutProvider; + config?: Record; + apiKey?: string | null; + enabled?: boolean; + }): Promise; + listLeadScoutSourceConfigsByWorkspace( + workspaceId: string, + ): Promise; + getLeadScoutSourceConfig( + workspaceId: string, + provider: LeadScoutProvider, + ): Promise; + + createLeadScoutScanRun(input: { + campaignId: string; + workspaceId: string; + provider: LeadScoutProvider; + }): Promise; + getLeadScoutScanRun(id: string): Promise; + /** Most recent first. */ + listLeadScoutScanRunsByCampaign(campaignId: string): Promise; + updateLeadScoutScanRun( + id: string, + patch: Partial<{ + status: LeadScoutScanRunStatus; + resultCount: number; + newLeadCount: number; + missingWebsiteCount: number; + summary: string | null; + errorMessage: string | null; + completedAt: Date | null; + }>, + ): Promise; + + createLeadScoutLead(input: { + workspaceId: string; + campaignId: string; + scanRunId?: string | null; + sourceProvider: LeadScoutProvider; + sourceId: string; + businessName: string; + category?: string | null; + niche?: string | null; + formattedAddress?: string | null; + postalCode?: string | null; + city?: string | null; + phone?: string | null; + email?: string | null; + website?: string | null; + websiteStatus?: LeadScoutWebsiteStatus; + confidence?: number; + evidenceSummary?: string | null; + missingReason?: string | null; + }): Promise; + /** Looks up a previously-ingested lead by its provider identity (unique + * per campaign+provider+sourceId) so a re-run scan reconciles instead of + * inserting a duplicate row for the same business. */ + getLeadScoutLeadBySource( + campaignId: string, + sourceProvider: LeadScoutProvider, + sourceId: string, + ): Promise; + listLeadScoutLeadsByCampaign(campaignId: string): Promise; + getLeadScoutLead(id: string): Promise; + updateLeadScoutLead( + id: string, + patch: Partial<{ + scanRunId: string | null; + category: string | null; + niche: string | null; + formattedAddress: string | null; + postalCode: string | null; + city: string | null; + phone: string | null; + email: string | null; + website: string | null; + websiteStatus: LeadScoutWebsiteStatus; + confidence: number; + evidenceSummary: string | null; + missingReason: string | null; + status: LeadScoutLeadStatus; + }>, + ): Promise; + /** Atomic compare-and-swap on lead.status — the sole guard against a lead + * being approved/sent twice from a concurrent request (same pattern as + * claimApprovalRequest). Returns null (not an error) if `fromStatus` no + * longer matches, so callers can distinguish "someone else already moved + * this" from a real failure. */ + claimLeadScoutLeadStatus(input: { + id: string; + fromStatus: LeadScoutLeadStatus; + toStatus: LeadScoutLeadStatus; + }): Promise; + + createLeadScoutPrototype(input: { + workspaceId: string; + leadId: string; + taskId?: string | null; + }): Promise; + getLeadScoutPrototype(id: string): Promise; + /** Most recent first. */ + listLeadScoutPrototypesByLead(leadId: string): Promise; + updateLeadScoutPrototype( + id: string, + patch: Partial<{ + status: LeadScoutPrototypeStatus; + concept: string | null; + heroCopy: string | null; + sections: string[]; + callToAction: string | null; + styleDirection: string | null; + artifactMarkdown: string | null; + approved: boolean; + errorMessage: string | null; + }>, + ): Promise; + + createLeadScoutOutreachDraft(input: { + workspaceId: string; + leadId: string; + prototypeId?: string | null; + taskId?: string | null; + }): Promise; + getLeadScoutOutreachDraft(id: string): Promise; + /** Most recent first. */ + listLeadScoutOutreachDraftsByLead(leadId: string): Promise; + updateLeadScoutOutreachDraft( + id: string, + patch: Partial<{ + subject: string | null; + bodyText: string | null; + bodyHtml: string | null; + status: LeadScoutDraftStatus; + errorMessage: string | null; + approvedAt: Date | null; + sentAt: Date | null; + }>, + ): Promise; + /** Atomic compare-and-swap on outreach_draft.status — the actual guard + * against sending the same draft twice from a race (approve-then-send and + * a duplicate click, or two concurrent send workers). Returns null if + * `fromStatus` no longer matches. */ + claimLeadScoutOutreachDraftStatus(input: { + id: string; + fromStatus: LeadScoutDraftStatus; + toStatus: LeadScoutDraftStatus; + }): Promise; + + createLeadScoutSuppression(input: { + workspaceId: string; + email?: string | null; + domain?: string | null; + reason: string; + }): Promise; + listLeadScoutSuppressionsByWorkspace(workspaceId: string): Promise; + /** Looks up a matching suppression row by exact email or domain — checked + * before every send (per-domain/per-email opt-out enforcement). */ + getLeadScoutSuppressionMatch( + workspaceId: string, + email: string | null, + domain: string | null, + ): Promise; + + getLeadScoutEmailSettings(workspaceId: string): Promise; + /** Creates the workspace's one settings row if it doesn't exist yet, + * otherwise updates it in place (unique constraint on workspaceId). */ + upsertLeadScoutEmailSettings(input: { + workspaceId: string; + provider?: LeadScoutEmailProvider; + fromName: string; + fromEmail: string; + replyTo?: string | null; + credentials?: LeadScoutEmailCredentials | null; + dailySendLimit?: number; + perCampaignSendLimit?: number; + dryRunMode?: boolean; + legalFooter?: string | null; + unsubscribeText?: string; + }): Promise; + /** Resets sendCountToday to 0 first if sendCountDate isn't `today`, then + * increments it by one — the daily send-limit counter. */ + incrementLeadScoutEmailSendCount( + workspaceId: string, + today: string, + ): Promise; } From 66676e50b6b08ec427a1ccb707285c24361ac2b8 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 3 Jul 2026 18:56:21 +0000 Subject: [PATCH 04/13] feat: add secure outreach email settings MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds a workspace-scoped email settings module (SMTP/Resend/Mailgun/ custom) with encrypted-at-rest credentials, a client-safe redacted shape, dry-run mode (on by default), a connection test, a test-send action, and a daily send-limit reservation used by the outreach flow. Nothing is dispatched — only settings storage and transport helpers. --- apps/server/package.json | 2 + apps/server/src/lead-scout-email.ts | 308 ++++++++++++++++++++++++++++ bun.lock | 20 +- 3 files changed, 323 insertions(+), 7 deletions(-) create mode 100644 apps/server/src/lead-scout-email.ts diff --git a/apps/server/package.json b/apps/server/package.json index 1c29a36..797dc91 100644 --- a/apps/server/package.json +++ b/apps/server/package.json @@ -24,12 +24,14 @@ "better-auth": "latest", "cron-parser": "latest", "hono": "latest", + "nodemailer": "^9.0.3", "playwright": "^1.61.1", "web-push": "^3.6.7", "zod": "latest" }, "devDependencies": { "@types/bun": "latest", + "@types/nodemailer": "^8.0.1", "@types/web-push": "^3.6.4", "typescript": "^6.0.3" } diff --git a/apps/server/src/lead-scout-email.ts b/apps/server/src/lead-scout-email.ts new file mode 100644 index 0000000..d74647f --- /dev/null +++ b/apps/server/src/lead-scout-email.ts @@ -0,0 +1,308 @@ +import nodemailer from "nodemailer"; +import type { LeadScoutEmailCredentials, LeadScoutEmailProvider, LeadScoutEmailSettingsRecord } from "@nyxel/db"; +import { getDb } from "@nyxel/db"; +import { logAudit } from "./audit"; + +/** + * Strips `credentials` before a settings row leaves the server — the client + * only needs to know a provider is configured, not its value (same + * convention as toClientSafeInstallation/toClientSafeMcpServer in + * trpc/router.ts). + */ +export function toClientSafeLeadScoutEmailSettings(settings: LeadScoutEmailSettingsRecord) { + const { credentials, ...rest } = settings; + return { + ...rest, + hasCredentials: credentials !== null && Object.keys(credentials).length > 0, + }; +} +export type LeadScoutEmailSettingsSummary = ReturnType; + +export async function getLeadScoutEmailSettings( + workspaceId: string, +): Promise { + return getDb().getLeadScoutEmailSettings(workspaceId); +} + +export async function upsertLeadScoutEmailSettings( + workspaceId: string, + input: { + provider?: LeadScoutEmailProvider; + fromName: string; + fromEmail: string; + replyTo?: string | null; + credentials?: LeadScoutEmailCredentials | null; + dailySendLimit?: number; + perCampaignSendLimit?: number; + dryRunMode?: boolean; + legalFooter?: string | null; + unsubscribeText?: string; + }, +): Promise { + const settings = await getDb().upsertLeadScoutEmailSettings({ workspaceId, ...input }); + await logAudit({ + workspaceId, + actor: "extension", + toolLabel: "local_lead_scout.email_settings.update", + input: { provider: input.provider, fromEmail: input.fromEmail, dryRunMode: input.dryRunMode }, + output: { hasCredentials: input.credentials !== undefined && input.credentials !== null }, + status: "success", + }); + return settings; +} + +function buildTransportForSmtp(credentials: LeadScoutEmailCredentials) { + if (!credentials.host) throw new Error("SMTP settings are missing a host."); + return nodemailer.createTransport({ + host: credentials.host, + port: Number(credentials.port ?? 587), + secure: credentials.secure === "true", + auth: credentials.username ? { user: credentials.username, pass: credentials.password } : undefined, + }); +} + +interface LeadScoutEmailMessage { + from: string; + replyTo?: string; + to: string; + subject: string; + text: string; + html?: string; +} + +async function sendViaSmtp( + credentials: LeadScoutEmailCredentials, + message: LeadScoutEmailMessage, +): Promise { + const transport = buildTransportForSmtp(credentials); + await transport.sendMail(message); +} + +async function sendViaResend( + credentials: LeadScoutEmailCredentials, + message: LeadScoutEmailMessage, +): Promise { + if (!credentials.apiKey) throw new Error("Resend settings are missing an API key."); + const res = await fetch("https://api.resend.com/emails", { + method: "POST", + headers: { Authorization: `Bearer ${credentials.apiKey}`, "Content-Type": "application/json" }, + body: JSON.stringify({ + from: message.from, + reply_to: message.replyTo, + to: [message.to], + subject: message.subject, + text: message.text, + html: message.html, + }), + }); + if (!res.ok) throw new Error(`Resend API error: HTTP ${res.status}`); +} + +async function sendViaMailgun( + credentials: LeadScoutEmailCredentials, + message: LeadScoutEmailMessage, +): Promise { + if (!credentials.apiKey || !credentials.domain) { + throw new Error("Mailgun settings are missing an API key or domain."); + } + const form = new URLSearchParams({ + from: message.from, + to: message.to, + subject: message.subject, + text: message.text, + }); + if (message.html) form.set("html", message.html); + if (message.replyTo) form.set("h:Reply-To", message.replyTo); + const res = await fetch(`https://api.mailgun.net/v3/${credentials.domain}/messages`, { + method: "POST", + headers: { + Authorization: `Basic ${Buffer.from(`api:${credentials.apiKey}`).toString("base64")}`, + "Content-Type": "application/x-www-form-urlencoded", + }, + body: form, + }); + if (!res.ok) throw new Error(`Mailgun API error: HTTP ${res.status}`); +} + +/** Generic compliant API adapter placeholder — posts to a user-configured + * webhook. Kept minimal per the extension's scope (no bespoke provider + * integrations beyond SMTP/Resend/Mailgun). */ +async function sendViaCustom( + credentials: LeadScoutEmailCredentials, + message: LeadScoutEmailMessage, +): Promise { + if (!credentials.webhookUrl) throw new Error("Custom email provider has no webhookUrl configured."); + const res = await fetch(credentials.webhookUrl, { + method: "POST", + headers: { + "Content-Type": "application/json", + ...(credentials.secret ? { Authorization: `Bearer ${credentials.secret}` } : {}), + }, + body: JSON.stringify(message), + }); + if (!res.ok) throw new Error(`Custom email provider error: HTTP ${res.status}`); +} + +async function dispatchByProvider( + provider: LeadScoutEmailProvider, + credentials: LeadScoutEmailCredentials, + message: LeadScoutEmailMessage, +): Promise { + if (provider === "smtp") return sendViaSmtp(credentials, message); + if (provider === "resend") return sendViaResend(credentials, message); + if (provider === "mailgun") return sendViaMailgun(credentials, message); + return sendViaCustom(credentials, message); +} + +/** + * Real outbound send through the workspace's configured provider — always + * gated by dryRunMode as a defense-in-depth check (the approval-gated + * outreach flow in lead-scout.ts is the primary gate; this is a second, + * cheaper backstop). Body content is deliberately excluded from what's + * logged to the audit trail (only recipient/subject), per the "never log + * full message bodies" requirement. + */ +export async function sendLeadScoutEmail( + workspaceId: string, + message: { to: string; subject: string; text: string; html?: string }, +): Promise<{ sent: boolean; dryRun: boolean }> { + const settings = await getDb().getLeadScoutEmailSettings(workspaceId); + if (!settings) throw new Error("Email settings aren't configured for this workspace yet."); + if (!settings.credentials && settings.provider !== "custom") { + throw new Error(`${settings.provider} isn't configured — add credentials in email settings first.`); + } + + const from = `${settings.fromName} <${settings.fromEmail}>`; + + if (settings.dryRunMode) { + await logAudit({ + workspaceId, + actor: "extension", + toolLabel: "local_lead_scout.send_email", + input: { to: message.to, subject: message.subject, dryRun: true }, + output: { sent: false, dryRun: true }, + status: "success", + }); + return { sent: false, dryRun: true }; + } + + try { + await dispatchByProvider(settings.provider, settings.credentials ?? {}, { + from, + replyTo: settings.replyTo ?? undefined, + to: message.to, + subject: message.subject, + text: message.text, + html: message.html, + }); + await logAudit({ + workspaceId, + actor: "extension", + toolLabel: "local_lead_scout.send_email", + input: { to: message.to, subject: message.subject, dryRun: false }, + output: { sent: true }, + status: "success", + }); + return { sent: true, dryRun: false }; + } catch (err) { + const errorMessage = err instanceof Error ? err.message : String(err); + await logAudit({ + workspaceId, + actor: "extension", + toolLabel: "local_lead_scout.send_email", + input: { to: message.to, subject: message.subject, dryRun: false }, + output: { sent: false, error: errorMessage }, + status: "error", + }); + throw err; + } +} + +export async function sendLeadScoutTestEmail( + workspaceId: string, + toEmail: string, +): Promise<{ sent: boolean; dryRun: boolean }> { + const settings = await getDb().getLeadScoutEmailSettings(workspaceId); + if (!settings) throw new Error("Email settings aren't configured for this workspace yet."); + const footer = [settings.unsubscribeText, settings.legalFooter].filter(Boolean).join("\n\n"); + return sendLeadScoutEmail(workspaceId, { + to: toEmail, + subject: "Local Lead Scout — test email", + text: `This is a test email from ${settings.fromName} confirming your Local Lead Scout email settings are working.\n\n${footer}`, + }); +} + +/** + * Lightweight connectivity check for the "test connection" action — verifies + * credentials work without sending an actual message where the provider's + * API supports that (SMTP handshake, Resend/Mailgun auth ping). `custom` has + * no generic way to test a webhook without invoking it, so it's reported as + * unsupported rather than guessed at. + */ +export async function testLeadScoutEmailConnection( + workspaceId: string, +): Promise<{ ok: boolean; message: string }> { + const settings = await getDb().getLeadScoutEmailSettings(workspaceId); + if (!settings) return { ok: false, message: "Email settings aren't configured for this workspace yet." }; + const credentials = settings.credentials ?? {}; + + try { + if (settings.provider === "smtp") { + await buildTransportForSmtp(credentials).verify(); + } else if (settings.provider === "resend") { + if (!credentials.apiKey) throw new Error("Missing API key."); + const res = await fetch("https://api.resend.com/domains", { + headers: { Authorization: `Bearer ${credentials.apiKey}` }, + }); + if (!res.ok) throw new Error(`HTTP ${res.status}`); + } else if (settings.provider === "mailgun") { + if (!credentials.apiKey) throw new Error("Missing API key."); + const res = await fetch("https://api.mailgun.net/v3/domains", { + headers: { + Authorization: `Basic ${Buffer.from(`api:${credentials.apiKey}`).toString("base64")}`, + }, + }); + if (!res.ok) throw new Error(`HTTP ${res.status}`); + } else { + return { ok: false, message: "Custom providers have no built-in connection test — send a test email instead." }; + } + await logAudit({ + workspaceId, + actor: "extension", + toolLabel: "local_lead_scout.email_settings.test_connection", + input: { provider: settings.provider }, + output: { ok: true }, + status: "success", + }); + return { ok: true, message: "Connection succeeded." }; + } catch (err) { + const message = err instanceof Error ? err.message : String(err); + await logAudit({ + workspaceId, + actor: "extension", + toolLabel: "local_lead_scout.email_settings.test_connection", + input: { provider: settings.provider }, + output: { ok: false, error: message }, + status: "error", + }); + return { ok: false, message }; + } +} + +/** + * Daily send-limit enforcement — called by the approval-gated send flow + * (lead-scout.ts) immediately before dispatching, so the counter and the + * actual send stay consistent. Throws rather than returning false so a + * blocked send can't be silently ignored by a caller that forgets to check + * a boolean. + */ +export async function reserveLeadScoutDailySendSlot(workspaceId: string): Promise { + const settings = await getDb().getLeadScoutEmailSettings(workspaceId); + if (!settings) throw new Error("Email settings aren't configured for this workspace yet."); + const today = new Date().toISOString().slice(0, 10); + const currentCount = settings.sendCountDate === today ? settings.sendCountToday : 0; + if (currentCount >= settings.dailySendLimit) { + throw new Error(`Daily send limit of ${settings.dailySendLimit} reached for this workspace.`); + } + await getDb().incrementLeadScoutEmailSendCount(workspaceId, today); +} diff --git a/bun.lock b/bun.lock index c1f2ea8..f61bed2 100644 --- a/bun.lock +++ b/bun.lock @@ -37,12 +37,14 @@ "better-auth": "latest", "cron-parser": "latest", "hono": "latest", + "nodemailer": "^9.0.3", "playwright": "^1.61.1", "web-push": "^3.6.7", "zod": "latest", }, "devDependencies": { "@types/bun": "latest", + "@types/nodemailer": "^8.0.1", "@types/web-push": "^3.6.4", "typescript": "^6.0.3", }, @@ -176,17 +178,17 @@ }, }, "packages": { - "@ai-sdk/anthropic": ["@ai-sdk/anthropic@4.0.6", "", { "dependencies": { "@ai-sdk/provider": "4.0.1", "@ai-sdk/provider-utils": "5.0.4" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-fiJFyYmqzppHg6+dkoiPg4zb3sW7vKz+/jsTsGgmuSaIgtcMusuf1Njgnx46XFCvyKnW/EJJNvo1f92nhNQoHg=="], + "@ai-sdk/anthropic": ["@ai-sdk/anthropic@4.0.7", "", { "dependencies": { "@ai-sdk/provider": "4.0.2", "@ai-sdk/provider-utils": "5.0.5" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-PaUAERndv7dI2IUlXM58RPmOx5MRl1jtC9ECkXl5TTi/08R3Ht8fY3d6X9ihKV3fk+JcnrEznR8gbhb1nguRZg=="], - "@ai-sdk/gateway": ["@ai-sdk/gateway@4.0.10", "", { "dependencies": { "@ai-sdk/provider": "4.0.1", "@ai-sdk/provider-utils": "5.0.4", "@vercel/oidc": "3.2.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-Eks3BNZL74SknXssQ5Z4Ud49vNEMcmMPHrbsoRBodp9dgXqwwKZTktN2uRcOHHNov2O1UZa9HQT8SL9t1mC0fw=="], + "@ai-sdk/gateway": ["@ai-sdk/gateway@4.0.11", "", { "dependencies": { "@ai-sdk/provider": "4.0.2", "@ai-sdk/provider-utils": "5.0.5", "@vercel/oidc": "3.2.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-ZdZzQnBxYfjJpWpSNkV+rRWycwTBhxyvwGvxcwx9g+WQoy3MK2xNahSySEgP9hD/X2xY9jkjd0xB67oDE3rLMA=="], - "@ai-sdk/openai": ["@ai-sdk/openai@4.0.6", "", { "dependencies": { "@ai-sdk/provider": "4.0.1", "@ai-sdk/provider-utils": "5.0.4" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-T8RrVBYFqLbsrYgPgv6IJdGufQOJc7KXHtJTgZsd+d3Tkph8x3s+EqPENeIjhTx00fRTiSS/sy/GsDReZuS3Iw=="], + "@ai-sdk/openai": ["@ai-sdk/openai@4.0.7", "", { "dependencies": { "@ai-sdk/provider": "4.0.2", "@ai-sdk/provider-utils": "5.0.5" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-55K0j8RRjcKosUvIl8u/+SMaS1wedq77xN2iuhlOvB/USbIgSmjkWKV9lqGhhp+Qxhnm3qg5NzrqOI1jmra9fQ=="], - "@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@3.0.4", "", { "dependencies": { "@ai-sdk/provider": "4.0.1", "@ai-sdk/provider-utils": "5.0.4" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-jnW9Wfkn75M7lN5O9HUlgs4il6jbJKGwoRtsuRWDovr4Fz81qwB9Kx/kxoEL7cZn0cBg5/CZVOuMXJsmkxi9zw=="], + "@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@3.0.5", "", { "dependencies": { "@ai-sdk/provider": "4.0.2", "@ai-sdk/provider-utils": "5.0.5" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-4UtDxT6Ga7U225o5fBEgtCFZHba4/iTDXRqMrU62yEfTrFks4o+F4jt0D3OWmUasR9LPFzLy0KnEITxFDtc89g=="], - "@ai-sdk/provider": ["@ai-sdk/provider@4.0.1", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-6p3C/vGqVIjcptBu1DnVd/BZJ2wWmV9TUv9192vT6ZvT9KNED8EwRTqyqFpoQZKgSbMDSvBSq3dqR524Nt/Crw=="], + "@ai-sdk/provider": ["@ai-sdk/provider@4.0.2", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-pfPoy9J1B1xV7cqJ8MYHOsDYrMv5tR3+EMNfI249OhkD2uRakvav3Fo7XpD2luuN/YNCBY7KfEQc7vEV7KEtyw=="], - "@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@5.0.4", "", { "dependencies": { "@ai-sdk/provider": "4.0.1", "@standard-schema/spec": "^1.1.0", "@workflow/serde": "4.1.0", "eventsource-parser": "^3.0.8" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-07zxTR0ajL+jsuCBK+UdKdQ9xQQOLhJtLr5LBrBSkeRSWS07GGhcj1jVZ8YyvJmofQSvNxUSka+QObOMv9qWig=="], + "@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@5.0.5", "", { "dependencies": { "@ai-sdk/provider": "4.0.2", "@standard-schema/spec": "^1.1.0", "@workflow/serde": "4.1.0", "eventsource-parser": "^3.0.8" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-oI0t3dvCoqWNV1I8o1Rybi2DXDvHES5r/TrwtJW90tuFLVepgJlftPxrcjh8vaSvjqC2diTuA2vXyjKAyHJm4A=="], "@alloc/quick-lru": ["@alloc/quick-lru@5.2.0", "", {}, "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw=="], @@ -718,6 +720,8 @@ "@types/node": ["@types/node@26.1.0", "", { "dependencies": { "undici-types": "~8.3.0" } }, "sha512-O0A1G3xPGy4w7AgQdAQYUlQ+BKk2Oovw8eRpofyp5KdBZULnbe+WqaOVNrm705SHphCiG4XHsACrSmPu1f+Kgw=="], + "@types/nodemailer": ["@types/nodemailer@8.0.1", "", { "dependencies": { "@types/node": "*" } }, "sha512-PxpaInm8V1JQDd4j0ds5HfvWQk8JupS1C0Picb96QJsrrRDjBH+DlK7L4ZdNSqNULhiZRQHc40nLVShaGxXAMw=="], + "@types/react": ["@types/react@19.2.17", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw=="], "@types/react-dom": ["@types/react-dom@19.2.3", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ=="], @@ -746,7 +750,7 @@ "agent-base": ["agent-base@7.1.4", "", {}, "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="], - "ai": ["ai@7.0.13", "", { "dependencies": { "@ai-sdk/gateway": "4.0.10", "@ai-sdk/provider": "4.0.1", "@ai-sdk/provider-utils": "5.0.4" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-1KnmzMQBqJau6GRdKg9IBAPU2scsFohxwIH1zut0X51gdIjEXDWeQLk9IitcEzDcETnQTbEVYwqhoKWTfSIqBA=="], + "ai": ["ai@7.0.14", "", { "dependencies": { "@ai-sdk/gateway": "4.0.11", "@ai-sdk/provider": "4.0.2", "@ai-sdk/provider-utils": "5.0.5" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-qA82fZyD4xh9IDB+s3SvwbjwjR+GGRmJjTYMwON1uROj8vPDIQbPTZLLq6ZWTVESn9daeH1GMv0zKfVLwNU2sQ=="], "ajv": ["ajv@8.20.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA=="], @@ -1368,6 +1372,8 @@ "node-releases": ["node-releases@2.0.50", "", {}, "sha512-J6l92tKHX6w8Jy5nO1Vuc01NoIiRGi/d6qBKVxh+IQ8Cr3b6HbVNfKiF8ZpFKufTwpwxMmce2W3iQZ861ZRyTg=="], + "nodemailer": ["nodemailer@9.0.3", "", {}, "sha512-n+YP+NKwR5zRWa60k3GiQ6Q3B4KXCoAw40dAKeCtYn020iNN74aWK2liXIC3ZEATeGql7we3tE3t8QwhY0eskw=="], + "npm-run-path": ["npm-run-path@6.0.0", "", { "dependencies": { "path-key": "^4.0.0", "unicorn-magic": "^0.3.0" } }, "sha512-9qny7Z9DsQU8Ou39ERsPU4OZQlSTP47ShQzuKZ6PRXpYLtIFgl/DEBYEXKlvcEa+9tHVcK8CF81Y2V72qaZhWA=="], "object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="], From 8e3ad9c0e5903e80b87fb6d3715b0d4d89e52cdf Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 3 Jul 2026 18:59:26 +0000 Subject: [PATCH 05/13] feat: add lead scan provider abstraction MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds the LeadSourceProvider interface (searchBusinesses / getBusinessDetails / normalizeBusiness / sourcePolicy) plus three compliant adapters: manual_csv (user-supplied CSV import), google_places_api (official Places API Text Search, New — minimal FieldMask, no Maps scraping, disabled without a configured key), and a minimal custom_api placeholder. Also adds a shared Nominatim geocoding helper (postal code -> lat/lon, rate-limited to the service's 1 req/s policy) used by google_places_api and, in a follow-up, osm_overpass. --- apps/server/src/lead-scout-geocode.ts | 65 +++++++++ .../src/lead-scout-providers/custom-api.ts | 75 ++++++++++ .../src/lead-scout-providers/google-places.ts | 134 ++++++++++++++++++ .../src/lead-scout-providers/manual-csv.ts | 80 +++++++++++ .../server/src/lead-scout-providers/shared.ts | 80 +++++++++++ apps/server/src/lead-scout-providers/types.ts | 69 +++++++++ 6 files changed, 503 insertions(+) create mode 100644 apps/server/src/lead-scout-geocode.ts create mode 100644 apps/server/src/lead-scout-providers/custom-api.ts create mode 100644 apps/server/src/lead-scout-providers/google-places.ts create mode 100644 apps/server/src/lead-scout-providers/manual-csv.ts create mode 100644 apps/server/src/lead-scout-providers/shared.ts create mode 100644 apps/server/src/lead-scout-providers/types.ts diff --git a/apps/server/src/lead-scout-geocode.ts b/apps/server/src/lead-scout-geocode.ts new file mode 100644 index 0000000..9a6309e --- /dev/null +++ b/apps/server/src/lead-scout-geocode.ts @@ -0,0 +1,65 @@ +/** + * Postal code -> coordinates via Nominatim (OpenStreetMap's own geocoder, + * free, no API key) — used by both the google_places_api provider (needs a + * center point for Nearby Search) and the osm_overpass provider (needs a + * bounding box). Respects Nominatim's usage policy: max 1 request/second + * and a descriptive User-Agent identifying the app. + * https://operations.osmfoundation.org/policies/nominatim/ + */ + +const NOMINATIM_USER_AGENT = "NyxelOS-LocalLeadScout/1.0 (local business discovery extension)"; +const NOMINATIM_MIN_INTERVAL_MS = 1100; + +// Process-local throttle, not cross-replica — matches the rest of the +// codebase's "single-process is the deployment target today" assumption +// (see scheduler.ts's automationsInFlight). +let lastNominatimCallAt = 0; + +async function throttleNominatim(): Promise { + const elapsed = Date.now() - lastNominatimCallAt; + if (elapsed < NOMINATIM_MIN_INTERVAL_MS) { + await new Promise((resolve) => setTimeout(resolve, NOMINATIM_MIN_INTERVAL_MS - elapsed)); + } + lastNominatimCallAt = Date.now(); +} + +export interface GeocodeResult { + lat: number; + lon: number; +} + +export async function geocodePostalCode(postalCode: string, country: string): Promise { + await throttleNominatim(); + const url = new URL("https://nominatim.openstreetmap.org/search"); + url.searchParams.set("postalcode", postalCode); + url.searchParams.set("country", country); + url.searchParams.set("format", "json"); + url.searchParams.set("limit", "1"); + + const res = await fetch(url, { headers: { "User-Agent": NOMINATIM_USER_AGENT } }); + if (!res.ok) throw new Error(`Nominatim geocoding failed: HTTP ${res.status}`); + const results = (await res.json()) as { lat: string; lon: string }[]; + const [first] = results; + if (!first) throw new Error(`Could not geocode postal code "${postalCode}, ${country}".`); + return { lat: Number(first.lat), lon: Number(first.lon) }; +} + +export interface BoundingBox { + south: number; + west: number; + north: number; + east: number; +} + +/** Rough equirectangular approximation — plenty accurate for a "search + * within N km of this postal code" radius, not a routing/distance product. */ +export function boundingBoxFromCenter(lat: number, lon: number, radiusKm: number): BoundingBox { + const latDelta = radiusKm / 111; + const lonDelta = radiusKm / (111 * Math.cos((lat * Math.PI) / 180)); + return { + south: lat - latDelta, + west: lon - lonDelta, + north: lat + latDelta, + east: lon + lonDelta, + }; +} diff --git a/apps/server/src/lead-scout-providers/custom-api.ts b/apps/server/src/lead-scout-providers/custom-api.ts new file mode 100644 index 0000000..5035aa7 --- /dev/null +++ b/apps/server/src/lead-scout-providers/custom-api.ts @@ -0,0 +1,75 @@ +import type { LeadSourceProvider, LeadSourceSearchInput } from "./types"; +import { classifyWebsite } from "./shared"; + +/** Minimal generic REST adapter placeholder — expects a compliant endpoint + * (configured per-workspace as sourceConfig.config.baseUrl) that returns a + * flat JSON array shaped like the manual_csv row fields. Real + * provider-specific behavior (auth scheme, pagination, rate limits) is left + * to whichever compliant API a workspace wires up here; this adapter only + * defines the minimal request/response contract. */ +interface CustomApiRow { + sourceId: string; + businessName: string; + category?: string; + formattedAddress?: string; + postalCode?: string; + city?: string; + phone?: string; + email?: string; + website?: string; +} + +export const customApiProvider: LeadSourceProvider = { + sourcePolicy: + "Generic compliant API adapter placeholder — only queries the endpoint you configure; " + + "you are responsible for that endpoint's own terms of service and rate limits.", + + async searchBusinesses(input: LeadSourceSearchInput) { + const baseUrl = input.sourceConfig?.config?.baseUrl; + if (!baseUrl || typeof baseUrl !== "string") { + throw new Error( + "custom_api requires a baseUrl configured in workspace source settings first.", + ); + } + const apiKey = input.sourceConfig?.apiKey; + const url = new URL(baseUrl); + url.searchParams.set("postalCode", input.postalCode); + url.searchParams.set("country", input.country); + url.searchParams.set("radiusKm", String(input.radiusKm)); + url.searchParams.set("niches", input.niches.join(",")); + url.searchParams.set("maxResults", String(input.maxResults)); + + const res = await fetch(url, { + headers: apiKey ? { Authorization: `Bearer ${apiKey}` } : undefined, + }); + if (!res.ok) throw new Error(`custom_api provider error: HTTP ${res.status}`); + const rows = (await res.json()) as CustomApiRow[]; + return Array.isArray(rows) ? rows : []; + }, + + async getBusinessDetails(raw) { + return raw; + }, + + normalizeBusiness(row) { + const { website, websiteStatus } = classifyWebsite(row.website); + return { + sourceId: row.sourceId, + businessName: row.businessName, + category: row.category ?? null, + formattedAddress: row.formattedAddress ?? null, + postalCode: row.postalCode ?? null, + city: row.city ?? null, + phone: row.phone ?? null, + email: row.email ?? null, + website, + websiteStatus, + confidence: 60, + evidenceSummary: + websiteStatus === "missing_website" + ? "custom_api response had no website value for this business." + : "custom_api reported a website for this business.", + missingReason: websiteStatus === "missing_website" ? "No website field in the custom API response." : null, + }; + }, +}; diff --git a/apps/server/src/lead-scout-providers/google-places.ts b/apps/server/src/lead-scout-providers/google-places.ts new file mode 100644 index 0000000..157db8e --- /dev/null +++ b/apps/server/src/lead-scout-providers/google-places.ts @@ -0,0 +1,134 @@ +import { geocodePostalCode } from "../lead-scout-geocode"; +import type { LeadSourceProvider, LeadSourceSearchInput } from "./types"; + +/** + * Official Places API (New) Text Search only — no browser automation, no + * Google Maps DOM scraping. FieldMask is explicit and minimal (never `*`), + * per the extension's compliance requirements: only what the workflow needs + * to judge a lead and dedupe it (`id`, name, address, location, status, + * types, phone, website, maps link). Reviews/photos/ratings are never + * requested and never stored. + */ +const FIELD_MASK = [ + "places.id", + "places.displayName", + "places.formattedAddress", + "places.location", + "places.businessStatus", + "places.types", + "places.nationalPhoneNumber", + "places.internationalPhoneNumber", + "places.websiteUri", + "places.googleMapsUri", +].join(","); + +const MAX_RESULTS_PER_QUERY = 20; +const QUERY_DELAY_MS = 250; + +interface GooglePlace { + id: string; + displayName?: { text: string }; + formattedAddress?: string; + location?: { latitude: number; longitude: number }; + businessStatus?: string; + types?: string[]; + nationalPhoneNumber?: string; + internationalPhoneNumber?: string; + websiteUri?: string; + googleMapsUri?: string; + /** Not part of the API response — stamped on by searchBusinesses so + * normalizeBusiness knows which niche query surfaced this result. */ + __niche?: string; +} + +async function searchTextOnce( + apiKey: string, + textQuery: string, + center: { lat: number; lon: number }, + radiusMeters: number, + maxResultCount: number, +): Promise { + const res = await fetch("https://places.googleapis.com/v1/places:searchText", { + method: "POST", + headers: { + "Content-Type": "application/json", + "X-Goog-Api-Key": apiKey, + "X-Goog-FieldMask": FIELD_MASK, + }, + body: JSON.stringify({ + textQuery, + locationBias: { + circle: { + center: { latitude: center.lat, longitude: center.lon }, + radius: radiusMeters, + }, + }, + maxResultCount: Math.min(maxResultCount, MAX_RESULTS_PER_QUERY), + }), + }); + if (!res.ok) { + throw new Error(`Google Places API error: HTTP ${res.status} ${await res.text()}`); + } + const body = (await res.json()) as { places?: GooglePlace[] }; + return body.places ?? []; +} + +export const googlePlacesProvider: LeadSourceProvider = { + sourcePolicy: + "Official Google Places API (Text Search, New) only — no Maps scraping, no DOM automation, no bulk harvesting. " + + "You are responsible for complying with the Google Maps Platform Terms of Service for any data retrieved this way.", + + async searchBusinesses(input: LeadSourceSearchInput) { + const apiKey = input.sourceConfig?.apiKey; + if (!apiKey) { + throw new Error( + "google_places_api requires an API key configured in workspace source settings first.", + ); + } + const center = await geocodePostalCode(input.postalCode, input.country); + const radiusMeters = Math.min(Math.max(input.radiusKm, 0.5), 50) * 1000; + const niches = input.niches.length > 0 ? input.niches : ["local business"]; + + const results: GooglePlace[] = []; + for (const niche of niches) { + if (results.length >= input.maxResults) break; + const remaining = input.maxResults - results.length; + const textQuery = `${niche} near ${input.postalCode}, ${input.country}`; + const places = await searchTextOnce(apiKey, textQuery, center, radiusMeters, remaining); + for (const place of places) results.push({ ...place, __niche: niche }); + if (niches.indexOf(niche) < niches.length - 1) { + await new Promise((resolve) => setTimeout(resolve, QUERY_DELAY_MS)); + } + } + return results; + }, + + async getBusinessDetails(raw) { + // Text Search (New) with the FieldMask above already returns everything + // the workflow needs — no separate Place Details call, which would be + // extra quota/latency for data already in hand. + return raw; + }, + + normalizeBusiness(place) { + const website = place.websiteUri?.trim() || null; + return { + sourceId: place.id, + businessName: place.displayName?.text ?? "Unknown business", + category: place.types?.[0] ?? null, + niche: place.__niche ?? null, + formattedAddress: place.formattedAddress ?? null, + postalCode: null, + city: null, + phone: place.nationalPhoneNumber ?? place.internationalPhoneNumber ?? null, + email: null, + website, + websiteStatus: website ? "has_website" : "missing_website", + confidence: 90, + evidenceSummary: website + ? `Google Places API returned a websiteUri (${website}).` + : "Google Places API (Text Search) returned no websiteUri for this business.", + missingReason: website ? null : "No websiteUri field in the Places API response.", + }; + }, +}; diff --git a/apps/server/src/lead-scout-providers/manual-csv.ts b/apps/server/src/lead-scout-providers/manual-csv.ts new file mode 100644 index 0000000..944c475 --- /dev/null +++ b/apps/server/src/lead-scout-providers/manual-csv.ts @@ -0,0 +1,80 @@ +import { createHash } from "node:crypto"; +import type { LeadSourceProvider, LeadSourceSearchInput } from "./types"; +import { classifyWebsite, parseCsv } from "./shared"; + +/** businessName is the only strictly required column — everything else is + * optional per the extension's field list (businessName, address, + * postalCode, city, category, phone, email, website, notes). */ +interface CsvLeadRow { + businessName: string; + address: string; + postalCode: string; + city: string; + category: string; + phone: string; + email: string; + website: string; + notes: string; +} + +/** Stable per-row identity: a CSV has no natural id, so hash the row's own + * content — re-importing the same file reconciles against existing leads + * instead of duplicating them (see lead_scout_lead's unique constraint). */ +function csvRowSourceId(row: Record): string { + const key = [row.businessName, row.address, row.postalCode, row.phone, row.email] + .join("|") + .toLowerCase(); + return createHash("sha256").update(key).digest("hex").slice(0, 32); +} + +export const manualCsvProvider: LeadSourceProvider = { + sourcePolicy: + "User-supplied CSV import — fully within the user's own control, no external data source involved.", + + async searchBusinesses(input: LeadSourceSearchInput) { + if (!input.csvText) { + throw new Error("manual_csv requires csvText — upload a CSV to import leads."); + } + const rows = parseCsv(input.csvText); + return rows + .filter((row) => row.businessName) + .map((row) => ({ + sourceId: csvRowSourceId(row), + businessName: row.businessName ?? "", + address: row.address ?? "", + postalCode: row.postalCode ?? "", + city: row.city ?? "", + category: row.category ?? "", + phone: row.phone ?? "", + email: row.email ?? "", + website: row.website ?? "", + notes: row.notes ?? "", + })); + }, + + async getBusinessDetails(raw) { + return raw; + }, + + normalizeBusiness(row) { + const { website, websiteStatus } = classifyWebsite(row.website); + return { + sourceId: row.sourceId, + businessName: row.businessName, + category: row.category || null, + formattedAddress: row.address || null, + postalCode: row.postalCode || null, + city: row.city || null, + phone: row.phone || null, + email: row.email || null, + website, + websiteStatus, + confidence: 100, + evidenceSummary: + websiteStatus === "missing_website" + ? "Imported via manual CSV; no website value was provided." + : `Imported via manual CSV.${row.notes ? ` Notes: ${row.notes}` : ""}`, + missingReason: websiteStatus === "missing_website" ? "No website column value in the CSV row." : null, + }; + }, +}; diff --git a/apps/server/src/lead-scout-providers/shared.ts b/apps/server/src/lead-scout-providers/shared.ts new file mode 100644 index 0000000..46972fb --- /dev/null +++ b/apps/server/src/lead-scout-providers/shared.ts @@ -0,0 +1,80 @@ +import type { LeadScoutWebsiteStatus } from "@nyxel/db"; + +/** A lead qualifies as missing_website only when no website value is present + * at all (per every provider's compliance requirements — never inferred + * from scraping). A present-but-unparseable value is reported separately + * (invalid_website) so a reviewer can tell "no site" from "bad data" apart. */ +export function classifyWebsite(raw: string | null | undefined): { + website: string | null; + websiteStatus: LeadScoutWebsiteStatus; +} { + const trimmed = raw?.trim(); + if (!trimmed) return { website: null, websiteStatus: "missing_website" }; + const withProtocol = /^https?:\/\//i.test(trimmed) ? trimmed : `https://${trimmed}`; + try { + new URL(withProtocol); + return { website: trimmed, websiteStatus: "has_website" }; + } catch { + return { website: trimmed, websiteStatus: "invalid_website" }; + } +} + +/** Minimal RFC4180-ish CSV parser (quoted fields, escaped `""`, embedded + * commas/newlines) — avoids a new dependency for the one thing manual_csv + * needs. Returns each row as a header-keyed record; ragged rows are padded + * with empty strings for missing trailing columns. */ +export function parseCsv(text: string): Record[] { + const rows: string[][] = []; + let row: string[] = []; + let field = ""; + let inQuotes = false; + + const pushField = () => { + row.push(field); + field = ""; + }; + const pushRow = () => { + pushField(); + rows.push(row); + row = []; + }; + + for (let i = 0; i < text.length; i++) { + const char = text[i]; + if (inQuotes) { + if (char === '"') { + if (text[i + 1] === '"') { + field += '"'; + i++; + } else { + inQuotes = false; + } + } else { + field += char; + } + continue; + } + if (char === '"') { + inQuotes = true; + } else if (char === ",") { + pushField(); + } else if (char === "\n") { + pushRow(); + } else if (char === "\r") { + // swallow, \n (or end of input) closes the row + } else { + field += char; + } + } + if (field.length > 0 || row.length > 0) pushRow(); + + const [header, ...dataRows] = rows.filter((r) => r.length > 0 && !(r.length === 1 && r[0] === "")); + if (!header) return []; + return dataRows.map((cells) => { + const record: Record = {}; + header.forEach((key, index) => { + record[key.trim()] = (cells[index] ?? "").trim(); + }); + return record; + }); +} diff --git a/apps/server/src/lead-scout-providers/types.ts b/apps/server/src/lead-scout-providers/types.ts new file mode 100644 index 0000000..7085fe3 --- /dev/null +++ b/apps/server/src/lead-scout-providers/types.ts @@ -0,0 +1,69 @@ +import type { LeadScoutSourceConfigRecord, LeadScoutWebsiteStatus } from "@nyxel/db"; + +/** What a scan needs to run one query — the campaign's own config, plus the + * workspace's per-provider source config (API keys, provider-specific + * settings) and, for manual_csv only, the raw CSV text to import. */ +export interface LeadSourceSearchInput { + workspaceId: string; + postalCode: string; + country: string; + radiusKm: number; + niches: string[]; + maxResults: number; + sourceConfig: LeadScoutSourceConfigRecord | null; + csvText?: string; +} + +/** The output shape every provider normalizes into — exactly what a + * reviewer needs to judge a lead (no reviews/photos/ratings/bulk metadata, + * per each provider's compliance requirements). */ +export interface NormalizedBusiness { + sourceId: string; + businessName: string; + category?: string | null; + niche?: string | null; + formattedAddress?: string | null; + postalCode?: string | null; + city?: string | null; + phone?: string | null; + email?: string | null; + website?: string | null; + websiteStatus: LeadScoutWebsiteStatus; + confidence: number; + evidenceSummary: string; + missingReason?: string | null; +} + +/** + * Provider adapter contract (see the extension's design doc). `TRaw` is + * whatever shape `searchBusinesses` returns per result (a search hit); the + * details/normalize split lets a provider fetch more per-result info before + * normalizing without every provider needing a separate details call — + * providers whose search response is already complete (Google Places New, + * manual CSV rows) just return the raw item unchanged from + * `getBusinessDetails`. + */ +export interface LeadSourceProvider { + readonly sourcePolicy: string; + searchBusinesses(input: LeadSourceSearchInput): Promise; + getBusinessDetails(raw: TRaw, input: LeadSourceSearchInput): Promise; + normalizeBusiness(details: TRaw): NormalizedBusiness; +} + +/** Runs one provider end-to-end and caps the result count — the one place + * every scan's maxResultsPerRun limit is actually enforced, regardless of + * how many raw results a provider's search returned. */ +export async function collectNormalizedBusinesses( + provider: LeadSourceProvider, + input: LeadSourceSearchInput, +): Promise { + const raws = await provider.searchBusinesses(input); + const normalized: NormalizedBusiness[] = []; + for (const raw of raws) { + if (normalized.length >= input.maxResults) break; + const details = await provider.getBusinessDetails(raw, input); + if (!details) continue; + normalized.push(provider.normalizeBusiness(details)); + } + return normalized; +} From aa5923a081a1c683d32c3013a473ba45485cd70d Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 3 Jul 2026 19:01:08 +0000 Subject: [PATCH 06/13] feat: add osm overpass lead source provider MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds the osm_overpass adapter: geocodes the campaign's postal code via Nominatim (rate-limited to its 1 req/s policy), queries the public Overpass API for shop/amenity/office/craft nodes within the resulting bounding box (descriptive User-Agent, capped results, no scraping), and normalizes tags into a lead — missing_website only when no website/contact:website tag is present, with a separate missing_email note since OSM rarely carries contact emails. Also adds a small provider registry mapping LeadScoutProvider -> adapter. Nominatim/Overpass calls aren't reachable from this sandbox's network policy, so the live request/response shape is unverified here — the query format follows each service's documented API. --- apps/server/src/lead-scout-providers/index.ts | 20 +++ .../src/lead-scout-providers/osm-overpass.ts | 115 ++++++++++++++++++ 2 files changed, 135 insertions(+) create mode 100644 apps/server/src/lead-scout-providers/index.ts create mode 100644 apps/server/src/lead-scout-providers/osm-overpass.ts diff --git a/apps/server/src/lead-scout-providers/index.ts b/apps/server/src/lead-scout-providers/index.ts new file mode 100644 index 0000000..f642378 --- /dev/null +++ b/apps/server/src/lead-scout-providers/index.ts @@ -0,0 +1,20 @@ +import type { LeadScoutProvider } from "@nyxel/db"; +import { customApiProvider } from "./custom-api"; +import { googlePlacesProvider } from "./google-places"; +import { manualCsvProvider } from "./manual-csv"; +import { osmOverpassProvider } from "./osm-overpass"; +import type { LeadSourceProvider } from "./types"; + +export { collectNormalizedBusinesses } from "./types"; +export type { LeadSourceProvider, LeadSourceSearchInput, NormalizedBusiness } from "./types"; + +const PROVIDERS: Record = { + manual_csv: manualCsvProvider, + google_places_api: googlePlacesProvider, + osm_overpass: osmOverpassProvider, + custom_api: customApiProvider, +}; + +export function getLeadSourceProvider(provider: LeadScoutProvider): LeadSourceProvider { + return PROVIDERS[provider]; +} diff --git a/apps/server/src/lead-scout-providers/osm-overpass.ts b/apps/server/src/lead-scout-providers/osm-overpass.ts new file mode 100644 index 0000000..3f0adbd --- /dev/null +++ b/apps/server/src/lead-scout-providers/osm-overpass.ts @@ -0,0 +1,115 @@ +import { boundingBoxFromCenter, geocodePostalCode } from "../lead-scout-geocode"; +import type { LeadSourceProvider, LeadSourceSearchInput } from "./types"; + +/** Descriptive User-Agent as required by the Overpass/OSM usage policy — + * https://operations.osmfoundation.org/policies/overpass/ */ +const OVERPASS_USER_AGENT = "NyxelOS-LocalLeadScout/1.0 (local business discovery extension)"; +const OVERPASS_ENDPOINT = "https://overpass-api.de/api/interpreter"; +const QUERY_TIMEOUT_S = 25; + +interface OverpassElement { + type: "node" | "way" | "relation"; + id: number; + tags?: Record; +} + +function buildOverpassQuery(bbox: { south: number; west: number; north: number; east: number }): string { + const bounds = `${bbox.south},${bbox.west},${bbox.north},${bbox.east}`; + // nwr matches node/way/relation in one clause per OSM Overpass QL — one + // clause per top-level key the extension is allowed to query (shop, + // amenity, office, craft), scoped to the bounding box. + return `[out:json][timeout:${QUERY_TIMEOUT_S}]; +( + nwr["shop"](${bounds}); + nwr["amenity"](${bounds}); + nwr["office"](${bounds}); + nwr["craft"](${bounds}); +); +out center tags;`; +} + +function elementCategory(tags: Record): string | null { + for (const key of ["shop", "amenity", "office", "craft"]) { + if (tags[key]) return tags[key]; + } + return null; +} + +function elementMatchesNiches(tags: Record, niches: string[]): boolean { + if (niches.length === 0) return true; + const haystack = [tags.name, tags.shop, tags.amenity, tags.office, tags.craft] + .filter(Boolean) + .join(" ") + .toLowerCase(); + return niches.some((niche) => haystack.includes(niche.toLowerCase())); +} + +function elementAddress(tags: Record): string | null { + if (tags["addr:full"]) return tags["addr:full"]; + const parts = [tags["addr:housenumber"], tags["addr:street"]].filter(Boolean); + return parts.length > 0 ? parts.join(" ") : null; +} + +export const osmOverpassProvider: LeadSourceProvider = { + sourcePolicy: + "Public Overpass API over OpenStreetMap data (ODbL license) — a compliant, key-free public API, not scraping. " + + "Fair-use: bounded bounding box per scan, capped result count, descriptive User-Agent.", + + async searchBusinesses(input: LeadSourceSearchInput) { + const center = await geocodePostalCode(input.postalCode, input.country); + const bbox = boundingBoxFromCenter(center.lat, center.lon, input.radiusKm); + const query = buildOverpassQuery(bbox); + + const res = await fetch(OVERPASS_ENDPOINT, { + method: "POST", + headers: { + "Content-Type": "application/x-www-form-urlencoded", + "User-Agent": OVERPASS_USER_AGENT, + }, + body: `data=${encodeURIComponent(query)}`, + }); + if (!res.ok) throw new Error(`Overpass API error: HTTP ${res.status}`); + const body = (await res.json()) as { elements?: OverpassElement[] }; + const elements = (body.elements ?? []).filter((el) => el.tags?.name); + + const matching = elements.filter((el) => elementMatchesNiches(el.tags ?? {}, input.niches)); + return matching.slice(0, input.maxResults); + }, + + async getBusinessDetails(raw) { + return raw; + }, + + normalizeBusiness(element) { + const tags = element.tags ?? {}; + const website = (tags.website || tags["contact:website"] || "").trim() || null; + const email = (tags.email || tags["contact:email"] || "").trim() || null; + const phone = (tags.phone || tags["contact:phone"] || "").trim() || null; + const missingEmail = !email; + + return { + sourceId: `${element.type}/${element.id}`, + businessName: tags.name ?? "Unknown business", + category: elementCategory(tags), + formattedAddress: elementAddress(tags), + postalCode: tags["addr:postcode"] ?? null, + city: tags["addr:city"] ?? null, + phone, + email, + website, + websiteStatus: website ? "has_website" : "missing_website", + // Lower than google_places_api's default (90) — OSM tag completeness + // varies a lot by region/contributor. + confidence: 70, + evidenceSummary: [ + website + ? `OSM tags include a website/contact:website value (${website}).` + : "No website or contact:website tag present on this OSM element.", + missingEmail ? "No email or contact:email tag present (missing_email) — outreach needs a manually supplied address." : null, + ] + .filter(Boolean) + .join(" "), + missingReason: website ? null : "No website/contact:website tag on the OSM node/way.", + }; + }, +}; From baa805c0f99ec992746fb99d5c5dbd7a6f036350 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 3 Jul 2026 19:03:00 +0000 Subject: [PATCH 07/13] feat: add lead scan orchestration and scheduler hook MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit runLeadScoutScan dispatches a campaign's configured provider, filters by minConfidence, reconciles results against already-ingested leads (by campaign+provider+sourceId) so re-scans update in place instead of duplicating, and records a scan_run with counts/summary — mirrors runSeoAnalysis's audit+notify shape. Scheduled campaigns are polled alongside due SEO projects; manual_csv campaigns can't run unattended (no CSV to read), so a scheduled one auto-disables its schedule. --- apps/server/src/lead-scout.ts | 140 ++++++++++++++++++++++++++++++++++ apps/server/src/scheduler.ts | 40 ++++++++++ 2 files changed, 180 insertions(+) create mode 100644 apps/server/src/lead-scout.ts diff --git a/apps/server/src/lead-scout.ts b/apps/server/src/lead-scout.ts new file mode 100644 index 0000000..aa702f4 --- /dev/null +++ b/apps/server/src/lead-scout.ts @@ -0,0 +1,140 @@ +import type { LeadScoutScanRunRecord } from "@nyxel/db"; +import { getDb } from "@nyxel/db"; +import { logAudit } from "./audit"; +import { collectNormalizedBusinesses, getLeadSourceProvider } from "./lead-scout-providers"; +import { notifyWorkspaceOwner } from "./push"; + +/** + * Runs one scan for a campaign: dispatches to its configured provider, + * reconciles results against already-ingested leads (same-source businesses + * update in place instead of duplicating — see lead_scout_lead's unique + * constraint), and records a scan_run with the outcome. Mirrors + * runSeoAnalysis's shape (create run -> do the work -> update run -> + * audit -> notify), synchronous end-to-end so both the manual tRPC mutation + * and the scheduler poll can just await it. + */ +export async function runLeadScoutScan( + campaignId: string, + options?: { csvText?: string }, +): Promise { + const db = getDb(); + const campaign = await db.getLeadScoutCampaign(campaignId); + if (!campaign) throw new Error(`Unknown lead scout campaign: ${campaignId}`); + + const sourceConfig = await db.getLeadScoutSourceConfig(campaign.workspaceId, campaign.provider); + const run = await db.createLeadScoutScanRun({ + campaignId, + workspaceId: campaign.workspaceId, + provider: campaign.provider, + }); + + try { + const provider = getLeadSourceProvider(campaign.provider); + const businesses = await collectNormalizedBusinesses(provider, { + workspaceId: campaign.workspaceId, + postalCode: campaign.postalCode, + country: campaign.country, + radiusKm: campaign.radiusKm, + niches: campaign.niches, + maxResults: campaign.maxResultsPerRun, + sourceConfig, + csvText: options?.csvText, + }); + + let newLeadCount = 0; + let missingWebsiteCount = 0; + for (const business of businesses) { + if (business.confidence < campaign.minConfidence) continue; + if (business.websiteStatus === "missing_website") missingWebsiteCount++; + + const existing = await db.getLeadScoutLeadBySource( + campaignId, + campaign.provider, + business.sourceId, + ); + if (existing) { + await db.updateLeadScoutLead(existing.id, { + scanRunId: run.id, + category: business.category ?? existing.category, + niche: business.niche ?? existing.niche, + formattedAddress: business.formattedAddress ?? existing.formattedAddress, + postalCode: business.postalCode ?? existing.postalCode, + city: business.city ?? existing.city, + phone: business.phone ?? existing.phone, + email: business.email ?? existing.email, + website: business.website ?? existing.website, + websiteStatus: business.websiteStatus, + confidence: business.confidence, + evidenceSummary: business.evidenceSummary, + missingReason: business.missingReason ?? null, + }); + continue; + } + + await db.createLeadScoutLead({ + workspaceId: campaign.workspaceId, + campaignId, + scanRunId: run.id, + sourceProvider: campaign.provider, + sourceId: business.sourceId, + businessName: business.businessName, + category: business.category, + niche: business.niche, + formattedAddress: business.formattedAddress, + postalCode: business.postalCode, + city: business.city, + phone: business.phone, + email: business.email, + website: business.website, + websiteStatus: business.websiteStatus, + confidence: business.confidence, + evidenceSummary: business.evidenceSummary, + missingReason: business.missingReason, + }); + newLeadCount++; + } + + const completed = await db.updateLeadScoutScanRun(run.id, { + status: "completed", + resultCount: businesses.length, + newLeadCount, + missingWebsiteCount, + summary: `${businesses.length} result(s) — ${newLeadCount} new, ${missingWebsiteCount} missing a website.`, + completedAt: new Date(), + }); + await db.updateLeadScoutCampaign(campaignId, { lastScanAt: new Date() }); + + await logAudit({ + workspaceId: campaign.workspaceId, + actor: "extension", + toolLabel: "local_lead_scout.scan", + input: { campaignId, provider: campaign.provider }, + output: { resultCount: businesses.length, newLeadCount, missingWebsiteCount }, + status: "success", + }); + await notifyWorkspaceOwner(campaign.workspaceId, { + title: "Lead scan complete", + body: `${campaign.name}: ${newLeadCount} new lead(s), ${missingWebsiteCount} missing a website.`, + url: `/workspace/${campaign.workspaceId}/extensions/local-lead-scout`, + tag: `lead-scout-scan-${run.id}`, + }); + + return completed; + } catch (err) { + const message = err instanceof Error ? err.message : String(err); + const failed = await db.updateLeadScoutScanRun(run.id, { + status: "failed", + errorMessage: message, + completedAt: new Date(), + }); + await logAudit({ + workspaceId: campaign.workspaceId, + actor: "extension", + toolLabel: "local_lead_scout.scan", + input: { campaignId, provider: campaign.provider }, + output: message, + status: "error", + }); + return failed; + } +} diff --git a/apps/server/src/scheduler.ts b/apps/server/src/scheduler.ts index 96ad085..cb08aaa 100644 --- a/apps/server/src/scheduler.ts +++ b/apps/server/src/scheduler.ts @@ -8,6 +8,7 @@ import { logAudit } from "./audit"; import { emitNyxelEvent } from "./event-bus"; import { NyxelEvent } from "./events"; import { reviewDueGoals } from "./goal-orchestrator"; +import { runLeadScoutScan } from "./lead-scout"; import { notifyWorkspaceOwner } from "./push"; import { runSeoAnalysis } from "./seo-analyzer"; import { runWorkflowAndWait } from "./workflow-runner"; @@ -374,6 +375,44 @@ async function checkDueSeoProjects(): Promise { } } +/** + * Checks every Local Lead Scout campaign with scheduling enabled whose + * nextScanAt has passed and runs its scan — same rationale as + * checkDueSeoProjects (a scan isn't an agent chat turn, so it's polled here + * rather than through the automation table). manual_csv campaigns can't run + * unattended (a scan needs freshly uploaded CSV text each time), so those + * are skipped even if scheduling was left on for one — the UI disables + * enabling a schedule for manual_csv campaigns, but a campaign switched to + * manual_csv after scheduling was enabled would otherwise be stuck retrying + * forever. + */ +async function checkDueLeadScoutCampaigns(): Promise { + const db = getDb(); + let due: Awaited>; + try { + due = await db.listDueLeadScoutCampaigns(new Date()); + } catch (err) { + console.error("Scheduler: failed to query due lead scout campaigns:", err); + return; + } + for (const campaign of due) { + if (campaign.provider === "manual_csv") { + await db.updateLeadScoutCampaign(campaign.id, { scheduleEnabled: false }); + continue; + } + try { + await runLeadScoutScan(campaign.id); + const now = new Date(); + const nextScanAt = campaign.scheduleCronExpression + ? computeNextRunAt(campaign.scheduleCronExpression, now) + : null; + await db.updateLeadScoutCampaign(campaign.id, { nextScanAt }); + } catch (err) { + console.error(`Scheduler: lead scout scan for "${campaign.name}" (${campaign.id}) failed:`, err); + } + } +} + /** * Recovers agent runs left stuck in "running" by a process that died or * restarted before it could mark them cancelled/completed/failed itself — @@ -484,6 +523,7 @@ export function startScheduler(): () => void { await checkFileWatchAutomations(); await checkDueSeoProjects(); + await checkDueLeadScoutCampaigns(); await checkGoalsForReview(); await checkStaleAgentRuns(); }, POLL_INTERVAL_MS); From f866e03cbb4061a0777a5904cf6f3f84fe85f63e Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 3 Jul 2026 19:06:41 +0000 Subject: [PATCH 08/13] feat: add prototype and email draft generation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Dispatches the campaign's content agent (auto-provisioned once, no special tools needed) through the existing task/agent-run flow to draft a lightweight website concept per lead, then — only once that prototype is explicitly approved — an outreach email body. Sender identity and opt-out text are appended programmatically from email settings rather than left to the model. Lead status moves through an explicit state machine (new/reviewed -> prototype_requested/ready -> email_drafted) via the atomic claim added earlier, so a lead can't be double-dispatched or skip the review gate when requireApprovalBeforePrototype is set. --- apps/server/src/lead-scout.ts | 336 +++++++++++++++++++++++++++++++++- packages/db/src/repo/types.ts | 2 + 2 files changed, 337 insertions(+), 1 deletion(-) diff --git a/apps/server/src/lead-scout.ts b/apps/server/src/lead-scout.ts index aa702f4..856c2b7 100644 --- a/apps/server/src/lead-scout.ts +++ b/apps/server/src/lead-scout.ts @@ -1,7 +1,16 @@ -import type { LeadScoutScanRunRecord } from "@nyxel/db"; +import type { + AgentRecord, + LeadScoutCampaignRecord, + LeadScoutOutreachDraftRecord, + LeadScoutPrototypeRecord, + LeadScoutScanRunRecord, +} from "@nyxel/db"; import { getDb } from "@nyxel/db"; +import { listAvailableModels } from "@nyxel/model-providers"; +import { executeManagedTask } from "./agent-runtime"; import { logAudit } from "./audit"; import { collectNormalizedBusinesses, getLeadSourceProvider } from "./lead-scout-providers"; +import { getInstalledProvidersForWorkspace } from "./models"; import { notifyWorkspaceOwner } from "./push"; /** @@ -138,3 +147,328 @@ export async function runLeadScoutScan( return failed; } } + +/** Marks a new lead reviewed — the human "I looked at this" gate that + * requireApprovalBeforePrototype checks for before letting a prototype be + * requested. */ +export async function markLeadScoutLeadReviewed(leadId: string): Promise { + const claimed = await getDb().claimLeadScoutLeadStatus({ + id: leadId, + fromStatus: "new", + toStatus: "reviewed", + }); + if (!claimed) throw new Error("Lead isn't in a reviewable state (already reviewed or moved on)."); +} + +async function pickDefaultModelId(workspaceId: string): Promise { + const db = getDb(); + const workspace = await db.getWorkspace(workspaceId); + if (workspace?.defaultModelId) return workspace.defaultModelId; + const providers = await getInstalledProvidersForWorkspace(workspaceId); + const models = await listAvailableModels(providers); + const [first] = models; + if (!first) { + throw new Error("No models are installed for this workspace — add one in Settings before generating content."); + } + return first.id; +} + +const LEAD_SCOUT_AGENT_SYSTEM_PROMPT = + "You are the Local Lead Scout content agent. You draft lightweight website concepts and short, " + + "respectful outreach emails for local businesses that currently have no website. You never make " + + "misleading claims, never invent a prior relationship with the business, and never use pressure " + + "tactics. Follow the requested output format exactly."; + +/** Lazily provisions (once) and thereafter reuses the campaign's content + * agent — mirrors configureSeoFixerAgent's "auto-provision unless the user + * pinned one" shape, but this agent needs no file tools (it only writes + * text), so there's no sandboxed tool setup to redo per dispatch. */ +async function configureLeadScoutAgent(campaign: LeadScoutCampaignRecord): Promise { + const db = getDb(); + if (campaign.prototypeAgentId) { + const existing = await db.getAgent(campaign.prototypeAgentId); + if (existing) return existing; + } + const modelId = await pickDefaultModelId(campaign.workspaceId); + const agent = await db.createAgent({ + workspaceId: campaign.workspaceId, + name: `Lead Scout Content Agent — ${campaign.name}`, + systemPrompt: LEAD_SCOUT_AGENT_SYSTEM_PROMPT, + modelId, + autonomyLevel: "assisted", + toolIds: [], + skillIds: [], + }); + await db.updateLeadScoutCampaign(campaign.id, { prototypeAgentId: agent.id }); + return agent; +} + +export function parsePrototypeOutput(output: string): { + concept: string | null; + heroCopy: string | null; + sections: string[]; + callToAction: string | null; + styleDirection: string | null; + artifactMarkdown: string | null; +} { + const concept = output.match(/CONCEPT:\s*(.+)/i)?.[1]?.trim() ?? null; + const heroCopy = output.match(/HERO_COPY:\s*(.+)/i)?.[1]?.trim() ?? null; + const sectionsRaw = output.match(/SECTIONS:\s*(.+)/i)?.[1]?.trim() ?? ""; + const sections = sectionsRaw + ? sectionsRaw + .split(",") + .map((s) => s.trim()) + .filter(Boolean) + : []; + const callToAction = output.match(/CTA:\s*(.+)/i)?.[1]?.trim() ?? null; + const styleDirection = output.match(/STYLE:\s*(.+)/i)?.[1]?.trim() ?? null; + const artifactRaw = output.split(/---ARTIFACT---/i)[1]?.trim() ?? null; + const artifactMarkdown = artifactRaw && artifactRaw.toLowerCase() !== "none" ? artifactRaw : null; + return { concept, heroCopy, sections, callToAction, styleDirection, artifactMarkdown }; +} + +const PROTOTYPE_OUTPUT_FORMAT = [ + "Respond in exactly this structure (plain text):", + "CONCEPT: ", + "HERO_COPY: ", + 'SECTIONS: ', + "CTA: ", + "STYLE: ", + "---ARTIFACT---", + "", +].join("\n"); + +/** + * Dispatches the campaign's content agent to draft a lightweight website + * concept for one lead — an existing NyxelOS agent/task run, not a bespoke + * LLM pipeline (see ADR-style rationale in seo-analyzer.ts's + * configureSeoFixerAgent). Gated on requireApprovalBeforePrototype: if set, + * the lead must already be "reviewed" (a human looked at it) before a + * prototype can be requested at all. + */ +export async function dispatchLeadScoutPrototype(leadId: string): Promise { + const db = getDb(); + const lead = await db.getLeadScoutLead(leadId); + if (!lead) throw new Error(`Unknown lead: ${leadId}`); + const campaign = await db.getLeadScoutCampaign(lead.campaignId); + if (!campaign) throw new Error(`Unknown lead scout campaign: ${lead.campaignId}`); + + if (campaign.requireApprovalBeforePrototype && lead.status !== "reviewed") { + throw new Error('This lead needs to be marked "reviewed" before generating a prototype.'); + } + if (lead.status !== "new" && lead.status !== "reviewed") { + throw new Error(`Lead is "${lead.status}" and can't have a prototype requested right now.`); + } + const claimed = await db.claimLeadScoutLeadStatus({ + id: leadId, + fromStatus: lead.status, + toStatus: "prototype_requested", + }); + if (!claimed) throw new Error("Lead status changed concurrently — refresh and try again."); + + const agent = await configureLeadScoutAgent(campaign); + const instruction = [ + "Generate a lightweight website concept for a local business that currently has no website.", + `Business: ${lead.businessName}`, + lead.category ? `Category/niche: ${lead.category}` : null, + lead.formattedAddress ? `Region: ${lead.formattedAddress}` : null, + `Evidence this business has no website: ${lead.evidenceSummary ?? "none recorded"}`, + "Target style: clean, modern, mobile-first, trustworthy for a local service business.", + "Requested prototype type: single-page marketing site concept.", + "", + PROTOTYPE_OUTPUT_FORMAT, + ] + .filter(Boolean) + .join("\n"); + + const prototype = await db.createLeadScoutPrototype({ workspaceId: lead.workspaceId, leadId }); + const task = await db.createTask({ + workspaceId: lead.workspaceId, + assignedAgentId: agent.id, + title: `Generate prototype — ${lead.businessName}`, + instruction, + input: { leadId, prototypeId: prototype.id }, + }); + + try { + const result = await executeManagedTask({ taskId: task.id, agent, trigger: "extension" }); + const parsed = parsePrototypeOutput(result.output); + const ready = await db.updateLeadScoutPrototype(prototype.id, { + status: "ready", + taskId: task.id, + ...parsed, + }); + await db.claimLeadScoutLeadStatus({ + id: leadId, + fromStatus: "prototype_requested", + toStatus: "prototype_ready", + }); + await logAudit({ + workspaceId: lead.workspaceId, + agentId: agent.id, + actor: "extension", + toolLabel: "local_lead_scout.generate_prototype", + input: { leadId }, + output: { prototypeId: prototype.id }, + status: "success", + }); + return ready; + } catch (err) { + const message = err instanceof Error ? err.message : String(err); + const failed = await db.updateLeadScoutPrototype(prototype.id, { + status: "failed", + taskId: task.id, + errorMessage: message, + }); + // Revert to reviewed so the user can retry instead of being stuck. + await db.claimLeadScoutLeadStatus({ + id: leadId, + fromStatus: "prototype_requested", + toStatus: "reviewed", + }); + await logAudit({ + workspaceId: lead.workspaceId, + agentId: agent.id, + actor: "extension", + toolLabel: "local_lead_scout.generate_prototype", + input: { leadId }, + output: message, + status: "error", + }); + return failed; + } +} + +/** Requires a `ready` prototype — approving a failed/pending one makes no + * sense, there'd be nothing reviewed. */ +export async function approveLeadScoutPrototype(prototypeId: string): Promise { + const db = getDb(); + const prototype = await db.getLeadScoutPrototype(prototypeId); + if (!prototype) throw new Error(`Unknown prototype: ${prototypeId}`); + if (prototype.status !== "ready") throw new Error("Only a ready prototype can be approved."); + const approved = await db.updateLeadScoutPrototype(prototypeId, { approved: true }); + await logAudit({ + workspaceId: prototype.workspaceId, + actor: "extension", + toolLabel: "local_lead_scout.approve_prototype", + input: { prototypeId }, + output: { approved: true }, + status: "success", + }); + return approved; +} + +export function parseEmailDraftOutput(output: string): { subject: string; body: string } { + const subject = output.match(/SUBJECT:\s*(.+)/i)?.[1]?.trim() || "A quick idea for your business"; + const body = output.split(/---BODY---/i)[1]?.trim() || output.trim(); + return { subject, body }; +} + +/** + * Dispatches the campaign's content agent to draft an outreach email for a + * lead, using its approved prototype (never an unapproved one — the + * compliance requirement that a human reviews the concept before it's + * referenced in outreach). Sender identity and opt-out text are appended + * programmatically from email settings after generation, rather than left + * to the model, so they're always present regardless of what it wrote. + */ +export async function dispatchLeadScoutEmailDraft(leadId: string): Promise { + const db = getDb(); + const lead = await db.getLeadScoutLead(leadId); + if (!lead) throw new Error(`Unknown lead: ${leadId}`); + const campaign = await db.getLeadScoutCampaign(lead.campaignId); + if (!campaign) throw new Error(`Unknown lead scout campaign: ${lead.campaignId}`); + + const prototypes = await db.listLeadScoutPrototypesByLead(leadId); + const prototype = prototypes.find((p) => p.approved); + if (!prototype) throw new Error("Approve a prototype for this lead before drafting an email."); + + const emailSettings = await db.getLeadScoutEmailSettings(lead.workspaceId); + if (!emailSettings) throw new Error("Configure email settings before drafting outreach emails."); + + if (lead.status !== "prototype_ready") { + throw new Error(`Lead is "${lead.status}" and can't have an email drafted right now.`); + } + const claimed = await db.claimLeadScoutLeadStatus({ + id: leadId, + fromStatus: "prototype_ready", + toStatus: "email_drafted", + }); + if (!claimed) throw new Error("Lead status changed concurrently — refresh and try again."); + + const agent = await configureLeadScoutAgent(campaign); + const instruction = [ + "Draft a short, respectful, non-pushy cold outreach email to a local business about a website concept it might consider.", + `Business: ${lead.businessName}`, + `Website concept: ${prototype.concept ?? "N/A"}`, + `Hero copy: ${prototype.heroCopy ?? "N/A"}`, + `Call to action: ${prototype.callToAction ?? "N/A"}`, + `Sender: ${emailSettings.fromName}`, + "Rules: no misleading claims, no fake prior relationship, no pressure tactics, under 150 words, one clear next step.", + "Do not include a signature or footer — those are added separately.", + "", + "Respond in exactly this format:", + "SUBJECT: ", + "---BODY---", + "", + ].join("\n"); + + const draft = await db.createLeadScoutOutreachDraft({ + workspaceId: lead.workspaceId, + leadId, + prototypeId: prototype.id, + }); + const task = await db.createTask({ + workspaceId: lead.workspaceId, + assignedAgentId: agent.id, + title: `Draft outreach email — ${lead.businessName}`, + instruction, + input: { leadId, draftId: draft.id }, + }); + + try { + const result = await executeManagedTask({ taskId: task.id, agent, trigger: "extension" }); + const { subject, body } = parseEmailDraftOutput(result.output); + const footer = [emailSettings.unsubscribeText, emailSettings.legalFooter].filter(Boolean).join("\n\n"); + const bodyText = [body, "", `— ${emailSettings.fromName}`, footer].filter(Boolean).join("\n"); + + const ready = await db.updateLeadScoutOutreachDraft(draft.id, { + status: "draft", + taskId: task.id, + subject, + bodyText, + }); + await logAudit({ + workspaceId: lead.workspaceId, + agentId: agent.id, + actor: "extension", + toolLabel: "local_lead_scout.draft_email", + input: { leadId }, + output: { draftId: draft.id }, + status: "success", + }); + return ready; + } catch (err) { + const message = err instanceof Error ? err.message : String(err); + const failed = await db.updateLeadScoutOutreachDraft(draft.id, { + status: "failed", + taskId: task.id, + errorMessage: message, + }); + await db.claimLeadScoutLeadStatus({ + id: leadId, + fromStatus: "email_drafted", + toStatus: "prototype_ready", + }); + await logAudit({ + workspaceId: lead.workspaceId, + agentId: agent.id, + actor: "extension", + toolLabel: "local_lead_scout.draft_email", + input: { leadId }, + output: message, + status: "error", + }); + return failed; + } +} diff --git a/packages/db/src/repo/types.ts b/packages/db/src/repo/types.ts index ab9ed7a..0729e52 100644 --- a/packages/db/src/repo/types.ts +++ b/packages/db/src/repo/types.ts @@ -2006,6 +2006,7 @@ export interface DbRepository { id: string, patch: Partial<{ status: LeadScoutPrototypeStatus; + taskId: string | null; concept: string | null; heroCopy: string | null; sections: string[]; @@ -2029,6 +2030,7 @@ export interface DbRepository { updateLeadScoutOutreachDraft( id: string, patch: Partial<{ + taskId: string | null; subject: string | null; bodyText: string | null; bodyHtml: string | null; From 94ec3f764877577f6faa98bf96666777208f9624 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 3 Jul 2026 19:09:51 +0000 Subject: [PATCH 09/13] feat: add approval gated lead outreach Adds approve/reject/send for outreach drafts: approving only unlocks a send attempt (nothing is dispatched), sending checks outreach mode, requires a real (never guessed) lead email, checks the suppression list by email and domain, enforces per-campaign and daily send limits, and claims both the draft and lead atomically so a duplicate click can't send twice. A sent lead is permanently done until an explicit reset action reopens it. Also fixes lead-scout-email.ts to check dryRunMode before validating provider credentials, so dry-run mode actually works without live credentials configured. --- apps/server/src/lead-scout-email.ts | 10 +- apps/server/src/lead-scout-send.ts | 236 ++++++++++++++++++++++++++++ 2 files changed, 241 insertions(+), 5 deletions(-) create mode 100644 apps/server/src/lead-scout-send.ts diff --git a/apps/server/src/lead-scout-email.ts b/apps/server/src/lead-scout-email.ts index d74647f..cf8f06c 100644 --- a/apps/server/src/lead-scout-email.ts +++ b/apps/server/src/lead-scout-email.ts @@ -168,11 +168,6 @@ export async function sendLeadScoutEmail( ): Promise<{ sent: boolean; dryRun: boolean }> { const settings = await getDb().getLeadScoutEmailSettings(workspaceId); if (!settings) throw new Error("Email settings aren't configured for this workspace yet."); - if (!settings.credentials && settings.provider !== "custom") { - throw new Error(`${settings.provider} isn't configured — add credentials in email settings first.`); - } - - const from = `${settings.fromName} <${settings.fromEmail}>`; if (settings.dryRunMode) { await logAudit({ @@ -186,6 +181,11 @@ export async function sendLeadScoutEmail( return { sent: false, dryRun: true }; } + if (!settings.credentials && settings.provider !== "custom") { + throw new Error(`${settings.provider} isn't configured — add credentials in email settings first.`); + } + const from = `${settings.fromName} <${settings.fromEmail}>`; + try { await dispatchByProvider(settings.provider, settings.credentials ?? {}, { from, diff --git a/apps/server/src/lead-scout-send.ts b/apps/server/src/lead-scout-send.ts new file mode 100644 index 0000000..0bf19b8 --- /dev/null +++ b/apps/server/src/lead-scout-send.ts @@ -0,0 +1,236 @@ +import type { LeadScoutLeadRecord, LeadScoutOutreachDraftRecord, LeadScoutSuppressionRecord } from "@nyxel/db"; +import { getDb } from "@nyxel/db"; +import { logAudit } from "./audit"; +import { reserveLeadScoutDailySendSlot, sendLeadScoutEmail } from "./lead-scout-email"; +import { notifyWorkspaceOwner } from "./push"; + +function domainFromEmail(email: string | null): string | null { + if (!email) return null; + const at = email.lastIndexOf("@"); + return at === -1 ? null : email.slice(at + 1).toLowerCase(); +} + +/** + * Approves a drafted outreach email — the one human decision that unlocks + * `sendLeadScoutOutreachDraft`. Nothing is sent here; this only moves the + * draft/lead into the state that permits a send attempt. Atomic on both the + * draft and the lead so a duplicate click can't approve twice. + */ +export async function approveLeadScoutOutreachDraft( + draftId: string, +): Promise { + const db = getDb(); + const draft = await db.getLeadScoutOutreachDraft(draftId); + if (!draft) throw new Error(`Unknown outreach draft: ${draftId}`); + + const claimed = await db.claimLeadScoutOutreachDraftStatus({ + id: draftId, + fromStatus: "draft", + toStatus: "approved", + }); + if (!claimed) throw new Error("This draft was already approved, rejected, or sent."); + + const updated = await db.updateLeadScoutOutreachDraft(draftId, { approvedAt: new Date() }); + await db.claimLeadScoutLeadStatus({ + id: draft.leadId, + fromStatus: "email_drafted", + toStatus: "approved_to_send", + }); + + await logAudit({ + workspaceId: draft.workspaceId, + actor: "extension", + toolLabel: "local_lead_scout.request_send_approval", + input: { draftId }, + output: { decision: "approved" }, + status: "success", + }); + return updated; +} + +/** Rejecting a draft is final for that draft — nothing sends, and the lead + * moves to `rejected` rather than looping back for another draft attempt + * automatically (a human should decide to re-draft explicitly). */ +export async function rejectLeadScoutOutreachDraft( + draftId: string, +): Promise { + const db = getDb(); + const draft = await db.getLeadScoutOutreachDraft(draftId); + if (!draft) throw new Error(`Unknown outreach draft: ${draftId}`); + + const claimed = await db.claimLeadScoutOutreachDraftStatus({ + id: draftId, + fromStatus: "draft", + toStatus: "rejected", + }); + if (!claimed) throw new Error("This draft was already approved, rejected, or sent."); + + await db.claimLeadScoutLeadStatus({ + id: draft.leadId, + fromStatus: "email_drafted", + toStatus: "rejected", + }); + + await logAudit({ + workspaceId: draft.workspaceId, + actor: "extension", + toolLabel: "local_lead_scout.request_send_approval", + input: { draftId }, + output: { decision: "rejected" }, + status: "rejected", + }); + return claimed; +} + +async function countSentForCampaign(campaignId: string): Promise { + const leads = await getDb().listLeadScoutLeadsByCampaign(campaignId); + return leads.filter((lead) => lead.status === "sent").length; +} + +/** + * Sends an approved outreach draft — the single irreversible action in this + * whole extension. Every compliance gate lives here: campaign outreach mode, + * no-guessed-email, suppression list, per-campaign and daily send limits, + * and an atomic claim (draft `approved` -> `sending`) so a duplicate click + * or a concurrent request can never send the same draft twice. On any + * failure past the claim, both the draft and lead revert to their + * pre-send state so the user can fix the issue and retry. + */ +export async function sendLeadScoutOutreachDraft( + draftId: string, +): Promise { + const db = getDb(); + const draft = await db.getLeadScoutOutreachDraft(draftId); + if (!draft) throw new Error(`Unknown outreach draft: ${draftId}`); + const lead = await db.getLeadScoutLead(draft.leadId); + if (!lead) throw new Error(`Unknown lead: ${draft.leadId}`); + const campaign = await db.getLeadScoutCampaign(lead.campaignId); + if (!campaign) throw new Error(`Unknown lead scout campaign: ${lead.campaignId}`); + const emailSettings = await db.getLeadScoutEmailSettings(lead.workspaceId); + if (!emailSettings) throw new Error("Configure email settings before sending outreach."); + + if (campaign.outreachMode !== "review_and_send") { + throw new Error("This campaign is in draft-only mode — sending is disabled."); + } + if (!lead.email) { + throw new Error("This lead has no email address — outreach is blocked until one is supplied."); + } + const suppressed = await db.getLeadScoutSuppressionMatch( + lead.workspaceId, + lead.email, + domainFromEmail(lead.email), + ); + if (suppressed) { + await db.claimLeadScoutOutreachDraftStatus({ id: draftId, fromStatus: "approved", toStatus: "rejected" }); + await db.claimLeadScoutLeadStatus({ id: lead.id, fromStatus: "approved_to_send", toStatus: "suppressed" }); + throw new Error(`This lead's email/domain is on the suppression list (${suppressed.reason}).`); + } + if (lead.status === "sent") { + throw new Error("This lead was already sent — use the explicit reset action before sending again."); + } + const sentSoFar = await countSentForCampaign(campaign.id); + if (sentSoFar >= emailSettings.perCampaignSendLimit) { + throw new Error(`This campaign's send limit (${emailSettings.perCampaignSendLimit}) has been reached.`); + } + + const claimedDraft = await db.claimLeadScoutOutreachDraftStatus({ + id: draftId, + fromStatus: "approved", + toStatus: "sending", + }); + if (!claimedDraft) throw new Error("This draft is no longer approved (already sent or reset)."); + const claimedLead = await db.claimLeadScoutLeadStatus({ + id: lead.id, + fromStatus: "approved_to_send", + toStatus: "sending", + }); + if (!claimedLead) { + // Lead moved on some other way (e.g. reset) between our reads and the + // claim above — revert the draft claim so nothing is left half-sent. + await db.claimLeadScoutOutreachDraftStatus({ id: draftId, fromStatus: "sending", toStatus: "approved" }); + throw new Error("Lead status changed concurrently — refresh and try again."); + } + + try { + await reserveLeadScoutDailySendSlot(lead.workspaceId); + await sendLeadScoutEmail(lead.workspaceId, { + to: lead.email, + subject: draft.subject ?? "A quick idea for your business", + text: draft.bodyText ?? "", + html: draft.bodyHtml ?? undefined, + }); + + const sent = await db.updateLeadScoutOutreachDraft(draftId, { status: "sent", sentAt: new Date() }); + await db.claimLeadScoutLeadStatus({ id: lead.id, fromStatus: "sending", toStatus: "sent" }); + + await logAudit({ + workspaceId: lead.workspaceId, + actor: "extension", + toolLabel: "local_lead_scout.send_email", + input: { draftId, leadId: lead.id }, + output: { sent: true }, + status: "success", + }); + await notifyWorkspaceOwner(lead.workspaceId, { + title: "Outreach email sent", + body: `${lead.businessName}: outreach email sent.`, + url: `/workspace/${lead.workspaceId}/extensions/local-lead-scout`, + tag: `lead-scout-send-${draftId}`, + }); + return sent; + } catch (err) { + const message = err instanceof Error ? err.message : String(err); + const failed = await db.updateLeadScoutOutreachDraft(draftId, { status: "failed", errorMessage: message }); + await db.claimLeadScoutLeadStatus({ id: lead.id, fromStatus: "sending", toStatus: "approved_to_send" }); + await logAudit({ + workspaceId: lead.workspaceId, + actor: "extension", + toolLabel: "local_lead_scout.send_email", + input: { draftId, leadId: lead.id }, + output: message, + status: "error", + }); + return failed; + } +} + +/** Explicit, manual escape hatch for "email this lead again" — a sent lead + * is otherwise permanently done (see the `lead.status === "sent"` check in + * sendLeadScoutOutreachDraft). Resets it to `reviewed` so the whole + * prototype -> draft -> approve -> send cycle can run again from scratch, + * rather than resurrecting old (possibly stale) prototype/draft rows. */ +export async function resetLeadScoutLeadForResend(leadId: string): Promise { + const db = getDb(); + const claimed = await db.claimLeadScoutLeadStatus({ id: leadId, fromStatus: "sent", toStatus: "reviewed" }); + if (!claimed) throw new Error("Only a previously-sent lead can be reset for resend."); + await logAudit({ + workspaceId: claimed.workspaceId, + actor: "extension", + toolLabel: "local_lead_scout.reset_for_resend", + input: { leadId }, + output: { status: "reviewed" }, + status: "success", + }); + return claimed; +} + +export async function addLeadScoutSuppression(input: { + workspaceId: string; + email?: string | null; + domain?: string | null; + reason: string; +}): Promise { + if (!input.email && !input.domain) { + throw new Error("A suppression entry needs at least an email or a domain."); + } + const record = await getDb().createLeadScoutSuppression(input); + await logAudit({ + workspaceId: input.workspaceId, + actor: "extension", + toolLabel: "local_lead_scout.add_suppression", + input: { email: input.email, domain: input.domain, reason: input.reason }, + output: { id: record.id }, + status: "success", + }); + return record; +} From 488622702345e59f21d4c91f870062b2324bcf8e Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 3 Jul 2026 19:13:35 +0000 Subject: [PATCH 10/13] feat: add lead scout tRPC router Wires campaigns, source configs, scans, leads, prototypes, outreach drafts, suppressions, and email settings into a leadScout tRPC namespace, following the existing workspaceProcedure/protectedProcedure + requireEntityWorkspaceOwner pattern. Client-facing reads through toClientSafeLeadScoutSourceConfig / toClientSafeLeadScoutEmailSettings so encrypted credentials never leave the server. Also applies biome formatting to the lead-scout modules added in prior commits. --- apps/server/src/lead-scout-email.ts | 27 +- apps/server/src/lead-scout-geocode.ts | 5 +- .../src/lead-scout-providers/custom-api.ts | 5 +- apps/server/src/lead-scout-providers/index.ts | 2 +- .../src/lead-scout-providers/manual-csv.ts | 5 +- .../src/lead-scout-providers/osm-overpass.ts | 11 +- .../server/src/lead-scout-providers/shared.ts | 4 +- apps/server/src/lead-scout-send.ts | 54 ++- apps/server/src/lead-scout.ts | 22 +- apps/server/src/trpc/router.ts | 350 ++++++++++++++++++ 10 files changed, 453 insertions(+), 32 deletions(-) diff --git a/apps/server/src/lead-scout-email.ts b/apps/server/src/lead-scout-email.ts index cf8f06c..ca88e70 100644 --- a/apps/server/src/lead-scout-email.ts +++ b/apps/server/src/lead-scout-email.ts @@ -1,6 +1,10 @@ -import nodemailer from "nodemailer"; -import type { LeadScoutEmailCredentials, LeadScoutEmailProvider, LeadScoutEmailSettingsRecord } from "@nyxel/db"; +import type { + LeadScoutEmailCredentials, + LeadScoutEmailProvider, + LeadScoutEmailSettingsRecord, +} from "@nyxel/db"; import { getDb } from "@nyxel/db"; +import nodemailer from "nodemailer"; import { logAudit } from "./audit"; /** @@ -57,7 +61,9 @@ function buildTransportForSmtp(credentials: LeadScoutEmailCredentials) { host: credentials.host, port: Number(credentials.port ?? 587), secure: credentials.secure === "true", - auth: credentials.username ? { user: credentials.username, pass: credentials.password } : undefined, + auth: credentials.username + ? { user: credentials.username, pass: credentials.password } + : undefined, }); } @@ -131,7 +137,8 @@ async function sendViaCustom( credentials: LeadScoutEmailCredentials, message: LeadScoutEmailMessage, ): Promise { - if (!credentials.webhookUrl) throw new Error("Custom email provider has no webhookUrl configured."); + if (!credentials.webhookUrl) + throw new Error("Custom email provider has no webhookUrl configured."); const res = await fetch(credentials.webhookUrl, { method: "POST", headers: { @@ -182,7 +189,9 @@ export async function sendLeadScoutEmail( } if (!settings.credentials && settings.provider !== "custom") { - throw new Error(`${settings.provider} isn't configured — add credentials in email settings first.`); + throw new Error( + `${settings.provider} isn't configured — add credentials in email settings first.`, + ); } const from = `${settings.fromName} <${settings.fromEmail}>`; @@ -243,7 +252,8 @@ export async function testLeadScoutEmailConnection( workspaceId: string, ): Promise<{ ok: boolean; message: string }> { const settings = await getDb().getLeadScoutEmailSettings(workspaceId); - if (!settings) return { ok: false, message: "Email settings aren't configured for this workspace yet." }; + if (!settings) + return { ok: false, message: "Email settings aren't configured for this workspace yet." }; const credentials = settings.credentials ?? {}; try { @@ -264,7 +274,10 @@ export async function testLeadScoutEmailConnection( }); if (!res.ok) throw new Error(`HTTP ${res.status}`); } else { - return { ok: false, message: "Custom providers have no built-in connection test — send a test email instead." }; + return { + ok: false, + message: "Custom providers have no built-in connection test — send a test email instead.", + }; } await logAudit({ workspaceId, diff --git a/apps/server/src/lead-scout-geocode.ts b/apps/server/src/lead-scout-geocode.ts index 9a6309e..b940b60 100644 --- a/apps/server/src/lead-scout-geocode.ts +++ b/apps/server/src/lead-scout-geocode.ts @@ -28,7 +28,10 @@ export interface GeocodeResult { lon: number; } -export async function geocodePostalCode(postalCode: string, country: string): Promise { +export async function geocodePostalCode( + postalCode: string, + country: string, +): Promise { await throttleNominatim(); const url = new URL("https://nominatim.openstreetmap.org/search"); url.searchParams.set("postalcode", postalCode); diff --git a/apps/server/src/lead-scout-providers/custom-api.ts b/apps/server/src/lead-scout-providers/custom-api.ts index 5035aa7..e782e9a 100644 --- a/apps/server/src/lead-scout-providers/custom-api.ts +++ b/apps/server/src/lead-scout-providers/custom-api.ts @@ -1,5 +1,5 @@ -import type { LeadSourceProvider, LeadSourceSearchInput } from "./types"; import { classifyWebsite } from "./shared"; +import type { LeadSourceProvider, LeadSourceSearchInput } from "./types"; /** Minimal generic REST adapter placeholder — expects a compliant endpoint * (configured per-workspace as sourceConfig.config.baseUrl) that returns a @@ -69,7 +69,8 @@ export const customApiProvider: LeadSourceProvider = { websiteStatus === "missing_website" ? "custom_api response had no website value for this business." : "custom_api reported a website for this business.", - missingReason: websiteStatus === "missing_website" ? "No website field in the custom API response." : null, + missingReason: + websiteStatus === "missing_website" ? "No website field in the custom API response." : null, }; }, }; diff --git a/apps/server/src/lead-scout-providers/index.ts b/apps/server/src/lead-scout-providers/index.ts index f642378..076f3c2 100644 --- a/apps/server/src/lead-scout-providers/index.ts +++ b/apps/server/src/lead-scout-providers/index.ts @@ -5,8 +5,8 @@ import { manualCsvProvider } from "./manual-csv"; import { osmOverpassProvider } from "./osm-overpass"; import type { LeadSourceProvider } from "./types"; -export { collectNormalizedBusinesses } from "./types"; export type { LeadSourceProvider, LeadSourceSearchInput, NormalizedBusiness } from "./types"; +export { collectNormalizedBusinesses } from "./types"; const PROVIDERS: Record = { manual_csv: manualCsvProvider, diff --git a/apps/server/src/lead-scout-providers/manual-csv.ts b/apps/server/src/lead-scout-providers/manual-csv.ts index 944c475..ec0b936 100644 --- a/apps/server/src/lead-scout-providers/manual-csv.ts +++ b/apps/server/src/lead-scout-providers/manual-csv.ts @@ -1,6 +1,6 @@ import { createHash } from "node:crypto"; -import type { LeadSourceProvider, LeadSourceSearchInput } from "./types"; import { classifyWebsite, parseCsv } from "./shared"; +import type { LeadSourceProvider, LeadSourceSearchInput } from "./types"; /** businessName is the only strictly required column — everything else is * optional per the extension's field list (businessName, address, @@ -74,7 +74,8 @@ export const manualCsvProvider: LeadSourceProvider; } -function buildOverpassQuery(bbox: { south: number; west: number; north: number; east: number }): string { +function buildOverpassQuery(bbox: { + south: number; + west: number; + north: number; + east: number; +}): string { const bounds = `${bbox.south},${bbox.west},${bbox.north},${bbox.east}`; // nwr matches node/way/relation in one clause per OSM Overpass QL — one // clause per top-level key the extension is allowed to query (shop, @@ -105,7 +110,9 @@ export const osmOverpassProvider: LeadSourceProvider = { website ? `OSM tags include a website/contact:website value (${website}).` : "No website or contact:website tag present on this OSM element.", - missingEmail ? "No email or contact:email tag present (missing_email) — outreach needs a manually supplied address." : null, + missingEmail + ? "No email or contact:email tag present (missing_email) — outreach needs a manually supplied address." + : null, ] .filter(Boolean) .join(" "), diff --git a/apps/server/src/lead-scout-providers/shared.ts b/apps/server/src/lead-scout-providers/shared.ts index 46972fb..05ec4b1 100644 --- a/apps/server/src/lead-scout-providers/shared.ts +++ b/apps/server/src/lead-scout-providers/shared.ts @@ -68,7 +68,9 @@ export function parseCsv(text: string): Record[] { } if (field.length > 0 || row.length > 0) pushRow(); - const [header, ...dataRows] = rows.filter((r) => r.length > 0 && !(r.length === 1 && r[0] === "")); + const [header, ...dataRows] = rows.filter( + (r) => r.length > 0 && !(r.length === 1 && r[0] === ""), + ); if (!header) return []; return dataRows.map((cells) => { const record: Record = {}; diff --git a/apps/server/src/lead-scout-send.ts b/apps/server/src/lead-scout-send.ts index 0bf19b8..b38e4f6 100644 --- a/apps/server/src/lead-scout-send.ts +++ b/apps/server/src/lead-scout-send.ts @@ -1,4 +1,8 @@ -import type { LeadScoutLeadRecord, LeadScoutOutreachDraftRecord, LeadScoutSuppressionRecord } from "@nyxel/db"; +import type { + LeadScoutLeadRecord, + LeadScoutOutreachDraftRecord, + LeadScoutSuppressionRecord, +} from "@nyxel/db"; import { getDb } from "@nyxel/db"; import { logAudit } from "./audit"; import { reserveLeadScoutDailySendSlot, sendLeadScoutEmail } from "./lead-scout-email"; @@ -121,16 +125,28 @@ export async function sendLeadScoutOutreachDraft( domainFromEmail(lead.email), ); if (suppressed) { - await db.claimLeadScoutOutreachDraftStatus({ id: draftId, fromStatus: "approved", toStatus: "rejected" }); - await db.claimLeadScoutLeadStatus({ id: lead.id, fromStatus: "approved_to_send", toStatus: "suppressed" }); + await db.claimLeadScoutOutreachDraftStatus({ + id: draftId, + fromStatus: "approved", + toStatus: "rejected", + }); + await db.claimLeadScoutLeadStatus({ + id: lead.id, + fromStatus: "approved_to_send", + toStatus: "suppressed", + }); throw new Error(`This lead's email/domain is on the suppression list (${suppressed.reason}).`); } if (lead.status === "sent") { - throw new Error("This lead was already sent — use the explicit reset action before sending again."); + throw new Error( + "This lead was already sent — use the explicit reset action before sending again.", + ); } const sentSoFar = await countSentForCampaign(campaign.id); if (sentSoFar >= emailSettings.perCampaignSendLimit) { - throw new Error(`This campaign's send limit (${emailSettings.perCampaignSendLimit}) has been reached.`); + throw new Error( + `This campaign's send limit (${emailSettings.perCampaignSendLimit}) has been reached.`, + ); } const claimedDraft = await db.claimLeadScoutOutreachDraftStatus({ @@ -147,7 +163,11 @@ export async function sendLeadScoutOutreachDraft( if (!claimedLead) { // Lead moved on some other way (e.g. reset) between our reads and the // claim above — revert the draft claim so nothing is left half-sent. - await db.claimLeadScoutOutreachDraftStatus({ id: draftId, fromStatus: "sending", toStatus: "approved" }); + await db.claimLeadScoutOutreachDraftStatus({ + id: draftId, + fromStatus: "sending", + toStatus: "approved", + }); throw new Error("Lead status changed concurrently — refresh and try again."); } @@ -160,7 +180,10 @@ export async function sendLeadScoutOutreachDraft( html: draft.bodyHtml ?? undefined, }); - const sent = await db.updateLeadScoutOutreachDraft(draftId, { status: "sent", sentAt: new Date() }); + const sent = await db.updateLeadScoutOutreachDraft(draftId, { + status: "sent", + sentAt: new Date(), + }); await db.claimLeadScoutLeadStatus({ id: lead.id, fromStatus: "sending", toStatus: "sent" }); await logAudit({ @@ -180,8 +203,15 @@ export async function sendLeadScoutOutreachDraft( return sent; } catch (err) { const message = err instanceof Error ? err.message : String(err); - const failed = await db.updateLeadScoutOutreachDraft(draftId, { status: "failed", errorMessage: message }); - await db.claimLeadScoutLeadStatus({ id: lead.id, fromStatus: "sending", toStatus: "approved_to_send" }); + const failed = await db.updateLeadScoutOutreachDraft(draftId, { + status: "failed", + errorMessage: message, + }); + await db.claimLeadScoutLeadStatus({ + id: lead.id, + fromStatus: "sending", + toStatus: "approved_to_send", + }); await logAudit({ workspaceId: lead.workspaceId, actor: "extension", @@ -201,7 +231,11 @@ export async function sendLeadScoutOutreachDraft( * rather than resurrecting old (possibly stale) prototype/draft rows. */ export async function resetLeadScoutLeadForResend(leadId: string): Promise { const db = getDb(); - const claimed = await db.claimLeadScoutLeadStatus({ id: leadId, fromStatus: "sent", toStatus: "reviewed" }); + const claimed = await db.claimLeadScoutLeadStatus({ + id: leadId, + fromStatus: "sent", + toStatus: "reviewed", + }); if (!claimed) throw new Error("Only a previously-sent lead can be reset for resend."); await logAudit({ workspaceId: claimed.workspaceId, diff --git a/apps/server/src/lead-scout.ts b/apps/server/src/lead-scout.ts index 856c2b7..41819a6 100644 --- a/apps/server/src/lead-scout.ts +++ b/apps/server/src/lead-scout.ts @@ -168,7 +168,9 @@ async function pickDefaultModelId(workspaceId: string): Promise { const models = await listAvailableModels(providers); const [first] = models; if (!first) { - throw new Error("No models are installed for this workspace — add one in Settings before generating content."); + throw new Error( + "No models are installed for this workspace — add one in Settings before generating content.", + ); } return first.id; } @@ -235,7 +237,7 @@ const PROTOTYPE_OUTPUT_FORMAT = [ "CTA: ", "STYLE: ", "---ARTIFACT---", - "", + '', ].join("\n"); /** @@ -246,7 +248,9 @@ const PROTOTYPE_OUTPUT_FORMAT = [ * the lead must already be "reviewed" (a human looked at it) before a * prototype can be requested at all. */ -export async function dispatchLeadScoutPrototype(leadId: string): Promise { +export async function dispatchLeadScoutPrototype( + leadId: string, +): Promise { const db = getDb(); const lead = await db.getLeadScoutLead(leadId); if (!lead) throw new Error(`Unknown lead: ${leadId}`); @@ -341,7 +345,9 @@ export async function dispatchLeadScoutPrototype(leadId: string): Promise { +export async function approveLeadScoutPrototype( + prototypeId: string, +): Promise { const db = getDb(); const prototype = await db.getLeadScoutPrototype(prototypeId); if (!prototype) throw new Error(`Unknown prototype: ${prototypeId}`); @@ -372,7 +378,9 @@ export function parseEmailDraftOutput(output: string): { subject: string; body: * programmatically from email settings after generation, rather than left * to the model, so they're always present regardless of what it wrote. */ -export async function dispatchLeadScoutEmailDraft(leadId: string): Promise { +export async function dispatchLeadScoutEmailDraft( + leadId: string, +): Promise { const db = getDb(); const lead = await db.getLeadScoutLead(leadId); if (!lead) throw new Error(`Unknown lead: ${leadId}`); @@ -429,7 +437,9 @@ export async function dispatchLeadScoutEmailDraft(leadId: string): Promise 0 }; } +// Same rationale — a lead scout source config's apiKey (google_places_api / +// custom_api) never needs to leave the server. +export function toClientSafeLeadScoutSourceConfig(config: LeadScoutSourceConfigRecord) { + const { apiKey, ...rest } = config; + return { ...rest, hasApiKey: apiKey !== null && apiKey.length > 0 }; +} + export const appRouter = router({ health: publicProcedure.query(() => ({ ok: true, name: "nyxel-server" })), @@ -2710,6 +2739,327 @@ export const appRouter = router({ }), }), + leadScout: router({ + listCampaigns: workspaceProcedure + .input(z.object({ workspaceId: z.string() })) + .query(({ input }) => getDb().listLeadScoutCampaignsByWorkspace(input.workspaceId)), + createCampaign: workspaceProcedure + .input( + z.object({ + workspaceId: z.string(), + name: z.string().min(1), + postalCode: z.string().min(1), + country: z.string().min(1).optional(), + radiusKm: z.number().positive().max(200).optional(), + niches: z.array(z.string()).optional(), + maxResultsPerRun: z.number().int().min(1).max(200).optional(), + provider: z.enum(["manual_csv", "google_places_api", "osm_overpass", "custom_api"]), + minConfidence: z.number().int().min(0).max(100).optional(), + outreachMode: z.enum(["draft_only", "review_and_send"]).optional(), + }), + ) + .mutation(async ({ input }) => { + const db = getDb(); + const ext = await db.getExtensionByKey(input.workspaceId, "local-lead-scout"); + if (!ext) + throw new Error("The Local Lead Scout extension isn't installed in this workspace."); + const { workspaceId, ...rest } = input; + return db.createLeadScoutCampaign({ workspaceId, extensionId: ext.id, ...rest }); + }), + updateCampaign: protectedProcedure + .input( + z.object({ + id: z.string(), + name: z.string().min(1).optional(), + postalCode: z.string().min(1).optional(), + country: z.string().min(1).optional(), + radiusKm: z.number().positive().max(200).optional(), + niches: z.array(z.string()).optional(), + maxResultsPerRun: z.number().int().min(1).max(200).optional(), + provider: z + .enum(["manual_csv", "google_places_api", "osm_overpass", "custom_api"]) + .optional(), + minConfidence: z.number().int().min(0).max(100).optional(), + outreachMode: z.enum(["draft_only", "review_and_send"]).optional(), + autoGeneratePrototype: z.boolean().optional(), + autoDraftEmail: z.boolean().optional(), + autoSendAfterApproval: z.boolean().optional(), + requireApprovalBeforePrototype: z.boolean().optional(), + requireApprovalBeforeEmailSend: z.boolean().optional(), + prototypeAgentId: z.string().nullable().optional(), + }), + ) + .mutation(async ({ input, ctx }) => { + await requireEntityWorkspaceOwner( + ctx.user.id, + () => getDb().getLeadScoutCampaign(input.id), + "Lead scout campaign not found", + ); + const { id, ...patch } = input; + return getDb().updateLeadScoutCampaign(id, patch); + }), + deleteCampaign: protectedProcedure + .input(z.object({ id: z.string() })) + .mutation(async ({ input, ctx }) => { + await requireEntityWorkspaceOwner( + ctx.user.id, + () => getDb().getLeadScoutCampaign(input.id), + "Lead scout campaign not found", + ); + return getDb().deleteLeadScoutCampaign(input.id); + }), + setCampaignSchedule: protectedProcedure + .input(z.object({ id: z.string(), cronExpression: z.string().nullable() })) + .mutation(async ({ input, ctx }) => { + const campaign = await requireEntityWorkspaceOwner( + ctx.user.id, + () => getDb().getLeadScoutCampaign(input.id), + "Lead scout campaign not found", + ); + if (input.cronExpression === null) { + return getDb().updateLeadScoutCampaign(input.id, { + scheduleEnabled: false, + scheduleCronExpression: null, + nextScanAt: null, + }); + } + if (campaign.provider === "manual_csv") { + throw new Error( + "manual_csv campaigns can't run on a schedule — there's no CSV to read automatically.", + ); + } + const nextScanAt = computeNextRunAt(input.cronExpression, new Date()); + if (!nextScanAt) + throw new Error(`"${input.cronExpression}" is not a valid cron expression.`); + return getDb().updateLeadScoutCampaign(input.id, { + scheduleEnabled: true, + scheduleCronExpression: input.cronExpression, + nextScanAt, + }); + }), + + listSourceConfigs: workspaceProcedure + .input(z.object({ workspaceId: z.string() })) + .query(async ({ input }) => { + const configs = await getDb().listLeadScoutSourceConfigsByWorkspace(input.workspaceId); + return configs.map(toClientSafeLeadScoutSourceConfig); + }), + upsertSourceConfig: workspaceProcedure + .input( + z.object({ + workspaceId: z.string(), + provider: z.enum(["manual_csv", "google_places_api", "osm_overpass", "custom_api"]), + config: z.record(z.string(), z.unknown()).optional(), + apiKey: z.string().nullable().optional(), + enabled: z.boolean().optional(), + }), + ) + .mutation(async ({ input }) => { + const record = await getDb().upsertLeadScoutSourceConfig(input); + await logAudit({ + workspaceId: input.workspaceId, + actor: "extension", + toolLabel: "local_lead_scout.source_config.update", + input: { provider: input.provider }, + output: { hasApiKey: Boolean(input.apiKey) }, + status: "success", + }); + return toClientSafeLeadScoutSourceConfig(record); + }), + + runScan: protectedProcedure + .input(z.object({ campaignId: z.string(), csvText: z.string().optional() })) + .mutation(async ({ input, ctx }) => { + await requireEntityWorkspaceOwner( + ctx.user.id, + () => getDb().getLeadScoutCampaign(input.campaignId), + "Lead scout campaign not found", + ); + return runLeadScoutScan(input.campaignId, { csvText: input.csvText }); + }), + listScanRuns: protectedProcedure + .input(z.object({ campaignId: z.string() })) + .query(async ({ input, ctx }) => { + await requireEntityWorkspaceOwner( + ctx.user.id, + () => getDb().getLeadScoutCampaign(input.campaignId), + "Lead scout campaign not found", + ); + return getDb().listLeadScoutScanRunsByCampaign(input.campaignId); + }), + + listLeads: protectedProcedure + .input(z.object({ campaignId: z.string() })) + .query(async ({ input, ctx }) => { + await requireEntityWorkspaceOwner( + ctx.user.id, + () => getDb().getLeadScoutCampaign(input.campaignId), + "Lead scout campaign not found", + ); + return getDb().listLeadScoutLeadsByCampaign(input.campaignId); + }), + getLead: protectedProcedure + .input(z.object({ id: z.string() })) + .query(async ({ input, ctx }) => { + return requireEntityWorkspaceOwner( + ctx.user.id, + () => getDb().getLeadScoutLead(input.id), + "Lead not found", + ); + }), + markLeadReviewed: protectedProcedure + .input(z.object({ id: z.string() })) + .mutation(async ({ input, ctx }) => { + await requireEntityWorkspaceOwner( + ctx.user.id, + () => getDb().getLeadScoutLead(input.id), + "Lead not found", + ); + await markLeadScoutLeadReviewed(input.id); + return getDb().getLeadScoutLead(input.id); + }), + resetLeadForResend: protectedProcedure + .input(z.object({ id: z.string() })) + .mutation(async ({ input, ctx }) => { + await requireEntityWorkspaceOwner( + ctx.user.id, + () => getDb().getLeadScoutLead(input.id), + "Lead not found", + ); + return resetLeadScoutLeadForResend(input.id); + }), + + listPrototypes: protectedProcedure + .input(z.object({ leadId: z.string() })) + .query(async ({ input, ctx }) => { + await requireEntityWorkspaceOwner( + ctx.user.id, + () => getDb().getLeadScoutLead(input.leadId), + "Lead not found", + ); + return getDb().listLeadScoutPrototypesByLead(input.leadId); + }), + generatePrototype: protectedProcedure + .input(z.object({ leadId: z.string() })) + .mutation(async ({ input, ctx }) => { + await requireEntityWorkspaceOwner( + ctx.user.id, + () => getDb().getLeadScoutLead(input.leadId), + "Lead not found", + ); + return dispatchLeadScoutPrototype(input.leadId); + }), + approvePrototype: protectedProcedure + .input(z.object({ id: z.string() })) + .mutation(async ({ input, ctx }) => { + await requireEntityWorkspaceOwner( + ctx.user.id, + () => getDb().getLeadScoutPrototype(input.id), + "Prototype not found", + ); + return approveLeadScoutPrototype(input.id); + }), + + listDrafts: protectedProcedure + .input(z.object({ leadId: z.string() })) + .query(async ({ input, ctx }) => { + await requireEntityWorkspaceOwner( + ctx.user.id, + () => getDb().getLeadScoutLead(input.leadId), + "Lead not found", + ); + return getDb().listLeadScoutOutreachDraftsByLead(input.leadId); + }), + generateDraft: protectedProcedure + .input(z.object({ leadId: z.string() })) + .mutation(async ({ input, ctx }) => { + await requireEntityWorkspaceOwner( + ctx.user.id, + () => getDb().getLeadScoutLead(input.leadId), + "Lead not found", + ); + return dispatchLeadScoutEmailDraft(input.leadId); + }), + approveDraft: protectedProcedure + .input(z.object({ id: z.string() })) + .mutation(async ({ input, ctx }) => { + await requireEntityWorkspaceOwner( + ctx.user.id, + () => getDb().getLeadScoutOutreachDraft(input.id), + "Outreach draft not found", + ); + return approveLeadScoutOutreachDraft(input.id); + }), + rejectDraft: protectedProcedure + .input(z.object({ id: z.string() })) + .mutation(async ({ input, ctx }) => { + await requireEntityWorkspaceOwner( + ctx.user.id, + () => getDb().getLeadScoutOutreachDraft(input.id), + "Outreach draft not found", + ); + return rejectLeadScoutOutreachDraft(input.id); + }), + sendDraft: protectedProcedure + .input(z.object({ id: z.string() })) + .mutation(async ({ input, ctx }) => { + await requireEntityWorkspaceOwner( + ctx.user.id, + () => getDb().getLeadScoutOutreachDraft(input.id), + "Outreach draft not found", + ); + return sendLeadScoutOutreachDraft(input.id); + }), + + listSuppressions: workspaceProcedure + .input(z.object({ workspaceId: z.string() })) + .query(({ input }) => getDb().listLeadScoutSuppressionsByWorkspace(input.workspaceId)), + addSuppression: workspaceProcedure + .input( + z.object({ + workspaceId: z.string(), + email: z.string().email().nullable().optional(), + domain: z.string().nullable().optional(), + reason: z.string().min(1), + }), + ) + .mutation(({ input }) => addLeadScoutSuppression(input)), + + getEmailSettings: workspaceProcedure + .input(z.object({ workspaceId: z.string() })) + .query(async ({ input }) => { + const settings = await getLeadScoutEmailSettings(input.workspaceId); + return settings ? toClientSafeLeadScoutEmailSettings(settings) : null; + }), + upsertEmailSettings: workspaceProcedure + .input( + z.object({ + workspaceId: z.string(), + provider: z.enum(["smtp", "resend", "mailgun", "custom"]).optional(), + fromName: z.string().min(1), + fromEmail: z.string().email(), + replyTo: z.string().email().nullable().optional(), + credentials: z.record(z.string(), z.string()).nullable().optional(), + dailySendLimit: z.number().int().min(0).max(1000).optional(), + perCampaignSendLimit: z.number().int().min(0).max(1000).optional(), + dryRunMode: z.boolean().optional(), + legalFooter: z.string().nullable().optional(), + unsubscribeText: z.string().min(1).optional(), + }), + ) + .mutation(async ({ input }) => { + const { workspaceId, ...rest } = input; + const settings = await upsertLeadScoutEmailSettings(workspaceId, rest); + return toClientSafeLeadScoutEmailSettings(settings); + }), + testEmailConnection: workspaceProcedure + .input(z.object({ workspaceId: z.string() })) + .mutation(({ input }) => testLeadScoutEmailConnection(input.workspaceId)), + sendTestEmail: workspaceProcedure + .input(z.object({ workspaceId: z.string(), toEmail: z.string().email() })) + .mutation(({ input }) => sendLeadScoutTestEmail(input.workspaceId, input.toEmail)), + }), + automations: router({ list: workspaceProcedure .input(z.object({ workspaceId: z.string() })) From 77a680717ad1b1eed9d89d89bfa57d1a4ed5b697 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 3 Jul 2026 19:33:00 +0000 Subject: [PATCH 11/13] feat: add local lead scout web UI MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds the extension page (campaigns, run-scan/CSV-import control, leads table, lead detail with prototype/draft review and approve/reject/send actions, scan history, suppression list, and email settings) plus the hand-written trpc.ts client contract for the new leadScout router. Wires it into the shared extensions/[key] page. Verified end-to-end in a real browser: install -> create campaign -> CSV scan -> lead detail -> mark reviewed -> email settings save/test. Fixes a bug found along the way: the email settings form initialized its fields from the query result before it had loaded, so previously saved settings appeared blank until edited — now synced in via a targeted effect keyed on the settings row id. --- .../[workspaceId]/extensions/[key]/page.tsx | 6 +- .../extensions/local-lead-scout-page.tsx | 934 ++++++++++++++++++ apps/web/src/lib/trpc.ts | 313 ++++++ 3 files changed, 1251 insertions(+), 2 deletions(-) create mode 100644 apps/web/src/components/extensions/local-lead-scout-page.tsx diff --git a/apps/web/src/app/workspace/[workspaceId]/extensions/[key]/page.tsx b/apps/web/src/app/workspace/[workspaceId]/extensions/[key]/page.tsx index 244e480..25a952e 100644 --- a/apps/web/src/app/workspace/[workspaceId]/extensions/[key]/page.tsx +++ b/apps/web/src/app/workspace/[workspaceId]/extensions/[key]/page.tsx @@ -3,10 +3,11 @@ import { useQuery } from "@tanstack/react-query"; import { useParams } from "next/navigation"; import type { ComponentType } from "react"; -import { PageHeaderSkeleton } from "@/components/loading"; -import { PageHeader } from "@/components/page-header"; +import { LocalLeadScoutExtensionPage } from "@/components/extensions/local-lead-scout-page"; import { SeoAnalyzerExtensionPage } from "@/components/extensions/seo-analyzer-page"; import { VideoStudioExtensionPage } from "@/components/extensions/video-studio-page"; +import { PageHeaderSkeleton } from "@/components/loading"; +import { PageHeader } from "@/components/page-header"; import { trpcClient } from "@/lib/trpc"; /** Per-extension page component, keyed by ExtensionCatalogEntry.route (see @@ -15,6 +16,7 @@ import { trpcClient } from "@/lib/trpc"; const EXTENSION_PAGES: Record> = { "seo-analyzer": SeoAnalyzerExtensionPage, "video-studio": VideoStudioExtensionPage, + "local-lead-scout": LocalLeadScoutExtensionPage, }; export default function ExtensionDetailPage() { diff --git a/apps/web/src/components/extensions/local-lead-scout-page.tsx b/apps/web/src/components/extensions/local-lead-scout-page.tsx new file mode 100644 index 0000000..00a7436 --- /dev/null +++ b/apps/web/src/components/extensions/local-lead-scout-page.tsx @@ -0,0 +1,934 @@ +"use client"; + +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; +import { useEffect, useState } from "react"; +import { PageHeader } from "@/components/page-header"; +import { Badge } from "@/components/ui/badge"; +import { Button } from "@/components/ui/button"; +import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { Switch } from "@/components/ui/switch"; +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; +import { Textarea } from "@/components/ui/textarea"; +import { + type LeadScoutCampaignSummary, + type LeadScoutDraftStatus, + type LeadScoutEmailProvider, + type LeadScoutLeadStatus, + type LeadScoutLeadSummary, + type LeadScoutProvider, + type LeadScoutWebsiteStatus, + trpcClient, +} from "@/lib/trpc"; + +const PROVIDER_LABEL: Record = { + manual_csv: "Manual CSV import", + google_places_api: "Google Places API", + osm_overpass: "OSM Overpass (free)", + custom_api: "Custom API", +}; + +const WEBSITE_STATUS_BADGE: Record = { + unknown: "bg-muted text-muted-foreground", + has_website: "bg-emerald-500/15 text-emerald-600", + missing_website: "bg-amber-500/15 text-amber-600", + invalid_website: "bg-destructive/15 text-destructive", +}; + +const LEAD_STATUS_LABEL: Record = { + new: "New", + reviewed: "Reviewed", + prototype_requested: "Prototype pending", + prototype_ready: "Prototype ready", + email_drafted: "Email draft pending", + approved_to_send: "Approval pending", + sending: "Sending", + sent: "Sent", + rejected: "Rejected", + suppressed: "Suppressed", +}; + +const DRAFT_STATUS_LABEL: Record = { + draft: "Draft", + approved: "Approved — ready to send", + rejected: "Rejected", + sending: "Sending", + sent: "Sent", + failed: "Failed", +}; + +function CreateCampaignForm({ + workspaceId, + onCreated, +}: { + workspaceId: string; + onCreated: () => void; +}) { + const [name, setName] = useState(""); + const [postalCode, setPostalCode] = useState(""); + const [country, setCountry] = useState("US"); + const [radiusKm, setRadiusKm] = useState("10"); + const [niches, setNiches] = useState(""); + const [maxResults, setMaxResults] = useState("25"); + const [provider, setProvider] = useState("manual_csv"); + const [minConfidence, setMinConfidence] = useState("50"); + + const createCampaign = useMutation({ + mutationFn: () => + trpcClient.leadScout.createCampaign.mutate({ + workspaceId, + name, + postalCode, + country, + radiusKm: Number(radiusKm), + niches: niches + .split(",") + .map((n) => n.trim()) + .filter(Boolean), + maxResultsPerRun: Number(maxResults), + provider, + minConfidence: Number(minConfidence), + }), + onSuccess: () => { + setName(""); + setPostalCode(""); + setNiches(""); + onCreated(); + }, + }); + + return ( +
+
+

New campaign

+

+ Finds local businesses with no website in a region, using a compliant source only. You are + responsible for lawful outreach and consent requirements for whatever you contact. +

+
+
+
+ + setName(e.target.value)} /> +
+
+ + setPostalCode(e.target.value)} + /> +
+
+ + setCountry(e.target.value)} /> +
+
+ + setRadiusKm(e.target.value)} + /> +
+
+ + setNiches(e.target.value)} + /> +
+
+ + +
+
+ + setMaxResults(e.target.value)} + /> +
+
+ + setMinConfidence(e.target.value)} + /> +
+
+ {provider === "google_places_api" && ( +

+ You are responsible for complying with the Google Maps Platform Terms of Service for any + data retrieved through this provider. Configure an API key in Source Settings first. +

+ )} + {createCampaign.error && ( +

{(createCampaign.error as Error).message}

+ )} + +
+ ); +} + +function RunScanControl({ campaign }: { campaign: LeadScoutCampaignSummary }) { + const queryClient = useQueryClient(); + const [csvText, setCsvText] = useState(""); + + const runScan = useMutation({ + mutationFn: () => + trpcClient.leadScout.runScan.mutate({ + campaignId: campaign.id, + csvText: campaign.provider === "manual_csv" ? csvText : undefined, + }), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ["leadScout", "scanRuns", campaign.id] }); + queryClient.invalidateQueries({ queryKey: ["leadScout", "leads", campaign.id] }); + }, + }); + + return ( +
+

Run scan — {PROVIDER_LABEL[campaign.provider]}

+ {campaign.provider === "manual_csv" ? ( + <> +

+ CSV columns: businessName, address, postalCode, city, category, phone, email, website, + notes. +

+