From e57768a8ce512c56209497c5ba059b27e54d70ce Mon Sep 17 00:00:00 2001 From: Ben Senescu <44480372+bensenescu@users.noreply.github.com> Date: Fri, 19 Jun 2026 13:20:52 -0400 Subject: [PATCH] Add onboarding chat helper --- drizzle/0024_clumsy_barracuda.sql | 4 + drizzle/meta/0024_snapshot.json | 2648 +++++++++++++++++ drizzle/meta/_journal.json | 7 + package.json | 3 + pnpm-lock.yaml | 82 +- specs/0005-onboarding-agent.md | 157 + specs/0006-onboarding-agent-implementation.md | 151 + src/client/components/Markdown.tsx | 141 + .../ai-search/components/MarkdownAnswer.tsx | 124 +- .../onboarding/OnboardingStrategy.tsx | 125 + .../onboarding/OnboardingStrategyChat.tsx | 259 ++ .../OnboardingStrategyChatParts.tsx | 339 +++ .../onboarding/PostSignupOnboarding.tsx | 51 + .../features/onboarding/onboardingModel.ts | 2 +- .../onboarding/onboardingStrategyQueries.ts | 13 + src/client/layout/AppShell.tsx | 29 +- src/client/navigation/items.ts | 9 - src/db/app.schema.ts | 8 + src/env.d.ts | 10 + src/middleware/ensure-user/delegated.ts | 2 + src/middleware/ensure-user/hosted.ts | 2 + src/middleware/ensure-user/resolve.ts | 22 + src/middleware/ensure-user/types.ts | 4 + src/middleware/ensureUser.ts | 23 +- src/routeTree.gen.ts | 86 +- src/routes/_authenticated.onboarding.chat.tsx | 6 + ...sx => _authenticated.onboarding.index.tsx} | 27 +- src/routes/api/gsc/oauth/callback.ts | 20 +- src/routes/api/onboarding/chat.ts | 162 + .../features/domain/services/DomainService.ts | 13 + .../features/onboarding/openseo-fact-sheet.md | 129 + src/server/features/onboarding/scrape.test.ts | 28 + src/server/features/onboarding/scrape.ts | 175 ++ src/server/features/onboarding/seed.ts | 144 + src/server/features/onboarding/synthesis.ts | 91 + src/server/lib/dataforseo/client.ts | 20 +- src/server/lib/http-errors.ts | 33 + src/server/lib/openrouter.ts | 18 + src/serverFunctions/middleware.ts | 1 + src/serverFunctions/onboardingStrategy.ts | 78 + src/shared/billing-credit-features.ts | 4 +- src/shared/onboardingChat.ts | 5 + 42 files changed, 5005 insertions(+), 250 deletions(-) create mode 100644 drizzle/0024_clumsy_barracuda.sql create mode 100644 drizzle/meta/0024_snapshot.json create mode 100644 specs/0005-onboarding-agent.md create mode 100644 specs/0006-onboarding-agent-implementation.md create mode 100644 src/client/components/Markdown.tsx create mode 100644 src/client/features/onboarding/OnboardingStrategy.tsx create mode 100644 src/client/features/onboarding/OnboardingStrategyChat.tsx create mode 100644 src/client/features/onboarding/OnboardingStrategyChatParts.tsx create mode 100644 src/client/features/onboarding/onboardingStrategyQueries.ts create mode 100644 src/middleware/ensure-user/resolve.ts create mode 100644 src/routes/_authenticated.onboarding.chat.tsx rename src/routes/{_authenticated.onboarding.tsx => _authenticated.onboarding.index.tsx} (89%) create mode 100644 src/routes/api/onboarding/chat.ts create mode 100644 src/server/features/onboarding/openseo-fact-sheet.md create mode 100644 src/server/features/onboarding/scrape.test.ts create mode 100644 src/server/features/onboarding/scrape.ts create mode 100644 src/server/features/onboarding/seed.ts create mode 100644 src/server/features/onboarding/synthesis.ts create mode 100644 src/server/lib/http-errors.ts create mode 100644 src/server/lib/openrouter.ts create mode 100644 src/serverFunctions/onboardingStrategy.ts create mode 100644 src/shared/onboardingChat.ts diff --git a/drizzle/0024_clumsy_barracuda.sql b/drizzle/0024_clumsy_barracuda.sql new file mode 100644 index 0000000..8506533 --- /dev/null +++ b/drizzle/0024_clumsy_barracuda.sql @@ -0,0 +1,4 @@ +ALTER TABLE `projects` ADD `location_code` integer DEFAULT 2840 NOT NULL;--> statement-breakpoint +ALTER TABLE `projects` ADD `language_code` text DEFAULT 'en' NOT NULL;--> statement-breakpoint +ALTER TABLE `projects` ADD `onboarding_run_status` text;--> statement-breakpoint +ALTER TABLE `projects` ADD `onboarding_run_at` text; \ No newline at end of file diff --git a/drizzle/meta/0024_snapshot.json b/drizzle/meta/0024_snapshot.json new file mode 100644 index 0000000..73f2543 --- /dev/null +++ b/drizzle/meta/0024_snapshot.json @@ -0,0 +1,2648 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "d46e8c77-32f4-4340-a658-9577be35cba0", + "prevId": "79c80d4c-cd3a-4cd6-a32c-ce9c648ef9cb", + "tables": { + "audit_lighthouse_results": { + "name": "audit_lighthouse_results", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "audit_id": { + "name": "audit_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "page_id": { + "name": "page_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "strategy": { + "name": "strategy", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "performance_score": { + "name": "performance_score", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "accessibility_score": { + "name": "accessibility_score", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "best_practices_score": { + "name": "best_practices_score", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "seo_score": { + "name": "seo_score", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "lcp_ms": { + "name": "lcp_ms", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cls": { + "name": "cls", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "inp_ms": { + "name": "inp_ms", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ttfb_ms": { + "name": "ttfb_ms", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "r2_key": { + "name": "r2_key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "payload_size_bytes": { + "name": "payload_size_bytes", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "audit_lighthouse_results_audit_id_idx": { + "name": "audit_lighthouse_results_audit_id_idx", + "columns": [ + "audit_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "audit_lighthouse_results_audit_id_audits_id_fk": { + "name": "audit_lighthouse_results_audit_id_audits_id_fk", + "tableFrom": "audit_lighthouse_results", + "tableTo": "audits", + "columnsFrom": [ + "audit_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "audit_lighthouse_results_page_id_audit_pages_id_fk": { + "name": "audit_lighthouse_results_page_id_audit_pages_id_fk", + "tableFrom": "audit_lighthouse_results", + "tableTo": "audit_pages", + "columnsFrom": [ + "page_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "audit_pages": { + "name": "audit_pages", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "audit_id": { + "name": "audit_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status_code": { + "name": "status_code", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "redirect_url": { + "name": "redirect_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "meta_description": { + "name": "meta_description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "canonical_url": { + "name": "canonical_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "robots_meta": { + "name": "robots_meta", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "og_title": { + "name": "og_title", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "og_description": { + "name": "og_description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "og_image": { + "name": "og_image", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "h1_count": { + "name": "h1_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "h2_count": { + "name": "h2_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "h3_count": { + "name": "h3_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "h4_count": { + "name": "h4_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "h5_count": { + "name": "h5_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "h6_count": { + "name": "h6_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "heading_order_json": { + "name": "heading_order_json", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "word_count": { + "name": "word_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "images_total": { + "name": "images_total", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "images_missing_alt": { + "name": "images_missing_alt", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "images_json": { + "name": "images_json", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "internal_link_count": { + "name": "internal_link_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "external_link_count": { + "name": "external_link_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "has_structured_data": { + "name": "has_structured_data", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "hreflang_tags_json": { + "name": "hreflang_tags_json", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_indexable": { + "name": "is_indexable", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "response_time_ms": { + "name": "response_time_ms", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "audit_pages_audit_id_idx": { + "name": "audit_pages_audit_id_idx", + "columns": [ + "audit_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "audit_pages_audit_id_audits_id_fk": { + "name": "audit_pages_audit_id_audits_id_fk", + "tableFrom": "audit_pages", + "tableTo": "audits", + "columnsFrom": [ + "audit_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "audits": { + "name": "audits", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "started_by_user_id": { + "name": "started_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "start_url": { + "name": "start_url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'running'" + }, + "workflow_instance_id": { + "name": "workflow_instance_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "config": { + "name": "config", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'{}'" + }, + "pages_crawled": { + "name": "pages_crawled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "pages_total": { + "name": "pages_total", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "lighthouse_total": { + "name": "lighthouse_total", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "lighthouse_completed": { + "name": "lighthouse_completed", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "lighthouse_failed": { + "name": "lighthouse_failed", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "current_phase": { + "name": "current_phase", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'discovery'" + }, + "started_at": { + "name": "started_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(current_timestamp)" + }, + "completed_at": { + "name": "completed_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "audits_project_id_idx": { + "name": "audits_project_id_idx", + "columns": [ + "project_id" + ], + "isUnique": false + }, + "audits_started_by_user_id_idx": { + "name": "audits_started_by_user_id_idx", + "columns": [ + "started_by_user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "audits_project_id_projects_id_fk": { + "name": "audits_project_id_projects_id_fk", + "tableFrom": "audits", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "keyword_metrics": { + "name": "keyword_metrics", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "keyword": { + "name": "keyword", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "location_code": { + "name": "location_code", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "language_code": { + "name": "language_code", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'en'" + }, + "search_volume": { + "name": "search_volume", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cpc": { + "name": "cpc", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "competition": { + "name": "competition", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "keyword_difficulty": { + "name": "keyword_difficulty", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "intent": { + "name": "intent", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "monthly_searches": { + "name": "monthly_searches", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "fetched_at": { + "name": "fetched_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(current_timestamp)" + } + }, + "indexes": { + "keyword_metrics_unique_project_keyword_location_language": { + "name": "keyword_metrics_unique_project_keyword_location_language", + "columns": [ + "project_id", + "keyword", + "location_code", + "language_code" + ], + "isUnique": true + }, + "keyword_metrics_lookup_idx": { + "name": "keyword_metrics_lookup_idx", + "columns": [ + "project_id", + "keyword", + "location_code", + "language_code", + "fetched_at" + ], + "isUnique": false + } + }, + "foreignKeys": { + "keyword_metrics_project_id_projects_id_fk": { + "name": "keyword_metrics_project_id_projects_id_fk", + "tableFrom": "keyword_metrics", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "projects": { + "name": "projects", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "domain": { + "name": "domain", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "location_code": { + "name": "location_code", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 2840 + }, + "language_code": { + "name": "language_code", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'en'" + }, + "onboarding_run_status": { + "name": "onboarding_run_status", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "onboarding_run_at": { + "name": "onboarding_run_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(current_timestamp)" + }, + "archived_at": { + "name": "archived_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "projects_one_default_per_organization_idx": { + "name": "projects_one_default_per_organization_idx", + "columns": [ + "organization_id" + ], + "isUnique": true, + "where": "\"projects\".\"name\" = 'Default' AND \"projects\".\"domain\" IS NULL AND \"projects\".\"archived_at\" IS NULL" + } + }, + "foreignKeys": { + "projects_organization_id_organization_id_fk": { + "name": "projects_organization_id_organization_id_fk", + "tableFrom": "projects", + "tableTo": "organization", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "rank_check_runs": { + "name": "rank_check_runs", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "config_id": { + "name": "config_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'pending'" + }, + "keywords_total": { + "name": "keywords_total", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "keywords_checked": { + "name": "keywords_checked", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "is_subset_run": { + "name": "is_subset_run", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "started_at": { + "name": "started_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(current_timestamp)" + }, + "completed_at": { + "name": "completed_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "rank_check_runs_config_idx": { + "name": "rank_check_runs_config_idx", + "columns": [ + "config_id", + "started_at" + ], + "isUnique": false + }, + "rank_check_runs_project_idx": { + "name": "rank_check_runs_project_idx", + "columns": [ + "project_id", + "started_at" + ], + "isUnique": false + }, + "rank_check_runs_one_active_per_config_idx": { + "name": "rank_check_runs_one_active_per_config_idx", + "columns": [ + "config_id" + ], + "isUnique": true, + "where": "\"rank_check_runs\".\"status\" IN ('pending', 'running')" + } + }, + "foreignKeys": { + "rank_check_runs_config_id_rank_tracking_configs_id_fk": { + "name": "rank_check_runs_config_id_rank_tracking_configs_id_fk", + "tableFrom": "rank_check_runs", + "tableTo": "rank_tracking_configs", + "columnsFrom": [ + "config_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "rank_check_runs_project_id_projects_id_fk": { + "name": "rank_check_runs_project_id_projects_id_fk", + "tableFrom": "rank_check_runs", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "rank_snapshots": { + "name": "rank_snapshots", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "run_id": { + "name": "run_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tracking_keyword_id": { + "name": "tracking_keyword_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "keyword": { + "name": "keyword", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "device": { + "name": "device", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "position": { + "name": "position", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "serp_features": { + "name": "serp_features", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "checked_at": { + "name": "checked_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(current_timestamp)" + } + }, + "indexes": { + "rank_snapshots_run_idx": { + "name": "rank_snapshots_run_idx", + "columns": [ + "run_id" + ], + "isUnique": false + }, + "rank_snapshots_keyword_device_idx": { + "name": "rank_snapshots_keyword_device_idx", + "columns": [ + "tracking_keyword_id", + "device", + "checked_at" + ], + "isUnique": false + }, + "rank_snapshots_run_keyword_device_idx": { + "name": "rank_snapshots_run_keyword_device_idx", + "columns": [ + "run_id", + "tracking_keyword_id", + "device" + ], + "isUnique": true + } + }, + "foreignKeys": { + "rank_snapshots_run_id_rank_check_runs_id_fk": { + "name": "rank_snapshots_run_id_rank_check_runs_id_fk", + "tableFrom": "rank_snapshots", + "tableTo": "rank_check_runs", + "columnsFrom": [ + "run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "rank_tracking_configs": { + "name": "rank_tracking_configs", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "domain": { + "name": "domain", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "location_code": { + "name": "location_code", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 2840 + }, + "language_code": { + "name": "language_code", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'en'" + }, + "devices": { + "name": "devices", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'both'" + }, + "serp_depth": { + "name": "serp_depth", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "schedule_interval": { + "name": "schedule_interval", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'weekly'" + }, + "is_active": { + "name": "is_active", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "last_checked_at": { + "name": "last_checked_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "next_check_at": { + "name": "next_check_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_skip_reason": { + "name": "last_skip_reason", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(current_timestamp)" + } + }, + "indexes": { + "rank_tracking_configs_project_domain_location_idx": { + "name": "rank_tracking_configs_project_domain_location_idx", + "columns": [ + "project_id", + "domain", + "location_code" + ], + "isUnique": true + } + }, + "foreignKeys": { + "rank_tracking_configs_project_id_projects_id_fk": { + "name": "rank_tracking_configs_project_id_projects_id_fk", + "tableFrom": "rank_tracking_configs", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "rank_tracking_keywords": { + "name": "rank_tracking_keywords", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "config_id": { + "name": "config_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "keyword": { + "name": "keyword", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "search_volume": { + "name": "search_volume", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "keyword_difficulty": { + "name": "keyword_difficulty", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cpc": { + "name": "cpc", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "metrics_fetched_at": { + "name": "metrics_fetched_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(current_timestamp)" + } + }, + "indexes": { + "rank_tracking_keywords_config_keyword_idx": { + "name": "rank_tracking_keywords_config_keyword_idx", + "columns": [ + "config_id", + "keyword" + ], + "isUnique": true + } + }, + "foreignKeys": { + "rank_tracking_keywords_config_id_rank_tracking_configs_id_fk": { + "name": "rank_tracking_keywords_config_id_rank_tracking_configs_id_fk", + "tableFrom": "rank_tracking_keywords", + "tableTo": "rank_tracking_configs", + "columnsFrom": [ + "config_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "saved_keyword_tag_assignments": { + "name": "saved_keyword_tag_assignments", + "columns": { + "saved_keyword_id": { + "name": "saved_keyword_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tag_id": { + "name": "tag_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(current_timestamp)" + } + }, + "indexes": { + "saved_keyword_tag_assignments_unique_idx": { + "name": "saved_keyword_tag_assignments_unique_idx", + "columns": [ + "saved_keyword_id", + "tag_id" + ], + "isUnique": true + }, + "saved_keyword_tag_assignments_keyword_idx": { + "name": "saved_keyword_tag_assignments_keyword_idx", + "columns": [ + "saved_keyword_id" + ], + "isUnique": false + }, + "saved_keyword_tag_assignments_tag_idx": { + "name": "saved_keyword_tag_assignments_tag_idx", + "columns": [ + "tag_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "saved_keyword_tag_assignments_saved_keyword_id_saved_keywords_id_fk": { + "name": "saved_keyword_tag_assignments_saved_keyword_id_saved_keywords_id_fk", + "tableFrom": "saved_keyword_tag_assignments", + "tableTo": "saved_keywords", + "columnsFrom": [ + "saved_keyword_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "saved_keyword_tag_assignments_tag_id_saved_keyword_tags_id_fk": { + "name": "saved_keyword_tag_assignments_tag_id_saved_keyword_tags_id_fk", + "tableFrom": "saved_keyword_tag_assignments", + "tableTo": "saved_keyword_tags", + "columnsFrom": [ + "tag_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "saved_keyword_tags": { + "name": "saved_keyword_tags", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "normalized_name": { + "name": "normalized_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(current_timestamp)" + } + }, + "indexes": { + "saved_keyword_tags_project_normalized_name_idx": { + "name": "saved_keyword_tags_project_normalized_name_idx", + "columns": [ + "project_id", + "normalized_name" + ], + "isUnique": true + }, + "saved_keyword_tags_project_name_idx": { + "name": "saved_keyword_tags_project_name_idx", + "columns": [ + "project_id", + "name" + ], + "isUnique": false + } + }, + "foreignKeys": { + "saved_keyword_tags_project_id_projects_id_fk": { + "name": "saved_keyword_tags_project_id_projects_id_fk", + "tableFrom": "saved_keyword_tags", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "saved_keywords": { + "name": "saved_keywords", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "keyword": { + "name": "keyword", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "location_code": { + "name": "location_code", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 2840 + }, + "language_code": { + "name": "language_code", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'en'" + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(current_timestamp)" + } + }, + "indexes": { + "saved_keywords_unique_project_keyword_location_language": { + "name": "saved_keywords_unique_project_keyword_location_language", + "columns": [ + "project_id", + "keyword", + "location_code", + "language_code" + ], + "isUnique": true + }, + "saved_keywords_project_created_idx": { + "name": "saved_keywords_project_created_idx", + "columns": [ + "project_id", + "created_at" + ], + "isUnique": false + } + }, + "foreignKeys": { + "saved_keywords_project_id_projects_id_fk": { + "name": "saved_keywords_project_id_projects_id_fk", + "tableFrom": "saved_keywords", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "user_onboarding_answers": { + "name": "user_onboarding_answers", + "columns": { + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "interested_features": { + "name": "interested_features", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'[]'" + }, + "work_for": { + "name": "work_for", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "client_website_count": { + "name": "client_website_count", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "found_via": { + "name": "found_via", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "mcp_setup_intent": { + "name": "mcp_setup_intent", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "completed_at": { + "name": "completed_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "gsc_nudge_dismissed_at": { + "name": "gsc_nudge_dismissed_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(current_timestamp)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(current_timestamp)" + } + }, + "indexes": { + "user_onboarding_answers_organization_idx": { + "name": "user_onboarding_answers_organization_idx", + "columns": [ + "organization_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "user_onboarding_answers_user_id_user_id_fk": { + "name": "user_onboarding_answers_user_id_user_id_fk", + "tableFrom": "user_onboarding_answers", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_onboarding_answers_organization_id_organization_id_fk": { + "name": "user_onboarding_answers_organization_id_organization_id_fk", + "tableFrom": "user_onboarding_answers", + "tableTo": "organization", + "columnsFrom": [ + "organization_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, + "default": "(cast(unixepoch('subsecond') * 1000 as integer))" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "account_userId_idx": { + "name": "account_userId_idx", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "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": {} + }, + "invitation": { + "name": "invitation", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'pending'" + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(cast(unixepoch('subsecond') * 1000 as integer))" + }, + "inviter_id": { + "name": "inviter_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "invitation_organizationId_idx": { + "name": "invitation_organizationId_idx", + "columns": [ + "organization_id" + ], + "isUnique": false + }, + "invitation_email_idx": { + "name": "invitation_email_idx", + "columns": [ + "email" + ], + "isUnique": false + } + }, + "foreignKeys": { + "invitation_organization_id_organization_id_fk": { + "name": "invitation_organization_id_organization_id_fk", + "tableFrom": "invitation", + "tableTo": "organization", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "invitation_inviter_id_user_id_fk": { + "name": "invitation_inviter_id_user_id_fk", + "tableFrom": "invitation", + "tableTo": "user", + "columnsFrom": [ + "inviter_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "member": { + "name": "member", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'member'" + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "member_organizationId_idx": { + "name": "member_organizationId_idx", + "columns": [ + "organization_id" + ], + "isUnique": false + }, + "member_userId_idx": { + "name": "member_userId_idx", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "member_organization_id_organization_id_fk": { + "name": "member_organization_id_organization_id_fk", + "tableFrom": "member", + "tableTo": "organization", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "member_user_id_user_id_fk": { + "name": "member_user_id_user_id_fk", + "tableFrom": "member", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "organization": { + "name": "organization", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "logo": { + "name": "logo", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "metadata": { + "name": "metadata", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "organization_slug_unique": { + "name": "organization_slug_unique", + "columns": [ + "slug" + ], + "isUnique": true + }, + "organization_slug_uidx": { + "name": "organization_slug_uidx", + "columns": [ + "slug" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "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, + "default": "(cast(unixepoch('subsecond') * 1000 as integer))" + }, + "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 + }, + "active_organization_id": { + "name": "active_organization_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "session_token_unique": { + "name": "session_token_unique", + "columns": [ + "token" + ], + "isUnique": true + }, + "session_userId_idx": { + "name": "session_userId_idx", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "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, + "default": "(cast(unixepoch('subsecond') * 1000 as integer))" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(cast(unixepoch('subsecond') * 1000 as integer))" + }, + "analytics_opted_out": { + "name": "analytics_opted_out", + "type": "integer", + "primaryKey": false, + "notNull": false, + "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": true, + "autoincrement": false, + "default": "(cast(unixepoch('subsecond') * 1000 as integer))" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(cast(unixepoch('subsecond') * 1000 as integer))" + } + }, + "indexes": { + "verification_identifier_idx": { + "name": "verification_identifier_idx", + "columns": [ + "identifier" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "billing_customer_status": { + "name": "billing_customer_status", + "columns": { + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "is_paying": { + "name": "is_paying", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "paid_plan_id": { + "name": "paid_plan_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "paid_plan_status": { + "name": "paid_plan_status", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "customer_json": { + "name": "customer_json", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "synced_at": { + "name": "synced_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(current_timestamp)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(current_timestamp)" + } + }, + "indexes": {}, + "foreignKeys": { + "billing_customer_status_organization_id_organization_id_fk": { + "name": "billing_customer_status_organization_id_organization_id_fk", + "tableFrom": "billing_customer_status", + "tableTo": "organization", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "gsc_connections": { + "name": "gsc_connections", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "site_url": { + "name": "site_url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "connected_by_user_id": { + "name": "connected_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "connected_account_email": { + "name": "connected_account_email", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(current_timestamp)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(current_timestamp)" + } + }, + "indexes": { + "gsc_connections_project_idx": { + "name": "gsc_connections_project_idx", + "columns": [ + "project_id" + ], + "isUnique": true + }, + "gsc_connections_organization_idx": { + "name": "gsc_connections_organization_idx", + "columns": [ + "organization_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "gsc_connections_project_id_projects_id_fk": { + "name": "gsc_connections_project_id_projects_id_fk", + "tableFrom": "gsc_connections", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "gsc_connections_organization_id_organization_id_fk": { + "name": "gsc_connections_organization_id_organization_id_fk", + "tableFrom": "gsc_connections", + "tableTo": "organization", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "reddit_attributions": { + "name": "reddit_attributions", + "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 + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "click_id": { + "name": "click_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "uuid": { + "name": "uuid", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "landing_page": { + "name": "landing_page", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "referrer": { + "name": "referrer", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "utm_source": { + "name": "utm_source", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "utm_medium": { + "name": "utm_medium", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "utm_campaign": { + "name": "utm_campaign", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "utm_term": { + "name": "utm_term", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "utm_content": { + "name": "utm_content", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "signup_sent_at": { + "name": "signup_sent_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "purchase_sent_at": { + "name": "purchase_sent_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(current_timestamp)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(current_timestamp)" + } + }, + "indexes": { + "reddit_attributions_user_idx": { + "name": "reddit_attributions_user_idx", + "columns": [ + "user_id" + ], + "isUnique": true + }, + "reddit_attributions_organization_idx": { + "name": "reddit_attributions_organization_idx", + "columns": [ + "organization_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "reddit_attributions_user_id_user_id_fk": { + "name": "reddit_attributions_user_id_user_id_fk", + "tableFrom": "reddit_attributions", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "reddit_attributions_organization_id_organization_id_fk": { + "name": "reddit_attributions_organization_id_organization_id_fk", + "tableFrom": "reddit_attributions", + "tableTo": "organization", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + } + }, + "views": {}, + "enums": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "indexes": {} + } +} \ No newline at end of file diff --git a/drizzle/meta/_journal.json b/drizzle/meta/_journal.json index f152577..96ef9b1 100644 --- a/drizzle/meta/_journal.json +++ b/drizzle/meta/_journal.json @@ -169,6 +169,13 @@ "when": 1781467831570, "tag": "0023_sad_cobalt_man", "breakpoints": true + }, + { + "idx": 24, + "version": "6", + "when": 1781809245835, + "tag": "0024_clumsy_barracuda", + "breakpoints": true } ] } \ No newline at end of file diff --git a/package.json b/package.json index a5d12e2..c3c964e 100644 --- a/package.json +++ b/package.json @@ -58,9 +58,11 @@ } }, "dependencies": { + "@ai-sdk/react": "^3.0.201", "@cloudflare/workers-oauth-provider": "^0.4.0", "@every-app/sdk": "^0.1.14", "@modelcontextprotocol/sdk": "1.29.0", + "@openrouter/ai-sdk-provider": "^2.9.0", "@tanstack/query-core": "^5.90.9", "@tanstack/react-form": "^1.25.0", "@tanstack/react-query": "^5.90.9", @@ -69,6 +71,7 @@ "@tanstack/react-start": "^1.167.16", "@tanstack/react-table": "^8.21.3", "agents": "0.12.3", + "ai": "^6.0.199", "autumn-js": "^1.1.7", "better-auth": "^1.5.5", "cheerio": "^1.2.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c0f2cd6..2fd7be8 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,6 +8,9 @@ importers: .: dependencies: + '@ai-sdk/react': + specifier: ^3.0.201 + version: 3.0.201(react@19.2.4)(zod@4.3.6) '@cloudflare/workers-oauth-provider': specifier: ^0.4.0 version: 0.4.0 @@ -17,6 +20,9 @@ importers: '@modelcontextprotocol/sdk': specifier: 1.29.0 version: 1.29.0(@cfworker/json-schema@4.1.1)(zod@4.3.6) + '@openrouter/ai-sdk-provider': + specifier: ^2.9.0 + version: 2.9.0(ai@6.0.199(zod@4.3.6))(zod@4.3.6) '@tanstack/query-core': specifier: ^5.90.9 version: 5.90.20 @@ -40,7 +46,10 @@ importers: version: 8.21.3(react-dom@19.2.4(react@19.2.4))(react@19.2.4) agents: specifier: 0.12.3 - version: 0.12.3(@babel/core@7.29.0)(@babel/runtime@7.29.2)(@cloudflare/workers-types@4.20260302.0)(ai@6.0.177(zod@4.3.6))(react@19.2.4)(rolldown@1.0.0)(vite@7.3.1(@types/node@22.19.11)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0))(zod@4.3.6) + version: 0.12.3(@babel/core@7.29.0)(@babel/runtime@7.29.2)(@cloudflare/workers-types@4.20260302.0)(ai@6.0.199(zod@4.3.6))(react@19.2.4)(rolldown@1.0.0)(vite@7.3.1(@types/node@22.19.11)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0))(zod@4.3.6) + ai: + specifier: ^6.0.199 + version: 6.0.199(zod@4.3.6) autumn-js: specifier: ^1.1.7 version: 1.1.7(better-auth@1.5.5(@cloudflare/workers-types@4.20260302.0)(@tanstack/react-start@1.167.16(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(vite@7.3.1(@types/node@22.19.11)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)))(drizzle-kit@0.31.9)(drizzle-orm@0.44.7(@cloudflare/workers-types@4.20260302.0)(@libsql/client@0.15.15)(@opentelemetry/api@1.9.1)(kysely@0.28.12))(mongodb@7.2.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(solid-js@1.9.11)(vitest@3.2.4(@types/debug@4.1.13)(@types/node@22.19.11)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)))(better-call@1.3.2(zod@4.3.6))(express@5.2.1)(hono@4.12.18)(react@19.2.4) @@ -189,8 +198,8 @@ importers: packages: - '@ai-sdk/gateway@3.0.112': - resolution: {integrity: sha512-jiBao9pR4owWyjo0BnuNc7WSQBGOD0thysE4AFgZXaG+zMFbISQXUkJr7ePw/phBvePy7jE5FSA2Lf7lwqUiiQ==} + '@ai-sdk/gateway@3.0.127': + resolution: {integrity: sha512-Obmw5hmE5x+ccRrMp/Djx5r0rpFVX87YqE6OY06g5fwYlRI30dA84ARfTzX45ivCvkW4eCnBpOVXVWQ/pjH85w==} engines: {node: '>=18'} peerDependencies: zod: ^3.25.76 || ^4.1.8 @@ -205,6 +214,12 @@ packages: resolution: {integrity: sha512-Q3BZ27qfpYqnCYGvE3vt+Qi6LGOF9R5Nmzn+9JoM1lCRsD9mYaIhfJLkSunN48nfGXJ6n+XNV0J/XVpqGQl7Dw==} engines: {node: '>=18'} + '@ai-sdk/react@3.0.201': + resolution: {integrity: sha512-sDYdkPB96aHPrF5WKP0BdjRjymgZOdIMzNt1SlYPCoWWvLnV+UMwnIqvqEG+OAa9n4Sq7J3A3N/JD7rozow+CA==} + engines: {node: '>=18'} + peerDependencies: + react: ^18 || ~19.0.1 || ~19.1.2 || ^19.2.1 + '@babel/code-frame@7.27.1': resolution: {integrity: sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==} engines: {node: '>=6.9.0'} @@ -1268,14 +1283,17 @@ packages: resolution: {integrity: sha512-hAX0pT/73190NLqBPPWSdBVGtbY6VOhWYK3qqHqtXQ1gK7kS2yz4+ivsN07hpJ6I3aeMtKP6J6npsEKOAzuTLA==} engines: {node: '>=20.0'} + '@openrouter/ai-sdk-provider@2.9.0': + resolution: {integrity: sha512-Seva+NCa0WUQnJIUE5GzHsUv1WTIeyqwz0ELl2VtS6NP+eF+77yCXGFVOMbvoCM7QMjlnhv7931e89R+8pJdcQ==} + engines: {node: '>=18'} + peerDependencies: + ai: ^6.0.0 + zod: ^3.25.0 || ^4.0.0 + '@opentelemetry/api-logs@0.208.0': resolution: {integrity: sha512-CjruKY9V6NMssL/T1kAFgzosF1v9o6oeN+aX5JB/C/xPNtmgIJqcXHG7fA82Ou1zCpWGl4lROQUKwUNE1pMCyg==} engines: {node: '>=8.0.0'} - '@opentelemetry/api@1.9.0': - resolution: {integrity: sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==} - engines: {node: '>=8.0.0'} - '@opentelemetry/api@1.9.1': resolution: {integrity: sha512-gLyJlPHPZYdAk1JENA9LeHejZe1Ti77/pTeFm/nMXmQH/HFZlcS/O2XJB+L8fkbrNSqhdtlvjBVjxwUYanNH5Q==} engines: {node: '>=8.0.0'} @@ -2476,8 +2494,8 @@ packages: vite: optional: true - ai@6.0.177: - resolution: {integrity: sha512-1xQtbeWwNcLyyM86ixZhkKvT+WRXc1lvarIKqPVtsyn8F9NDikwUMBqYu+aQKDgMht50SMXh4qboYuU8MeHZZA==} + ai@6.0.199: + resolution: {integrity: sha512-6H9RPEjzBQECM+eU1JxAh6jHcZPU/6q5QZ8D8QV8agubf0Mm/kcBlwqrFcFtup6RQzmEvMkVaQOoLCZ8bQ13lA==} engines: {node: '>=18'} peerDependencies: zod: ^3.25.76 || ^4.1.8 @@ -4287,6 +4305,11 @@ packages: resolution: {integrity: sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g==} engines: {node: '>=18'} + swr@2.4.1: + resolution: {integrity: sha512-2CC6CiKQtEwaEeNiqWTAw9PGykW8SR5zZX8MZk6TeAvEAnVS7Visz8WzphqgtQ8v2xz/4Q5K+j+SeMaKXeeQIA==} + peerDependencies: + react: ^16.11.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + tailwindcss@4.2.1: resolution: {integrity: sha512-/tBrSQ36vCleJkAOsy9kbNTgaxvGbyOamC30PRePTQe/o1MFwEKHQk4Cn7BNGaPtjp+PuUrByJehM1hgxfq4sw==} @@ -4294,6 +4317,10 @@ packages: resolution: {integrity: sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==} engines: {node: '>=6'} + throttleit@2.1.0: + resolution: {integrity: sha512-nt6AMGKW1p/70DF/hGBdJB57B8Tspmbp5gfJ8ilhLnt7kkr2ye7hzD6NVG8GGErk2HWF34igrL2CXmNIkzKqKw==} + engines: {node: '>=18'} + tiny-invariant@1.3.3: resolution: {integrity: sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==} @@ -4683,7 +4710,7 @@ packages: snapshots: - '@ai-sdk/gateway@3.0.112(zod@4.3.6)': + '@ai-sdk/gateway@3.0.127(zod@4.3.6)': dependencies: '@ai-sdk/provider': 3.0.10 '@ai-sdk/provider-utils': 4.0.27(zod@4.3.6) @@ -4701,6 +4728,16 @@ snapshots: dependencies: json-schema: 0.4.0 + '@ai-sdk/react@3.0.201(react@19.2.4)(zod@4.3.6)': + dependencies: + '@ai-sdk/provider-utils': 4.0.27(zod@4.3.6) + ai: 6.0.199(zod@4.3.6) + react: 19.2.4 + swr: 2.4.1(react@19.2.4) + throttleit: 2.1.0 + transitivePeerDependencies: + - zod + '@babel/code-frame@7.27.1': dependencies: '@babel/helper-validator-identifier': 7.28.5 @@ -5532,12 +5569,15 @@ snapshots: '@oozcitak/util@10.0.0': {} + '@openrouter/ai-sdk-provider@2.9.0(ai@6.0.199(zod@4.3.6))(zod@4.3.6)': + dependencies: + ai: 6.0.199(zod@4.3.6) + zod: 4.3.6 + '@opentelemetry/api-logs@0.208.0': dependencies: '@opentelemetry/api': 1.9.1 - '@opentelemetry/api@1.9.0': {} - '@opentelemetry/api@1.9.1': {} '@opentelemetry/core@2.2.0(@opentelemetry/api@1.9.1)': @@ -6554,13 +6594,13 @@ snapshots: dependencies: humanize-ms: 1.2.1 - agents@0.12.3(@babel/core@7.29.0)(@babel/runtime@7.29.2)(@cloudflare/workers-types@4.20260302.0)(ai@6.0.177(zod@4.3.6))(react@19.2.4)(rolldown@1.0.0)(vite@7.3.1(@types/node@22.19.11)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0))(zod@4.3.6): + agents@0.12.3(@babel/core@7.29.0)(@babel/runtime@7.29.2)(@cloudflare/workers-types@4.20260302.0)(ai@6.0.199(zod@4.3.6))(react@19.2.4)(rolldown@1.0.0)(vite@7.3.1(@types/node@22.19.11)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0))(zod@4.3.6): dependencies: '@babel/plugin-proposal-decorators': 7.29.0(@babel/core@7.29.0) '@cfworker/json-schema': 4.1.1 '@modelcontextprotocol/sdk': 1.29.0(@cfworker/json-schema@4.1.1)(zod@4.3.6) '@rolldown/plugin-babel': 0.2.3(@babel/core@7.29.0)(@babel/runtime@7.29.2)(rolldown@1.0.0)(vite@7.3.1(@types/node@22.19.11)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)) - ai: 6.0.177(zod@4.3.6) + ai: 6.0.199(zod@4.3.6) cron-schedule: 6.0.0 mimetext: 3.0.28 nanoid: 5.1.11 @@ -6579,12 +6619,12 @@ snapshots: - rolldown - supports-color - ai@6.0.177(zod@4.3.6): + ai@6.0.199(zod@4.3.6): dependencies: - '@ai-sdk/gateway': 3.0.112(zod@4.3.6) + '@ai-sdk/gateway': 3.0.127(zod@4.3.6) '@ai-sdk/provider': 3.0.10 '@ai-sdk/provider-utils': 4.0.27(zod@4.3.6) - '@opentelemetry/api': 1.9.0 + '@opentelemetry/api': 1.9.1 zod: 4.3.6 ajv-formats@3.0.1(ajv@8.20.0): @@ -8604,10 +8644,18 @@ snapshots: supports-color@10.2.2: {} + swr@2.4.1(react@19.2.4): + dependencies: + dequal: 2.0.3 + react: 19.2.4 + use-sync-external-store: 1.6.0(react@19.2.4) + tailwindcss@4.2.1: {} tapable@2.3.0: {} + throttleit@2.1.0: {} + tiny-invariant@1.3.3: {} tinybench@2.9.0: {} diff --git a/specs/0005-onboarding-agent.md b/specs/0005-onboarding-agent.md new file mode 100644 index 0000000..d88cb6c --- /dev/null +++ b/specs/0005-onboarding-agent.md @@ -0,0 +1,157 @@ +# Onboarding agent + +## Status + +Proposed (June 2026) — v1 product spec, pending technical design. + +> **Update (June 2026):** the first PR ships the chat + live strategy generation +> only. Persisting the strategy (the "Project Context" store, R2 versioning) and +> the `get_project_context` MCP tool described below are **deferred to a later +> PR** — the generated strategy is shown in the chat but not yet saved. + +## Goal + +Turn signup into activation. When a new user onboards, an "agent" analyzes +their actual website live, then proposes a tailored SEO strategy. The strategy +is free and stands on its own; _acting on it_ (rank tracking, content briefs, +the ongoing coach) is the paid surface. The strategy becomes the project's +durable "Project Context" — readable in the app and over MCP. + +This is the top activation priority because it gives every new user a concrete, +personalized "here's what to do" moment before they're asked to pay. + +## What "agent" means here + +A **guided pipeline narrated live**, not an LLM agent loop. The steps are +mostly deterministic (scrape, fetch keyword data); a single LLM call at the end +synthesizes the strategy narrative. The "agent" feeling comes from streaming the +steps in real time ("Reading your homepage… you look like a Notion +alternative… 4 keywords ranking, 30 worth targeting…") and streaming the final +write-up token-by-token. No Anthropic SDK / agent infra is required for v1. + +## The experience + +**Stage 0 — Profile (extend existing onboarding forms).** Collect: domain, +experience level, and primary goal. **Website and default country are captured +on the same step**, since the country is a property of the site/project being +analyzed — keeping them together makes the relationship obvious and avoids a +stray standalone country field. That step carries a short note that they can add +more projects with different websites (and their own countries) later, so users +don't feel they must cram every site into this first one. Keep the whole stage +to ~4 fields — the onboarding UX audit flagged form friction. Country and +language become the project's default location (see Project defaults) and are +reused for every DataForSEO call going forward. + +**Stage 1 — Discover (live).** Sitemap-first using existing robots.txt + +sitemap discovery; fall back to a shallow crawl. Produces a map of the site's +shape from URLs/titles. Discover many URLs (cheap); scrape few. + +**Stage 2 — Read (live).** Scrape ~3–5 key pages (home + top product/nav pages) +to markdown. This is the one net-new capability: page → markdown via Cloudflare +Browser Rendering. Honest "we couldn't read your site" flagging with a manual +"tell us what you do" fallback so the run never dead-ends. + +**Stage 3 — Signal (live).** See Stage 3 data below. + +**Stage 4 — Synthesize (streaming LLM).** One LLM call over +{profile + scraped markdown + keyword data} produces: a positioning statement, +3–5 themes/clusters, a starter keyword table (volume/difficulty), and a +prioritized "do this next" list. Must produce a credible strategy even when the +site has zero existing rankings — the cold-start case is the _default_ for the +indie-founder ICP, not an edge case. + +**Stage 5 — Persist + gate.** Save as the Project Context artifact (markdown, +MCP-readable). Present the full strategy free; the paywall lands on _executing_ +it. + +## Stage 3 data (v1 — kept minimal) + +Stage 3 is **archetype-conditional**: detect the site type cheaply, then frame +the goal accordingly. For v1 the archetype branches the _narrative_, not a large +matrix of API calls. Two calls baseline: + +| Call | Endpoint | Purpose | When | +| ---------------------- | ------------------------------------------------------ | ---------------------------------------------------- | -------------------------------------------------------------------- | +| `domain_rank_overview` | `/v3/dataforseo_labs/google/domain_rank_overview/live` | traffic + # ranking keywords; the archetype detector | always | +| `keyword_ideas` | `/v3/dataforseo_labs/google/keyword_ideas/live` | starter keyword list, seeded from scraped themes | always | +| `ranked_keywords` | `/v3/dataforseo_labs/google/ranked_keywords/live` | what they already rank for | only if overview shows real rankings (usually skipped for new sites) | + +Archetypes (narrative only in v1): **new/pre-traffic**, **established content +site**, **local business**, **SaaS/product** (core ICP). + +**Clarifying question:** for **beginners**, skip it and use the archetype's +default goal (fewer decisions = less drop-off). For **non-beginners**, ask one +targeted question after detection to confirm intent (e.g. "grow existing +rankings, or expand into new topics?") — agency without a survey. + +**Deferred to v2:** `serp_competitors`, local-business tools +(`get_local_serp_results`, `search_local_businesses`, +`get_google_business_questions`), and Google Ads volume for non-Labs countries. + +## Cost per free onboarding + +The whole run is free (paywall is after), so cost-per-signup must be bounded: + +- DataForSEO: ~$0.04–0.08 (2 Labs live calls; metered from the real `cost` + field in the response envelope, so we can hard-cap). +- Browser Rendering scrape: ~negligible. +- LLM synthesis: ~$0.05–0.15 (one call over a few pages of markdown). +- **Total ≈ $0.10–0.25 per onboarding.** + +Guardrails: one run per user, results cached hard, and **email verification +gates Stage 3** (the DataForSEO spend) to kill drive-by abuse. + +## Project Context artifact (MCP-readable) + +The strategy is stored as a **markdown document** — a living "Project Context" +that is the shared source of truth across the in-app agent, the web UI, and any +MCP client. + +- Storage: a `project_context` row in D1 (`projectId`, `markdown`, `updatedAt`, + `version`). Small enough for D1; R2 only if it grows. +- MCP: a `get_project_context` tool (and ideally an MCP _resource_ so it + auto-loads into the agent's context). The user's own Claude/Codex reads the + same strategy the app shows. +- The onboarding agent **authors** the context; the paid execution actions + (rank tracking, briefs, coach) **read from and append to** it. That append-back + is the natural shape of the gated surface. + +## Project defaults (country/language) + +Projects don't currently store a default location — only `rank_tracking_configs` +does (defaults 2840/US, "en"). Add `location_code` + `language_code` to +`projects`, set from the Stage 0 country, and reuse them everywhere project work +needs a location. A small curated country → `location_code` map is enough for +v1. + +## Paywall + +Free: the full strategy, positioning, themes, capped starter keyword list +(~15–20), and the "do next" list. Gated (execution): rank-tracking the proposed +keywords, full keyword expansion, content briefs per theme, and the ongoing +`seo-coach`. Every gated action is a named thing tied to a strategy item the +user already believes in — stronger pull than "refine further." GSC stays behind +the gate; we do **not** prompt for it during onboarding (connecting then hitting +a paywall is exactly the bait-and-switch the UX audit warned against). + +## Unify the seo-coach skill + +Update the `seo-coach` skill (and `onboarding-checklist`) so the coach and the +onboarding agent are one continuous experience: the coach reads Project Context +via MCP, picks up where onboarding left off ("here's your strategy — let's work +the backlog"), and can answer both SEO questions and OpenSEO product questions. +The onboarding agent sets the backlog; the coach executes it. + +## Open questions + +- Country → `location_code` source: hand-curate a short list for v1, or reuse an + existing DataForSEO locations dataset? +- Re-run policy: confirmed one strategy per project, regenerate in place; new + domain = new project (a soft upgrade nudge). +- Synthesis model choice (cost vs quality) — decide at technical design. + +## Out of scope for v1 + +Conversational/iterative agent loop, competitor and local-business analysis, +GSC-enriched strategy, multi-language strategies, automatic content generation. +These are incremental improvements once the core activation loop is proven. diff --git a/specs/0006-onboarding-agent-implementation.md b/specs/0006-onboarding-agent-implementation.md new file mode 100644 index 0000000..c804ac7 --- /dev/null +++ b/specs/0006-onboarding-agent-implementation.md @@ -0,0 +1,151 @@ +# Onboarding agent — implementation plan (chat + seed function) + +## Status + +Accepted (June 2026) — technical plan for `specs/0005-onboarding-agent.md`. + +Supersedes an earlier draft that proposed Cloudflare Project Think + Durable +Objects + a Workflow. We dropped all of that (see "Why not Think / Workflows"). + +> **Update (June 2026):** to keep the first PR small, strategy **persistence** +> (the `project_context_versions` store + R2 markdown versioning) and the +> `get_project_context` **MCP tool** were deferred to a later PR. `seed.ts` now +> returns the synthesized strategy to the chat without saving it; the +> `claimRun` spend guard still bounds the paid run. Sections below describing +> the store, versioning, and the MCP read tool reflect the original plan, not +> what shipped. + +## TL;DR + +Onboarding has two simple pieces, no agent framework: + +1. **A seed function** (plain async): discover sitemap → scrape 3–5 pages to + markdown (Browser Rendering) → 2 paid DataForSEO calls → one OpenRouter + synthesis call → save the result as the project's first **Project Context** + version. Runs once when onboarding kicks off. +2. **A normal streaming chat** (Vercel AI SDK `streamText` over OpenRouter): + the user asks questions / refines; an `update_project_context` tool writes a + new version. Backed by a plain API route on the existing better-auth session. + +The Project Context is **versioned**: immutable markdown blobs in R2, an +append-only log in D1. Reverts reuse a prior blob's key. It's exposed over MCP +via `get_project_context`. + +## Why not Think / Workflows (and why `agents` stays) + +- **No durable execution needed.** The paid DataForSEO services are _cache-first_ + (`getCached` runs before `createDataforseoClient`/metering), so a crash-and-retry + of the same domain re-hits the 12h R2 cache → no double-spend. That removed the + only reason for fibers/Workflows. +- **No agent host needed.** The capabilities we want (stream answers, a future + docs tool, save/update the artifact) are all plain `streamText({ tools })`. + Think's distinctive value (durable DO sessions, scheduled turns, sub-agents) + isn't used by any of them. +- **The `agents` package stays** — it's used by the MCP handler + (`agents/mcp` → `createMcpHandler` in `src/server/mcp/transport.ts`), not as an + agent runtime. No version bump. +- **Graduate later** only when a capability genuinely needs durable sessions, + `schedule` (weekly rank tracking), or sub-agents (per-competitor). The seed + function and chat route port straight over. + +## Data model + +**`projects`** — add `location_code` (int, default 2840), `language_code` (text, +default `'en'`), `onboarding_run_status` (text nullable: +`running|complete|failed`), `onboarding_run_at` (text nullable). + +**`project_context_versions`** (new, append-only log): +`id` pk · `project_id` FK · `r2_key` · `author` (`onboarding|chat|user`) · +`note` nullable · `reverted_from_id` nullable · `created_at`. Current version = +latest row per project. Index on `(project_id, created_at)`. + +**R2** — immutable markdown blob per version at +`project-context/{projectId}/{versionId}.md`. **Write R2 first, then the D1 +row** (a failure leaves a harmless orphan blob, never a row pointing at nothing). +A revert inserts a new row reusing the target's `r2_key` (no new blob). + +## The seed function + +`runOnboardingSeed({ projectId, organizationId, userId, userEmail, domain })`: + +1. **Admission marker** — atomic `UPDATE projects SET onboarding_run_status = +'running' WHERE id = ? AND onboarding_run_status IS NULL`; proceed only if + one row changed (else a run is already in flight). This is the at-most-once + guard. +2. **Discover** — `fetch()` robots.txt + sitemap.xml; shallow fallback. +3. **Read** — scrape 3–5 key pages to markdown via the `BROWSER` binding, + sequentially. Failure → flag it, never throw the whole run. +4. **Signal** — `DomainService.getOverview` (always) + keyword research seeded + from scraped themes, both with `creditFeature: 'onboarding'`; + `getSuggestedKeywords` only if the overview shows real rankings. +5. **Synthesize** — one OpenRouter `generateText`/`streamText` over + {profile + markdown + signal} → strategy markdown. +6. **Persist** — write version `v1` (author `onboarding`); set + `onboarding_run_status = 'complete'`. On any throw → `'failed'` (re-runnable; + retry is cache-backed and cheap). + +**Metering** is unchanged: paid calls go through the existing +`createDataforseoClient` seam. A new `'onboarding'` `CreditFeature` tags spend. +A `skipBalanceAssert` flag on the metering path lets a zero-balance new signup +run Stage "Signal" without dead-ending on `INSUFFICIENT_CREDITS`, while still +calling `trackDataforseoCost` (spend metered, not balance-gated). Self-host +already skips Autumn entirely. + +## The chat + +`POST /api/onboarding/chat` (TanStack `createFileRoute` server handler): + +- Auth: resolve the better-auth session from the request; assert the user owns + `projectId` (org scoping via `ProjectRepository.getProjectForOrganization`). + No new transport, so no extra auth surface. +- `streamText({ model: openrouter(MODEL), system: seededWithContext, messages, +tools: { update_project_context } })`, returned as a UI message stream. +- `update_project_context({ markdown, note })` writes a new version (author + `chat`). For v1 it applies directly; the append-only log makes any unwanted + change one revert away. + +Client: AI SDK `useChat({ api: '/api/onboarding/chat' })`. The strategy renders +above the chat; the upgrade CTA is a UI state shown once `v1` exists. + +## Auth + email-verify + +- Surface `emailVerified` on `EnsuredUserContext` (from + `session.user.emailVerified`); self-host = treated verified. +- The seed asserts `emailVerified` before the paid "Signal" stage. Discover + + Read (free) may run unverified. + +## MCP + +`get_project_context(projectId)` — read-only tool (`readOnlyHint: true`) using +`withMcpProjectAuth`; resolves the latest version → R2 get → returns markdown. +`list_project_context_versions` is a fast-follow. + +## Local testing + +Real providers, no fixture system: + +- Add `OPENROUTER_API_KEY` to `.env.local` (the one new key), `wrangler login` + for the `BROWSER` binding (`remote: true` in dev). +- DataForSEO creds you already have; the cache makes repeat runs free. +- Drive the real onboarding UI with the Playwriter skill, screenshotting each + step. **Test domain: `openseo.so`.** + +## Build order (stacked PRs) + +1. **Foundation** — schema + migration (`projects` cols, `project_context_versions`); + `emailVerified` on context; `'onboarding'` CreditFeature + label + + `skipBalanceAssert` flag; country→`location_code` map. No behavior. +2. **Stage 0 form** — domain + country on one step → `projects`. +3. **Project Context store** — R2 blob helper + versions repository + + `get_project_context` MCP tool. +4. **Scrape + seed + synthesis** — `BROWSER` binding, scrape-to-markdown, the + seed function, OpenRouter dep, the "generating → strategy" UI. +5. **Chat** — `/api/onboarding/chat` + `update_project_context` tool + + `useChat` UI + revert. + +(There is no fixture-provider PR — we test against real providers.) + +## Out of scope (v1) + +Think/DO/Workflows, sub-agents, scheduled rank tracking, GSC-enriched strategy, +multi-language, auto content generation, the MCP context _resource_ (tool only). diff --git a/src/client/components/Markdown.tsx b/src/client/components/Markdown.tsx new file mode 100644 index 0000000..f8095b3 --- /dev/null +++ b/src/client/components/Markdown.tsx @@ -0,0 +1,141 @@ +import type { ComponentPropsWithoutRef, ReactNode } from "react"; +import ReactMarkdown from "react-markdown"; +import remarkGfm from "remark-gfm"; + +type Props = { + /** Raw Markdown source to render. */ + children: string; + className?: string; +}; + +/** + * Shared Markdown renderer with explicit per-element Tailwind classes. + * + * OpenSEO doesn't ship `@tailwindcss/typography`, so `prose` classes are + * no-ops — every block element is styled here instead. Tables use daisyUI's + * `table table-xs` so model- and strategy-generated tables stay readable. + * + * Anchor URLs are sanitized to http(s) only — LLMs can be coaxed into + * emitting `javascript:` payloads. + */ +export function Markdown({ children, className }: Props) { + return ( +
{children}
+ ), + ul: ({ children }: { children?: ReactNode }) => ( ++ {children} ++ ), + hr: () =>
{children};
+ }
+ return (
+
+ {children}
+
+ );
+ },
+ pre: ({ children }: { children?: ReactNode }) => (
+
+ {children}
+
+ ),
+ table: ({ children }: { children?: ReactNode }) => (
+ {children}
- ), - ul: ({ children }: { children?: ReactNode }) => ( -- {children} -- ), - hr: () =>
{children};
- }
- return (
-
- {children}
-
- );
- },
- pre: ({ children }: { children?: ReactNode }) => (
-
- {children}
-
- ),
- table: ({ children }: { children?: ReactNode }) => (
- + {/* useChat surfaces the failed response body as error.message; + the server's 402 backstop sends the paywall copy below. */} + {(error?.message ?? "").includes("free strategy questions") + ? "You've reached the free question limit. Upgrade to continue." + : "Something went wrong. Please refresh and try again."} +
++ {remaining} free question{remaining === 1 ? "" : "s"} left.{" "} + +
+ ) : null} +Hey, I’m Sam — welcome to OpenSEO.
++ You’re on the free preview: {FREE_ONBOARDING_QUESTION_LIMIT}{" "} + questions with me before you upgrade. I can draft an SEO strategy + for {domain}{" "} + and answer questions about OpenSEO or SEO. +
++ Want me to analyze{" "} + {domain} and + draft a strategy, or do you have questions first? +
+Want Sam to keep going?
++ Upgrade to run keyword research, rank tracking, and site audits on{" "} + {domain}. +
+ + {checkoutError ? ( +{checkoutError}
+ ) : null} ++ That’s all {FREE_ONBOARDING_QUESTION_LIMIT} free questions +
++ Upgrade to keep working with Sam and unlock the full OpenSEO app. +
+ ++ 30-day money-back guarantee +
+
+ + Your subscription’s active. +
++ Two quick steps left — connect Google Search Console, then set up + MCP for your agent. +
+