diff --git a/.agents/skills/competitive-landscape/SKILL.md b/.agents/skills/competitive-landscape/SKILL.md index 0883708..0036b9d 100644 --- a/.agents/skills/competitive-landscape/SKILL.md +++ b/.agents/skills/competitive-landscape/SKILL.md @@ -18,6 +18,15 @@ Use this when the user wants a market-level view across several competitors. For - Optional known competitors - Optional location/language +## Project context + +The project-context tools are free and shared with the app and other agents. + +1. Call `get_project_context` first and ground the market read in it — the saved competitors are the starting roster, and the business and positioning decide who counts as a competitor. +2. This skill needs competitors. If none are saved, run a minimal inline setup: ask the user who they compete with, or infer a shortlist from `find_serp_competitors` and the site and confirm it, write it back with `update_project_context` (`addCompetitors`), then continue the landscape work. Never front-load the full interview; suggest `seo-project-setup` at the end for the rest. +3. Before spending credits, check the research log. If the same research ran within the last 30 days, reuse that result and say so instead of re-buying it. +4. On finish, write back what is durable with `update_project_context` — every confirmed competitor via `addCompetitors` with a short note on why they matter, plus `removeCompetitors` for entries you added that turned out irrelevant (leave rows the user added alone) — and append a research log entry: `{ appendResearchLog: { summary: "Competitive landscape: . Verdict: " } }`. + ## OpenSEO MCP tools - `research_keywords`: discover representative market queries. diff --git a/.agents/skills/competitor-analysis/SKILL.md b/.agents/skills/competitor-analysis/SKILL.md index 1ca9333..f84f5d7 100644 --- a/.agents/skills/competitor-analysis/SKILL.md +++ b/.agents/skills/competitor-analysis/SKILL.md @@ -18,6 +18,15 @@ Use this for a named competitor. For identifying the market leaders first, use ` - User's domain when comparison is requested - Optional topic/category/location/language +## Project context + +The project-context tools are free and shared with the app and other agents. + +1. Call `get_project_context` first and ground the analysis in it — the saved competitors say whether this domain is already known and what was concluded about it before. +2. This skill needs competitors. If none are saved, run a minimal inline setup: save the competitor being analyzed, and ask the user (or infer from `find_serp_competitors` and confirm) whether there are others, write them back with `update_project_context` (`addCompetitors`), then continue the analysis. Never front-load the full interview; suggest `seo-project-setup` at the end for the rest. +3. Before spending credits, check the research log. If the same research ran within the last 30 days, reuse that result and say so instead of re-buying it. +4. On finish, write back what is durable with `update_project_context` — an `addCompetitors` upsert for this domain with a short note on its strengths and where it is vulnerable — and append a research log entry: `{ appendResearchLog: { summary: "Competitor analysis: . Verdict: " } }`. + ## OpenSEO MCP tools - `get_domain_overview`: baseline organic traffic and keyword count. diff --git a/.agents/skills/create-repo-skill/SKILL.md b/.agents/skills/create-repo-skill/SKILL.md index 5364f6e..411a737 100644 --- a/.agents/skills/create-repo-skill/SKILL.md +++ b/.agents/skills/create-repo-skill/SKILL.md @@ -34,15 +34,17 @@ metadata: ln -s ../../.agents/skills/ .claude/skills/ ``` - - **Public product skill** (a customer-facing SEO workflow): no `internal` flag, usually no `.claude/skills` symlink (repo agents don't need customer workflows). Register it everywhere users discover skills: + - **Public product skill** (a customer-facing SEO workflow): no `internal` flag, usually no `.claude/skills` symlink (repo agents don't need customer workflows). A public skill is **auto-served to SAM, the live in-app agent** — the marking is fail-open, so a missing `internal: true` ships repo-dev instructions to end users. Give it the standard "Project context" preamble (copy a sibling like `seo-audit`) with the skill's required sections, and register it everywhere users discover skills: + - `src/server/features/sam/samSkills.test.ts` — add the name to the pinned public roster (the test fails otherwise; that failure is the guard) - `web/content/docs/skills/.mdx` — docs page (mirror a sibling like `competitor-analysis.mdx`: what it does, when to use it, what you get back, how to get the best result) - `web/content/docs/skills/index.md` — bullet in the right workflow section - `web/content/docs/skills/meta.json` — nav entry + - `src/routes/_app/ai.tsx` — `SKILL_NAMES` - `.agents/skills/seo-coach/SKILL.md` — one line in the "What each workflow does" roster - `plugins/openseo/skills/` — add the skill to the `skills` list in `scripts/sync-plugin-skills.mjs`, then run `pnpm sync-plugin-skills` (this directory holds real copies, not symlinks — the Claude Code and Codex plugins bundle from here, and Codex's installer silently skips symlinked files, so a symlink would ship a skill-less plugin). `pnpm ci:check` re-runs the sync and fails on drift, so a missed update here is caught, but the skill count and roster below are prose and aren't checked — update them by hand: both `plugins/openseo/*/plugin.json` `description` fields, the Codex manifest's `interface.longDescription`, and the skill lists in `web/content/docs/claude-code-plugin.md` and `web/content/docs/codex-plugin.md` - Optional: `web/src/lib/feature-pages.ts` and `web/content/docs/skills/setup.md` if it deserves marketing/setup placement -3. If the skill references MCP tools, use exact tool names and keep them in sync with `src/server/mcp/server.ts` — the tool names in skills are load-bearing for agents following them. +3. If the skill references MCP tools, use exact tool names and keep them in sync with `src/server/mcp/server.ts` — the tool names in skills are load-bearing for agents following them. For public skills also check `src/server/features/sam/samChatTools.ts`: SAM's toolset is a curated subset, and a skill step that names a tool SAM lacks dead-ends in the in-app agent. 4. `pnpm format:write` (covers the docs pages; `.agents/skills` itself is intentionally untouched), then commit. Skill prose follows `openseo-review-web-content` standards when public. diff --git a/.agents/skills/keyword-clustering/SKILL.md b/.agents/skills/keyword-clustering/SKILL.md index 0fbadf7..84ac92b 100644 --- a/.agents/skills/keyword-clustering/SKILL.md +++ b/.agents/skills/keyword-clustering/SKILL.md @@ -17,6 +17,15 @@ Group keywords into page-level clusters and decide which existing or new page sh If keywords are not provided, use `list_saved_keywords` for saved sets, `research_keywords` for seed discovery, or `get_ranked_keywords` when the user starts from a target domain. +## Project context + +The project-context tools are free and shared with the app and other agents. + +1. Call `get_project_context` first and ground the mapping in it — the saved key pages are the existing pages clusters should map to, and the business and goal decide which clusters are worth targeting. +2. This skill needs key pages. If none are saved, run a minimal inline setup: ask the user for the pages that matter, or propose a shortlist from the site, an audit, or Search Console and confirm it, write it back with `update_project_context` (`addKeyPages`), then continue the clustering. Never front-load the full interview; suggest `seo-project-setup` at the end for the rest. +3. Before spending credits, check the research log. If the same research ran within the last 30 days, reuse that result and say so instead of re-buying it. +4. On finish, write back what is durable with `update_project_context` — new or corrected `addKeyPages` entries with the topic each page now targets — and append a research log entry: `{ appendResearchLog: { summary: "Keyword clustering: . Verdict: " } }`. + ## OpenSEO MCP tools - `list_saved_keywords`: fetch an existing keyword set, optionally filtered by tags. diff --git a/.agents/skills/keyword-research/SKILL.md b/.agents/skills/keyword-research/SKILL.md index 4bdfc40..23918fc 100644 --- a/.agents/skills/keyword-research/SKILL.md +++ b/.agents/skills/keyword-research/SKILL.md @@ -17,6 +17,15 @@ Turn seed topics into a prioritized keyword opportunity set using OpenSEO MCP da If `projectId` is missing, use `list_projects` first. If the target market/location/language is unclear and would materially affect keyword metrics, ask the user; otherwise use the MCP tool defaults. +## Project context + +The project-context tools are free and shared with the app and other agents. + +1. Call `get_project_context` first and ground the research in it — the business, the goal, the markets, and the competitors and key pages already saved. +2. This skill needs `business_overview` and `current_goal`. If either is empty, run a minimal inline setup: ask the user, or infer from the site and confirm, just enough to fill them, write them back with `update_project_context`, then continue the research. Never front-load the full interview; suggest `seo-project-setup` at the end for the rest. +3. Before spending credits, check the research log. If the same research ran within the last 30 days, reuse that result and say so instead of re-buying it. +4. On finish, write back what is durable — a sharpened `business_overview` or `current_goal`, competitors that kept appearing in the SERPs via `addCompetitors`, pages the keywords should land on via `addKeyPages` — and append a research log entry: `{ appendResearchLog: { summary: "Keyword research: . Verdict: " } }`. + ## OpenSEO MCP tools - `research_keywords`: primary discovery tool. Use 1-5 seeds per call and prefer 150 results unless the user asks for exhaustive research. diff --git a/.agents/skills/link-prospecting/SKILL.md b/.agents/skills/link-prospecting/SKILL.md index e9b9fb7..6858211 100644 --- a/.agents/skills/link-prospecting/SKILL.md +++ b/.agents/skills/link-prospecting/SKILL.md @@ -17,6 +17,15 @@ Find realistic pages, sites, and authors that might reference the user's page, p - Optional competitors - Optional market/location/language +## Project context + +The project-context tools are free and shared with the app and other agents. + +1. Call `get_project_context` first and ground the outreach in it — positioning supplies the claim that makes a link worth giving, and the saved competitors are the backlink profiles to mine. +2. This skill needs `positioning` and competitors. If either is empty, run a minimal inline setup: ask the user why someone would cite them and who they compete with, or infer from the site and `find_serp_competitors` and confirm, write it back with `update_project_context`, then continue the prospecting. Never front-load the full interview; suggest `seo-project-setup` at the end for the rest. +3. Before spending credits, check the research log. If the same research ran within the last 30 days, reuse that result and say so instead of re-buying it. +4. On finish, write back what is durable — the linkable asset via `addKeyPages`, any competitor whose backlink profile proved useful via `addCompetitors` — and append a research log entry: `{ appendResearchLog: { summary: "Link prospecting: . Verdict: " } }`. + ## OpenSEO MCP tools - `get_serp_results`: find ranking articles, listicles, resource pages, comparisons, and topical publishers. diff --git a/.agents/skills/local-seo/SKILL.md b/.agents/skills/local-seo/SKILL.md index a7e7f3a..d74fae9 100644 --- a/.agents/skills/local-seo/SKILL.md +++ b/.agents/skills/local-seo/SKILL.md @@ -18,6 +18,15 @@ Use this when rankings depend on a physical location or service area. For nation - Its coordinate (latitude/longitude) — derive it from a `search_local_businesses` / `get_local_serp_results` row; only ask the user when derivation is ambiguous - One to three keywords customers actually search (e.g. "emergency plumber", not the brand name) +## Project context + +The project-context tools are free and shared with the app and other agents. + +1. Call `get_project_context` first and ground the work in it — what the business does and where it operates decides which keywords and radius matter. +2. This skill needs `business_overview`. If it is empty, run a minimal inline setup: infer what the business does and its location from the site and confirm it with the user in one question, write it back with `update_project_context`, then continue. Never front-load the full interview; suggest `seo-project-setup` at the end for the rest. +3. Before spending credits, check the research log. If the same research ran within the last 30 days, reuse that result and say so instead of re-buying it. +4. On finish, write back what is durable with `update_project_context` — local competitors that have a website via `addCompetitors` (competitor rows are keyed by domain, so skip listings without one), a corrected `business_overview` — and append a research log entry: `{ appendResearchLog: { summary: "Local SEO: near . Verdict: " } }`. + ## OpenSEO MCP tools - `search_local_businesses`: nearby listings, filterable by `minRating`, `minReviews`, and `isClaimed` — use `isClaimed: false` to find unclaimed listings when prospecting. One call with the brand name as `query` and a wide radius returns category, rating, review count, claimed status, coordinates, and `cid` for every location of a chain — usually enough that per-location `get_business_profile` calls are unnecessary. diff --git a/.agents/skills/seo-audit/SKILL.md b/.agents/skills/seo-audit/SKILL.md index 0da5a1a..6093600 100644 --- a/.agents/skills/seo-audit/SKILL.md +++ b/.agents/skills/seo-audit/SKILL.md @@ -16,6 +16,15 @@ Use this when asked for an SEO audit or review of a domain, especially when the - Domain to audit - `projectId` (use `list_projects`; if no project matches the domain, create one with `create_project`) +## Project context + +The project-context tools are free and shared with the app and other agents. + +1. Call `get_project_context` first and ground the report in it — what the business does decides which findings matter and what the one thing should be. +2. This skill needs `business_overview`. If it is empty, run a minimal inline setup: infer what the business does from the site and confirm it with the user in one question, write it back with `update_project_context`, then continue the audit. Never front-load the full interview; suggest `seo-project-setup` at the end for the rest. +3. Before spending credits, check the research log. If the same research ran within the last 30 days, reuse that result and say so instead of re-buying it. +4. On finish, write back what is durable — a corrected `business_overview`, the pages the report singles out via `addKeyPages` — and append a research log entry: `{ appendResearchLog: { summary: "Site audit: . Verdict: " } }`. + ## OpenSEO MCP tools - `whoami`: confirm connection and remaining credits before spending anything. If OpenSEO is not connected, stop and ask the user to connect it. diff --git a/.agents/skills/seo-coach/SKILL.md b/.agents/skills/seo-coach/SKILL.md index 82bbebb..0da5cf8 100644 --- a/.agents/skills/seo-coach/SKILL.md +++ b/.agents/skills/seo-coach/SKILL.md @@ -13,6 +13,15 @@ Act as a friendly SEO coach for users working with OpenSEO and an AI agent. Help Be warm, direct, and beginner-friendly. Ask whether the user is new to SEO and adapt the explanation depth. Avoid sounding like a course or a consultant deck. Make SEO feel doable. +## Project context + +The project-context tools are free and shared with the app and other agents. + +1. Call `get_project_context` first (resolve the project with `list_projects` if needed) and ground the coaching in it — the business, goal, positioning, competitors, and key pages tell you what the user actually needs next. +2. This skill requires no section. Read whatever is there, and let the `missingSections` list shape the recommendation: empty context usually means the next step is `seo-project-setup`. Never front-load the full interview. +3. Before spending credits, check the research log. If the same research ran within the last 30 days, reuse that result and say so instead of re-buying it. +4. On finish, write back what is durable — anything the user tells you about the business, goal, or positioning, via `update_project_context` — and append a research log entry when a session spends credits: `{ appendResearchLog: { summary: ": . Verdict: " } }`. + ## First response When this mode starts, orient the user: @@ -38,7 +47,7 @@ Good starting points: ## What each workflow does -- `seo-project-setup`: sets up the workspace, verifies MCP, captures goals and positioning, and connects Google Search Console (or imports GSC exports). +- `seo-project-setup`: verifies MCP, interviews the user about scope, goals, positioning, competitors, and key pages, and saves it all to the project's shared context. Also connects Google Search Console (or imports GSC exports). - `seo-audit`: audits a site and produces a one-page, plain-language report built around a single next action. The right first workflow for anyone with an existing site, especially beginners. - `keyword-research`: finds search opportunities from seed topics and evaluates volume, difficulty, CPC, intent, and SERPs. - `keyword-clustering`: groups keywords by intent and maps clusters to existing or proposed pages. @@ -55,9 +64,10 @@ Explain the difference between data sources: - Google Search Console (when connected on the project's Integrations page) is the user's own first-party data — real clicks, impressions, CTR, and position. Read it live with `get_search_console_performance` instead of asking for CSV exports. It's free (no credits) and the best starting point for "what already ranks" and near-ranking opportunities. - Web search can find current market context, recent pages, reviews, docs, social profiles, and contact paths outside OpenSEO. - Browser/page scraping can extract page copy, headings, author names, contact links, schema, and content structure. -- Local files can preserve strategy, GSC CSVs, content briefs, crawls, prospect lists, and prior decisions over time. +- Project context (`get_project_context` / `update_project_context`) is the project's shared memory: business, goal, positioning, writing preferences, competitors, key pages, and a research log. It is free, every skill reads it, and the user can edit it on the project's Context settings page. +- Local files are for file work: GSC CSVs, crawls, drafts, briefs, and reports. -Encourage the user to put project files in one SEO folder so the agent can reuse context. +Encourage the user to keep project knowledge in project context rather than in a local file, so it follows them across sessions and agents. ## Coaching patterns diff --git a/.agents/skills/seo-project-setup/SKILL.md b/.agents/skills/seo-project-setup/SKILL.md index 688397e..ce62392 100644 --- a/.agents/skills/seo-project-setup/SKILL.md +++ b/.agents/skills/seo-project-setup/SKILL.md @@ -1,46 +1,52 @@ --- name: seo-project-setup -description: Set up a durable local SEO workspace with project context, notes, goals, positioning, preferences, MCP checks, and Search Console data intake. +description: Populate a project's shared OpenSEO context — site scope, goals, positioning, competitors, key pages, and preferences — plus MCP checks and Search Console intake. --- # OpenSEO SEO Project Setup ## Goal -Help the user set up a local SEO workspace for one website or SEO project. The folder is where the agent saves notes, goals, exports, briefs, reports, preferences, and project context over time. This is a workspace and context setup workflow, not a full audit. +Interview the user once about one website or SEO project, and store the answers in that project's shared context in OpenSEO with `update_project_context`. That context is read by every other skill, by SAM in the app, and by the user on the project's Context settings page — so it survives new sessions, new machines, and new agents. This is a context setup workflow, not a full audit. ## Tone Be friendly, practical, and structured. Ask questions in small batches. Explain why each item matters only when useful. Do not overwhelm a beginner with jargon. +## Where the answers go + +Two project-context MCP tools do all the writing. Both are free — they spend no credits. + +- `get_project_context(projectId)`: everything already known about the project, plus a `missingSections` list. +- `update_project_context(projectId, updates)`: a list of patch ops. The ones this skill uses: + - `{ section: "business_overview" | "current_goal" | "positioning" | "writing_preferences", content }` + - `{ addCompetitors: [{ domain, name?, notes? }] }` + - `{ addKeyPages: [{ url, role: "hub" | "spoke" | "money" | "other", topic?, notes? }] }` + - `{ customSection: "", title?, content }` for anything that does not fit a typed section + - `{ appendResearchLog: { summary } }` when this session spends credits + +Write in batches as the interview progresses — do not hold every answer until the end. Sections are prose (~4,000 characters each), so a few tight paragraphs, not a transcript. + ## Checklist -### 1. Pick a working folder +### 1. Verify OpenSEO MCP and resolve the project -Suggest that the user choose or create a local folder for SEO work, for example: +Writes need a `projectId`, so do this first: -- `~/SEO//` -- `~/Documents/SEO//` -- A repo or workspace folder if SEO work should live beside website/content files +1. Use `whoami` if available. +2. Use `list_projects` to confirm the user can access projects. +3. Match the project to the website/domain they want to rank for. +4. If the project list is ambiguous, ask the user which project should be used. +5. If no project matches, offer to create one with `create_project`. +6. If the MCP is unavailable, tell the user to connect OpenSEO MCP; without it, nothing can be saved. -Explain that keeping notes, exports, briefs, scraped pages, reports, and preferences in one folder helps the agent build context over time. Future SEO workflows can use that folder rather than starting from a blank conversation. +Do not run research tools just to test connectivity; `whoami` and `list_projects` are enough. -Recommended starter structure: +### 2. Read what is already there -```text -seo-workspace/ - README.md - gsc/ - keywords/ - competitors/ - content/ - outreach/ - reports/ -``` +Call `get_project_context`. Show the user a short summary of what OpenSEO already knows and what is missing. Confirm or correct existing entries rather than re-asking questions that are already answered — this skill is often re-run after another skill filled in part of the context. -Do not create folders unless the user asks. If file tools are available and the user asks, create a simple structure and a short `README.md` with the current goals, known sites, and user preferences for how the agent should approach SEO for this project. - -### 2. Collect website scope +### 3. Collect website scope Ask for: @@ -51,7 +57,9 @@ Ask for: - Whether the site is new, established, migrating, or recovering from a drop - CMS or publishing workflow, if relevant -### 3. Capture goals +Write the durable parts to `business_overview`: what the business does, who it is for, the target markets/locales, and the site's current stage. + +### 4. Capture goals Ask the user what they want from SEO: @@ -65,7 +73,9 @@ Ask the user what they want from SEO: Ask for success metrics and timeframe. If goals are vague, help turn them into measurable goals such as "increase non-branded organic signups" or "rank top 10 for 20 buying-intent terms." -### 4. Capture positioning and strategy context +Write the result to `current_goal`, including the metric and timeframe. + +### 5. Capture positioning and strategy context Ask what research they have already done about the company, product, audience, and competitors. Request any notes, docs, customer interviews, positioning docs, pitch decks, landing pages, or strategy memos they can share. @@ -82,25 +92,35 @@ Probe for: If the user has not done this yet, offer to help research positioning using the company website, competitor pages, reviews, forums, and web search. -### 5. Verify OpenSEO MCP +Write to `positioning`: audience, the problem, the differentiator, and any claims the user wants defended. Ask about voice, banned words or phrases, and topics to avoid, and write those to `writing_preferences` — content-drafting workflows read that section. -After the user has described the company, website, goals, and positioning, check that OpenSEO MCP is configured and mapped to the right project: +### 6. Save competitors -1. Use `whoami` if available. -2. Use `list_projects` to confirm the user can access projects. -3. Match the project to the website/domain they want to rank for. -4. If the project list is ambiguous, ask the user which project should be used. -5. If the MCP is unavailable, tell the user to connect OpenSEO MCP before continuing with live OpenSEO data. +Turn the competitors and substitutes from step 5 into `addCompetitors` entries: one row per domain, with a short `notes` line on why they matter ("direct competitor, owns the comparison pages"). If the user is unsure who competes in search, `find_serp_competitors` on a handful of seed keywords will name them — confirm the list with the user before saving, and log the spend. -Do not run research tools just to test connectivity; `whoami` and `list_projects` are enough. +Competitors saved here are reused by `competitive-landscape`, `competitor-analysis`, and `link-prospecting`. -### 6. Connect Google Search Console +### 7. Inventory key assets + +Ask for or discover: + +- Sitemap or important URL list +- Current blog/resources/content library +- Product/category/feature pages +- Existing keyword lists +- Current rank trackers +- Backlink or PR assets +- Linkable assets such as studies, templates, tools, datasets, calculators, or original opinions + +Save the pages that actually matter with `addKeyPages` — money pages, topic hubs, and the linkable assets. This is a curated shortlist, not a site inventory: 10 to 30 URLs is normal. Give each one a `role` and, where known, the `topic` it targets. + +### 8. Connect Google Search Console GSC is the richest first-party signal: existing impressions, near-ranking terms, cannibalization, and pages that already have search demand. **Preferred (hosted): connect it natively.** On the project's Integrations page, connect Google Search Console and pull live data with `get_search_console_performance`. Once connected, the agent reads it directly in `keyword-research` and `keyword-clustering` — no manual files to maintain. -**Fallback (self-hosted, or if the user prefers files):** ask the user to export CSVs from Search Console into the SEO working folder. +**Fallback (self-hosted, or if the user prefers files):** ask the user to export CSVs from Search Console into a local working folder (see step 9). Recommended exports: @@ -118,19 +138,22 @@ gsc/queries-last-16-months.csv gsc/pages-last-16-months.csv ``` -### 7. Inventory existing assets +### 9. Set up a local folder only for file work -Ask for or discover: +Project knowledge lives in OpenSEO, not on disk. A local folder is still useful for the things that are actually files: GSC CSV exports, crawls, drafts, briefs, and reports. -- Sitemap or important URL list -- Current blog/resources/content library -- Product/category/feature pages -- Existing keyword lists -- Current rank trackers -- Backlink or PR assets -- Linkable assets such as studies, templates, tools, datasets, calculators, or original opinions +If the user wants one, suggest `~/SEO//` or a folder beside the website/content repo, with a structure like: -### 8. Recommend first workflow +```text +seo-workspace/ + gsc/ + drafts/ + reports/ +``` + +Do not create folders unless the user asks, and do not duplicate goals, positioning, or competitors into a local file — that is what the project context is for. + +### 10. Recommend first workflow After intake, recommend one next OpenSEO workflow: @@ -150,17 +173,23 @@ Use a checklist with statuses: Then summarize: -- Working folder - OpenSEO MCP/project status - Sites in scope - Goals - Known positioning -- Uploaded data/files +- Competitors saved +- Key pages saved +- Search Console status and any local files +- Sections still missing from project context - Recommended next workflow +Tell the user they can read and edit everything saved here on the project's Context settings page. + ## Guardrails - Keep setup lightweight. The user should feel oriented, not assigned homework. +- Confirm facts with the user before writing them. Inferences from the site are fine to propose, but they get saved as agreed answers, not guesses. - Do not pretend a GSC CSV has been uploaded unless you can see it, and do not claim Search Console is connected unless `get_search_console_performance` confirms it (it returns a "not connected" message otherwise). -- Keep project setup focused on setup and context unless the user asks for live research. +- Keep project setup focused on setup and context unless the user asks for live research. If a step does spend credits, append a research log entry so other skills do not re-buy it. - If web search or scraping is used for positioning research, distinguish source evidence from inference. +- Overwriting a section replaces it. When context already exists, merge the new answers into the existing prose instead of discarding it. diff --git a/drizzle-pg/0020_project_memory.sql b/drizzle-pg/0020_project_memory.sql new file mode 100644 index 0000000..7593345 --- /dev/null +++ b/drizzle-pg/0020_project_memory.sql @@ -0,0 +1,49 @@ +CREATE TABLE "project_competitors" ( + "id" text PRIMARY KEY NOT NULL, + "project_id" text NOT NULL, + "domain" text NOT NULL, + "name" text, + "notes" text, + "updated_at" text DEFAULT to_char(now() AT TIME ZONE 'utc', 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"') NOT NULL, + "updated_by" text NOT NULL +); +--> statement-breakpoint +CREATE TABLE "project_context_sections" ( + "project_id" text NOT NULL, + "key" text NOT NULL, + "title" text, + "content" text NOT NULL, + "updated_at" text DEFAULT to_char(now() AT TIME ZONE 'utc', 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"') NOT NULL, + "updated_by" text NOT NULL, + CONSTRAINT "project_context_sections_project_id_key_pk" PRIMARY KEY("project_id","key") +); +--> statement-breakpoint +CREATE TABLE "project_key_pages" ( + "id" text PRIMARY KEY NOT NULL, + "project_id" text NOT NULL, + "url" text NOT NULL, + "role" text NOT NULL, + "topic" text, + "notes" text, + "updated_at" text DEFAULT to_char(now() AT TIME ZONE 'utc', 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"') NOT NULL, + "updated_by" text NOT NULL +); +--> statement-breakpoint +CREATE TABLE "project_research_log" ( + "id" text PRIMARY KEY NOT NULL, + "project_id" text NOT NULL, + "entry_date" text NOT NULL, + "summary" text NOT NULL, + "created_by" text NOT NULL, + "created_at" text DEFAULT to_char(now() AT TIME ZONE 'utc', 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"') NOT NULL +); +--> statement-breakpoint +ALTER TABLE "project_competitors" ADD CONSTRAINT "project_competitors_project_id_projects_id_fk" FOREIGN KEY ("project_id") REFERENCES "public"."projects"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "project_context_sections" ADD CONSTRAINT "project_context_sections_project_id_projects_id_fk" FOREIGN KEY ("project_id") REFERENCES "public"."projects"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "project_key_pages" ADD CONSTRAINT "project_key_pages_project_id_projects_id_fk" FOREIGN KEY ("project_id") REFERENCES "public"."projects"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "project_research_log" ADD CONSTRAINT "project_research_log_project_id_projects_id_fk" FOREIGN KEY ("project_id") REFERENCES "public"."projects"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +CREATE UNIQUE INDEX "project_competitors_project_domain_idx" ON "project_competitors" USING btree ("project_id","domain");--> statement-breakpoint +CREATE UNIQUE INDEX "project_key_pages_project_url_idx" ON "project_key_pages" USING btree ("project_id","url");--> statement-breakpoint +CREATE INDEX "project_research_log_project_date_idx" ON "project_research_log" USING btree ("project_id","entry_date"); +--> statement-breakpoint +DROP TABLE "sam_project_memory" CASCADE; diff --git a/drizzle-pg/meta/0020_snapshot.json b/drizzle-pg/meta/0020_snapshot.json new file mode 100644 index 0000000..9e812df --- /dev/null +++ b/drizzle-pg/meta/0020_snapshot.json @@ -0,0 +1,4093 @@ +{ + "id": "ac4619e0-4754-4c86-9bdb-417b10629526", + "prevId": "45157b1f-9604-4223-b9ca-0a3e2ccdacf2", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.backlink_snapshots": { + "name": "backlink_snapshots", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "domain": { + "name": "domain", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "rank": { + "name": "rank", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "backlinks": { + "name": "backlinks", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "referring_domains": { + "name": "referring_domains", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "broken_backlinks": { + "name": "broken_backlinks", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "new_backlinks": { + "name": "new_backlinks", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "lost_backlinks": { + "name": "lost_backlinks", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "new_referring_domains": { + "name": "new_referring_domains", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "lost_referring_domains": { + "name": "lost_referring_domains", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "captured_at": { + "name": "captured_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "to_char(now() AT TIME ZONE 'utc', 'YYYY-MM-DD\"T\"HH24:MI:SS.MS\"Z\"')" + } + }, + "indexes": { + "backlink_snapshots_project_captured_idx": { + "name": "backlink_snapshots_project_captured_idx", + "columns": [ + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "captured_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "backlink_snapshots_project_id_projects_id_fk": { + "name": "backlink_snapshots_project_id_projects_id_fk", + "tableFrom": "backlink_snapshots", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.keyword_metrics": { + "name": "keyword_metrics", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "keyword": { + "name": "keyword", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "location_code": { + "name": "location_code", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "language_code": { + "name": "language_code", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'en'" + }, + "search_volume": { + "name": "search_volume", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "cpc": { + "name": "cpc", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "competition": { + "name": "competition", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "keyword_difficulty": { + "name": "keyword_difficulty", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "intent": { + "name": "intent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "monthly_searches": { + "name": "monthly_searches", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "fetched_at": { + "name": "fetched_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "to_char(now() AT TIME ZONE 'utc', 'YYYY-MM-DD\"T\"HH24:MI:SS.MS\"Z\"')" + } + }, + "indexes": { + "keyword_metrics_unique_project_keyword_location_language": { + "name": "keyword_metrics_unique_project_keyword_location_language", + "columns": [ + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "keyword", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "location_code", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "language_code", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "keyword_metrics_lookup_idx": { + "name": "keyword_metrics_lookup_idx", + "columns": [ + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "keyword", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "location_code", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "language_code", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "fetched_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "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": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.organization_activation_state": { + "name": "organization_activation_state", + "schema": "", + "columns": { + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "first_mcp_authorized_at": { + "name": "first_mcp_authorized_at", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "first_mcp_tool_call_at": { + "name": "first_mcp_tool_call_at", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "to_char(now() AT TIME ZONE 'utc', 'YYYY-MM-DD\"T\"HH24:MI:SS.MS\"Z\"')" + } + }, + "indexes": {}, + "foreignKeys": { + "organization_activation_state_organization_id_organization_id_fk": { + "name": "organization_activation_state_organization_id_organization_id_fk", + "tableFrom": "organization_activation_state", + "tableTo": "organization", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.project_activation_state": { + "name": "project_activation_state", + "schema": "", + "columns": { + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "competitor_step_clicked_at": { + "name": "competitor_step_clicked_at", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "mcp_card_dismissed_at": { + "name": "mcp_card_dismissed_at", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ga4_card_dismissed_at": { + "name": "ga4_card_dismissed_at", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "to_char(now() AT TIME ZONE 'utc', 'YYYY-MM-DD\"T\"HH24:MI:SS.MS\"Z\"')" + } + }, + "indexes": {}, + "foreignKeys": { + "project_activation_state_project_id_projects_id_fk": { + "name": "project_activation_state_project_id_projects_id_fk", + "tableFrom": "project_activation_state", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.project_competitors": { + "name": "project_competitors", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "domain": { + "name": "domain", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "to_char(now() AT TIME ZONE 'utc', 'YYYY-MM-DD\"T\"HH24:MI:SS.MS\"Z\"')" + }, + "updated_by": { + "name": "updated_by", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "project_competitors_project_domain_idx": { + "name": "project_competitors_project_domain_idx", + "columns": [ + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "domain", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "project_competitors_project_id_projects_id_fk": { + "name": "project_competitors_project_id_projects_id_fk", + "tableFrom": "project_competitors", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.project_context_sections": { + "name": "project_context_sections", + "schema": "", + "columns": { + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "to_char(now() AT TIME ZONE 'utc', 'YYYY-MM-DD\"T\"HH24:MI:SS.MS\"Z\"')" + }, + "updated_by": { + "name": "updated_by", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "project_context_sections_project_id_projects_id_fk": { + "name": "project_context_sections_project_id_projects_id_fk", + "tableFrom": "project_context_sections", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "project_context_sections_project_id_key_pk": { + "name": "project_context_sections_project_id_key_pk", + "columns": [ + "project_id", + "key" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.project_key_pages": { + "name": "project_key_pages", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "topic": { + "name": "topic", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "to_char(now() AT TIME ZONE 'utc', 'YYYY-MM-DD\"T\"HH24:MI:SS.MS\"Z\"')" + }, + "updated_by": { + "name": "updated_by", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "project_key_pages_project_url_idx": { + "name": "project_key_pages_project_url_idx", + "columns": [ + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "url", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "project_key_pages_project_id_projects_id_fk": { + "name": "project_key_pages_project_id_projects_id_fk", + "tableFrom": "project_key_pages", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.project_research_log": { + "name": "project_research_log", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "entry_date": { + "name": "entry_date", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "summary": { + "name": "summary", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "to_char(now() AT TIME ZONE 'utc', 'YYYY-MM-DD\"T\"HH24:MI:SS.MS\"Z\"')" + } + }, + "indexes": { + "project_research_log_project_date_idx": { + "name": "project_research_log_project_date_idx", + "columns": [ + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entry_date", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "project_research_log_project_id_projects_id_fk": { + "name": "project_research_log_project_id_projects_id_fk", + "tableFrom": "project_research_log", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.projects": { + "name": "projects", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "domain": { + "name": "domain", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "location_code": { + "name": "location_code", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 2840 + }, + "language_code": { + "name": "language_code", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'en'" + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "to_char(now() AT TIME ZONE 'utc', 'YYYY-MM-DD\"T\"HH24:MI:SS.MS\"Z\"')" + }, + "archived_at": { + "name": "archived_at", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "projects_one_default_per_organization_idx": { + "name": "projects_one_default_per_organization_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"projects\".\"name\" = 'Default' AND \"projects\".\"domain\" IS NULL AND \"projects\".\"archived_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "projects_organization_id_idx": { + "name": "projects_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "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": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.rank_check_runs": { + "name": "rank_check_runs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "config_id": { + "name": "config_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "keywords_total": { + "name": "keywords_total", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "keywords_checked": { + "name": "keywords_checked", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "is_subset_run": { + "name": "is_subset_run", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "started_at": { + "name": "started_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "to_char(now() AT TIME ZONE 'utc', 'YYYY-MM-DD\"T\"HH24:MI:SS.MS\"Z\"')" + }, + "completed_at": { + "name": "completed_at", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "rank_check_runs_config_idx": { + "name": "rank_check_runs_config_idx", + "columns": [ + { + "expression": "config_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "rank_check_runs_project_idx": { + "name": "rank_check_runs_project_idx", + "columns": [ + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "rank_check_runs_one_active_per_config_idx": { + "name": "rank_check_runs_one_active_per_config_idx", + "columns": [ + { + "expression": "config_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"rank_check_runs\".\"status\" IN ('pending', 'running')", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "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": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.rank_snapshots": { + "name": "rank_snapshots", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "run_id": { + "name": "run_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tracking_keyword_id": { + "name": "tracking_keyword_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "keyword": { + "name": "keyword", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "device": { + "name": "device", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "position": { + "name": "position", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "serp_features": { + "name": "serp_features", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "checked_at": { + "name": "checked_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "to_char(now() AT TIME ZONE 'utc', 'YYYY-MM-DD\"T\"HH24:MI:SS.MS\"Z\"')" + } + }, + "indexes": { + "rank_snapshots_keyword_device_idx": { + "name": "rank_snapshots_keyword_device_idx", + "columns": [ + { + "expression": "tracking_keyword_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "device", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "checked_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "rank_snapshots_run_keyword_device_idx": { + "name": "rank_snapshots_run_keyword_device_idx", + "columns": [ + { + "expression": "run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "tracking_keyword_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "device", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "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": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.rank_tracking_configs": { + "name": "rank_tracking_configs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "domain": { + "name": "domain", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "location_code": { + "name": "location_code", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 2840 + }, + "language_code": { + "name": "language_code", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'en'" + }, + "devices": { + "name": "devices", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'both'" + }, + "serp_depth": { + "name": "serp_depth", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "schedule_interval": { + "name": "schedule_interval", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'weekly'" + }, + "location_name": { + "name": "location_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "last_checked_at": { + "name": "last_checked_at", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "next_check_at": { + "name": "next_check_at", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_skip_reason": { + "name": "last_skip_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "to_char(now() AT TIME ZONE 'utc', 'YYYY-MM-DD\"T\"HH24:MI:SS.MS\"Z\"')" + } + }, + "indexes": { + "rank_tracking_configs_project_active_created_idx": { + "name": "rank_tracking_configs_project_active_created_idx", + "columns": [ + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "is_active", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "rank_tracking_configs_national_idx": { + "name": "rank_tracking_configs_national_idx", + "columns": [ + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "domain", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "location_code", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"rank_tracking_configs\".\"location_name\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "rank_tracking_configs_local_idx": { + "name": "rank_tracking_configs_local_idx", + "columns": [ + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "domain", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "location_code", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "location_name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"rank_tracking_configs\".\"location_name\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "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": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.rank_tracking_keywords": { + "name": "rank_tracking_keywords", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "config_id": { + "name": "config_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "keyword": { + "name": "keyword", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "search_volume": { + "name": "search_volume", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "keyword_difficulty": { + "name": "keyword_difficulty", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "cpc": { + "name": "cpc", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "metrics_fetched_at": { + "name": "metrics_fetched_at", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "to_char(now() AT TIME ZONE 'utc', 'YYYY-MM-DD\"T\"HH24:MI:SS.MS\"Z\"')" + } + }, + "indexes": { + "rank_tracking_keywords_config_keyword_idx": { + "name": "rank_tracking_keywords_config_keyword_idx", + "columns": [ + { + "expression": "config_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "keyword", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "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": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.saved_keyword_tag_assignments": { + "name": "saved_keyword_tag_assignments", + "schema": "", + "columns": { + "saved_keyword_id": { + "name": "saved_keyword_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tag_id": { + "name": "tag_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "to_char(now() AT TIME ZONE 'utc', 'YYYY-MM-DD\"T\"HH24:MI:SS.MS\"Z\"')" + } + }, + "indexes": { + "saved_keyword_tag_assignments_unique_idx": { + "name": "saved_keyword_tag_assignments_unique_idx", + "columns": [ + { + "expression": "saved_keyword_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "tag_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "saved_keyword_tag_assignments_tag_idx": { + "name": "saved_keyword_tag_assignments_tag_idx", + "columns": [ + { + "expression": "tag_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "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": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.saved_keyword_tags": { + "name": "saved_keyword_tags", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "normalized_name": { + "name": "normalized_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "to_char(now() AT TIME ZONE 'utc', 'YYYY-MM-DD\"T\"HH24:MI:SS.MS\"Z\"')" + } + }, + "indexes": { + "saved_keyword_tags_project_normalized_name_idx": { + "name": "saved_keyword_tags_project_normalized_name_idx", + "columns": [ + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "normalized_name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "saved_keyword_tags_project_name_idx": { + "name": "saved_keyword_tags_project_name_idx", + "columns": [ + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "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": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.saved_keywords": { + "name": "saved_keywords", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "keyword": { + "name": "keyword", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "location_code": { + "name": "location_code", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 2840 + }, + "language_code": { + "name": "language_code", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'en'" + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "to_char(now() AT TIME ZONE 'utc', 'YYYY-MM-DD\"T\"HH24:MI:SS.MS\"Z\"')" + } + }, + "indexes": { + "saved_keywords_unique_project_keyword_location_language": { + "name": "saved_keywords_unique_project_keyword_location_language", + "columns": [ + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "keyword", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "location_code", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "language_code", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "saved_keywords_project_created_idx": { + "name": "saved_keywords_project_created_idx", + "columns": [ + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "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": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_onboarding_answers": { + "name": "user_onboarding_answers", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "interested_features": { + "name": "interested_features", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'[]'" + }, + "work_for": { + "name": "work_for", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "client_website_count": { + "name": "client_website_count", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "found_via": { + "name": "found_via", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "mcp_setup_intent": { + "name": "mcp_setup_intent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "gsc_nudge_dismissed_at": { + "name": "gsc_nudge_dismissed_at", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "to_char(now() AT TIME ZONE 'utc', 'YYYY-MM-DD\"T\"HH24:MI:SS.MS\"Z\"')" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "to_char(now() AT TIME ZONE 'utc', 'YYYY-MM-DD\"T\"HH24:MI:SS.MS\"Z\"')" + } + }, + "indexes": { + "user_onboarding_answers_organization_idx": { + "name": "user_onboarding_answers_organization_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "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": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.audit_issues": { + "name": "audit_issues", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "audit_id": { + "name": "audit_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "page_id": { + "name": "page_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "page_url": { + "name": "page_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "issue_type": { + "name": "issue_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "severity": { + "name": "severity", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'info'" + }, + "details_json": { + "name": "details_json", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "audit_issues_audit_type_idx": { + "name": "audit_issues_audit_type_idx", + "columns": [ + { + "expression": "audit_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "issue_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "audit_issues_page_id_idx": { + "name": "audit_issues_page_id_idx", + "columns": [ + { + "expression": "page_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "audit_issues_audit_id_audits_id_fk": { + "name": "audit_issues_audit_id_audits_id_fk", + "tableFrom": "audit_issues", + "tableTo": "audits", + "columnsFrom": [ + "audit_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "audit_issues_page_id_audit_pages_id_fk": { + "name": "audit_issues_page_id_audit_pages_id_fk", + "tableFrom": "audit_issues", + "tableTo": "audit_pages", + "columnsFrom": [ + "page_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.audit_lighthouse_results": { + "name": "audit_lighthouse_results", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "audit_id": { + "name": "audit_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "page_id": { + "name": "page_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "strategy": { + "name": "strategy", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "performance_score": { + "name": "performance_score", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "accessibility_score": { + "name": "accessibility_score", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "best_practices_score": { + "name": "best_practices_score", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "seo_score": { + "name": "seo_score", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "lcp_ms": { + "name": "lcp_ms", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "cls": { + "name": "cls", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "inp_ms": { + "name": "inp_ms", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "ttfb_ms": { + "name": "ttfb_ms", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "r2_key": { + "name": "r2_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "payload_size_bytes": { + "name": "payload_size_bytes", + "type": "integer", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "audit_lighthouse_results_audit_id_idx": { + "name": "audit_lighthouse_results_audit_id_idx", + "columns": [ + { + "expression": "audit_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "audit_lighthouse_results_page_id_idx": { + "name": "audit_lighthouse_results_page_id_idx", + "columns": [ + { + "expression": "page_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "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": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.audit_pages": { + "name": "audit_pages", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "audit_id": { + "name": "audit_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status_code": { + "name": "status_code", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "redirect_url": { + "name": "redirect_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "meta_description": { + "name": "meta_description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "canonical_url": { + "name": "canonical_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "robots_meta": { + "name": "robots_meta", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "og_title": { + "name": "og_title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "og_description": { + "name": "og_description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "og_image": { + "name": "og_image", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "h1_count": { + "name": "h1_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "h2_count": { + "name": "h2_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "h3_count": { + "name": "h3_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "h4_count": { + "name": "h4_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "h5_count": { + "name": "h5_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "h6_count": { + "name": "h6_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "heading_order_json": { + "name": "heading_order_json", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "word_count": { + "name": "word_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "images_total": { + "name": "images_total", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "images_missing_alt": { + "name": "images_missing_alt", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "images_json": { + "name": "images_json", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "internal_link_count": { + "name": "internal_link_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "external_link_count": { + "name": "external_link_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "has_structured_data": { + "name": "has_structured_data", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "hreflang_tags_json": { + "name": "hreflang_tags_json", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_indexable": { + "name": "is_indexable", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "x_robots_tag": { + "name": "x_robots_tag", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "header_canonical_url": { + "name": "header_canonical_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "crawl_depth": { + "name": "crawl_depth", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "in_sitemap": { + "name": "in_sitemap", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "content_hash": { + "name": "content_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "fetch_class": { + "name": "fetch_class", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'ok'" + }, + "response_time_ms": { + "name": "response_time_ms", + "type": "integer", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "audit_pages_audit_url_idx": { + "name": "audit_pages_audit_url_idx", + "columns": [ + { + "expression": "audit_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "url", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "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": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.audits": { + "name": "audits", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "started_by_user_id": { + "name": "started_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "start_url": { + "name": "start_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'running'" + }, + "workflow_instance_id": { + "name": "workflow_instance_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "config": { + "name": "config", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "pages_crawled": { + "name": "pages_crawled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "pages_total": { + "name": "pages_total", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "lighthouse_total": { + "name": "lighthouse_total", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "lighthouse_completed": { + "name": "lighthouse_completed", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "lighthouse_failed": { + "name": "lighthouse_failed", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "current_phase": { + "name": "current_phase", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'discovery'" + }, + "error_code": { + "name": "error_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "error_detail": { + "name": "error_detail", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "failed_phase": { + "name": "failed_phase", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "started_at": { + "name": "started_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "to_char(now() AT TIME ZONE 'utc', 'YYYY-MM-DD\"T\"HH24:MI:SS.MS\"Z\"')" + }, + "completed_at": { + "name": "completed_at", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "audits_project_id_idx": { + "name": "audits_project_id_idx", + "columns": [ + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "audits_started_by_user_id_idx": { + "name": "audits_started_by_user_id_idx", + "columns": [ + { + "expression": "started_by_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "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": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sam_sessions": { + "name": "sam_sessions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'New chat'" + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "to_char(now() AT TIME ZONE 'utc', 'YYYY-MM-DD\"T\"HH24:MI:SS.MS\"Z\"')" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "to_char(now() AT TIME ZONE 'utc', 'YYYY-MM-DD\"T\"HH24:MI:SS.MS\"Z\"')" + }, + "archived_at": { + "name": "archived_at", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "sam_sessions_project_updated_idx": { + "name": "sam_sessions_project_updated_idx", + "columns": [ + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "sam_sessions_project_id_projects_id_fk": { + "name": "sam_sessions_project_id_projects_id_fk", + "tableFrom": "sam_sessions", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "sam_sessions_user_id_user_id_fk": { + "name": "sam_sessions_user_id_user_id_fk", + "tableFrom": "sam_sessions", + "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 with time zone", + "primaryKey": false, + "notNull": false + }, + "refresh_token_expires_at": { + "name": "refresh_token_expires_at", + "type": "timestamp with time zone", + "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 with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "account_userId_idx": { + "name": "account_userId_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "account_accountId_providerId_idx": { + "name": "account_accountId_providerId_idx", + "columns": [ + { + "expression": "account_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "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.apikey": { + "name": "apikey", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "config_id": { + "name": "config_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'default'" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "start": { + "name": "start", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "prefix": { + "name": "prefix", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "reference_id": { + "name": "reference_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "refill_interval": { + "name": "refill_interval", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "refill_amount": { + "name": "refill_amount", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "last_refill_at": { + "name": "last_refill_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": true + }, + "rate_limit_enabled": { + "name": "rate_limit_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": true + }, + "rate_limit_time_window": { + "name": "rate_limit_time_window", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 60000 + }, + "rate_limit_max": { + "name": "rate_limit_max", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 120 + }, + "request_count": { + "name": "request_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "remaining": { + "name": "remaining", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "last_request": { + "name": "last_request", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "permissions": { + "name": "permissions", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "apikey_configId_idx": { + "name": "apikey_configId_idx", + "columns": [ + { + "expression": "config_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "apikey_referenceId_idx": { + "name": "apikey_referenceId_idx", + "columns": [ + { + "expression": "reference_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "apikey_key_idx": { + "name": "apikey_key_idx", + "columns": [ + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.invitation": { + "name": "invitation", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "inviter_id": { + "name": "inviter_id", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "invitation_organizationId_idx": { + "name": "invitation_organizationId_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "invitation_email_idx": { + "name": "invitation_email_idx", + "columns": [ + { + "expression": "email", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "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": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.member": { + "name": "member", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'member'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "member_organizationId_idx": { + "name": "member_organizationId_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "member_userId_idx": { + "name": "member_userId_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "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": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.organization": { + "name": "organization", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "logo": { + "name": "logo", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "metadata": { + "name": "metadata", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "organization_slug_uidx": { + "name": "organization_slug_uidx", + "columns": [ + { + "expression": "slug", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "organization_slug_unique": { + "name": "organization_slug_unique", + "nullsNotDistinct": false, + "columns": [ + "slug" + ] + } + }, + "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 with time zone", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "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 + }, + "active_organization_id": { + "name": "active_organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "session_userId_idx": { + "name": "session_userId_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "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 with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "analytics_opted_out": { + "name": "analytics_opted_out", + "type": "boolean", + "primaryKey": false, + "notNull": false + } + }, + "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 with time zone", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "verification_identifier_idx": { + "name": "verification_identifier_idx", + "columns": [ + { + "expression": "identifier", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "verification_expiresAt_idx": { + "name": "verification_expiresAt_idx", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.billing_customer_status": { + "name": "billing_customer_status", + "schema": "", + "columns": { + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "is_paying": { + "name": "is_paying", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "paid_plan_id": { + "name": "paid_plan_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "paid_plan_status": { + "name": "paid_plan_status", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "customer_json": { + "name": "customer_json", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "synced_at": { + "name": "synced_at", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "to_char(now() AT TIME ZONE 'utc', 'YYYY-MM-DD\"T\"HH24:MI:SS.MS\"Z\"')" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "to_char(now() AT TIME ZONE 'utc', 'YYYY-MM-DD\"T\"HH24:MI:SS.MS\"Z\"')" + } + }, + "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": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ga4_connections": { + "name": "ga4_connections", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "property_id": { + "name": "property_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "property_display_name": { + "name": "property_display_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "property_time_zone": { + "name": "property_time_zone", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "property_currency_code": { + "name": "property_currency_code", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "connected_by_user_id": { + "name": "connected_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "ga4_account_id": { + "name": "ga4_account_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "connected_account_email": { + "name": "connected_account_email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "to_char(now() AT TIME ZONE 'utc', 'YYYY-MM-DD\"T\"HH24:MI:SS.MS\"Z\"')" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "to_char(now() AT TIME ZONE 'utc', 'YYYY-MM-DD\"T\"HH24:MI:SS.MS\"Z\"')" + } + }, + "indexes": { + "ga4_connections_project_idx": { + "name": "ga4_connections_project_idx", + "columns": [ + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "ga4_connections_organization_idx": { + "name": "ga4_connections_organization_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "ga4_connections_connector_idx": { + "name": "ga4_connections_connector_idx", + "columns": [ + { + "expression": "connected_by_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "ga4_account_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "ga4_connections_project_id_projects_id_fk": { + "name": "ga4_connections_project_id_projects_id_fk", + "tableFrom": "ga4_connections", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ga4_connections_organization_id_organization_id_fk": { + "name": "ga4_connections_organization_id_organization_id_fk", + "tableFrom": "ga4_connections", + "tableTo": "organization", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.gsc_connections": { + "name": "gsc_connections", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "site_url": { + "name": "site_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "connected_by_user_id": { + "name": "connected_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "gsc_account_id": { + "name": "gsc_account_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "connected_account_email": { + "name": "connected_account_email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "to_char(now() AT TIME ZONE 'utc', 'YYYY-MM-DD\"T\"HH24:MI:SS.MS\"Z\"')" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "to_char(now() AT TIME ZONE 'utc', 'YYYY-MM-DD\"T\"HH24:MI:SS.MS\"Z\"')" + } + }, + "indexes": { + "gsc_connections_project_idx": { + "name": "gsc_connections_project_idx", + "columns": [ + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "gsc_connections_organization_idx": { + "name": "gsc_connections_organization_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "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": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.telemetry_state": { + "name": "telemetry_state", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "default": 1 + }, + "install_id": { + "name": "install_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "installed_at": { + "name": "installed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_heartbeat_at": { + "name": "last_heartbeat_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_version": { + "name": "last_version", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "mcp_tool_call_count": { + "name": "mcp_tool_call_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": {}, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} diff --git a/drizzle-pg/meta/_journal.json b/drizzle-pg/meta/_journal.json index da11704..9eb6f46 100644 --- a/drizzle-pg/meta/_journal.json +++ b/drizzle-pg/meta/_journal.json @@ -141,6 +141,13 @@ "when": 1786209409297, "tag": "0019_clammy_selene", "breakpoints": true + }, + { + "idx": 20, + "version": "7", + "when": 1787099999115, + "tag": "0020_project_memory", + "breakpoints": true } ] } \ No newline at end of file diff --git a/drizzle/0042_project_memory.sql b/drizzle/0042_project_memory.sql new file mode 100644 index 0000000..b46d3c1 --- /dev/null +++ b/drizzle/0042_project_memory.sql @@ -0,0 +1,49 @@ +CREATE TABLE `project_competitors` ( + `id` text PRIMARY KEY NOT NULL, + `project_id` text NOT NULL, + `domain` text NOT NULL, + `name` text, + `notes` text, + `updated_at` text DEFAULT (current_timestamp) NOT NULL, + `updated_by` text NOT NULL, + FOREIGN KEY (`project_id`) REFERENCES `projects`(`id`) ON UPDATE no action ON DELETE cascade +); +--> statement-breakpoint +CREATE UNIQUE INDEX `project_competitors_project_domain_idx` ON `project_competitors` (`project_id`,`domain`);--> statement-breakpoint +CREATE TABLE `project_context_sections` ( + `project_id` text NOT NULL, + `key` text NOT NULL, + `title` text, + `content` text NOT NULL, + `updated_at` text DEFAULT (current_timestamp) NOT NULL, + `updated_by` text NOT NULL, + PRIMARY KEY(`project_id`, `key`), + FOREIGN KEY (`project_id`) REFERENCES `projects`(`id`) ON UPDATE no action ON DELETE cascade +); +--> statement-breakpoint +CREATE TABLE `project_key_pages` ( + `id` text PRIMARY KEY NOT NULL, + `project_id` text NOT NULL, + `url` text NOT NULL, + `role` text NOT NULL, + `topic` text, + `notes` text, + `updated_at` text DEFAULT (current_timestamp) NOT NULL, + `updated_by` text NOT NULL, + FOREIGN KEY (`project_id`) REFERENCES `projects`(`id`) ON UPDATE no action ON DELETE cascade +); +--> statement-breakpoint +CREATE UNIQUE INDEX `project_key_pages_project_url_idx` ON `project_key_pages` (`project_id`,`url`);--> statement-breakpoint +CREATE TABLE `project_research_log` ( + `id` text PRIMARY KEY NOT NULL, + `project_id` text NOT NULL, + `entry_date` text NOT NULL, + `summary` text NOT NULL, + `created_by` text NOT NULL, + `created_at` text DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ','now')) NOT NULL, + FOREIGN KEY (`project_id`) REFERENCES `projects`(`id`) ON UPDATE no action ON DELETE cascade +); +--> statement-breakpoint +CREATE INDEX `project_research_log_project_date_idx` ON `project_research_log` (`project_id`,`entry_date`); +--> statement-breakpoint +DROP TABLE `sam_project_memory`; diff --git a/drizzle/meta/0042_snapshot.json b/drizzle/meta/0042_snapshot.json new file mode 100644 index 0000000..c53b30a --- /dev/null +++ b/drizzle/meta/0042_snapshot.json @@ -0,0 +1,3732 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "f630c50c-3593-4109-bbc5-2de95ef1f74e", + "prevId": "71cb79b4-8b4a-4853-b400-e1d73a127498", + "tables": { + "backlink_snapshots": { + "name": "backlink_snapshots", + "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 + }, + "domain": { + "name": "domain", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "rank": { + "name": "rank", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "backlinks": { + "name": "backlinks", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "referring_domains": { + "name": "referring_domains", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "broken_backlinks": { + "name": "broken_backlinks", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "new_backlinks": { + "name": "new_backlinks", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "lost_backlinks": { + "name": "lost_backlinks", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "new_referring_domains": { + "name": "new_referring_domains", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "lost_referring_domains": { + "name": "lost_referring_domains", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "captured_at": { + "name": "captured_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(current_timestamp)" + } + }, + "indexes": { + "backlink_snapshots_project_captured_idx": { + "name": "backlink_snapshots_project_captured_idx", + "columns": [ + "project_id", + "captured_at" + ], + "isUnique": false + } + }, + "foreignKeys": { + "backlink_snapshots_project_id_projects_id_fk": { + "name": "backlink_snapshots_project_id_projects_id_fk", + "tableFrom": "backlink_snapshots", + "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": {} + }, + "organization_activation_state": { + "name": "organization_activation_state", + "columns": { + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "first_mcp_authorized_at": { + "name": "first_mcp_authorized_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "first_mcp_tool_call_at": { + "name": "first_mcp_tool_call_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(current_timestamp)" + } + }, + "indexes": {}, + "foreignKeys": { + "organization_activation_state_organization_id_organization_id_fk": { + "name": "organization_activation_state_organization_id_organization_id_fk", + "tableFrom": "organization_activation_state", + "tableTo": "organization", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "project_activation_state": { + "name": "project_activation_state", + "columns": { + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "competitor_step_clicked_at": { + "name": "competitor_step_clicked_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "mcp_card_dismissed_at": { + "name": "mcp_card_dismissed_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ga4_card_dismissed_at": { + "name": "ga4_card_dismissed_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(current_timestamp)" + } + }, + "indexes": {}, + "foreignKeys": { + "project_activation_state_project_id_projects_id_fk": { + "name": "project_activation_state_project_id_projects_id_fk", + "tableFrom": "project_activation_state", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "project_competitors": { + "name": "project_competitors", + "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 + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(current_timestamp)" + }, + "updated_by": { + "name": "updated_by", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "project_competitors_project_domain_idx": { + "name": "project_competitors_project_domain_idx", + "columns": [ + "project_id", + "domain" + ], + "isUnique": true + } + }, + "foreignKeys": { + "project_competitors_project_id_projects_id_fk": { + "name": "project_competitors_project_id_projects_id_fk", + "tableFrom": "project_competitors", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "project_context_sections": { + "name": "project_context_sections", + "columns": { + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(current_timestamp)" + }, + "updated_by": { + "name": "updated_by", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "project_context_sections_project_id_projects_id_fk": { + "name": "project_context_sections_project_id_projects_id_fk", + "tableFrom": "project_context_sections", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "project_context_sections_project_id_key_pk": { + "columns": [ + "project_id", + "key" + ], + "name": "project_context_sections_project_id_key_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "project_key_pages": { + "name": "project_key_pages", + "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 + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "topic": { + "name": "topic", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(current_timestamp)" + }, + "updated_by": { + "name": "updated_by", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "project_key_pages_project_url_idx": { + "name": "project_key_pages_project_url_idx", + "columns": [ + "project_id", + "url" + ], + "isUnique": true + } + }, + "foreignKeys": { + "project_key_pages_project_id_projects_id_fk": { + "name": "project_key_pages_project_id_projects_id_fk", + "tableFrom": "project_key_pages", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "project_research_log": { + "name": "project_research_log", + "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 + }, + "entry_date": { + "name": "entry_date", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "summary": { + "name": "summary", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(strftime('%Y-%m-%dT%H:%M:%fZ','now'))" + } + }, + "indexes": { + "project_research_log_project_date_idx": { + "name": "project_research_log_project_date_idx", + "columns": [ + "project_id", + "entry_date" + ], + "isUnique": false + } + }, + "foreignKeys": { + "project_research_log_project_id_projects_id_fk": { + "name": "project_research_log_project_id_projects_id_fk", + "tableFrom": "project_research_log", + "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'" + }, + "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" + }, + "projects_organization_id_idx": { + "name": "projects_organization_id_idx", + "columns": [ + "organization_id" + ], + "isUnique": false + } + }, + "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_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'" + }, + "location_name": { + "name": "location_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "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_active_created_idx": { + "name": "rank_tracking_configs_project_active_created_idx", + "columns": [ + "project_id", + "is_active", + "created_at" + ], + "isUnique": false + }, + "rank_tracking_configs_national_idx": { + "name": "rank_tracking_configs_national_idx", + "columns": [ + "project_id", + "domain", + "location_code" + ], + "isUnique": true, + "where": "\"rank_tracking_configs\".\"location_name\" IS NULL" + }, + "rank_tracking_configs_local_idx": { + "name": "rank_tracking_configs_local_idx", + "columns": [ + "project_id", + "domain", + "location_code", + "location_name" + ], + "isUnique": true, + "where": "\"rank_tracking_configs\".\"location_name\" IS NOT NULL" + } + }, + "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_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": {} + }, + "audit_issues": { + "name": "audit_issues", + "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": false, + "autoincrement": false + }, + "page_url": { + "name": "page_url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "issue_type": { + "name": "issue_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "severity": { + "name": "severity", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'info'" + }, + "details_json": { + "name": "details_json", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "audit_issues_audit_type_idx": { + "name": "audit_issues_audit_type_idx", + "columns": [ + "audit_id", + "issue_type" + ], + "isUnique": false + }, + "audit_issues_page_id_idx": { + "name": "audit_issues_page_id_idx", + "columns": [ + "page_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "audit_issues_audit_id_audits_id_fk": { + "name": "audit_issues_audit_id_audits_id_fk", + "tableFrom": "audit_issues", + "tableTo": "audits", + "columnsFrom": [ + "audit_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "audit_issues_page_id_audit_pages_id_fk": { + "name": "audit_issues_page_id_audit_pages_id_fk", + "tableFrom": "audit_issues", + "tableTo": "audit_pages", + "columnsFrom": [ + "page_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "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 + }, + "audit_lighthouse_results_page_id_idx": { + "name": "audit_lighthouse_results_page_id_idx", + "columns": [ + "page_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 + }, + "x_robots_tag": { + "name": "x_robots_tag", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "header_canonical_url": { + "name": "header_canonical_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "crawl_depth": { + "name": "crawl_depth", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "in_sitemap": { + "name": "in_sitemap", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "content_hash": { + "name": "content_hash", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "fetch_class": { + "name": "fetch_class", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'ok'" + }, + "response_time_ms": { + "name": "response_time_ms", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "audit_pages_audit_url_idx": { + "name": "audit_pages_audit_url_idx", + "columns": [ + "audit_id", + "url" + ], + "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'" + }, + "error_code": { + "name": "error_code", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "error_detail": { + "name": "error_detail", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "failed_phase": { + "name": "failed_phase", + "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": { + "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": {} + }, + "sam_sessions": { + "name": "sam_sessions", + "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 + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'New chat'" + }, + "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)" + }, + "archived_at": { + "name": "archived_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "sam_sessions_project_updated_idx": { + "name": "sam_sessions_project_updated_idx", + "columns": [ + "project_id", + "updated_at" + ], + "isUnique": false + } + }, + "foreignKeys": { + "sam_sessions_project_id_projects_id_fk": { + "name": "sam_sessions_project_id_projects_id_fk", + "tableFrom": "sam_sessions", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "sam_sessions_user_id_user_id_fk": { + "name": "sam_sessions_user_id_user_id_fk", + "tableFrom": "sam_sessions", + "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, + "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 + }, + "account_accountId_providerId_idx": { + "name": "account_accountId_providerId_idx", + "columns": [ + "account_id", + "provider_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": {} + }, + "apikey": { + "name": "apikey", + "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, + "default": "'default'" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "start": { + "name": "start", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "prefix": { + "name": "prefix", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "reference_id": { + "name": "reference_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "refill_interval": { + "name": "refill_interval", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "refill_amount": { + "name": "refill_amount", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_refill_at": { + "name": "last_refill_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": true + }, + "rate_limit_enabled": { + "name": "rate_limit_enabled", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": true + }, + "rate_limit_time_window": { + "name": "rate_limit_time_window", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 60000 + }, + "rate_limit_max": { + "name": "rate_limit_max", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 120 + }, + "request_count": { + "name": "request_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 0 + }, + "remaining": { + "name": "remaining", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_request": { + "name": "last_request", + "type": "integer", + "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 + }, + "permissions": { + "name": "permissions", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "metadata": { + "name": "metadata", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "apikey_configId_idx": { + "name": "apikey_configId_idx", + "columns": [ + "config_id" + ], + "isUnique": false + }, + "apikey_referenceId_idx": { + "name": "apikey_referenceId_idx", + "columns": [ + "reference_id" + ], + "isUnique": false + }, + "apikey_key_idx": { + "name": "apikey_key_idx", + "columns": [ + "key" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "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 + }, + "verification_expiresAt_idx": { + "name": "verification_expiresAt_idx", + "columns": [ + "expires_at" + ], + "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": {} + }, + "ga4_connections": { + "name": "ga4_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 + }, + "property_id": { + "name": "property_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "property_display_name": { + "name": "property_display_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "property_time_zone": { + "name": "property_time_zone", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "property_currency_code": { + "name": "property_currency_code", + "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 + }, + "ga4_account_id": { + "name": "ga4_account_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": { + "ga4_connections_project_idx": { + "name": "ga4_connections_project_idx", + "columns": [ + "project_id" + ], + "isUnique": true + }, + "ga4_connections_organization_idx": { + "name": "ga4_connections_organization_idx", + "columns": [ + "organization_id" + ], + "isUnique": false + }, + "ga4_connections_connector_idx": { + "name": "ga4_connections_connector_idx", + "columns": [ + "connected_by_user_id", + "ga4_account_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "ga4_connections_project_id_projects_id_fk": { + "name": "ga4_connections_project_id_projects_id_fk", + "tableFrom": "ga4_connections", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ga4_connections_organization_id_organization_id_fk": { + "name": "ga4_connections_organization_id_organization_id_fk", + "tableFrom": "ga4_connections", + "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 + }, + "gsc_account_id": { + "name": "gsc_account_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "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": {} + }, + "telemetry_state": { + "name": "telemetry_state", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": false, + "default": 1 + }, + "install_id": { + "name": "install_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "installed_at": { + "name": "installed_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_heartbeat_at": { + "name": "last_heartbeat_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_version": { + "name": "last_version", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "mcp_tool_call_count": { + "name": "mcp_tool_call_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + } + }, + "views": {}, + "enums": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "indexes": {} + } +} diff --git a/drizzle/meta/_journal.json b/drizzle/meta/_journal.json index 90a8c29..35b08cb 100644 --- a/drizzle/meta/_journal.json +++ b/drizzle/meta/_journal.json @@ -295,6 +295,13 @@ "when": 1786209407629, "tag": "0041_free_marvex", "breakpoints": true + }, + { + "idx": 42, + "version": "6", + "when": 1787099999115, + "tag": "0042_project_memory", + "breakpoints": true } ] } \ No newline at end of file diff --git a/package.json b/package.json index 2fc6221..e61a9b4 100644 --- a/package.json +++ b/package.json @@ -118,6 +118,7 @@ "sonner": "^2.0.7", "tailwindcss": "^4.1.16", "tldts": "^7.0.25", + "yaml": "^2.9.0", "zod": "^4.1.12" }, "devDependencies": { diff --git a/plugins/openseo/skills/competitive-landscape/SKILL.md b/plugins/openseo/skills/competitive-landscape/SKILL.md index 0883708..0036b9d 100644 --- a/plugins/openseo/skills/competitive-landscape/SKILL.md +++ b/plugins/openseo/skills/competitive-landscape/SKILL.md @@ -18,6 +18,15 @@ Use this when the user wants a market-level view across several competitors. For - Optional known competitors - Optional location/language +## Project context + +The project-context tools are free and shared with the app and other agents. + +1. Call `get_project_context` first and ground the market read in it — the saved competitors are the starting roster, and the business and positioning decide who counts as a competitor. +2. This skill needs competitors. If none are saved, run a minimal inline setup: ask the user who they compete with, or infer a shortlist from `find_serp_competitors` and the site and confirm it, write it back with `update_project_context` (`addCompetitors`), then continue the landscape work. Never front-load the full interview; suggest `seo-project-setup` at the end for the rest. +3. Before spending credits, check the research log. If the same research ran within the last 30 days, reuse that result and say so instead of re-buying it. +4. On finish, write back what is durable with `update_project_context` — every confirmed competitor via `addCompetitors` with a short note on why they matter, plus `removeCompetitors` for entries you added that turned out irrelevant (leave rows the user added alone) — and append a research log entry: `{ appendResearchLog: { summary: "Competitive landscape: . Verdict: " } }`. + ## OpenSEO MCP tools - `research_keywords`: discover representative market queries. diff --git a/plugins/openseo/skills/competitor-analysis/SKILL.md b/plugins/openseo/skills/competitor-analysis/SKILL.md index 1ca9333..f84f5d7 100644 --- a/plugins/openseo/skills/competitor-analysis/SKILL.md +++ b/plugins/openseo/skills/competitor-analysis/SKILL.md @@ -18,6 +18,15 @@ Use this for a named competitor. For identifying the market leaders first, use ` - User's domain when comparison is requested - Optional topic/category/location/language +## Project context + +The project-context tools are free and shared with the app and other agents. + +1. Call `get_project_context` first and ground the analysis in it — the saved competitors say whether this domain is already known and what was concluded about it before. +2. This skill needs competitors. If none are saved, run a minimal inline setup: save the competitor being analyzed, and ask the user (or infer from `find_serp_competitors` and confirm) whether there are others, write them back with `update_project_context` (`addCompetitors`), then continue the analysis. Never front-load the full interview; suggest `seo-project-setup` at the end for the rest. +3. Before spending credits, check the research log. If the same research ran within the last 30 days, reuse that result and say so instead of re-buying it. +4. On finish, write back what is durable with `update_project_context` — an `addCompetitors` upsert for this domain with a short note on its strengths and where it is vulnerable — and append a research log entry: `{ appendResearchLog: { summary: "Competitor analysis: . Verdict: " } }`. + ## OpenSEO MCP tools - `get_domain_overview`: baseline organic traffic and keyword count. diff --git a/plugins/openseo/skills/keyword-clustering/SKILL.md b/plugins/openseo/skills/keyword-clustering/SKILL.md index 0fbadf7..84ac92b 100644 --- a/plugins/openseo/skills/keyword-clustering/SKILL.md +++ b/plugins/openseo/skills/keyword-clustering/SKILL.md @@ -17,6 +17,15 @@ Group keywords into page-level clusters and decide which existing or new page sh If keywords are not provided, use `list_saved_keywords` for saved sets, `research_keywords` for seed discovery, or `get_ranked_keywords` when the user starts from a target domain. +## Project context + +The project-context tools are free and shared with the app and other agents. + +1. Call `get_project_context` first and ground the mapping in it — the saved key pages are the existing pages clusters should map to, and the business and goal decide which clusters are worth targeting. +2. This skill needs key pages. If none are saved, run a minimal inline setup: ask the user for the pages that matter, or propose a shortlist from the site, an audit, or Search Console and confirm it, write it back with `update_project_context` (`addKeyPages`), then continue the clustering. Never front-load the full interview; suggest `seo-project-setup` at the end for the rest. +3. Before spending credits, check the research log. If the same research ran within the last 30 days, reuse that result and say so instead of re-buying it. +4. On finish, write back what is durable with `update_project_context` — new or corrected `addKeyPages` entries with the topic each page now targets — and append a research log entry: `{ appendResearchLog: { summary: "Keyword clustering: . Verdict: " } }`. + ## OpenSEO MCP tools - `list_saved_keywords`: fetch an existing keyword set, optionally filtered by tags. diff --git a/plugins/openseo/skills/keyword-research/SKILL.md b/plugins/openseo/skills/keyword-research/SKILL.md index 4bdfc40..23918fc 100644 --- a/plugins/openseo/skills/keyword-research/SKILL.md +++ b/plugins/openseo/skills/keyword-research/SKILL.md @@ -17,6 +17,15 @@ Turn seed topics into a prioritized keyword opportunity set using OpenSEO MCP da If `projectId` is missing, use `list_projects` first. If the target market/location/language is unclear and would materially affect keyword metrics, ask the user; otherwise use the MCP tool defaults. +## Project context + +The project-context tools are free and shared with the app and other agents. + +1. Call `get_project_context` first and ground the research in it — the business, the goal, the markets, and the competitors and key pages already saved. +2. This skill needs `business_overview` and `current_goal`. If either is empty, run a minimal inline setup: ask the user, or infer from the site and confirm, just enough to fill them, write them back with `update_project_context`, then continue the research. Never front-load the full interview; suggest `seo-project-setup` at the end for the rest. +3. Before spending credits, check the research log. If the same research ran within the last 30 days, reuse that result and say so instead of re-buying it. +4. On finish, write back what is durable — a sharpened `business_overview` or `current_goal`, competitors that kept appearing in the SERPs via `addCompetitors`, pages the keywords should land on via `addKeyPages` — and append a research log entry: `{ appendResearchLog: { summary: "Keyword research: . Verdict: " } }`. + ## OpenSEO MCP tools - `research_keywords`: primary discovery tool. Use 1-5 seeds per call and prefer 150 results unless the user asks for exhaustive research. diff --git a/plugins/openseo/skills/link-prospecting/SKILL.md b/plugins/openseo/skills/link-prospecting/SKILL.md index e9b9fb7..6858211 100644 --- a/plugins/openseo/skills/link-prospecting/SKILL.md +++ b/plugins/openseo/skills/link-prospecting/SKILL.md @@ -17,6 +17,15 @@ Find realistic pages, sites, and authors that might reference the user's page, p - Optional competitors - Optional market/location/language +## Project context + +The project-context tools are free and shared with the app and other agents. + +1. Call `get_project_context` first and ground the outreach in it — positioning supplies the claim that makes a link worth giving, and the saved competitors are the backlink profiles to mine. +2. This skill needs `positioning` and competitors. If either is empty, run a minimal inline setup: ask the user why someone would cite them and who they compete with, or infer from the site and `find_serp_competitors` and confirm, write it back with `update_project_context`, then continue the prospecting. Never front-load the full interview; suggest `seo-project-setup` at the end for the rest. +3. Before spending credits, check the research log. If the same research ran within the last 30 days, reuse that result and say so instead of re-buying it. +4. On finish, write back what is durable — the linkable asset via `addKeyPages`, any competitor whose backlink profile proved useful via `addCompetitors` — and append a research log entry: `{ appendResearchLog: { summary: "Link prospecting: . Verdict: " } }`. + ## OpenSEO MCP tools - `get_serp_results`: find ranking articles, listicles, resource pages, comparisons, and topical publishers. diff --git a/plugins/openseo/skills/local-seo/SKILL.md b/plugins/openseo/skills/local-seo/SKILL.md index a7e7f3a..d74fae9 100644 --- a/plugins/openseo/skills/local-seo/SKILL.md +++ b/plugins/openseo/skills/local-seo/SKILL.md @@ -18,6 +18,15 @@ Use this when rankings depend on a physical location or service area. For nation - Its coordinate (latitude/longitude) — derive it from a `search_local_businesses` / `get_local_serp_results` row; only ask the user when derivation is ambiguous - One to three keywords customers actually search (e.g. "emergency plumber", not the brand name) +## Project context + +The project-context tools are free and shared with the app and other agents. + +1. Call `get_project_context` first and ground the work in it — what the business does and where it operates decides which keywords and radius matter. +2. This skill needs `business_overview`. If it is empty, run a minimal inline setup: infer what the business does and its location from the site and confirm it with the user in one question, write it back with `update_project_context`, then continue. Never front-load the full interview; suggest `seo-project-setup` at the end for the rest. +3. Before spending credits, check the research log. If the same research ran within the last 30 days, reuse that result and say so instead of re-buying it. +4. On finish, write back what is durable with `update_project_context` — local competitors that have a website via `addCompetitors` (competitor rows are keyed by domain, so skip listings without one), a corrected `business_overview` — and append a research log entry: `{ appendResearchLog: { summary: "Local SEO: near . Verdict: " } }`. + ## OpenSEO MCP tools - `search_local_businesses`: nearby listings, filterable by `minRating`, `minReviews`, and `isClaimed` — use `isClaimed: false` to find unclaimed listings when prospecting. One call with the brand name as `query` and a wide radius returns category, rating, review count, claimed status, coordinates, and `cid` for every location of a chain — usually enough that per-location `get_business_profile` calls are unnecessary. diff --git a/plugins/openseo/skills/seo-audit/SKILL.md b/plugins/openseo/skills/seo-audit/SKILL.md index 0da5a1a..6093600 100644 --- a/plugins/openseo/skills/seo-audit/SKILL.md +++ b/plugins/openseo/skills/seo-audit/SKILL.md @@ -16,6 +16,15 @@ Use this when asked for an SEO audit or review of a domain, especially when the - Domain to audit - `projectId` (use `list_projects`; if no project matches the domain, create one with `create_project`) +## Project context + +The project-context tools are free and shared with the app and other agents. + +1. Call `get_project_context` first and ground the report in it — what the business does decides which findings matter and what the one thing should be. +2. This skill needs `business_overview`. If it is empty, run a minimal inline setup: infer what the business does from the site and confirm it with the user in one question, write it back with `update_project_context`, then continue the audit. Never front-load the full interview; suggest `seo-project-setup` at the end for the rest. +3. Before spending credits, check the research log. If the same research ran within the last 30 days, reuse that result and say so instead of re-buying it. +4. On finish, write back what is durable — a corrected `business_overview`, the pages the report singles out via `addKeyPages` — and append a research log entry: `{ appendResearchLog: { summary: "Site audit: . Verdict: " } }`. + ## OpenSEO MCP tools - `whoami`: confirm connection and remaining credits before spending anything. If OpenSEO is not connected, stop and ask the user to connect it. diff --git a/plugins/openseo/skills/seo-coach/SKILL.md b/plugins/openseo/skills/seo-coach/SKILL.md index 82bbebb..0da5cf8 100644 --- a/plugins/openseo/skills/seo-coach/SKILL.md +++ b/plugins/openseo/skills/seo-coach/SKILL.md @@ -13,6 +13,15 @@ Act as a friendly SEO coach for users working with OpenSEO and an AI agent. Help Be warm, direct, and beginner-friendly. Ask whether the user is new to SEO and adapt the explanation depth. Avoid sounding like a course or a consultant deck. Make SEO feel doable. +## Project context + +The project-context tools are free and shared with the app and other agents. + +1. Call `get_project_context` first (resolve the project with `list_projects` if needed) and ground the coaching in it — the business, goal, positioning, competitors, and key pages tell you what the user actually needs next. +2. This skill requires no section. Read whatever is there, and let the `missingSections` list shape the recommendation: empty context usually means the next step is `seo-project-setup`. Never front-load the full interview. +3. Before spending credits, check the research log. If the same research ran within the last 30 days, reuse that result and say so instead of re-buying it. +4. On finish, write back what is durable — anything the user tells you about the business, goal, or positioning, via `update_project_context` — and append a research log entry when a session spends credits: `{ appendResearchLog: { summary: ": . Verdict: " } }`. + ## First response When this mode starts, orient the user: @@ -38,7 +47,7 @@ Good starting points: ## What each workflow does -- `seo-project-setup`: sets up the workspace, verifies MCP, captures goals and positioning, and connects Google Search Console (or imports GSC exports). +- `seo-project-setup`: verifies MCP, interviews the user about scope, goals, positioning, competitors, and key pages, and saves it all to the project's shared context. Also connects Google Search Console (or imports GSC exports). - `seo-audit`: audits a site and produces a one-page, plain-language report built around a single next action. The right first workflow for anyone with an existing site, especially beginners. - `keyword-research`: finds search opportunities from seed topics and evaluates volume, difficulty, CPC, intent, and SERPs. - `keyword-clustering`: groups keywords by intent and maps clusters to existing or proposed pages. @@ -55,9 +64,10 @@ Explain the difference between data sources: - Google Search Console (when connected on the project's Integrations page) is the user's own first-party data — real clicks, impressions, CTR, and position. Read it live with `get_search_console_performance` instead of asking for CSV exports. It's free (no credits) and the best starting point for "what already ranks" and near-ranking opportunities. - Web search can find current market context, recent pages, reviews, docs, social profiles, and contact paths outside OpenSEO. - Browser/page scraping can extract page copy, headings, author names, contact links, schema, and content structure. -- Local files can preserve strategy, GSC CSVs, content briefs, crawls, prospect lists, and prior decisions over time. +- Project context (`get_project_context` / `update_project_context`) is the project's shared memory: business, goal, positioning, writing preferences, competitors, key pages, and a research log. It is free, every skill reads it, and the user can edit it on the project's Context settings page. +- Local files are for file work: GSC CSVs, crawls, drafts, briefs, and reports. -Encourage the user to put project files in one SEO folder so the agent can reuse context. +Encourage the user to keep project knowledge in project context rather than in a local file, so it follows them across sessions and agents. ## Coaching patterns diff --git a/plugins/openseo/skills/seo-project-setup/SKILL.md b/plugins/openseo/skills/seo-project-setup/SKILL.md index 688397e..ce62392 100644 --- a/plugins/openseo/skills/seo-project-setup/SKILL.md +++ b/plugins/openseo/skills/seo-project-setup/SKILL.md @@ -1,46 +1,52 @@ --- name: seo-project-setup -description: Set up a durable local SEO workspace with project context, notes, goals, positioning, preferences, MCP checks, and Search Console data intake. +description: Populate a project's shared OpenSEO context — site scope, goals, positioning, competitors, key pages, and preferences — plus MCP checks and Search Console intake. --- # OpenSEO SEO Project Setup ## Goal -Help the user set up a local SEO workspace for one website or SEO project. The folder is where the agent saves notes, goals, exports, briefs, reports, preferences, and project context over time. This is a workspace and context setup workflow, not a full audit. +Interview the user once about one website or SEO project, and store the answers in that project's shared context in OpenSEO with `update_project_context`. That context is read by every other skill, by SAM in the app, and by the user on the project's Context settings page — so it survives new sessions, new machines, and new agents. This is a context setup workflow, not a full audit. ## Tone Be friendly, practical, and structured. Ask questions in small batches. Explain why each item matters only when useful. Do not overwhelm a beginner with jargon. +## Where the answers go + +Two project-context MCP tools do all the writing. Both are free — they spend no credits. + +- `get_project_context(projectId)`: everything already known about the project, plus a `missingSections` list. +- `update_project_context(projectId, updates)`: a list of patch ops. The ones this skill uses: + - `{ section: "business_overview" | "current_goal" | "positioning" | "writing_preferences", content }` + - `{ addCompetitors: [{ domain, name?, notes? }] }` + - `{ addKeyPages: [{ url, role: "hub" | "spoke" | "money" | "other", topic?, notes? }] }` + - `{ customSection: "", title?, content }` for anything that does not fit a typed section + - `{ appendResearchLog: { summary } }` when this session spends credits + +Write in batches as the interview progresses — do not hold every answer until the end. Sections are prose (~4,000 characters each), so a few tight paragraphs, not a transcript. + ## Checklist -### 1. Pick a working folder +### 1. Verify OpenSEO MCP and resolve the project -Suggest that the user choose or create a local folder for SEO work, for example: +Writes need a `projectId`, so do this first: -- `~/SEO//` -- `~/Documents/SEO//` -- A repo or workspace folder if SEO work should live beside website/content files +1. Use `whoami` if available. +2. Use `list_projects` to confirm the user can access projects. +3. Match the project to the website/domain they want to rank for. +4. If the project list is ambiguous, ask the user which project should be used. +5. If no project matches, offer to create one with `create_project`. +6. If the MCP is unavailable, tell the user to connect OpenSEO MCP; without it, nothing can be saved. -Explain that keeping notes, exports, briefs, scraped pages, reports, and preferences in one folder helps the agent build context over time. Future SEO workflows can use that folder rather than starting from a blank conversation. +Do not run research tools just to test connectivity; `whoami` and `list_projects` are enough. -Recommended starter structure: +### 2. Read what is already there -```text -seo-workspace/ - README.md - gsc/ - keywords/ - competitors/ - content/ - outreach/ - reports/ -``` +Call `get_project_context`. Show the user a short summary of what OpenSEO already knows and what is missing. Confirm or correct existing entries rather than re-asking questions that are already answered — this skill is often re-run after another skill filled in part of the context. -Do not create folders unless the user asks. If file tools are available and the user asks, create a simple structure and a short `README.md` with the current goals, known sites, and user preferences for how the agent should approach SEO for this project. - -### 2. Collect website scope +### 3. Collect website scope Ask for: @@ -51,7 +57,9 @@ Ask for: - Whether the site is new, established, migrating, or recovering from a drop - CMS or publishing workflow, if relevant -### 3. Capture goals +Write the durable parts to `business_overview`: what the business does, who it is for, the target markets/locales, and the site's current stage. + +### 4. Capture goals Ask the user what they want from SEO: @@ -65,7 +73,9 @@ Ask the user what they want from SEO: Ask for success metrics and timeframe. If goals are vague, help turn them into measurable goals such as "increase non-branded organic signups" or "rank top 10 for 20 buying-intent terms." -### 4. Capture positioning and strategy context +Write the result to `current_goal`, including the metric and timeframe. + +### 5. Capture positioning and strategy context Ask what research they have already done about the company, product, audience, and competitors. Request any notes, docs, customer interviews, positioning docs, pitch decks, landing pages, or strategy memos they can share. @@ -82,25 +92,35 @@ Probe for: If the user has not done this yet, offer to help research positioning using the company website, competitor pages, reviews, forums, and web search. -### 5. Verify OpenSEO MCP +Write to `positioning`: audience, the problem, the differentiator, and any claims the user wants defended. Ask about voice, banned words or phrases, and topics to avoid, and write those to `writing_preferences` — content-drafting workflows read that section. -After the user has described the company, website, goals, and positioning, check that OpenSEO MCP is configured and mapped to the right project: +### 6. Save competitors -1. Use `whoami` if available. -2. Use `list_projects` to confirm the user can access projects. -3. Match the project to the website/domain they want to rank for. -4. If the project list is ambiguous, ask the user which project should be used. -5. If the MCP is unavailable, tell the user to connect OpenSEO MCP before continuing with live OpenSEO data. +Turn the competitors and substitutes from step 5 into `addCompetitors` entries: one row per domain, with a short `notes` line on why they matter ("direct competitor, owns the comparison pages"). If the user is unsure who competes in search, `find_serp_competitors` on a handful of seed keywords will name them — confirm the list with the user before saving, and log the spend. -Do not run research tools just to test connectivity; `whoami` and `list_projects` are enough. +Competitors saved here are reused by `competitive-landscape`, `competitor-analysis`, and `link-prospecting`. -### 6. Connect Google Search Console +### 7. Inventory key assets + +Ask for or discover: + +- Sitemap or important URL list +- Current blog/resources/content library +- Product/category/feature pages +- Existing keyword lists +- Current rank trackers +- Backlink or PR assets +- Linkable assets such as studies, templates, tools, datasets, calculators, or original opinions + +Save the pages that actually matter with `addKeyPages` — money pages, topic hubs, and the linkable assets. This is a curated shortlist, not a site inventory: 10 to 30 URLs is normal. Give each one a `role` and, where known, the `topic` it targets. + +### 8. Connect Google Search Console GSC is the richest first-party signal: existing impressions, near-ranking terms, cannibalization, and pages that already have search demand. **Preferred (hosted): connect it natively.** On the project's Integrations page, connect Google Search Console and pull live data with `get_search_console_performance`. Once connected, the agent reads it directly in `keyword-research` and `keyword-clustering` — no manual files to maintain. -**Fallback (self-hosted, or if the user prefers files):** ask the user to export CSVs from Search Console into the SEO working folder. +**Fallback (self-hosted, or if the user prefers files):** ask the user to export CSVs from Search Console into a local working folder (see step 9). Recommended exports: @@ -118,19 +138,22 @@ gsc/queries-last-16-months.csv gsc/pages-last-16-months.csv ``` -### 7. Inventory existing assets +### 9. Set up a local folder only for file work -Ask for or discover: +Project knowledge lives in OpenSEO, not on disk. A local folder is still useful for the things that are actually files: GSC CSV exports, crawls, drafts, briefs, and reports. -- Sitemap or important URL list -- Current blog/resources/content library -- Product/category/feature pages -- Existing keyword lists -- Current rank trackers -- Backlink or PR assets -- Linkable assets such as studies, templates, tools, datasets, calculators, or original opinions +If the user wants one, suggest `~/SEO//` or a folder beside the website/content repo, with a structure like: -### 8. Recommend first workflow +```text +seo-workspace/ + gsc/ + drafts/ + reports/ +``` + +Do not create folders unless the user asks, and do not duplicate goals, positioning, or competitors into a local file — that is what the project context is for. + +### 10. Recommend first workflow After intake, recommend one next OpenSEO workflow: @@ -150,17 +173,23 @@ Use a checklist with statuses: Then summarize: -- Working folder - OpenSEO MCP/project status - Sites in scope - Goals - Known positioning -- Uploaded data/files +- Competitors saved +- Key pages saved +- Search Console status and any local files +- Sections still missing from project context - Recommended next workflow +Tell the user they can read and edit everything saved here on the project's Context settings page. + ## Guardrails - Keep setup lightweight. The user should feel oriented, not assigned homework. +- Confirm facts with the user before writing them. Inferences from the site are fine to propose, but they get saved as agreed answers, not guesses. - Do not pretend a GSC CSV has been uploaded unless you can see it, and do not claim Search Console is connected unless `get_search_console_performance` confirms it (it returns a "not connected" message otherwise). -- Keep project setup focused on setup and context unless the user asks for live research. +- Keep project setup focused on setup and context unless the user asks for live research. If a step does spend credits, append a research log entry so other skills do not re-buy it. - If web search or scraping is used for positioning research, distinguish source evidence from inference. +- Overwriting a section replaces it. When context already exists, merge the new answers into the existing prose instead of discarding it. diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b10a95e..1aff209 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -153,6 +153,9 @@ importers: tldts: specifier: ^7.0.25 version: 7.0.25 + yaml: + specifier: ^2.9.0 + version: 2.9.0 zod: specifier: ^4.1.12 version: 4.3.6 diff --git a/specs/0010-project-memory.md b/specs/0010-project-memory.md new file mode 100644 index 0000000..2430bbc --- /dev/null +++ b/specs/0010-project-memory.md @@ -0,0 +1,215 @@ +# Project memory (shared AI context per project) + +## Status + +Accepted + +## Context + +Qualitative knowledge about a project — what the business does, the current +goal, positioning, writing preferences, competitors, which pages matter — is +scattered across surfaces that cannot see each other: + +- **SAM** keeps it in `sam_project_memory`, two free-form markdown blobs + (`memory`, `research_log`) per project, writable only by the model via + Think's `set_context`. There is no UI; users cannot see or correct what SAM + believes. +- **Skills** (Claude Code / Codex users) keep it in a local folder the + `seo-project-setup` skill scaffolds — a `README.md` with goals, sites, and + preferences that never reaches the server, so SAM and the app never benefit. +- **Competitors are never persisted anywhere.** `find_serp_competitors` + results are returned and discarded. +- The onboarding chat agent produces a positioning/themes/keywords strategy + and persists none of it. + +The result: every surface re-interviews the user or re-infers the same facts, +paid research gets repeated because no surface knows another already ran it, +and there is no place a user can inspect or edit what the AI knows about their +project. + +## Decision + +One project-scoped memory store, shared by SAM, the MCP server, and a new +settings UI. Hybrid shape: a fixed set of typed sections with real schemas, +plus agent-creatable custom sections for anything that doesn't fit yet. It +replaces `sam_project_memory` entirely. + +### Data model + +List-shaped entities are normalized tables; prose lives in a sections table. +All tables follow the D1 + Postgres dual-schema convention (`src/db/*.schema.ts` +mirrored in `src/db/pg/*.schema.ts`, schema-parity test, `pnpm db:generate` +for both dialects). + +``` +project_context_sections + project_id FK → projects (cascade) + key text -- typed key or "custom:" + title text? -- custom sections only + content text -- markdown + updated_at text + updated_by text -- "user" | "sam" | "mcp" + PK (project_id, key) + +project_competitors + id, project_id FK (cascade) + domain text -- normalized host, unique per project + name text? + notes text? -- "direct competitor, strong on comparison pages" + updated_at, updated_by + UNIQUE (project_id, domain) + +project_key_pages + id, project_id FK (cascade) + url text -- unique per project + role text -- "hub" | "spoke" | "money" | "other" + topic text? -- target topic/keyword + notes text? + updated_at, updated_by + UNIQUE (project_id, url) + +project_research_log + id, project_id FK (cascade) + entry_date text -- YYYY-MM-DD, server-stamped + summary text -- ": . Verdict: " + created_by text -- "user" | "sam" | "mcp" +``` + +Typed section keys: `business_overview` (what the business does, who it's +for, target market/locales), `current_goal`, `positioning`, +`writing_preferences` (voice, banned words/phrases, topics to avoid). + +Guardrails: prose sections capped at ~4,000 chars; custom sections capped at +20 per project and ~4,000 chars each; competitors and key pages capped at 100 +rows each; research log pruned to 90 days on append. Caps keep the full +context small enough to inject into every SAM turn and return cheaply from +MCP. + +**Deliberately not stored:** a sitemap or crawl copy. The page inventory +lives in `audit_pages` (latest audit) and GSC, reachable through existing +tools. `project_key_pages` is a curated shortlist, not an inventory; agents +may propose entries from audit/GSC data. + +### MCP tools (two, free, no credits) + +- **`get_project_context(projectId)`** — read-only. Returns everything: + typed sections, competitors, key pages, custom sections, recent research + log. `text` is a rendered markdown digest; `structuredContent` carries the + typed data. Empty sections are listed explicitly ("missing: positioning, + writing_preferences") so agents know what to fill and can suggest + `seo-project-setup`. +- **`update_project_context(projectId, updates[])`** — an array of patch + ops, discriminated union: + - `{ section, content }` — set a typed section (empty string clears) + - `{ customSection, title?, content }` / `{ deleteCustomSection }` + - `{ addCompetitors: [{domain, name?, notes?}] }` (upsert by domain), + `{ removeCompetitors: [domain] }` + - `{ addKeyPages: [{url, role, topic?, notes?}] }` (upsert by url), + `{ removeKeyPages: [url] }` + - `{ appendResearchLog: { summary } }` — server stamps the date; + `{ removeResearchLog: [id] }` + +Both use `withMcpProjectAuth`, standard layering (tool → `ProjectContextService` +→ repository), `readOnlyHint` annotations, deep-link `meta` to the Context +settings page. Writes record `updated_by: "mcp"`. Register in +`src/server/mcp/server.ts` and the hand-maintained catalogue in +`src/client/features/ai-mcp/AvailableTools.tsx`. + +### SAM integration + +`sam_project_memory` is removed; the `SamChatAgent` block provider seam is +where the swap happens: + +- The writable `memory`/`research_log` blocks are replaced by a single + **read-only** context block rendering `get_project_context` output + (refreshed after each turn, as today, so cross-session writes land). +- SAM writes through the same adapted `update_project_context` tool that + MCP clients use (via `adaptMcpTool`, projectId injected), recorded as + `updated_by: "sam"`. One write path, one validation surface. +- The system prompt keeps its contract but points at the typed sections: + intake mode triggers when `business_overview` is empty; the bootstrap flow + (read site, infer, confirm, write) now writes typed sections and + competitors instead of a prose blob; the 30-day research-staleness rule + reads `project_research_log`. + +**Migration:** none. SAM usage is low, so `sam_project_memory` is dropped +outright (schema removal + drop migration); existing SAM memories are +discarded and SAM re-runs its intake flow on next use. + +### UI + +Project settings gets a gear button on the project switcher (replacing its +absence from any navigation) and splits into sub-pages: + +- **General** — existing name/domain/market form +- **Context** (new) — the memory UI: editable forms for the four prose + sections, tables with inline add/edit/delete for competitors and key pages, + cards for custom sections (rename/edit/delete), research log list + (read-only + delete). Every item shows provenance: "Updated by SAM · 2d + ago". Edits record `updated_by: "user"`. +- **Integrations** — Search Console + Analytics cards move here + +The gear lives on the switcher, so settings is reachable from every view +(including SAM's chat tab); existing deep links +(`#google-analytics`, GSC connect) keep working via redirects to the +Integrations sub-page. Server functions follow the standard +`requireProjectContext` → service → repository path with Zod schemas in +`src/types/schemas/`. + +### Skills + +Skills reach the store only through the two MCP tools (skills are distributed +by file copy; there is no server-side delivery). + +- **`seo-project-setup` rewritten** to be the canonical populate flow: the + interview steps (site scope, goals, positioning, competitors, key assets) + now end in `update_project_context` writes instead of a local `README.md`. + Local-folder scaffolding remains only for file-based work (GSC CSV + fallback, drafts). +- **Every SEO skill gets a standard "Project context" preamble**: + 1. Call `get_project_context` first; use it to ground the work. + 2. If the sections this skill _requires_ are empty, run a minimal inline + setup — ask (or infer from the site and confirm) just enough to fill + them, write them back, then continue the actual task. Suggest the full + `seo-project-setup` at the end. Never front-load the full interview. + 3. Before paid research, check the research log (30-day staleness rule). + 4. On finish, write back: durable learnings → sections/entities, research + spend → `appendResearchLog`. + + Required sections per skill: keyword-research → business_overview + + current_goal; competitive-landscape / competitor-analysis → competitors; + keyword-clustering → key pages; link-prospecting → positioning + + competitors; seo-audit → business_overview; seo-coach → reads everything, + requires nothing. Content-drafting flows additionally require + writing_preferences. + +Hand-maintained lists to update when skills change: `src/routes/_app/ai.tsx` +(`SKILL_NAMES` etc.) and `web/content/docs/skills/*`. + +### Rollout + +1. **Schema + service + MCP tools** (with the `sam_project_memory` drop) — + the store exists, Claude Code users can use it end-to-end. +2. **SAM cutover** — block provider swap, prompt update, tool adaptation. +3. **UI** — switcher gear, settings sub-pages, Context page. +4. **Skills pass** — rewrite `seo-project-setup`, add the preamble to the + SEO skills, update docs pages. + +In practice all four phases landed together on one branch. + +## Consequences + +- One source of truth: SAM, MCP clients, and the UI read and write the same + records; users can finally inspect and correct agent beliefs, with + provenance on every item. +- Cross-surface research dedupe: the shared log stops SAM and Claude Code + from independently re-buying the same research. +- Competitors and key pages become joinable product data — future + rank-tracker comparisons, share-of-voice, and audit cross-references can + reference them without parsing prose. +- Typed sections require a code change to extend; the custom-section + overflow is the pressure valve and tells us which section to promote next. +- Two more MCP tool schemas in every client's token budget (mitigated by + keeping the patch union compact). +- The onboarding chat agent still discards its strategy output; persisting it + into these sections is a natural follow-up, out of scope here. diff --git a/src/client/components/chat/ChatMessage.tsx b/src/client/components/chat/ChatMessage.tsx index 51ff596..cd2a57e 100644 --- a/src/client/components/chat/ChatMessage.tsx +++ b/src/client/components/chat/ChatMessage.tsx @@ -31,6 +31,20 @@ export function humanizeToolLabel(partType: string): ToolLabel { return { running: label, done: label }; } +// activate_skill is the one tool where the target matters more than the tool +// name: surface which skill the agent loaded instead of a bare "Activate +// skill" badge. +function skillNameFromPart(part: UIMessage["parts"][number]): string | null { + if (part.type !== "tool-activate_skill" || !("input" in part)) return null; + const input: unknown = part.input; + return typeof input === "object" && + input !== null && + "name" in input && + typeof input.name === "string" + ? input.name + : null; +} + // Whether an assistant message already shows something — visible text, reasoning, // or a tool badge. Used to decide when the standalone typing indicator is still // needed: a running tool badge already reads as progress, so the dots would @@ -174,6 +188,9 @@ function ToolBadge({ }) { const labels = resolveToolLabel(part.type); if (!labels) return null; + const skillName = skillNameFromPart(part); + const runningText = skillName ? `Activating ${skillName}` : labels.running; + const doneText = skillName ? `Skill: ${skillName}` : labels.done; const state = "state" in part ? part.state : undefined; const isDone = state === "output-available"; // A "running" part in a message that is no longer being generated never @@ -193,7 +210,7 @@ function ToolBadge({ ) : ( )} - {isRunning ? `${labels.running}…` : labels.done} + {isRunning ? `${runningText}…` : doneText} ); } diff --git a/src/client/features/ai-mcp/AvailableTools.tsx b/src/client/features/ai-mcp/AvailableTools.tsx index e8eb399..9d9b6d2 100644 --- a/src/client/features/ai-mcp/AvailableTools.tsx +++ b/src/client/features/ai-mcp/AvailableTools.tsx @@ -12,6 +12,23 @@ type ToolCategory = { }; const toolCategories: ToolCategory[] = [ + { + label: "Project Context", + tools: [ + { + name: "get_project_context", + title: "Get project context", + description: + "Read your project's goals, positioning, competitors, and key pages.", + }, + { + name: "update_project_context", + title: "Update project context", + description: + "Save what an agent learned back to your shared project context.", + }, + ], + }, { label: "Keywords", tools: [ diff --git a/src/client/features/gsc/GscReEngagementModal.tsx b/src/client/features/gsc/GscReEngagementModal.tsx index ee2c8ea..591cb4a 100644 --- a/src/client/features/gsc/GscReEngagementModal.tsx +++ b/src/client/features/gsc/GscReEngagementModal.tsx @@ -87,10 +87,10 @@ export function GscReEngagementModal({ // screen, and on return they'll either have a grant (which suppresses this // anyway) or have abandoned it — neither case should re-nag. persistDismiss(); - // Land them on the project's settings page so they can pick a property + // Land them on the project's integrations page so they can pick a property // right after granting access (the grant alone has no property bound yet). const callbackURL = projectId - ? `${window.location.origin}/p/${projectId}/settings#search-console` + ? `${window.location.origin}/p/${projectId}/settings/integrations` : window.location.href; void startGoogleLink("gsc", callbackURL); } diff --git a/src/client/features/projects/CreateProjectModal.tsx b/src/client/features/projects/CreateProjectModal.tsx index 0f1f1e5..5be7166 100644 --- a/src/client/features/projects/CreateProjectModal.tsx +++ b/src/client/features/projects/CreateProjectModal.tsx @@ -36,10 +36,10 @@ export function CreateProjectModal({ onClose }: { onClose: () => void }) { await queryClient.invalidateQueries({ queryKey: ["projects"] }); onClose(); toast.success("Project created"); - // Land on the new project's settings so they can connect Search Console - // and finish setting up the workspace. + // Land on the new project's integrations so they can connect Search + // Console and finish setting up the workspace. void navigate({ - to: "/p/$projectId/settings", + to: "/p/$projectId/settings/integrations", params: { projectId: created.id }, }); }, diff --git a/src/client/features/projects/ProjectSettings.tsx b/src/client/features/projects/ProjectGeneralSettings.tsx similarity index 80% rename from src/client/features/projects/ProjectSettings.tsx rename to src/client/features/projects/ProjectGeneralSettings.tsx index 43d80e4..4f34d5e 100644 --- a/src/client/features/projects/ProjectSettings.tsx +++ b/src/client/features/projects/ProjectGeneralSettings.tsx @@ -1,10 +1,7 @@ import * as React from "react"; -import { Link, useNavigate } from "@tanstack/react-router"; +import { useNavigate } from "@tanstack/react-router"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; -import { ChevronLeft } from "lucide-react"; import { toast } from "sonner"; -import { SearchConsoleConnectionCard } from "@/client/features/gsc/SearchConsoleConnectionCard"; -import { GoogleAnalyticsConnectionCard } from "@/client/features/ga4/GoogleAnalyticsConnectionCard"; import { ProjectMarketFields } from "@/client/features/projects/ProjectMarketFields"; import { getStandardErrorMessage } from "@/client/lib/error-messages"; import { @@ -18,7 +15,7 @@ import { } from "@/serverFunctions/projects"; import type { ProjectSummary } from "./types"; -export function ProjectSettings({ projectId }: { projectId: string }) { +export function ProjectGeneralSettings({ projectId }: { projectId: string }) { const projectsQuery = useQuery({ queryKey: ["projects"], queryFn: () => getProjects(), @@ -28,51 +25,16 @@ export function ProjectSettings({ projectId }: { projectId: string }) { if (!project) { return ( -
+
); } return ( -
-
- - - Projects - -
-

- Project settings -

-

{project.name}

-
-
- +
{/* key resets the form's local state when switching between projects */} - -
-

- Search Console -

- -
- -
- - Analytics - - } - /> -
- 1} />
); diff --git a/src/client/features/projects/ProjectSwitcher.tsx b/src/client/features/projects/ProjectSwitcher.tsx index b8545de..42453a6 100644 --- a/src/client/features/projects/ProjectSwitcher.tsx +++ b/src/client/features/projects/ProjectSwitcher.tsx @@ -1,7 +1,14 @@ import * as React from "react"; -import { Link, useNavigate } from "@tanstack/react-router"; +import { Link, useRouter } from "@tanstack/react-router"; import { useQuery } from "@tanstack/react-query"; -import { Check, ChevronsUpDown, FolderCog, Plus, Search } from "lucide-react"; +import { + Check, + ChevronsUpDown, + FolderCog, + Plus, + Search, + Settings, +} from "lucide-react"; import { getProjects } from "@/serverFunctions/projects"; import { setLastProjectId } from "@/client/lib/active-project"; import { CreateProjectModal } from "@/client/features/projects/CreateProjectModal"; @@ -19,7 +26,10 @@ export function ProjectSwitcher({ // drawer overlay. onCloseDrawer?: () => void; }) { - const navigate = useNavigate(); + // Matches are read off router.state at click time; subscribing via + // useMatches() would re-render the whole sidebar on every route change for + // a value only a click needs. + const router = useRouter(); const [creating, setCreating] = React.useState(false); // Controlled open state rather than daisyUI's CSS focus-within dropdown: // focus-within can't guarantee the search input ends up focused on open @@ -76,9 +86,27 @@ export function ProjectSwitcher({ onCloseDrawer?.(); if (project.id === activeProjectId) return; setLastProjectId(project.id); - void navigate({ - to: "/p/$projectId", - params: { projectId: project.id }, + // Stay on the current page in the new project: the deepest matched route + // whose path's only dynamic segment is the project id. Deeper routes (a + // rank tracker, an audit result) reference entities owned by the old + // project, so they fall back to their section. Filtering on the path + // template rather than match.params matters: the router gives every match + // the location's full param set, so params can't tell layers apart. + const stayable = router.state.matches.findLast( + (match) => + match.fullPath.includes("$projectId") && + match.fullPath + .split("/") + .every( + (segment) => !segment.startsWith("$") || segment === "$projectId", + ), + ); + // Navigating by href keeps typed-route generics out of a dynamic target + // while still running search validation; search params are deliberately + // not carried over — filters and session ids belong to the old project. + const template = stayable?.fullPath ?? "/p/$projectId"; + void router.navigate({ + href: template.split("$projectId").join(project.id).replace(/\/$/, ""), }); }; @@ -190,28 +218,45 @@ export function ProjectSwitcher({ // trigger still has focus). className="relative w-full" > - + {activeProject?.domain ? ( + + {activeProject.domain} + + ) : null} + + + + {activeProject ? ( + { + closePanel(); + onCloseDrawer?.(); + }} + className="flex shrink-0 items-center justify-center rounded-r-lg border-l border-base-300 px-2.5 text-base-content/60 transition-colors hover:bg-base-200 hover:text-base-content" + > + + + ) : null} +
{open ? (
diff --git a/src/client/features/projects/project-context/CompetitorsSection.tsx b/src/client/features/projects/project-context/CompetitorsSection.tsx new file mode 100644 index 0000000..5ddfaf5 --- /dev/null +++ b/src/client/features/projects/project-context/CompetitorsSection.tsx @@ -0,0 +1,221 @@ +import * as React from "react"; +import { Pencil, Plus } from "lucide-react"; +import type { ProjectContextUpdate } from "@/types/schemas/projectContext"; +import { + ConfirmDeleteButton, + EmptyState, + FormActions, + listClass, + Provenance, + RowActions, + SectionHeader, + useContextUpdate, + type ContextCompetitor, +} from "./shared"; + +export function CompetitorsSection({ + projectId, + competitors, +}: { + projectId: string; + competitors: ContextCompetitor[]; +}) { + const update = useContextUpdate(projectId); + const [adding, setAdding] = React.useState(false); + const [editingId, setEditingId] = React.useState(null); + + const save = (previousDomain: string | null, draft: CompetitorDraft) => { + const ops: ProjectContextUpdate[] = []; + // Competitors upsert by domain, so a retyped domain has to drop the old row + // before the new one lands. + if (previousDomain && previousDomain !== draft.domain.trim()) { + ops.push({ removeCompetitors: [previousDomain] }); + } + // Send the fields even when blank: an omitted field means "keep what's + // stored" (so agent writes merge), so clearing one from the form has to + // send the empty string. + ops.push({ + addCompetitors: [ + { + domain: draft.domain.trim(), + name: draft.name.trim(), + notes: draft.notes.trim(), + }, + ], + }); + update.mutate(ops, { + onSuccess: () => { + setAdding(false); + setEditingId(null); + }, + }); + }; + + return ( +
+ setAdding(true)} + > + + Add competitor + + } + /> + + {adding ? ( +
+ setAdding(false)} + onSave={(draft) => save(null, draft)} + /> +
+ ) : null} + + {competitors.length === 0 ? ( + adding ? null : ( + + No competitors yet. Add the sites you compete with, or ask SAM to + find them from your rankings and save them here. + + ) + ) : ( +
    + {competitors.map((competitor) => + editingId === competitor.id ? ( +
  • + setEditingId(null)} + onSave={(draft) => save(competitor.domain, draft)} + /> +
  • + ) : ( +
  • +
    +
    + + {competitor.domain} + + {competitor.name ? ( + + {competitor.name} + + ) : null} +
    + {competitor.notes ? ( +

    + {competitor.notes} +

    + ) : null} + +
    + + + + update.mutate([ + { removeCompetitors: [competitor.domain] }, + ]) + } + /> + +
  • + ), + )} +
+ )} +
+ ); +} + +type CompetitorDraft = { domain: string; name: string; notes: string }; + +function CompetitorForm({ + initial, + pending, + onCancel, + onSave, +}: { + initial?: ContextCompetitor; + pending: boolean; + onCancel: () => void; + onSave: (draft: CompetitorDraft) => void; +}) { + const [draft, setDraft] = React.useState({ + domain: initial?.domain ?? "", + name: initial?.name ?? "", + notes: initial?.notes ?? "", + }); + + return ( +
{ + event.preventDefault(); + if (!draft.domain.trim() || pending) return; + onSave(draft); + }} + > +
+ + setDraft({ ...draft, domain: event.target.value }) + } + placeholder="competitor.com" + maxLength={255} + className="input input-bordered input-sm w-full" + aria-label="Competitor domain" + /> + setDraft({ ...draft, name: event.target.value })} + placeholder="Name (optional)" + maxLength={120} + className="input input-bordered input-sm w-full" + aria-label="Competitor name" + /> +
+ setDraft({ ...draft, notes: event.target.value })} + placeholder="Why they matter — e.g. wins every comparison keyword (optional)" + maxLength={500} + className="input input-bordered input-sm w-full" + aria-label="Competitor notes" + /> + + + ); +} diff --git a/src/client/features/projects/project-context/KeyPagesSection.tsx b/src/client/features/projects/project-context/KeyPagesSection.tsx new file mode 100644 index 0000000..c064180 --- /dev/null +++ b/src/client/features/projects/project-context/KeyPagesSection.tsx @@ -0,0 +1,254 @@ +import * as React from "react"; +import { Pencil, Plus } from "lucide-react"; +import { + KEY_PAGE_ROLES, + type KeyPageRole, + type ProjectContextUpdate, +} from "@/types/schemas/projectContext"; +import { + ConfirmDeleteButton, + EmptyState, + FormActions, + listClass, + Provenance, + RowActions, + SectionHeader, + useContextUpdate, + type ContextKeyPage, +} from "./shared"; + +const ROLE_LABELS: Record = { + hub: "Hub page", + spoke: "Supporting page", + money: "Money page", + other: "Other", +}; + +export function KeyPagesSection({ + projectId, + keyPages, +}: { + projectId: string; + keyPages: ContextKeyPage[]; +}) { + const update = useContextUpdate(projectId); + const [adding, setAdding] = React.useState(false); + const [editingId, setEditingId] = React.useState(null); + + const save = (previousUrl: string | null, draft: KeyPageDraft) => { + const ops: ProjectContextUpdate[] = []; + // Key pages upsert by URL, so a retyped URL has to drop the old row before + // the new one lands. + if (previousUrl && previousUrl !== draft.url.trim()) { + ops.push({ removeKeyPages: [previousUrl] }); + } + // Send the fields even when blank: an omitted field means "keep what's + // stored" (so agent writes merge), so clearing one from the form has to + // send the empty string. + ops.push({ + addKeyPages: [ + { + url: draft.url.trim(), + role: draft.role, + topic: draft.topic.trim(), + notes: draft.notes.trim(), + }, + ], + }); + update.mutate(ops, { + onSuccess: () => { + setAdding(false); + setEditingId(null); + }, + }); + }; + + return ( +
+ setAdding(true)} + > + + Add page + + } + /> + + {adding ? ( +
+ setAdding(false)} + onSave={(draft) => save(null, draft)} + /> +
+ ) : null} + + {keyPages.length === 0 ? ( + adding ? null : ( + + No key pages yet. Add the handful that has to rank, or let an agent + propose them from your last site audit. + + ) + ) : ( +
    + {keyPages.map((page) => + editingId === page.id ? ( +
  • + setEditingId(null)} + onSave={(draft) => save(page.url, draft)} + /> +
  • + ) : ( +
  • +
    +
    + + {page.url} + + + {ROLE_LABELS[page.role]} + +
    + {page.topic ? ( +

    + Target: {page.topic} +

    + ) : null} + {page.notes ? ( +

    {page.notes}

    + ) : null} + +
    + + + + update.mutate([{ removeKeyPages: [page.url] }]) + } + /> + +
  • + ), + )} +
+ )} +
+ ); +} + +type KeyPageDraft = { + url: string; + role: KeyPageRole; + topic: string; + notes: string; +}; + +function KeyPageForm({ + initial, + pending, + onCancel, + onSave, +}: { + initial?: ContextKeyPage; + pending: boolean; + onCancel: () => void; + onSave: (draft: KeyPageDraft) => void; +}) { + const [draft, setDraft] = React.useState({ + url: initial?.url ?? "", + role: initial?.role ?? "other", + topic: initial?.topic ?? "", + notes: initial?.notes ?? "", + }); + + return ( +
{ + event.preventDefault(); + if (!draft.url.trim() || pending) return; + onSave(draft); + }} + > + setDraft({ ...draft, url: event.target.value })} + placeholder="example.com/pricing" + maxLength={2048} + className="input input-bordered input-sm w-full" + aria-label="Page URL" + /> +
+ + + setDraft({ ...draft, topic: event.target.value }) + } + placeholder="Target topic (optional)" + maxLength={200} + className="input input-bordered input-sm w-full" + aria-label="Target topic" + /> +
+ setDraft({ ...draft, notes: event.target.value })} + placeholder="Notes (optional)" + maxLength={500} + className="input input-bordered input-sm w-full" + aria-label="Page notes" + /> + + + ); +} diff --git a/src/client/features/projects/project-context/ProjectContextPage.tsx b/src/client/features/projects/project-context/ProjectContextPage.tsx new file mode 100644 index 0000000..2d8cc28 --- /dev/null +++ b/src/client/features/projects/project-context/ProjectContextPage.tsx @@ -0,0 +1,404 @@ +import * as React from "react"; +import { useQuery } from "@tanstack/react-query"; +import { Pencil } from "lucide-react"; +import { getStandardErrorMessage } from "@/client/lib/error-messages"; +import { getProjectContext } from "@/serverFunctions/projectContext"; +import { + PROJECT_CONTEXT_SECTION_KEYS, + PROJECT_CONTEXT_SECTION_LABELS, + PROSE_MAX_CHARS, + type ProjectContextSectionKey, +} from "@/types/schemas/projectContext"; +import { CompetitorsSection } from "./CompetitorsSection"; +import { KeyPagesSection } from "./KeyPagesSection"; +import { + ConfirmDeleteButton, + EmptyState, + FormActions, + listClass, + Provenance, + RowActions, + SectionHeader, + projectContextQueryKey, + useContextUpdate, + type ProjectContextData, +} from "./shared"; + +const SECTION_HINTS: Record = { + business_overview: "What you sell, who buys it, and where.", + current_goal: "What you're pushing for right now, and by when.", + positioning: "Why someone picks you over the alternatives.", + writing_preferences: "Voice, words to avoid, topics that are off-limits.", +}; + +const SECTION_PLACEHOLDERS: Record = { + business_overview: + "e.g. Booking software for independent restaurants in the US and Canada. Buyers are owner-operators, not marketers.", + current_goal: + "e.g. Double organic signups by Q4. Comparison pages are the current bet.", + positioning: + "e.g. The only booking tool that sets up in an afternoon. Cheaper than the incumbents, simpler than the DIY stack.", + writing_preferences: + "e.g. Plain and direct, no hype. Never say 'seamless' or 'game-changing'. Don't write about competitor pricing.", +}; + +export function ProjectContextPage({ projectId }: { projectId: string }) { + const contextQuery = useQuery({ + queryKey: projectContextQueryKey(projectId), + queryFn: () => getProjectContext({ data: { projectId } }), + // This page exists to inspect what agents just wrote; the app-wide + // 5-minute staleTime would show pre-SAM-turn memory as current. + staleTime: 0, + }); + + if (contextQuery.isPending) { + return ( +
+ +
+ ); + } + + if (contextQuery.isError) { + return ( +
+ + {getStandardErrorMessage( + contextQuery.error, + "Failed to load project context", + )} + +
+ ); + } + + const context = contextQuery.data; + + return ( + // key remounts the whole page when the project switches under it, so no + // draft, open form, or edit state can carry over to another project. +
+

+ What SAM, Claude Code, and any connected MCP client know about this + project. They read it before they work and write back what they learn, + so correct anything that looks wrong. +

+ + + + + + + + + + +
+ ); +} + +function ProseSections({ + projectId, + sections, + missingSections, +}: { + projectId: string; + sections: ProjectContextData["sections"]; + missingSections: ProjectContextData["missingSections"]; +}) { + const update = useContextUpdate(projectId); + const stored = new Map(sections.map((section) => [section.key, section])); + // Only the fields the user actually touched are pinned locally; the rest + // render straight from the query, so a write from SAM shows up on refetch. + const [drafts, setDrafts] = React.useState>({}); + + const draftOf = (key: ProjectContextSectionKey) => + drafts[key] ?? stored.get(key)?.content ?? ""; + + // Content is trimmed server-side, so compare trimmed values — otherwise a + // stray newline leaves the form permanently "unsaved". + const changed = PROJECT_CONTEXT_SECTION_KEYS.filter( + (key) => draftOf(key).trim() !== (stored.get(key)?.content ?? ""), + ); + + const handleSubmit = (event: React.FormEvent) => { + event.preventDefault(); + if (update.isPending || changed.length === 0) return; + update.mutate( + changed.map((key) => ({ section: key, content: draftOf(key).trim() })), + // Unpin every draft the save made redundant — one that now matches the + // server — so those sections render from the query again (a pinned + // draft would silently overwrite a later agent write on the next + // save). Anything typed while the request was in flight still differs + // and stays pinned instead of snapping back. + { + onSuccess: (context) => { + const saved = new Map( + context.sections.map((section) => [section.key, section.content]), + ); + setDrafts((current) => + Object.fromEntries( + Object.entries(current).filter( + ([key, value]) => value.trim() !== (saved.get(key) ?? ""), + ), + ), + ); + }, + }, + ); + }; + + return ( +
+ {missingSections.length === PROJECT_CONTEXT_SECTION_KEYS.length ? ( + + Nothing written down yet. Fill in what you can — or ask SAM to draft + it from your site and confirm what it got right. + + ) : null} + + {PROJECT_CONTEXT_SECTION_KEYS.map((key) => { + const section = stored.get(key); + return ( +
+
+ + {section ? ( + + ) : ( + Empty + )} +
+

{SECTION_HINTS[key]}

+