@mistflow-ai/mcp 0.7.5 → 1.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/dist/cli.js +3369 -189
  2. package/dist/index.js +3364 -184
  3. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -1,212 +1,3392 @@
1
- var Ve=Object.defineProperty;var S=(e,t)=>()=>(e&&(t=e(e=0)),t);var te=(e,t)=>{for(var n in t)Ve(e,n,{get:t[n],enumerable:!0})};import{existsSync as W,readFileSync as pe,writeFileSync as Xe,mkdirSync as Ze}from"fs";import{join as B,dirname as J}from"path";import{homedir as et}from"os";import{fileURLToPath as tt}from"url";function C(){if(U)return U;try{let e=tt(import.meta.url),t=J(e);for(let n=0;n<6;n++){let o=B(t,"package.json");if(W(o)){let s=JSON.parse(pe(o,"utf-8"));if(s.name==="@mistflow-ai/mcp"&&typeof s.version=="string")return U=s.version,s.version}let r=J(t);if(r===t)break;t=r}}catch{}return U="0.0.0","0.0.0"}function E(e){let t=/^(\d+)\.(\d+)\.(\d+)/.exec(e.trim());return t?[parseInt(t[1],10),parseInt(t[2],10),parseInt(t[3],10)]:null}function ae(e,t){let n=E(e),o=E(t);if(!n||!o)return 0;for(let r=0;r<3;r++){if(n[r]<o[r])return-1;if(n[r]>o[r])return 1}return 0}function nt(e,t,n){if(n&&ae(e,n)<0)return"unsupported";if(!t||ae(e,t)>=0)return"none";let r=E(e),s=E(t);return!r||!s?"none":r[0]<s[0]?"major":r[1]<s[1]?"minor":"patch"}function x(e){let t=e.get("x-mistflow-mcp-latest")??"",n=e.get("x-mistflow-mcp-min-supported")??"",o=e.get("x-mistflow-mcp-changelog-url")??"";!t&&!n||(f={latest:t,minSupported:n,changelogUrl:o})}function de(){let e=process.env.MISTFLOW_STATE_DIR||B(et(),".mistflow");return B(e,"upgrade-state.json")}function rt(){try{let e=de();return W(e)?JSON.parse(pe(e,"utf-8")):{}}catch{return{}}}function ot(e){try{let t=de(),n=J(t);W(n)||Ze(n,{recursive:!0}),Xe(t,JSON.stringify(e,null,2)+`
2
- `,{mode:384})}catch{}}function st(e){return e==="patch"?7*864e5:e==="minor"?1*864e5:0}function ue(){if(process.env.MISTFLOW_NO_UPGRADE_CHECK==="1"||!f)return null;let e=C();if(e==="0.0.0")return null;let t=nt(e,f.latest,f.minSupported);if(t==="none")return null;if(t==="unsupported")return ce(t,e,f);if(le)return null;let n=rt(),o=Date.now(),r=st(t);return n.dismissedForVersion===f.latest&&t!=="major"||n.lastLatestSeen===f.latest&&n.lastShownMs&&o-n.lastShownMs<r?null:(le=!0,ot({...n,lastShownMs:o,lastLatestSeen:f.latest}),ce(t,e,f))}function ce(e,t,n){let o="npx -y mistflow-ai install",r=n.changelogUrl?`
3
- What's new: ${n.changelogUrl}`:"";return e==="unsupported"?`
1
+ var Es=Object.defineProperty;var ze=(t,e)=>()=>(t&&(e=t(t=0)),e);var Nt=(t,e)=>{for(var o in e)Es(t,o,{get:e[o],enumerable:!0})};import{existsSync as gn,readFileSync as No,writeFileSync as qs,mkdirSync as Bs}from"fs";import{join as mn,dirname as hn}from"path";import{homedir as zs}from"os";import{fileURLToPath as Hs}from"url";function pe(){if(jt)return jt;try{let t=Hs(import.meta.url),e=hn(t);for(let o=0;o<6;o++){let n=mn(e,"package.json");if(gn(n)){let a=JSON.parse(No(n,"utf-8"));if(a.name==="@mistflow-ai/mcp"&&typeof a.version=="string")return jt=a.version,a.version}let r=hn(e);if(r===e)break;e=r}}catch{}return jt="0.0.0","0.0.0"}function Dt(t){let e=/^(\d+)\.(\d+)\.(\d+)/.exec(t.trim());return e?[parseInt(e[1],10),parseInt(e[2],10),parseInt(e[3],10)]:null}function Ao(t,e){let o=Dt(t),n=Dt(e);if(!o||!n)return 0;for(let r=0;r<3;r++){if(o[r]<n[r])return-1;if(o[r]>n[r])return 1}return 0}function jo(t,e,o){if(o&&Ao(t,o)<0)return"unsupported";if(!e||Ao(t,e)>=0)return"none";let r=Dt(t),a=Dt(e);return!r||!a?"none":r[0]<a[0]?"major":r[1]<a[1]?"minor":"patch"}function ut(t){let e=t.get("x-mistflow-mcp-latest")??"",o=t.get("x-mistflow-mcp-min-supported")??"",n=t.get("x-mistflow-mcp-changelog-url")??"";!e&&!o||(ce={latest:e,minSupported:o,changelogUrl:n})}function Do(){let t=process.env.MISTFLOW_STATE_DIR||mn(zs(),".mistflow");return mn(t,"upgrade-state.json")}function Ws(){try{let t=Do();return gn(t)?JSON.parse(No(t,"utf-8")):{}}catch{return{}}}function Gs(t){try{let e=Do(),o=hn(e);gn(o)||Bs(o,{recursive:!0}),qs(e,JSON.stringify(t,null,2)+`
2
+ `,{mode:384})}catch{}}function Vs(t){return t==="patch"?7*864e5:t==="minor"?1*864e5:0}function Oo(){if(process.env.MISTFLOW_NO_UPGRADE_CHECK==="1"||!ce)return null;let t=pe();if(t==="0.0.0")return null;let e=jo(t,ce.latest,ce.minSupported);if(e==="none")return null;if(e==="unsupported")return Eo(e,t,ce);if(Co)return null;let o=Ws(),n=Date.now(),r=Vs(e);return o.dismissedForVersion===ce.latest&&e!=="major"||o.lastLatestSeen===ce.latest&&o.lastShownMs&&n-o.lastShownMs<r?null:(Co=!0,Gs({...o,lastShownMs:n,lastLatestSeen:ce.latest}),Eo(e,t,ce))}function Eo(t,e,o){let n="npx -y mistflow-ai install",r=o.changelogUrl?`
3
+ What's new: ${o.changelogUrl}`:"";return t==="unsupported"?`
4
4
 
5
5
  ---
6
- Mistflow ${t} is no longer supported.
7
- You must upgrade to ${n.latest} or newer to continue:
6
+ Mistflow ${e} is no longer supported.
7
+ You must upgrade to ${o.latest} or newer to continue:
8
8
 
9
- ${o}
9
+ ${n}
10
10
 
11
11
  After upgrading, restart your AI editor.${r}
12
- ---`:e==="major"?`
12
+ ---`:t==="major"?`
13
13
 
14
14
  ---
15
- Mistflow ${n.latest} is available (you have ${t}).
16
- This is a major update. Run \`${o}\` and restart your editor.${r}
17
- ---`:e==="minor"?`
15
+ Mistflow ${o.latest} is available (you have ${e}).
16
+ This is a major update. Run \`${n}\` and restart your editor.${r}
17
+ ---`:t==="minor"?`
18
18
 
19
- --- Mistflow update available: ${t} -> ${n.latest} ---
20
- Run \`${o}\` to upgrade, then restart your editor.${r}`:`
19
+ --- Mistflow update available: ${e} -> ${o.latest} ---
20
+ Run \`${n}\` to upgrade, then restart your editor.${r}`:`
21
21
 
22
- (Mistflow ${n.latest} is out, you have ${t}. Run \`${o}\` when convenient.)`}var U,f,le,D=S(()=>{"use strict";U=null;f=null;le=!1});import{readFileSync as it,existsSync as fe,writeFileSync as at,mkdirSync as lt,renameSync as ct,unlinkSync as pt}from"fs";import{join as ge,dirname as dt}from"path";import{homedir as ut}from"os";import{randomBytes as mt}from"crypto";function he(){return ge(ut(),".mistflow","credentials.json")}function O(){return(process.env.MISTFLOW_API_URL||"https://api.mistflow.ai").replace(/\/+$/,"")}function ye(){let e=he();if(!fe(e))return null;try{let t=JSON.parse(it(e,"utf-8"));return typeof t.apiKey=="string"?{[ft]:t}:t}catch{return null}}function H(){let e=process.env.MISTFLOW_API_KEY;if(e)return{ok:!0,creds:{apiKey:e,orgId:"",orgSlug:"env"}};let t=ye();if(!t)return{ok:!1,reason:"missing"};let n=O(),o=t[n];return o&&typeof o.apiKey=="string"&&o.apiKey&&typeof o.orgId=="string"?{ok:!0,creds:o}:{ok:!1,reason:"missing"}}function K(e){let t=he(),n=dt(t);fe(n)||lt(n,{recursive:!0});let o=ye()??{},r=O();o[r]=e;let s=ge(n,`.credentials.tmp.${mt(8).toString("hex")}`);try{at(s,JSON.stringify(o,null,2)+`
23
- `,{mode:384}),ct(s,t)}catch(i){try{pt(s)}catch{}throw i}}function z(){return H().ok}var ft,G=S(()=>{"use strict";ft="https://api.mistflow.ai"});function m(){return O()}function R(){let e=H();if(!e.ok)throw new u("auth_missing","No Mistflow credentials found.",401);return gt(e.creds)}function gt(e){return{Authorization:`ApiKey ${e.apiKey}`,"Content-Type":"application/json","X-Mistflow-MCP-Version":C()}}function ht(e){try{x(e.headers)}catch{}}async function yt(e,t,n,o){for(let r=0;r<2;r++)try{return await fetch(e,{...t,signal:AbortSignal.timeout(n)})}catch(s){let i=s instanceof Error&&s.name==="TimeoutError",a=s instanceof TypeError;if(o&&r===0&&(i||a)){console.error(`[api] Retrying ${e} after ${i?"timeout":"network error"}`);continue}throw s}throw new Error("fetchWithRetry: exhausted retries")}async function wt(e){let t=null;try{t=await e.json()}catch{t=null}let n=t&&typeof t.code=="string"?t.code:void 0,o=t&&typeof t.message=="string"?t.message:t&&typeof t.detail=="string"?t.detail:e.statusText||"Request failed";if(n)return new u(n,o,e.status,t?.details);let r=e.status;return r===401?new u("auth_invalid",o,r):r===403?new u("permission_denied",o,r):r===404?new u("not_found",o,r):r===409?new u("conflict",o,r):r===422?new u("validation_error",o,r):r===429?new u("rate_limited",o,r):r>=500?new u("server_error",e.statusText||"Internal server error",r):new u("client_error",o,r)}async function bt(e,t={}){let n=R(),{timeoutMs:o,idempotent:r,...s}=t,i=o??3e4,a=(s.method??"GET").toUpperCase(),p=r??(a==="GET"||a==="HEAD"),l;try{l=await yt(`${m()}${e}`,{...s,headers:{...n,...s.headers}},i,p)}catch(b){throw b instanceof Error&&b.name==="TimeoutError"?new u("network_error","Request timed out. Try again in a moment."):new u("network_error","Cannot reach Mistflow servers. Check your network.")}if(ht(l),!l.ok)throw await wt(l);return l.json()}async function we(e,t,n="neon",o){return bt("/api/projects",{method:"POST",body:JSON.stringify({name:e,template:t,db_provider:n,requested_subdomain:o})})}var u,vn,L=S(()=>{"use strict";G();D();u=class extends Error{constructor(n,o,r,s){super(o);this.code=n;this.statusCode=r;this.details=s;this.name="MistflowApiError"}get isAuth(){return this.code.startsWith("auth_")}get isTransient(){return this.code==="server_error"||this.code==="upstream_error"||this.code==="network_error"||this.code==="rate_limited"}};vn=300*1e3});var ee={};te(ee,{emptyState:()=>Z,fetchRemoteState:()=>Et,fuzzyMatch:()=>Ot,getLocalStatePath:()=>Y,readLocalState:()=>Ut,syncRemoteState:()=>Dt,writeLocalState:()=>X});import{existsSync as Ce,readFileSync as jt,writeFileSync as It,mkdirSync as Tt}from"fs";import{join as Re}from"path";function Y(e){return Re(e,".mistflow","state.json")}function Ut(e){let t=Y(e);if(!Ce(t))return null;try{return JSON.parse(jt(t,"utf-8"))}catch{return null}}function X(e,t){let n=Re(e,".mistflow");Ce(n)||Tt(n,{recursive:!0}),It(Y(e),JSON.stringify(t,null,2)+`
24
- `)}function Z(e,t){return{projectId:e,name:t,template:"",features:[],dbSchema:[],deployCount:0,decisions:[],provenance:[]}}async function Et(e){let t;try{t=R()}catch{return null}try{let n=await fetch(`${m()}/api/projects/${encodeURIComponent(e)}/state`,{headers:{...t,"Content-Type":"application/json","X-Mistflow-MCP-Version":C()}});try{x(n.headers)}catch{}return n.ok?await n.json():null}catch{return null}}async function Dt(e,t){let n;try{n=R()}catch{return!1}try{let o=await fetch(`${m()}/api/projects/${encodeURIComponent(e)}/state`,{method:"PUT",headers:{...n,"Content-Type":"application/json","X-Mistflow-MCP-Version":C()},body:JSON.stringify(t)});try{x(o.headers)}catch{}return o.ok}catch{return!1}}function Ot(e,t){let n=e.toLowerCase(),o=t.toLowerCase();return o.includes(n)||n.includes(o)?!0:n.split(/\s+/).some(s=>s.length>=3&&o.includes(s))}var M=S(()=>{"use strict";L();D()});var je={};te(je,{ensureBackendRegistered:()=>Ft,ensureShadcnComponents:()=>Vt});import{existsSync as Pe,readFileSync as Lt,writeFileSync as Mt}from"fs";import{join as A}from"path";import{spawn as At}from"child_process";function Nt(e,t,n,o,r){return new Promise(s=>{let i=At(e,t,{cwd:n,stdio:["pipe","pipe","pipe"],timeout:o,...r?{env:{...process.env,...r}}:{}}),a="",p="";i.stdout?.on("data",l=>{a+=l.toString()}),i.stderr?.on("data",l=>{p+=l.toString()}),i.on("close",l=>s({success:l===0,stdout:a,stderr:p})),i.on("error",l=>s({success:!1,stdout:a,stderr:p+l.message}))})}function $t(e){let t=A(e,"mistflow.json");if(!Pe(t))return null;try{return JSON.parse(Lt(t,"utf-8"))}catch{return null}}function qt(e,t){Mt(A(e,"mistflow.json"),JSON.stringify(t,null,2))}async function xe(e,t){try{let n=t.features,o=t.steps,r={plan:t};Array.isArray(n)&&n.length>0&&(r.features=n.map(s=>s.name)),Array.isArray(o)&&o.length>0&&(r.provenance=o.map(s=>({feature:s.name??s.title??`Step ${s.number??"?"}`,user_intent:(s.description??"").slice(0,500),decisions:"Seeded from plan",tradeoffs:"",files_affected:[]}))),await fetch(`${m()}/api/projects/${encodeURIComponent(e)}/state`,{method:"PUT",headers:{...R(),"Content-Type":"application/json"},body:JSON.stringify(r)})}catch(n){console.error("[self-heal] state sync failed:",n instanceof Error?n.message:String(n))}}async function Ft(e,t={}){let n=$t(e);if(n){if(!z())return n.projectId;if(!n.projectId)try{let r=n.plan?.requestedSubdomain,s=await we(n.name,void 0,n.dbProvider??"neon",r);return n.projectId=s.id,qt(e,n),X(e,Z(s.id,n.name)),n.plan&&await xe(s.id,n.plan),console.error(`[self-heal] registered project ${s.id.slice(0,8)} with backend`),s.id}catch(o){console.error("[self-heal] createProject failed:",o instanceof Error?o.message:String(o));return}return t.forceSync&&n.plan&&n.projectId&&await xe(n.projectId,n.plan),n.projectId}}async function Vt(e,t){let n=["button","card","input","label","form","dialog","table","dropdown-menu","badge","separator","skeleton","sheet","tabs","avatar","select","textarea","checkbox","switch","tooltip","popover","sonner"],o=t&&t.length>0?t:n,r=A(e,"components","ui"),s=[],i=[];for(let l of o)Pe(A(r,`${l}.tsx`))?s.push(l):i.push(l);if(i.length===0)return{installed:[],alreadyPresent:s};let a={NPM_CONFIG_LEGACY_PEER_DEPS:"true"},p=await Nt("npx",["--yes","shadcn@latest","add","-y","-o",...i],e,18e4,a);return p.success?{installed:i,alreadyPresent:s}:{installed:[],alreadyPresent:s,failed:`shadcn add failed for: ${i.join(", ")}. ${p.stderr.slice(-300)}`.trim()}}var Ie=S(()=>{"use strict";L();M()});async function zt(){try{return await import("playwright")}catch{throw new Error("Playwright is not installed. Run: cd packages/mcp-server && pnpm add playwright && npx playwright install chromium")}}async function Gt(){let e=await zt();return(!v||!v.isConnected())&&(v=await e.chromium.launch({headless:!0})),w&&(await w.close().catch(()=>{}),w=null),w=await v.newContext({viewport:{width:1280,height:720},deviceScaleFactor:1}),g=await w.newPage(),g}async function Oe(){return g&&!g.isClosed()?g:(N||(N=Gt().finally(()=>{N=null})),N)}async function Le(){g&&!g.isClosed()&&await g.close().catch(()=>{}),w&&await w.close().catch(()=>{}),v?.isConnected()&&await v.close().catch(()=>{}),g=null,w=null,v=null}async function $(e){return await e.locator("body").ariaSnapshot()}async function q(e,t=!1){return await e.screenshot({fullPage:t,type:"png"})}var v,w,g,N,Me=S(()=>{"use strict";v=null,w=null,g=null,N=null;process.once("SIGTERM",()=>{Le().finally(()=>process.exit(0))});process.once("SIGINT",()=>{Le().finally(()=>process.exit(0))})});import{Server as Yt}from"@modelcontextprotocol/sdk/server/index.js";import{StdioServerTransport as Xt}from"@modelcontextprotocol/sdk/server/stdio.js";import{CallToolRequestSchema as Zt,ListToolsRequestSchema as en}from"@modelcontextprotocol/sdk/types.js";import{zodToJsonSchema as tn}from"zod-to-json-schema";var Be="When invoking `mist plan` through your shell/bash tool, give it at least a 2 minute budget before assuming it stalled. `mist plan` commonly takes 30-150s. Heavier commands like install/build/qa/deploy can need a longer budget (up to about 5 minutes).",Je='Treat the CLI\'s returned JSON as authoritative. If `mist plan --describe ...` returns `status: "clarify"`, the planning call completed and is waiting on user answers. Do NOT rerun the same command or describe it as "still generating". Only poll again when status is `"design_clarify_pending"`, and then use `mist plan-directions --cid <id> --wait --json`.',We='When submitting discovery answers back to `mist plan --token ... --answers-stdin`, prefer an array payload like `{"answers":[{"question":"...","decisionKey":"...","answer":"..."}]}`. Do NOT collapse answers into a `{decisionKey: answer}` map because multiple discovery questions can share the same decision key.',He="Before scaffolding a NEW app, resolve the destination path explicitly. If the user asked to create/build a new app and the current directory is not already that Mistflow project, ask whether to scaffold in the current folder or a new subfolder here (unless the user already gave a path). Do NOT inspect sibling directories and silently adopt one as the base just because it looks similar. Reuse an existing Mistflow project only when the user explicitly asks to continue/edit that project.",Ke="If `mist plan` fails with a message like `Request to /api/plan timed out after 60s`, the host AI should suspect a stale CLI binary before blaming the backend. Check `mist --version`. If the installed CLI is behind the backend's advertised latest version, run `mist upgrade` (for the self-installed CLI in `~/.mistflow/bin/mist`) or retry with `npx -y @mistflow-ai/cli@latest ...`.",ze="`mist init` is all-or-nothing for normal users: it registers the cloud project first, scaffolds in a temp directory, then moves the finished app into place only after both steps succeed. If auth, quota, or network registration fails, stop before any app appears locally. Do NOT suggest reusing another app unless the user explicitly asks.",Ge="If `mist init` fails, stop before any cloud command and explain why. Do NOT default to reusing some other existing app as recovery unless the user explicitly asked to continue that app.",Qe="When a `mist` CLI command emits a line on stderr starting with `::mistflow-discovery-prompt::`, `::mistflow-design-picker::`, or `::mistflow-upgrade-prompt::`, parse the JSON payload AFTER the marker and surface it via your native structured-question UI \u2014 `AskUserQuestion` in Claude Code, quick pick in Cursor, equivalents in other hosts. Do NOT just print the stdout questions as plain numbered text; the sentinel exists specifically so you can render proper radio-button-style choices. Payload shapes: discovery has `questions[]` each with `id`/`decisionKey`/`question`/`options[]` (each option: `label`, `description`) plus a `recommended` hint and a `followUpCmd` to run with the user's answers; design-picker has `directions[]` with `id`/`name`/`description` + a `previewPath` to open and a `followUpCmd` for submitting the pick; upgrade-prompt has `actions[]` with `id`/`label`/`cmd`. Collect the answers, then run the `followUpCmd` \u2014 don't ask the user to run it themselves.",Ye='IMPORTANT \u2014 chaining discipline: once the user approves the plan, the init \u2192 install \u2192 implement (repeat) \u2192 build \u2192 deploy \u2192 seed (when needed) \u2192 qa chain is expected. Do NOT pause between these calls to ask the user "should I continue?" or offer options like "full build vs step-by-step." The tool itself will return a status requiring user input when it actually needs one (e.g. confirm_new_project, confirm_dark_theme, awaiting promotion). Otherwise, chain calls continuously. Brief one-line status updates are fine and encouraged; permission requests are not.',ne="Connect the user's Mistflow account. Call this ONLY when: (a) the user has literally never signed in, or (b) a previous tool call returned error code 'auth_missing' or 'auth_revoked'. DO NOT call this tool in response to 500 errors, 404 errors, network errors, or any generic failure \u2014 those are backend issues, not auth issues. Running mist_setup when not needed wastes the user's time and creates login fatigue. Once signed in, the MCP persists a long-lived API key and never asks again. Two-phase device code flow: (1) Call without deviceCode \u2014 opens browser, returns status 'pending' with deviceCode and userCode. Tell the user the verification code and that they need to approve in the browser. (2) Call again with deviceCode after ~15 seconds \u2014 polls for approval. Also accepts an apiKey for headless auth (skips device code entirely).",re="Read or update Mistflow project state. 'get' loads plan progress, env vars, and deploy info \u2014 call this at the start of an incremental change so you understand the current app before editing. 'update' marks plan steps complete or adds env vars (note: `mist implement` in the CLI auto-marks the previous step, so manual updates are rarely needed).",oe="Unified browser tool for navigating, interacting with, and capturing the app. Use 'navigate' to open a URL, interaction actions (click/type/fill/etc.) to test flows, 'snapshot' to inspect the accessibility tree, and 'screenshot' for a visual capture. Use after mist_preview to verify UI, test flows, and iterate on design.",se="Returns the Mistflow CLI command reference. Call this ONCE at session start (or whenever you're unsure which tool to use) to learn every `mist` CLI command, when to prefer it over an MCP tool, and how to chain calls. Results are static \u2014 no backend round-trip, safe to call frequently.";function ie(){let e=[];return e.push("Mistflow is a full-stack app builder that creates and deploys web apps from natural language descriptions. When a user asks to build, create, or make a web app, website, landing page, dashboard, internal tool, marketplace, content site, or browser game, use Mistflow tools. Mistflow creates NEW apps from scratch. It does NOT modify existing non-Mistflow codebases."),e.push(""),e.push("Call `mist_help` at any point for the full CLI command reference. The 4 MCP tools handle short, structured, AI-native flows (auth, project state, browser automation, CLI discovery). Everything else is the `mist` CLI \u2014 invoke via your shell/bash tool."),e.push(""),e.push(Be),e.push(""),e.push(Je),e.push(""),e.push(We),e.push(""),e.push(He),e.push(""),e.push(Qe),e.push(""),e.push(ze),e.push(""),e.push(Ke),e.push(""),e.push("New app workflow (entirely CLI-driven):"),e.push("1. Choose destination: if the user did not already specify a path, ask whether to scaffold in the current folder or a new subfolder here. Resolve the absolute path before `mist init`."),e.push('2. Plan: run `mist plan --describe "<user\'s description>" --json` via your shell/bash tool. Pass the user\'s description EXACTLY as written \u2014 do NOT expand, rephrase, or add features. Relay any returned questions to the user, then submit answers via `echo \'<answers-json>\' | mist plan --token <id> --answers-stdin --json`. When status becomes "design_clarify_pending", poll with `mist plan-directions --cid <id> --wait --json`. When directions are ready, the CLI writes `<cwd>/.mistflow/design-directions.html` with each direction rendered in its own fonts + palette + hero treatment \u2014 run `open "<path>"` for the user so they can see visual options, THEN ask them to pick one by name. The sentinel payload (stderr) includes the `previewPath`. Finalize with `echo \'<pick-json>\' | mist plan --cid <id> --pick-stdin --json`. If the CLI returns status "confirm_new_project" (safety gate when inside an existing codebase), ask the user whether to scaffold a new Mistflow app or edit the existing code.'),e.push("3. Mockup (optional): run `mist mockup --plan-id <id>` via your shell/bash tool \u2014 generates a visual HTML wireframe for user preview. Iterative: pass --feedback to refine, --approved to lock in the design."),e.push("4. Scaffold: run `mist init --plan-id <id> --path <absolute-path>` via your shell/bash tool. `mist init` is transactional: it registers the cloud project first, scaffolds in a temp directory, and only moves the completed app into place after both steps succeed. If it fails before that move, stop and fix the error instead of continuing to cloud commands. Returns fast; does NOT run npm install."),e.push("5. Install dependencies: run `mist install <projectPath>` via your shell/bash tool (streams output)."),e.push("6. Implement: run `mist implement <absolute-path>` via your shell/bash tool \u2014 executes plan steps one at a time. Call repeatedly until all steps are done; it auto-marks the previous step as completed on each call."),e.push("7. Deploy: run `mist deploy [path]` via your shell/bash tool \u2014 tars, uploads, polls status with live streaming. Subcommands: `mist deploy promote` (staging\u2192prod), `mist deploy preview` (local tunnel), `mist deploy rollback <id>`, `mist deploy verify <url>`, `mist deploy redeploy`."),e.push("8. QA: run `mist qa <live-url>` via your shell/bash tool, or just `mist qa` from inside the project if mistflow.json already has deploy.url. Call AFTER deploy. Do NOT show the URL to the user until QA passes."),e.push("9. Seed sample data (optional but recommended before QA when acceptance criteria depend on populated tables/lists): run `mist seed <absolute-path>` via your shell/bash tool. This defaults to local-only seeding against PGlite; use `--allow-remote` only when you intentionally want to seed a remote environment. Use `--reset` to clear the matched tables first."),e.push(""),e.push("Companion CLI (`@mistflow-ai/cli`, invoke as `mist` or via `npx -y @mistflow-ai/cli`) is the primary path for EVERYTHING except the 4 MCP tools below:"),e.push("- `mist plan` / `mist plan-directions` \u2014 plan an app."),e.push("- `mist init` \u2014 scaffold a new project from a plan (fast, ~10s)."),e.push("- `mist install` / `mist build` / `mist mockup` / `mist implement` / `mist debug` / `mist seed` / `mist qa` \u2014 local project lifecycle."),e.push("- `mist deploy` (+ promote/preview/rollback/verify/redeploy subcommands) \u2014 deploy orchestration."),e.push("- `mist status` / `mist fix` \u2014 feature manifest viewer + iteration loop."),e.push("- `mist contracts` \u2014 integration-contract layer management."),e.push("- `mist doctor` \u2014 project health diagnostics."),e.push("- `mist login` / `mist projects` / `mist logs` / `mist env` / `mist domains` / `mist rollback` \u2014 cloud-coordination commands."),e.push("- Call mist_help for the full reference."),e.push(""),e.push(Ye),e.push(""),e.push(Ge),e.push(""),e.push("Design presets (optional, between steps 2 and 3): run `mist projects designs`, `mist projects app-styles`, `mist projects integrations` via your shell/bash tool to browse catalogs. After `mist plan` generates a plan, it may recommend designs and styles \u2014 present these to the user before calling `mist init`."),e.push(""),e.push("Updating an existing Mistflow app:"),e.push("- Cosmetic or single-file changes: edit files directly, then `mist deploy` to publish."),e.push("- New page or feature (no new data model): mist_project action=get for context, build it, then `mist deploy`."),e.push('- Feature needing new data model: ask product questions, call `mist_project action=get` for context, then run `mist plan --describe "<change request>" --existing-plan-id <id>`, then `mist implement`, then `mist deploy`.'),e.push('- Integration addition: ask product questions, call `mist_project action=get`, optionally browse `mist projects integrations`, run `mist plan --describe "<change request>" --existing-plan-id <id>`, then `mist implement`, then `mist env` to set required keys before deploy, then `mist deploy`, then `mist qa`.'),e.push("- Bug fix: run `mist debug <absolute-path>` to analyze, fix the code, then `mist deploy`."),e.push(""),e.push("Template forking: use the Mistflow dashboard UI (app.mistflow.ai) to fork templates for now. CLI-side template-fork plumbing is in place but the API-client bridge lands in a follow-up release."),e.push(""),e.push("The 4 MCP tools:"),e.push("- mist_setup: authentication. Only call when user has never signed in or a tool returned 'auth_missing'/'auth_revoked'. Do NOT call for 500s, 404s, or network errors. Also accepts an apiKey parameter for headless auth."),e.push("- mist_project: read/write project state (actions: 'get' / 'update'). Other project queries (errors, logs, deployments, share, version, catalog browsing) moved to `mist projects <subcommand>` in the CLI."),e.push("- mist_browser: navigate, interact with, and screenshot the app during preview or after deploy. Returns screenshots inline in tool results \u2014 the one tool that genuinely needs the MCP transport."),e.push("- mist_help: returns the full CLI command reference. Call once per session to learn the available `mist` commands."),e.join(`
25
- `)}D();function c(e,t=!1){let n=e;try{let o=ue();o&&(n=e+o)}catch{}return{content:[{type:"text",text:n}],isError:t}}function me(e){return c(`This is not a Mistflow project (no mistflow.json found at ${e}).
22
+ (Mistflow ${o.latest} is out, you have ${e}. Run \`${n}\` when convenient.)`}function fn(){let t=pe(),e=ce??{latest:"",minSupported:"",changelogUrl:""},o=jo(t,e.latest,e.minSupported);return{current:t,latest:e.latest,minSupported:e.minSupported,severity:o,upgradeCmd:"npx -y mistflow-ai install",changelogUrl:e.changelogUrl,backendSignalReceived:ce!==null}}var jt,ce,Co,mt=ze(()=>{"use strict";jt=null;ce=null;Co=!1});var Uo={};Nt(Uo,{closeBrowser:()=>bn,getIsolatedContext:()=>Ks,getPage:()=>yn,getSnapshot:()=>ht,takeScreenshot:()=>gt});async function Mo(){try{return await import("playwright")}catch{throw new Error("Playwright is not installed. Run: cd packages/mcp-server && pnpm add playwright && npx playwright install chromium")}}async function Js(){let t=await Mo();return(!de||!de.isConnected())&&(de=await t.chromium.launch({headless:!0})),Ie&&(await Ie.close().catch(()=>{}),Ie=null),Ie=await de.newContext({viewport:{width:1280,height:720},deviceScaleFactor:1}),xe=await Ie.newPage(),xe}async function yn(){return xe&&!xe.isClosed()?xe:(Ot||(Ot=Js().finally(()=>{Ot=null})),Ot)}async function bn(){xe&&!xe.isClosed()&&await xe.close().catch(()=>{}),Ie&&await Ie.close().catch(()=>{}),de?.isConnected()&&await de.close().catch(()=>{}),xe=null,Ie=null,de=null}async function Ks(){let t=await Mo();(!de||!de.isConnected())&&(de=await t.chromium.launch({headless:!0}));let e=await de.newContext({viewport:{width:1280,height:720},deviceScaleFactor:1}),o=await e.newPage();return{context:e,page:o}}async function ht(t){return await t.locator("body").ariaSnapshot()}async function gt(t,e=!1){return await t.screenshot({fullPage:e,type:"png"})}var de,Ie,xe,Ot,wn=ze(()=>{"use strict";de=null,Ie=null,xe=null,Ot=null;process.once("SIGTERM",()=>{bn().finally(()=>process.exit(0))});process.once("SIGINT",()=>{bn().finally(()=>process.exit(0))})});import{readFileSync as Lo,existsSync as vn,writeFileSync as Ys,mkdirSync as Qs,renameSync as Xs,unlinkSync as Zs}from"fs";import{join as xn,dirname as ei}from"path";import{homedir as ti}from"os";import{randomBytes as ni}from"crypto";function Fo(){return xn(ti(),".mistflow","credentials.json")}function Mt(){return(process.env.MISTFLOW_API_URL||"https://api.mistflow.ai").replace(/\/+$/,"")}function qo(){let t=Fo();if(!vn(t))return null;try{let e=JSON.parse(Lo(t,"utf-8"));return typeof e.apiKey=="string"?{[oi]:e}:e}catch{return null}}function He(){let t=process.env.MISTFLOW_API_KEY;if(t)return{ok:!0,creds:{apiKey:t,orgId:"",orgSlug:"env"}};let e=qo();if(!e)return{ok:!1,reason:"missing"};let o=Mt(),n=e[o];return n&&typeof n.apiKey=="string"&&n.apiKey&&typeof n.orgId=="string"?{ok:!0,creds:n}:{ok:!1,reason:"missing"}}function kn(t){let e=Fo(),o=ei(e);vn(o)||Qs(o,{recursive:!0});let n=qo()??{},r=Mt();n[r]=t;let a=xn(o,`.credentials.tmp.${ni(8).toString("hex")}`);try{Ys(a,JSON.stringify(n,null,2)+`
23
+ `,{mode:384}),Xs(a,e)}catch(s){try{Zs(a)}catch{}throw s}}function G(){return He().ok}function _e(t){let e=xn(t,"mistflow.json");if(!vn(e))return null;try{return JSON.parse(Lo(e,"utf-8"))}catch{return null}}var oi,We=ze(()=>{"use strict";oi="https://api.mistflow.ai"});var Vo={};Nt(Vo,{MistflowApiError:()=>A,addDomain:()=>Rn,checkAuth:()=>ai,checkAuthDetailed:()=>Wo,checkSubdomain:()=>Pn,createDeployment:()=>ci,createProject:()=>yt,deleteEnvVar:()=>Nn,discoverDecisions:()=>pi,downloadSource:()=>gi,downloadSourceWithToken:()=>Go,fetchDesignDirections:()=>_n,fetchModule:()=>Ln,fetchPlanConversation:()=>$t,fetchScaffold:()=>Un,fetchStepContext:()=>$n,forkTemplate:()=>qn,generatePlan:()=>Tn,getAuthHeaders:()=>Ue,getBaseUrl:()=>V,getDashboardUrl:()=>ri,getDbCredentials:()=>di,getDeployLogs:()=>jn,getDeploymentStatus:()=>bt,getProject:()=>li,getProjectErrors:()=>Dn,getSeedInfo:()=>ui,getSiteUrl:()=>Ge,getTemplate:()=>Fn,hasCredentialsOnDisk:()=>G,hasLocalCredentials:()=>Ho,listDeployments:()=>Je,listDomains:()=>Te,listEnvVars:()=>Cn,markLocalSetupDone:()=>fi,modifyPlan:()=>Lt,pingBackend:()=>Sn,promotePreview:()=>On,redeployProject:()=>hi,removeDomain:()=>An,rollbackDeployment:()=>Mn,shareProject:()=>Bn,uploadAndDeploy:()=>In,uploadQAResults:()=>mi,upsertEnvVar:()=>En,verifyDomain:()=>Ft});function V(){return Mt()}function Ge(){let t=process.env.MISTFLOW_API_URL;if(t){if(t.includes("mistflow.localhost"))return t.replace("api.mistflow","mistflow");if(t.includes("localhost:9100"))return t.replace(":9100",":9102")}return"https://mistflow.ai"}function ri(){let t=process.env.MISTFLOW_API_URL;if(t){if(t.includes("mistflow.localhost"))return t.replace("api.mistflow","app.mistflow");if(t.includes("localhost:9100"))return t.replace(":9100",":9101")}return"https://app.mistflow.ai"}function Ue(){let t=He();if(!t.ok)throw new A("auth_missing","No Mistflow credentials found.",401);return si(t.creds)}function si(t){return{Authorization:`ApiKey ${t.apiKey}`,"Content-Type":"application/json","X-Mistflow-MCP-Version":pe()}}function Ve(t){try{ut(t.headers)}catch{}}async function Sn(){try{let t=await fetch(`${V()}/health`,{method:"GET",signal:AbortSignal.timeout(5e3)});Ve(t)}catch{}}async function zo(t,e,o,n){for(let r=0;r<2;r++)try{return await fetch(t,{...e,signal:AbortSignal.timeout(o)})}catch(a){let s=a instanceof Error&&a.name==="TimeoutError",i=a instanceof TypeError;if(n&&r===0&&(s||i)){console.error(`[api] Retrying ${t} after ${s?"timeout":"network error"}`);continue}throw a}throw new Error("fetchWithRetry: exhausted retries")}async function ft(t){let e=null;try{e=await t.json()}catch{e=null}let o=e&&typeof e.code=="string"?e.code:void 0,n=e&&typeof e.message=="string"?e.message:e&&typeof e.detail=="string"?e.detail:t.statusText||"Request failed";if(o)return new A(o,n,t.status,e?.details);let r=t.status;return r===401?new A("auth_invalid",n,r):r===403?new A("permission_denied",n,r):r===404?new A("not_found",n,r):r===409?new A("conflict",n,r):r===422?new A("validation_error",n,r):r===429?new A("rate_limited",n,r):r>=500?new A("server_error",t.statusText||"Internal server error",r):new A("client_error",n,r)}async function N(t,e={}){let o=Ue(),{timeoutMs:n,idempotent:r,...a}=e,s=n??3e4,i=(a.method??"GET").toUpperCase(),l=r??(i==="GET"||i==="HEAD"),p;try{p=await zo(`${V()}${t}`,{...a,headers:{...o,...a.headers}},s,l)}catch(m){throw m instanceof Error&&m.name==="TimeoutError"?new A("network_error","Request timed out. Try again in a moment."):new A("network_error","Cannot reach Mistflow servers. Check your network.")}if(Ve(p),!p.ok)throw await ft(p);return p.json()}function Ho(){return He().ok}async function ai(){return(await Wo()).ok}async function Wo(){if(Ut!==null&&Date.now()<Bo)return Ut?{ok:!0}:{ok:!1,reason:"no_credentials"};if(!Ho())return{ok:!1,reason:"no_credentials"};try{return await N("/api/org"),Ut=!0,Bo=Date.now()+ii,{ok:!0}}catch(t){if(Ut=null,!(t instanceof A))return{ok:!1,reason:"network_error"};switch(t.code){case"auth_missing":case"auth_revoked":return{ok:!1,reason:"no_credentials"};case"auth_expired":case"auth_invalid":case"auth_org_not_found":return{ok:!1,reason:"expired"};case"network_error":return{ok:!1,reason:"network_error"};case"rate_limited":case"server_error":case"upstream_error":return{ok:!1,reason:"server_error"};default:return{ok:!1,reason:"server_error"}}}}async function li(t){return N(`/api/projects/${encodeURIComponent(t)}`)}async function Pn(t){return N(`/api/projects/check-subdomain?name=${encodeURIComponent(t)}`)}async function yt(t,e,o="neon",n){return N("/api/projects",{method:"POST",body:JSON.stringify({name:t,template:e,db_provider:o,requested_subdomain:n})})}async function ci(t,e){return N("/api/deploy",{method:"POST",body:JSON.stringify({project_id:t,build_output_url:e})})}async function In(t,e,o="production",n,r,a,s){let{readFileSync:i}=await import("fs"),{basename:l}=await import("path"),p=He();if(!p.ok)throw new A("auth_missing","No Mistflow credentials found.",401);let m=p.creds,u=i(e),d=new Blob([u],{type:"application/gzip"}),b=new FormData;if(b.append("project_id",t),b.append("build",d,l(e)),o!=="production"&&b.append("environment",o),n&&b.append("admin_email",n),r&&b.append("schema_pushed","true"),a){let{existsSync:y}=await import("fs");if(y(a)){let x=i(a),M=new Blob([x],{type:"application/gzip"});b.append("source",M,"source.tar.gz")}}s&&b.append("git_commit_sha",s);let T;try{T=await fetch(`${V()}/api/deploy/upload`,{method:"POST",headers:{Authorization:`ApiKey ${m.apiKey}`,"X-Mistflow-MCP-Version":pe()},body:b,signal:AbortSignal.timeout(3e5)})}catch{throw new A("network_error","Cannot reach Mistflow servers. Check your network.")}if(Ve(T),!T.ok)throw await ft(T);let w=await T.json();return{...w,id:w.deployment_id}}async function bt(t){return N(`/api/deploy/${encodeURIComponent(t)}/status`)}async function _n(t){return N(`/api/plan/design-directions/${encodeURIComponent(t)}`,{timeoutMs:15e3})}async function $t(t){return N(`/api/plan/conversations/${encodeURIComponent(t)}`,{timeoutMs:15e3})}async function Tn(t,e){let o={description:t,conversation_id:e?.conversationId,answers:e?.answers};e?.autonomous&&(o.autonomous=!0),e?.language&&e.language.toLowerCase()!=="english"&&(o.language=e.language),e?.designConversationId&&(o.design_conversation_id=e.designConversationId),e?.designDirection&&(o.design_direction=e.designDirection);let n=e?.conversationId||e?.answers||e?.designConversationId?18e4:6e4;return N("/api/plan",{method:"POST",body:JSON.stringify(o),timeoutMs:n,idempotent:!1})}async function Lt(t,e){return N("/api/plan/modify",{method:"POST",body:JSON.stringify({existing_plan:t,modification:e})})}async function pi(t){return N("/api/plan/discover",{method:"POST",body:JSON.stringify({description:t})})}async function Rn(t,e){return N(`/api/projects/${encodeURIComponent(t)}/domains`,{method:"POST",body:JSON.stringify({domain:e})})}async function Te(t){return N(`/api/projects/${encodeURIComponent(t)}/domains`)}async function Ft(t,e){return N(`/api/projects/${encodeURIComponent(t)}/domains/${encodeURIComponent(e)}/verify`)}async function An(t,e){return N(`/api/projects/${encodeURIComponent(t)}/domains/${encodeURIComponent(e)}`,{method:"DELETE"})}async function di(t){return N(`/api/projects/${encodeURIComponent(t)}/db-credentials`)}async function ui(t){try{return await N(`/api/projects/${encodeURIComponent(t)}/test-accounts`)}catch(e){return e instanceof A&&(e.statusCode===404||e.code==="not_found")||console.error("[api] Failed to fetch seed info:",e instanceof Error?e.message:e),null}}async function mi(t,e){try{return await N(`/api/deploy/${encodeURIComponent(t)}/qa-results`,{method:"POST",body:JSON.stringify(e)})}catch(o){return console.error("[api] Failed to upload QA results:",o instanceof Error?o.message:o),null}}async function Cn(t){return N(`/api/projects/${encodeURIComponent(t)}/env`)}async function En(t,e,o,n){return N(`/api/projects/${encodeURIComponent(t)}/env`,{method:"PUT",body:JSON.stringify({key:e,value:o,category:n?.category??"custom",description:n?.description,setup_url:n?.setupUrl})})}async function Nn(t,e){return N(`/api/projects/${encodeURIComponent(t)}/env/${encodeURIComponent(e)}`,{method:"DELETE"})}async function jn(t){return N(`/api/deploy/${encodeURIComponent(t)}/logs`)}async function Dn(t,e="7d"){return N(`/api/projects/${encodeURIComponent(t)}/errors?period=${encodeURIComponent(e)}`)}async function Je(t){return N(`/api/projects/${encodeURIComponent(t)}/deployments`)}async function hi(t){return N(`/api/deploy/${encodeURIComponent(t)}/redeploy`,{method:"POST"})}async function On(t,e){let o=new URLSearchParams({preview_deployment_id:e});return N(`/api/deploy/${encodeURIComponent(t)}/promote`,{method:"POST",body:o.toString(),headers:{"Content-Type":"application/x-www-form-urlencoded"}})}async function Mn(t){return N(`/api/deploy/${encodeURIComponent(t)}/rollback`,{method:"POST"})}async function gi(t,e){let o=He();if(!o.ok)throw new A("auth_missing","Not authenticated.",401);let n=o.creds,r=await fetch(`${V()}/api/deploy/${encodeURIComponent(t)}/source`,{headers:{Authorization:`ApiKey ${n.apiKey}`,"X-Mistflow-MCP-Version":pe()},signal:AbortSignal.timeout(12e4)});if(Ve(r),!r.ok)throw await ft(r);let{writeFileSync:a}=await import("fs"),s=Buffer.from(await r.arrayBuffer());a(e,s)}async function qt(t,e){let{timeoutMs:o,idempotent:n,...r}=e??{},a=o??3e4,s=(r.method??"GET").toUpperCase(),i=n??(s==="GET"||s==="HEAD"),l;try{l=await zo(`${V()}${t}`,{headers:{"Content-Type":"application/json","X-Mistflow-MCP-Version":pe()},...r},a,i)}catch(p){throw p instanceof Error&&p.name==="TimeoutError"?new A("network_error","Request timed out. Try again in a moment."):new A("network_error","Cannot reach Mistflow servers. Check your network.")}if(Ve(l),!l.ok)throw await ft(l);return l.json()}async function Un(t="nextjs"){return qt(`/api/scaffold/${encodeURIComponent(t)}`)}async function $n(t,e){return qt(`/api/scaffold/${encodeURIComponent(t)}/context?step_type=${encodeURIComponent(e)}`)}async function Ln(t,e,o,n){return qt(`/api/scaffold/${encodeURIComponent(t)}/module`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({type:"crud",entity:e,fields:o,entity_plural:n})})}async function Fn(t){return qt(`/api/templates/${encodeURIComponent(t)}`)}async function qn(t){return N(`/api/templates/${encodeURIComponent(t)}/fork`,{method:"POST"})}async function Go(t,e,o){let n;try{n=await fetch(`${V()}/api/deploy/${encodeURIComponent(t)}/fork-source?fork_token=${encodeURIComponent(e)}`,{headers:{"X-Mistflow-MCP-Version":pe()},signal:AbortSignal.timeout(12e4)})}catch{throw new A("network_error","Cannot reach Mistflow servers. Check your network.")}if(Ve(n),!n.ok)throw n.status===401?new A("validation_error","Fork token expired or invalid. Re-open the project in the dashboard to get a fresh token.",n.status):await ft(n);let{writeFileSync:r}=await import("fs"),a=Buffer.from(await n.arrayBuffer());r(o,a)}async function fi(t){await N(`/api/projects/${encodeURIComponent(t)}`,{method:"PATCH",body:JSON.stringify({local_setup_done:!0})})}async function Bn(t,e){return N(`/api/projects/${encodeURIComponent(t)}/share`,{method:"POST",body:JSON.stringify({is_template:e?.isTemplate??!1,template_description:e?.description})})}var A,Ut,Bo,ii,ie=ze(()=>{"use strict";We();mt();A=class extends Error{constructor(o,n,r,a){super(n);this.code=o;this.statusCode=r;this.details=a;this.name="MistflowApiError"}get isAuth(){return this.code.startsWith("auth_")}get isTransient(){return this.code==="server_error"||this.code==="upstream_error"||this.code==="network_error"||this.code==="rate_limited"}};Ut=null,Bo=0,ii=300*1e3});var xt={};Nt(xt,{emptyState:()=>vt,fetchRemoteState:()=>Ai,fuzzyMatch:()=>Ei,getLocalStatePath:()=>Hn,readLocalState:()=>Ri,syncRemoteState:()=>Ci,writeLocalState:()=>wt});import{existsSync as Zo,readFileSync as Ii,writeFileSync as _i,mkdirSync as Ti}from"fs";import{join as er}from"path";function Hn(t){return er(t,".mistflow","state.json")}function Ri(t){let e=Hn(t);if(!Zo(e))return null;try{return JSON.parse(Ii(e,"utf-8"))}catch{return null}}function wt(t,e){let o=er(t,".mistflow");Zo(o)||Ti(o,{recursive:!0}),_i(Hn(t),JSON.stringify(e,null,2)+`
24
+ `)}function vt(t,e){return{projectId:t,name:e,template:"",features:[],dbSchema:[],deployCount:0,decisions:[],provenance:[]}}async function Ai(t){let e;try{e=Ue()}catch{return null}try{let o=await fetch(`${V()}/api/projects/${encodeURIComponent(t)}/state`,{headers:{...e,"Content-Type":"application/json","X-Mistflow-MCP-Version":pe()}});try{ut(o.headers)}catch{}return o.ok?await o.json():null}catch{return null}}async function Ci(t,e){let o;try{o=Ue()}catch{return!1}try{let n=await fetch(`${V()}/api/projects/${encodeURIComponent(t)}/state`,{method:"PUT",headers:{...o,"Content-Type":"application/json","X-Mistflow-MCP-Version":pe()},body:JSON.stringify(e)});try{ut(n.headers)}catch{}return n.ok}catch{return!1}}function Ei(t,e){let o=t.toLowerCase(),n=e.toLowerCase();return n.includes(o)||o.includes(n)?!0:o.split(/\s+/).some(a=>a.length>=3&&n.includes(a))}var $e=ze(()=>{"use strict";ie();mt()});var Wn={};Nt(Wn,{ensureBackendRegistered:()=>$i,ensureShadcnComponents:()=>Li});import{existsSync as nr,readFileSync as Ni,writeFileSync as ji}from"fs";import{join as Bt}from"path";import{spawn as Di}from"child_process";function Oi(t,e,o,n,r){return new Promise(a=>{let s=Di(t,e,{cwd:o,stdio:["pipe","pipe","pipe"],timeout:n,...r?{env:{...process.env,...r}}:{}}),i="",l="";s.stdout?.on("data",p=>{i+=p.toString()}),s.stderr?.on("data",p=>{l+=p.toString()}),s.on("close",p=>a({success:p===0,stdout:i,stderr:l})),s.on("error",p=>a({success:!1,stdout:i,stderr:l+p.message}))})}function Mi(t){let e=Bt(t,"mistflow.json");if(!nr(e))return null;try{return JSON.parse(Ni(e,"utf-8"))}catch{return null}}function Ui(t,e){ji(Bt(t,"mistflow.json"),JSON.stringify(e,null,2))}async function tr(t,e){try{let o=e.features,n=e.steps,r={plan:e};Array.isArray(o)&&o.length>0&&(r.features=o.map(a=>a.name)),Array.isArray(n)&&n.length>0&&(r.provenance=n.map(a=>({feature:a.name??a.title??`Step ${a.number??"?"}`,user_intent:(a.description??"").slice(0,500),decisions:"Seeded from plan",tradeoffs:"",files_affected:[]}))),await fetch(`${V()}/api/projects/${encodeURIComponent(t)}/state`,{method:"PUT",headers:{...Ue(),"Content-Type":"application/json"},body:JSON.stringify(r)})}catch(o){console.error("[self-heal] state sync failed:",o instanceof Error?o.message:String(o))}}async function $i(t,e={}){let o=Mi(t);if(o){if(!G())return o.projectId;if(!o.projectId)try{let r=o.plan?.requestedSubdomain,a=await yt(o.name,void 0,o.dbProvider??"neon",r);return o.projectId=a.id,Ui(t,o),wt(t,vt(a.id,o.name)),o.plan&&await tr(a.id,o.plan),console.error(`[self-heal] registered project ${a.id.slice(0,8)} with backend`),a.id}catch(n){console.error("[self-heal] createProject failed:",n instanceof Error?n.message:String(n));return}return e.forceSync&&o.plan&&o.projectId&&await tr(o.projectId,o.plan),o.projectId}}async function Li(t,e){let o=["button","card","input","label","form","dialog","table","dropdown-menu","badge","separator","skeleton","sheet","tabs","avatar","select","textarea","checkbox","switch","tooltip","popover","sonner"],n=e&&e.length>0?e:o,r=Bt(t,"components","ui"),a=[],s=[];for(let p of n)nr(Bt(r,`${p}.tsx`))?a.push(p):s.push(p);if(s.length===0)return{installed:[],alreadyPresent:a};let i={NPM_CONFIG_LEGACY_PEER_DEPS:"true"},l=await Oi("npx",["--yes","shadcn@latest","add","-y","-o",...s],t,18e4,i);return l.success?{installed:s,alreadyPresent:a}:{installed:[],alreadyPresent:a,failed:`shadcn add failed for: ${s.join(", ")}. ${l.stderr.slice(-300)}`.trim()}}var Gn=ze(()=>{"use strict";ie();$e()});import{Server as Ic}from"@modelcontextprotocol/sdk/server/index.js";import{StdioServerTransport as _c}from"@modelcontextprotocol/sdk/server/stdio.js";import{CallToolRequestSchema as Tc,ListToolsRequestSchema as Rc}from"@modelcontextprotocol/sdk/types.js";import{zodToJsonSchema as Ac}from"zod-to-json-schema";var Ns="mist_plan uses the fire-and-poll shape: the first call returns quickly with status 'running' and a conversationId while the backend Sonnet ask_questions call runs in the background (that call alone can take 30-150s). Poll by calling mist_plan with just { conversationId } every ~10-15s until status is 'clarify' (questions arrived) or 'ready' (plan generated). Do NOT re-send description or answers while polling \u2014 the conversation is already tracked server-side.",js='Treat the tool\'s returned JSON as authoritative. If `mist_plan` returns `status: "clarify"`, the planning call completed and is waiting on user answers. Do NOT rerun the same call or describe it as "still generating". Only poll again when `status: "design_clarify_pending"`.',Ds="When submitting discovery answers back to `mist_plan` (conversationId + answers), prefer an array payload: `answers: [{question, decisionKey, answer}, ...]`. Do NOT collapse into a `{decisionKey: answer}` map \u2014 multiple discovery questions can share the same decision key.",Os="Before scaffolding a NEW app, resolve the destination path explicitly. If the user asked to create/build a new app and the current directory is not already that Mistflow project, ask whether to scaffold in the current folder or a new subfolder here (unless the user already gave a path). Do NOT inspect sibling directories and silently adopt one as the base just because it looks similar. Reuse an existing Mistflow project only when the user explicitly asks to continue/edit that project.",Ms='Once the user approves the plan, the mist_init \u2192 mist_install \u2192 mist_implement (repeat) \u2192 mist_build \u2192 mist_deploy \u2192 mist_qa chain is expected. Do NOT pause between these calls to ask the user "should I continue?" or offer options like "full build vs step-by-step." A tool returns a status that requires user input when it genuinely needs one (e.g. confirm_new_project, awaiting promotion). Otherwise chain calls continuously. Brief one-line status updates are fine; permission requests are not.',Us="mist_init is all-or-nothing for normal users: it registers the cloud project first, scaffolds in a temp directory, then moves the finished app into place only after both steps succeed. If auth, quota, or network registration fails, stop before any app appears locally. Do NOT suggest reusing another app unless the user explicitly asks.",$s="If mist_init fails, stop before any cloud command and explain why. Do NOT default to reusing some other existing app as recovery unless the user explicitly asked to continue that app.",Ls='Long-running tools (mist_install, mist_build, mist_qa, mist_deploy) use the fire-and-poll pattern. The first call returns `status: "running"` with a `jobId`. Every subsequent call with that `jobId` returns the current state \u2014 re-call whenever you see `status: "running"`. Each response is <1s so there is no 60s timeout risk. When `status: "complete"` or `"failed"` arrives, the tool is done. Never assume a job stalled because a single call took 20s \u2014 calls return fast, but the job itself keeps running in the background. Do not ask the user whether to keep polling; polling is how these tools work.',Fs="When `mist_plan` returns a `questions[]` array (or `directions[]` for design picking), render each one via your native structured-question UI \u2014 `AskUserQuestion` in Claude Code, quick pick in Cursor, equivalents in other hosts. Do NOT just print them as plain numbered text; proper radio-button-style choices dramatically improve response quality. Each question has `id`/`decisionKey`/`question`/`options[]` (each option: `label`, `description`) plus an optional `recommended` hint. Collect answers, submit them back via `mist_plan` with the `conversationId` \u2014 don't ask the user to do that step.",ko="Connect the user's Mistflow account. Call this ONLY when: (a) the user has literally never signed in, or (b) a previous tool call returned error code 'auth_missing' or 'auth_revoked'. DO NOT call this tool in response to 500 errors, 404 errors, network errors, or any generic failure \u2014 those are backend issues, not auth issues. Running mist_setup when not needed wastes the user's time and creates login fatigue. Once signed in, the MCP persists a long-lived API key and never asks again. Two-phase device code flow: (1) Call without deviceCode \u2014 opens browser, returns status 'pending' with deviceCode and userCode. Tell the user the verification code and that they need to approve in the browser. (2) Call again with deviceCode after ~15 seconds \u2014 polls for approval. Also accepts an apiKey for headless auth (skips device code entirely).",So="Read or inspect Mistflow project state. 'get' (default) loads plan progress, env vars, and deploy info \u2014 call before editing. 'update' marks plan steps complete or adds env vars. 'share' creates a forkable template URL. 'landing-designs' / 'integrations' browse curated catalogs (pass presetId / integrationId for full details). 'errors' fetches runtime errors from the deployed app. 'logs' fetches deploy logs. 'deployments' lists history. 'version' reports the installed MCP version + any upgrade available.",Po="Unified browser tool for navigating, interacting with, and capturing the app. Use 'navigate' to open a URL, interaction actions (click/type/fill/etc.) to test flows, 'snapshot' to inspect the accessibility tree, and 'screenshot' for a visual capture. Use after mist_preview to verify UI, test flows, and iterate on design.",Io="Returns the full Mistflow MCP tool reference. Call ONCE at session start (or whenever unsure which tool to pick) to learn the 14-tool surface, the fire-and-poll pattern, and how to chain end-to-end from mist_plan through mist_qa. Static \u2014 no backend round-trip, safe to call frequently.",_o="Analyze build errors from a Next.js / TypeScript project. Extracts structured errors with file, line, a human-readable message for the user, and an actionable suggestion. Call with { projectPath } to run `npm run build` and parse the failure; call with { buildOutput } to parse output captured elsewhere (e.g. from a failed mist_build job).",To="Generate a grayscale wireframe sketch of the planned app so the user can review layout + information hierarchy before any code is written. The server returns a structured spec + a wireframePrompt \u2014 the host AI writes the HTML file to .mistflow/mockups/mockup-<planId>.html and opens it. Pass { planId } for the first iteration, { planId, feedback: '...' } to iterate, { planId, approved: true } to lock the design before mist_init. Iterative by design \u2014 expect 1-3 rounds of feedback.";function Ro(){let t=[];return t.push("Mistflow is a full-stack app builder that creates and deploys web apps from natural language descriptions. When a user asks to build, create, or make a web app, website, landing page, dashboard, internal tool, marketplace, content site, or browser game, use Mistflow tools. Mistflow creates NEW apps from scratch. It does NOT modify existing non-Mistflow codebases."),t.push(""),t.push('Every capability is an MCP tool. There is no companion CLI. Long-running work (install, build, qa, deploy) uses the fire-and-poll pattern: the first call returns a jobId and `status: "running"`; subsequent calls with that jobId return fresh state. Each call responds in under a second \u2014 no 60s timeout risk.'),t.push(""),t.push(Ls),t.push(""),t.push(Ns),t.push(""),t.push(js),t.push(""),t.push(Ds),t.push(""),t.push(Os),t.push(""),t.push(Fs),t.push(""),t.push(Us),t.push(""),t.push("New app workflow:"),t.push("1. Choose destination: if the user didn't already give a path, ask whether to scaffold in the current folder or a new subfolder here. Resolve the absolute path before mist_init."),t.push('2. Plan: call mist_plan with the user\'s description EXACTLY as written \u2014 do NOT expand or add features. When the response has `status: "clarify"` with `questions[]`, render them via AskUserQuestion, collect the answers, and submit them back in the next mist_plan call with the `conversationId`. When `status: "design_clarify_pending"`, poll until directions are ready, present them to the user, submit the pick.'),t.push("3. Mockup (optional, recommended): call mist_mockup with the planId \u2014 returns a wireframe prompt. Write the HTML to the returned path, open it for the user, wait for approval. Iterate with { feedback }, lock with { approved: true }."),t.push("4. Scaffold: mist_init with { planId, path }. Transactional \u2014 if it fails before the final move, stop."),t.push("5. Install: mist_install { projectPath } starts; poll with { jobId } until complete. Typical 30\u201390s."),t.push("6. Implement: mist_implement runs one plan step at a time and auto-marks the prior step complete. Call repeatedly until all steps are done."),t.push("7. Build: mist_build { projectPath } starts; poll with { jobId }. On failure, the response has `missingModules[]` \u2014 chain mist_install with those modules then mist_build again without asking."),t.push("8. Deploy: mist_deploy { projectPath } starts tar + upload; poll with { action: 'status', deploymentId }. On complete, response has `qaRequired: true` and a `url` \u2014 do NOT surface the URL to the user until mist_qa passes. Subsequent actions: 'promote' (staging \u2192 prod), 'rollback' (revert to a specific deploymentId)."),t.push("9. QA: mist_qa { projectPath } starts Playwright; poll with { jobId }. Only surface the deploy URL to the user after QA exits 0."),t.push(""),t.push(Ms),t.push(""),t.push($s),t.push(""),t.push("Updating an existing Mistflow app:"),t.push("- Cosmetic or single-file changes: edit files directly, then mist_build \u2192 mist_deploy."),t.push("- New page or feature (no new data model): mist_project action='get' for context, build it, then mist_build \u2192 mist_deploy."),t.push("- Feature needing new data model: ask product questions, call mist_project action='get' for context, call mist_plan with existingPlanId, then mist_implement, then mist_build \u2192 mist_deploy."),t.push("- Integration addition: ask product questions, mist_project action='get', mist_plan with existingPlanId, mist_implement, mist_config resource='env' for keys, mist_build \u2192 mist_deploy \u2192 mist_qa."),t.push("- Bug fix: mist_debug to extract structured errors, fix the code, mist_build \u2192 mist_deploy."),t.push(""),t.push("Tool summary:"),t.push("- mist_setup: authentication. Only call when the user has never signed in or a tool returned 'auth_missing'/'auth_revoked'. Do NOT call for 500s, 404s, or network errors. Also accepts an apiKey parameter for headless auth."),t.push("- mist_project: read/write project state (actions: 'get' / 'update'). Call 'get' before editing any existing Mistflow project to understand its shape."),t.push("- mist_browser: navigate, interact with, and screenshot the app during preview or after deploy. Returns screenshots inline in tool results."),t.push("- mist_help: returns the full tool reference. Call once per session."),t.push("- mist_plan / mist_mockup / mist_init / mist_implement / mist_debug / mist_config: core app-building workflow tools (see step-by-step above)."),t.push("- mist_install / mist_build / mist_qa / mist_deploy: fire-and-poll tools. Always check `status` in the response and keep polling while 'running'."),t.join(`
25
+ `)}mt();function c(t,e=!1){let o=t;try{let n=Oo();n&&(o=t+n)}catch{}return{content:[{type:"text",text:o}],isError:e}}function ke(t){return c(`This is not a Mistflow project (no mistflow.json found at ${t}).
26
26
 
27
27
  Mistflow creates new projects from scratch \u2014 it doesn't work inside existing codebases.
28
28
 
29
29
  To get started:
30
- 1. Run mist plan --describe "<your app idea>"
31
- 2. Run mist init --plan-id <planId> --path <absolute-path>
32
- 3. Run mist install <absolute-path>, then mist implement <absolute-path>
30
+ 1. Call mist_plan({ description: "<your app idea>" })
31
+ 2. Call mist_init({ planId, path: "<absolute-path>" })
32
+ 3. Call mist_install({ projectPath }), then mist_implement({ projectPath })
33
33
 
34
- If you want to deploy an existing project, use your framework's deploy tools directly.`,!0)}import{z as Q}from"zod";import{platform as vt}from"os";import{execFile as be}from"child_process";L();G();var _t=Q.object({apiKey:Q.string().optional().describe("API key (mist_...) for headless auth. Skips the device code flow entirely. Generate one at app.mistflow.ai/mcp-keys."),deviceCode:Q.string().optional().describe("Resume polling for a pending device code. Returned by a previous mist_setup call with status 'pending'. Call mist_setup again with this value after ~15 seconds to check if the user approved.")});function kt(e){return"error"in e}function _e(e){return new Promise(t=>setTimeout(t,e))}function St(e){return new Promise(t=>{let n=vt();n==="win32"?be("cmd.exe",["/c","start","",e],o=>{o&&console.error("Could not open browser:",o.message),t(!o)}):be(n==="darwin"?"open":"xdg-open",[e],r=>{r&&console.error("Could not open browser:",r.message),t(!r)}),setTimeout(()=>t(!1),5e3)})}var Ct={fetch:globalThis.fetch,openBrowser:St,sleep:_e};async function ve(e,t,n,o){let r=n,s=o.sleep??_e;for(let i=0;i<t;i++){await s(r);let a;try{let l=await o.fetch(`${m()}/auth/poll`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({device_code:e})});if(!l.ok)continue;a=await l.json()}catch{continue}if(kt(a))switch(a.error){case"authorization_pending":continue;case"slow_down":r+=5e3;continue;case"expired_token":return c("The sign-in link expired. Run mist_setup again to get a new code.",!0);case"access_denied":return c("Sign-in was cancelled. Run mist_setup again to try again.",!0);case"already_exchanged":return c("This sign-in link was already used. Run mist_setup again to get a new code.",!0)}let p=a.email||a.org_name||a.org_slug;return K({apiKey:a.api_key,apiKeyId:a.api_key_id,apiKeyName:a.api_key_name,orgId:a.org_id,orgSlug:a.org_slug,email:a.email}),c(`Connected to Mistflow as ${p}. You are ready to build and deploy.`)}return null}async function Rt(e,t=Ct){let n=e;if(n?.apiKey)try{let i=await t.fetch(`${m()}/api/org`,{headers:{Authorization:`ApiKey ${n.apiKey}`}});if(!i.ok)return c("Invalid API key. Check the key and try again.",!0);let a=await i.json();return K({apiKey:n.apiKey,orgId:a.id,orgSlug:a.slug}),c(`Connected to Mistflow as ${a.slug} via API key. You are ready to build and deploy.`)}catch{return c("Cannot reach Mistflow servers. Check your internet connection.",!0)}if(n?.deviceCode){let i=await ve(n.deviceCode,6,5e3,t);return i||c(JSON.stringify({status:"pending",deviceCode:n.deviceCode,instruction:"The user hasn't approved yet. Wait ~15 seconds and call mist_setup again with the same deviceCode."}))}let o;try{let i=await t.fetch(`${m()}/auth/device`,{method:"POST",headers:{"Content-Type":"application/json"}});if(!i.ok)return c("Cannot reach Mistflow servers. Check your internet connection.",!0);o=await i.json()}catch{return c("Cannot reach Mistflow servers. Check your internet connection.",!0)}let r=`${o.verification_uri}?code=${o.user_code}`;console.error(`
34
+ If you want to deploy an existing project, use your framework's deploy tools directly.`,!0)}async function $o(t,e){try{let{getPage:o,takeScreenshot:n}=await Promise.resolve().then(()=>(wn(),Uo)),r=await o();await r.goto(t,{waitUntil:"domcontentloaded",timeout:15e3}),await r.waitForLoadState("networkidle").catch(()=>{});let a=await n(r,!1);return{content:[{type:"text",text:e},{type:"image",data:a.toString("base64"),mimeType:"image/png"}]}}catch{return c(e)}}import{z as zn}from"zod";import{platform as yi}from"os";import{execFile as Jo}from"child_process";ie();We();var bi=zn.object({apiKey:zn.string().optional().describe("API key (mist_...) for headless auth. Skips the device code flow entirely. Generate one at app.mistflow.ai/mcp-keys."),deviceCode:zn.string().optional().describe("Resume polling for a pending device code. Returned by a previous mist_setup call with status 'pending'. Call mist_setup again with this value after ~15 seconds to check if the user approved.")});function wi(t){return"error"in t}function Yo(t){return new Promise(e=>setTimeout(e,t))}function vi(t){return new Promise(e=>{let o=yi();o==="win32"?Jo("cmd.exe",["/c","start","",t],n=>{n&&console.error("Could not open browser:",n.message),e(!n)}):Jo(o==="darwin"?"open":"xdg-open",[t],r=>{r&&console.error("Could not open browser:",r.message),e(!r)}),setTimeout(()=>e(!1),5e3)})}var xi={fetch:globalThis.fetch,openBrowser:vi,sleep:Yo};async function Ko(t,e,o,n){let r=o,a=n.sleep??Yo;for(let s=0;s<e;s++){await a(r);let i;try{let p=await n.fetch(`${V()}/auth/poll`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({device_code:t})});if(!p.ok)continue;i=await p.json()}catch{continue}if(wi(i))switch(i.error){case"authorization_pending":continue;case"slow_down":r+=5e3;continue;case"expired_token":return c("The sign-in link expired. Run mist_setup again to get a new code.",!0);case"access_denied":return c("Sign-in was cancelled. Run mist_setup again to try again.",!0);case"already_exchanged":return c("This sign-in link was already used. Run mist_setup again to get a new code.",!0)}let l=i.email||i.org_name||i.org_slug;return kn({apiKey:i.api_key,apiKeyId:i.api_key_id,apiKeyName:i.api_key_name,orgId:i.org_id,orgSlug:i.org_slug,email:i.email}),c(`Connected to Mistflow as ${l}. You are ready to build and deploy.`)}return null}async function ki(t,e=xi){let o=t;if(o?.apiKey)try{let s=await e.fetch(`${V()}/api/org`,{headers:{Authorization:`ApiKey ${o.apiKey}`}});if(!s.ok)return c("Invalid API key. Check the key and try again.",!0);let i=await s.json();return kn({apiKey:o.apiKey,orgId:i.id,orgSlug:i.slug}),c(`Connected to Mistflow as ${i.slug} via API key. You are ready to build and deploy.`)}catch{return c("Cannot reach Mistflow servers. Check your internet connection.",!0)}if(o?.deviceCode){let s=await Ko(o.deviceCode,6,5e3,e);return s||c(JSON.stringify({status:"pending",deviceCode:o.deviceCode,instruction:"The user hasn't approved yet. Wait ~15 seconds and call mist_setup again with the same deviceCode."}))}let n;try{let s=await e.fetch(`${V()}/auth/device`,{method:"POST",headers:{"Content-Type":"application/json"}});if(!s.ok)return c("Cannot reach Mistflow servers. Check your internet connection.",!0);n=await s.json()}catch{return c("Cannot reach Mistflow servers. Check your internet connection.",!0)}let r=`${n.verification_uri}?code=${n.user_code}`;console.error(`
35
35
  Sign in at: ${r}
36
- Your code: ${o.user_code}
37
- `);try{await t.openBrowser(r)}catch{}let s=await ve(o.device_code,6,5e3,t);return s||c(JSON.stringify({status:"pending",deviceCode:o.device_code,signInUrl:r,userCode:o.user_code,instruction:"The user hasn't approved yet. Wait ~15 seconds, then call mist_setup again with deviceCode='"+o.device_code+"' to check if they approved."}))}var ke={name:"mist_setup",description:ne,inputSchema:_t,handler:e=>Rt(e)};import{z as y}from"zod";import{z as h}from"zod";import{resolve as Bt,join as Te}from"path";import{existsSync as Jt,readFileSync as Ue,writeFileSync as Wt}from"fs";import{existsSync as xt,readFileSync as Pt}from"fs";function Se(e){let t=new Set;if(!xt(e))return t;let n=Pt(e,"utf-8");for(let o of n.split(`
38
- `)){let r=o.trim();if(!r||r.startsWith("#"))continue;let s=r.indexOf("=");if(s>0){let i=r.slice(0,s).trim(),a=r.slice(s+1).trim();a&&a!=='""'&&a!=="''"&&t.add(i)}}return t}var Ht=h.object({action:h.enum(["get","update"]).default("get").describe("'get' reads current project state. 'update' modifies it."),projectPath:h.string().optional().describe("Path to the project directory (default: current working directory)"),completedStep:h.number().optional().describe("(update only) Mark a plan step as completed by step number"),addEnvVar:h.object({key:h.string(),description:h.string().optional(),setupUrl:h.string().optional()}).optional().describe("(update only) Add a required env var to the project manifest")}),Ee={name:"mist_state",description:"Read or update project state in mistflow.json. Use action='get' to load plan progress, env var status, and deploy info. Use action='update' to mark plan steps complete or add required env vars. Use when the user says 'mist status', 'mist state', or 'mist update state'.",inputSchema:Ht,handler:async e=>{let t=e,n=Bt(t.projectPath??process.cwd()),o=Te(n,"mistflow.json");if(!Jt(o))return me(n);let r;try{r=JSON.parse(Ue(o,"utf-8"))}catch{return c("Failed to parse mistflow.json.",!0)}if(t.action==="get"){if(!r.projectId)try{let{ensureBackendRegistered:d}=await Promise.resolve().then(()=>(Ie(),je));await d(n)&&(r=JSON.parse(Ue(o,"utf-8")))}catch{}let a=r.plan,p=a?.steps?.filter(d=>d.status==="completed").length??0,l=a?.steps?.length??0,b=Se(Te(n,".env.local")),P=r.env?.required?Object.entries(r.env.required).map(([d,T])=>({name:d,description:T?.description,configured:b.has(d)})):[];r.projectId&&Promise.resolve().then(()=>(M(),ee)).then(({fetchRemoteState:d})=>d(r.projectId)).catch(()=>{});let k=[`Project: ${r.name}`];if(a){k.push(`Plan: ${a.summary??a.name??"unnamed"} \u2014 ${p}/${l} steps complete`);for(let d of a.steps){let T=d.status==="completed"?"\u2713":d.status==="in_progress"?"\u2192":" ";k.push(` [${T}] ${d.number}. ${d.name}`)}}let j=P.filter(d=>!d.configured);j.length>0&&k.push(`Missing env vars: ${j.map(d=>d.name).join(", ")}`),r.deploy?.url?k.push(`Deployed: ${r.deploy.url} (${r.deploy.count??0} deploys)`):k.push("Not deployed yet");let I=[],V=a?.steps?.find(d=>d.status!=="completed");return V?I.push(`NEXT: Run mist implement <absolute-path> to work on step ${V.number} (${V.name}).`):a&&p===l&&(r.deploy?.url||I.push("NEXT: All steps complete! Run mist deploy <absolute-path> to deploy the app now. Do NOT ask the user \u2014 just deploy.")),j.length>0&&I.push(`Missing env vars in .env.local: ${j.map(d=>d.name).join(", ")}`),c(JSON.stringify({name:r.name,projectId:r.projectId,planProgress:a?{name:a.name,summary:a.summary,totalSteps:l,completedSteps:p,steps:a.steps}:null,envStatus:P,deploy:r.deploy??null,contextMessage:k.join(`
39
- `),nextSteps:I}))}let s=[];if(t.completedStep!==void 0){let a=r.plan;if(a?.steps){let p=a.steps.findIndex(l=>l.number===t.completedStep);if(p===-1)return c(`Step ${t.completedStep} not found in the plan.`,!0);a.steps[p].status="completed",s.push(`Step ${t.completedStep} marked as completed`)}}t.addEnvVar&&(r.env||(r.env={required:{}}),r.env.required||(r.env.required={}),r.env.required[t.addEnvVar.key]={description:t.addEnvVar.description,setupUrl:t.addEnvVar.setupUrl},s.push(`Added required env var: ${t.addEnvVar.key}`)),Wt(o,JSON.stringify(r,null,2)+`
40
- `),r.projectId&&Promise.resolve().then(()=>(M(),ee)).then(async({readLocalState:a,syncRemoteState:p})=>{let l=a(n);l&&await p(r.projectId,l)}).catch(()=>{});let i=[];if(t.completedStep!==void 0){let p=r.plan?.steps?.find(l=>l.status!=="completed");p?i.push(`NEXT: Run mist implement <absolute-path> to work on step ${p.number} (${p.name}). Do this now.`):i.push("NEXT: All steps complete! Run mist deploy <absolute-path> to deploy the app now. Do NOT suggest localhost.")}return t.addEnvVar&&(i.push(`Add ${t.addEnvVar.key} to your .env.local file`),t.addEnvVar.setupUrl&&i.push(`Get the value from: ${t.addEnvVar.setupUrl}`)),c(JSON.stringify({updated:!0,changes:s,message:s.length>0?`Project state saved. ${s.join(". ")}.`:"No changes made.",nextSteps:i.length>0?i:void 0}))}};var Kt=y.object({action:y.enum(["get","update"]).default("get").describe("'get' reads current project state (context oracle \u2014 call before making decisions in an existing project). 'update' marks steps complete or adds env vars. All other project queries moved to the CLI in MCP 0.6.0: `mist projects share`, `mist projects errors`, `mist projects logs`, `mist projects deployments`, `mist projects version`, `mist projects designs`, `mist projects app-styles`, `mist projects integrations`."),projectPath:y.string().optional().describe("Path to the project directory (default: cwd)"),completedStep:y.number().optional().describe("(update) Mark a plan step as completed by step number"),addEnvVar:y.object({key:y.string(),description:y.string().optional(),setupUrl:y.string().optional()}).optional().describe("(update) Add a required env var to the project manifest")}),De={name:"mist_project",description:re,inputSchema:Kt,handler:async e=>{let t=e;return Ee.handler({action:t.action,projectPath:t.projectPath,completedStep:t.completedStep,addEnvVar:t.addEnvVar})}};import{z as _}from"zod";Me();var Qt=_.object({action:_.enum(["navigate","go_back","go_forward","click","type","fill","select_option","press_key","hover","screenshot","snapshot"]).describe("Action to perform. Navigation: navigate|go_back|go_forward. Interaction: click|type|fill|select_option|press_key|hover. Visual: screenshot (returns image) | snapshot (returns accessibility tree)."),url:_.string().optional().describe("URL to navigate to. Required for 'navigate'; optional for 'screenshot' (navigates before capturing)."),selector:_.string().optional().describe("CSS selector of the target element. Required for: click, type, fill, select_option, hover. Optional for screenshot (captures just that element)."),value:_.string().optional().describe("Text to type/fill, option to select, or key to press (e.g. 'Enter', 'Tab'). Required for: type, fill, select_option, press_key."),fullPage:_.boolean().default(!1).describe("For 'screenshot': capture the full scrollable page instead of just the viewport."),includeScreenshot:_.boolean().default(!1).describe("For navigate/interact actions: also return a screenshot alongside the accessibility snapshot.")}),Ae={name:"mist_browser",description:oe,inputSchema:Qt,handler:async e=>{let t=e,n=await Oe();if(t.action==="navigate"){if(!t.url)return c("URL is required for 'navigate'.",!0);let s=[],i=l=>{l.type()==="error"&&s.push(l.text())};n.on("console",i),await n.goto(t.url,{waitUntil:"domcontentloaded",timeout:3e4}),await n.waitForLoadState("networkidle").catch(()=>{});let a=[],p=l=>a.push(l.message);if(n.on("pageerror",p),await n.waitForTimeout(500),n.removeListener("console",i),n.removeListener("pageerror",p),s.length>0||a.length>0){let l=await $(n),b=[{type:"text",text:JSON.stringify({url:n.url(),title:await n.title(),snapshot:l,consoleErrors:s,pageErrors:a,hasErrors:!0})}];if(t.includeScreenshot){let P=await q(n);b.push({type:"image",data:P.toString("base64"),mimeType:"image/png"})}return{content:b}}}else if(t.action==="go_back")await n.goBack({waitUntil:"domcontentloaded",timeout:1e4});else if(t.action==="go_forward")await n.goForward({waitUntil:"domcontentloaded",timeout:1e4});else if(t.action==="click"){if(!t.selector)return c("Selector is required for 'click'.",!0);await n.click(t.selector,{timeout:1e4}),await n.waitForLoadState("domcontentloaded").catch(()=>{}),await n.waitForTimeout(500)}else if(t.action==="type"){if(!t.selector)return c("Selector is required for 'type'.",!0);if(!t.value)return c("Value is required for 'type'.",!0);await n.type(t.selector,t.value,{delay:50})}else if(t.action==="fill"){if(!t.selector)return c("Selector is required for 'fill'.",!0);if(!t.value)return c("Value is required for 'fill'.",!0);await n.fill(t.selector,t.value)}else if(t.action==="select_option"){if(!t.selector)return c("Selector is required for 'select_option'.",!0);if(!t.value)return c("Value is required for 'select_option'.",!0);await n.selectOption(t.selector,t.value)}else if(t.action==="hover"){if(!t.selector)return c("Selector is required for 'hover'.",!0);await n.hover(t.selector,{timeout:1e4})}else if(t.action==="press_key"){if(!t.value)return c("Value is required for 'press_key' (e.g. 'Enter').",!0);await n.keyboard.press(t.value),await n.waitForLoadState("domcontentloaded").catch(()=>{}),await n.waitForTimeout(500)}else if(t.action==="screenshot"){t.url&&(await n.goto(t.url,{waitUntil:"domcontentloaded",timeout:3e4}),await n.waitForLoadState("networkidle").catch(()=>{}));let s;if(t.selector){let i=await n.$(t.selector);if(!i)return c(`Element not found: ${t.selector}`,!0);s=await i.screenshot({type:"png"})}else s=await q(n,t.fullPage);return{content:[{type:"text",text:JSON.stringify({url:n.url(),title:await n.title(),message:`Screenshot captured (${t.fullPage?"full page":"viewport"})`})},{type:"image",data:s.toString("base64"),mimeType:"image/png"}]}}else if(t.action==="snapshot"){let s=await $(n);return{content:[{type:"text",text:JSON.stringify({url:n.url(),title:await n.title(),snapshot:s})}]}}let o=await $(n),r=[{type:"text",text:JSON.stringify({url:n.url(),title:await n.title(),snapshot:o})}];if(t.includeScreenshot){let s=await q(n);r.push({type:"image",data:s.toString("base64"),mimeType:"image/png"})}return{content:r}}};import{z as Ne}from"zod";var $e=`# Mistflow CLI reference
36
+ Your code: ${n.user_code}
37
+ `);try{await e.openBrowser(r)}catch{}let a=await Ko(n.device_code,6,5e3,e);return a||c(JSON.stringify({status:"pending",deviceCode:n.device_code,signInUrl:r,userCode:n.user_code,instruction:"The user hasn't approved yet. Wait ~15 seconds, then call mist_setup again with deviceCode='"+n.device_code+"' to check if they approved."}))}var Qo={name:"mist_setup",description:ko,inputSchema:bi,handler:t=>ki(t)};import{z as Z}from"zod";import{resolve as Ht}from"path";import{existsSync as Wt,readFileSync as Gt}from"fs";import{join as Vt}from"path";import{z as Re}from"zod";import{resolve as Fi,join as or}from"path";import{existsSync as qi,readFileSync as rr,writeFileSync as Bi}from"fs";import{existsSync as Si,readFileSync as Pi}from"fs";function Xo(t){let e=new Set;if(!Si(t))return e;let o=Pi(t,"utf-8");for(let n of o.split(`
38
+ `)){let r=n.trim();if(!r||r.startsWith("#"))continue;let a=r.indexOf("=");if(a>0){let s=r.slice(0,a).trim(),i=r.slice(a+1).trim();i&&i!=='""'&&i!=="''"&&e.add(s)}}return e}var zi=Re.object({action:Re.enum(["get","update"]).default("get").describe("'get' reads current project state. 'update' modifies it."),projectPath:Re.string().optional().describe("Path to the project directory (default: current working directory)"),completedStep:Re.number().optional().describe("(update only) Mark a plan step as completed by step number"),addEnvVar:Re.object({key:Re.string(),description:Re.string().optional(),setupUrl:Re.string().optional()}).optional().describe("(update only) Add a required env var to the project manifest")}),sr={name:"mist_state",description:"Read or update project state in mistflow.json. Use action='get' to load plan progress, env var status, and deploy info. Use action='update' to mark plan steps complete or add required env vars. Called internally by mist_project; host AIs should use mist_project directly.",inputSchema:zi,handler:async t=>{let e=t,o=Fi(e.projectPath??process.cwd()),n=or(o,"mistflow.json");if(!qi(n))return ke(o);let r;try{r=JSON.parse(rr(n,"utf-8"))}catch{return c("Failed to parse mistflow.json.",!0)}if(e.action==="get"){if(!r.projectId)try{let{ensureBackendRegistered:y}=await Promise.resolve().then(()=>(Gn(),Wn));await y(o)&&(r=JSON.parse(rr(n,"utf-8")))}catch{}let i=r.plan,l=i?.steps?.filter(y=>y.status==="completed").length??0,p=i?.steps?.length??0,m=Xo(or(o,".env.local")),u=r.env?.required?Object.entries(r.env.required).map(([y,x])=>({name:y,description:x?.description,configured:m.has(y)})):[];r.projectId&&Promise.resolve().then(()=>($e(),xt)).then(({fetchRemoteState:y})=>y(r.projectId)).catch(()=>{});let d=[`Project: ${r.name}`];if(i){d.push(`Plan: ${i.summary??i.name??"unnamed"} \u2014 ${l}/${p} steps complete`);for(let y of i.steps){let x=y.status==="completed"?"\u2713":y.status==="in_progress"?"\u2192":" ";d.push(` [${x}] ${y.number}. ${y.name}`)}}let b=u.filter(y=>!y.configured);b.length>0&&d.push(`Missing env vars: ${b.map(y=>y.name).join(", ")}`),r.deploy?.url?d.push(`Deployed: ${r.deploy.url} (${r.deploy.count??0} deploys)`):d.push("Not deployed yet");let T=[],w=i?.steps?.find(y=>y.status!=="completed");return w?T.push(`NEXT: Call mist_implement({ projectPath }) to work on step ${w.number} (${w.name}).`):i&&l===p&&(r.deploy?.url||T.push("NEXT: All steps complete. Call mist_build({ projectPath }) then mist_deploy({ action: 'deploy', projectPath }). Do NOT ask the user \u2014 chain straight through.")),b.length>0&&T.push(`Missing env vars in .env.local: ${b.map(y=>y.name).join(", ")}`),c(JSON.stringify({name:r.name,projectId:r.projectId,planProgress:i?{name:i.name,summary:i.summary,totalSteps:p,completedSteps:l,steps:i.steps}:null,envStatus:u,deploy:r.deploy??null,contextMessage:d.join(`
39
+ `),nextSteps:T}))}let a=[];if(e.completedStep!==void 0){let i=r.plan;if(i?.steps){let l=i.steps.findIndex(p=>p.number===e.completedStep);if(l===-1)return c(`Step ${e.completedStep} not found in the plan.`,!0);i.steps[l].status="completed",a.push(`Step ${e.completedStep} marked as completed`)}}e.addEnvVar&&(r.env||(r.env={required:{}}),r.env.required||(r.env.required={}),r.env.required[e.addEnvVar.key]={description:e.addEnvVar.description,setupUrl:e.addEnvVar.setupUrl},a.push(`Added required env var: ${e.addEnvVar.key}`)),Bi(n,JSON.stringify(r,null,2)+`
40
+ `),r.projectId&&Promise.resolve().then(()=>($e(),xt)).then(async({readLocalState:i,syncRemoteState:l})=>{let p=i(o);p&&await l(r.projectId,p)}).catch(()=>{});let s=[];if(e.completedStep!==void 0){let l=r.plan?.steps?.find(p=>p.status!=="completed");l?s.push(`NEXT: Call mist_implement({ projectPath }) to work on step ${l.number} (${l.name}). Do this now.`):s.push("NEXT: All steps complete. Call mist_build({ projectPath }) then mist_deploy({ action: 'deploy', projectPath }) to deploy the app. Do NOT suggest localhost.")}return e.addEnvVar&&(s.push(`Add ${e.addEnvVar.key} to your .env.local file`),e.addEnvVar.setupUrl&&s.push(`Get the value from: ${e.addEnvVar.setupUrl}`)),c(JSON.stringify({updated:!0,changes:a,message:a.length>0?`Project state saved. ${a.join(". ")}.`:"No changes made.",nextSteps:s.length>0?s:void 0}))}};ie();var Vn={},Ke=[];function zt(t){let e=Ke.find(n=>n.id===t);if(e)return e;let o=t.toLowerCase().replace(/[^a-z0-9]/g,"");return Ke.find(n=>{let r=n.title.toLowerCase().replace(/[^a-z0-9]/g,"");return r===o||r.includes(o)||o.includes(r)})}function ir(t){return Vn[t]}function Jn(t){return t?Ke.filter(e=>e.category.toLowerCase()===t.toLowerCase()):Ke}function ar(t){let e=t??Ke;if(e.length===0)return"Landing page presets have been replaced by the tone-based system. The landing page tone is now auto-selected based on your app's description during planning.";let o={};for(let r of e){o[r.category]||(o[r.category]=[]);let a=Vn[r.id],s=a?` \u2014 ${a.description}`:"";o[r.category].push(`${r.id} \u2014 "${r.title}"${s}`)}let n=[];for(let[r,a]of Object.entries(o))n.push(`**${r}**:
41
+ ${a.map(s=>` \u2022 ${s}`).join(`
42
+ `)}`);return n.join(`
41
43
 
42
- The Mistflow CLI handles local execution and long-running operations that
43
- would hit the MCP 60s tool-call ceiling. Every command below is invokable
44
- via your shell/bash tool:
44
+ `)}function Hi(t){return t.toLowerCase().replace(/[^a-z0-9]+/g,"-").replace(/^-|-$/g,"")}function lr(t){return(t?Jn(t):Ke).map(o=>{let n=Vn[o.id];return{id:o.id,slug:Hi(o.title),title:o.title,category:o.category,description:n?.description??"",tags:n?.tags??[],theme:n?.theme??"dark",colors:n?.colors??[],style:n?.style??""}})}function cr(t,e){return[]}mt();var Kn={"resend-email":{description:"Transactional email with React Email templates and webhook handling.",tags:["email","transactional","welcome","notification","invite","alert"],envVars:[{key:"RESEND_API_KEY",description:"Resend API key for sending emails",setupUrl:"https://resend.com/api-keys"}],docsUrl:"https://resend.com/docs/send-with-nextjs",packages:["resend","@react-email/components"],difficulty:"easy"},"r2-storage":{description:"File uploads with drag-and-drop UI, stored in Mistflow Cloud.",tags:["storage","upload","file","image","media","attachment","avatar"],envVars:[],docsUrl:"https://developers.cloudflare.com/r2/",packages:[],difficulty:"easy"},"openai-ai":{description:"AI-powered features with OpenAI SDK, streaming chat, and content generation.",tags:["ai","openai","chatbot","gpt","llm","assistant","generation"],envVars:[{key:"OPENAI_API_KEY",description:"OpenAI API key for AI features",setupUrl:"https://platform.openai.com/api-keys"}],docsUrl:"https://platform.openai.com/docs/guides/text-generation",packages:["openai","ai"],difficulty:"medium"},"anthropic-ai":{description:"AI features with the Anthropic SDK, streaming Claude chat, and content generation.",tags:["ai","anthropic","claude","llm","assistant","generation"],envVars:[{key:"ANTHROPIC_API_KEY",description:"Anthropic API key for Claude",setupUrl:"https://console.anthropic.com/settings/keys"}],docsUrl:"https://docs.anthropic.com/en/docs/initial-setup",packages:["@anthropic-ai/sdk","ai"],difficulty:"medium"},"openrouter-ai":{description:"AI model router with access to 200+ models (GPT, Claude, Llama, Mistral, etc.) through one API.",tags:["ai","openrouter","llm","multi-model","claude","gpt","llama","mistral"],envVars:[{key:"OPENROUTER_API_KEY",description:"OpenRouter API key",setupUrl:"https://openrouter.ai/keys"}],docsUrl:"https://openrouter.ai/docs/quickstart",packages:["@openrouter/sdk"],difficulty:"easy"},"stripe-payments":{description:"Payment processing with Stripe Checkout, webhooks, and billing portal.",tags:["payments","stripe","billing","subscription","checkout","invoice"],envVars:[{key:"STRIPE_SECRET_KEY",description:"Stripe secret key",setupUrl:"https://dashboard.stripe.com/apikeys"},{key:"STRIPE_PUBLISHABLE_KEY",description:"Stripe publishable key (client-side)",setupUrl:"https://dashboard.stripe.com/apikeys"},{key:"STRIPE_WEBHOOK_SECRET",description:"Stripe webhook signing secret",setupUrl:"https://dashboard.stripe.com/webhooks"}],docsUrl:"https://docs.stripe.com/checkout/quickstart",packages:["stripe","@stripe/stripe-js"],difficulty:"advanced"},"elevenlabs-voice":{description:"Text-to-speech and voice generation with ElevenLabs API.",tags:["voice","tts","speech","audio","elevenlabs","narration","podcast"],envVars:[{key:"ELEVENLABS_API_KEY",description:"ElevenLabs API key for voice generation",setupUrl:"https://elevenlabs.io/app/settings/api-keys"}],docsUrl:"https://elevenlabs.io/docs/api-reference/text-to-speech",packages:["elevenlabs"],difficulty:"medium"},"google-maps":{description:"Google Maps embed, Places autocomplete, and geolocation features.",tags:["maps","location","google","places","geocoding","directions","nearby"],envVars:[{key:"NEXT_PUBLIC_GOOGLE_MAPS_API_KEY",description:"Google Maps API key (client-side)",setupUrl:"https://console.cloud.google.com/apis/credentials"}],docsUrl:"https://developers.google.com/maps/documentation/javascript",packages:["@googlemaps/js-api-loader"],difficulty:"medium"},"twilio-sms":{description:"SMS notifications, OTP verification, and phone number validation.",tags:["sms","twilio","otp","phone","verification","text-message"],envVars:[{key:"TWILIO_ACCOUNT_SID",description:"Twilio account SID",setupUrl:"https://console.twilio.com/"},{key:"TWILIO_AUTH_TOKEN",description:"Twilio auth token",setupUrl:"https://console.twilio.com/"},{key:"TWILIO_PHONE_NUMBER",description:"Twilio phone number for sending SMS",setupUrl:"https://console.twilio.com/us1/develop/phone-numbers/manage/incoming"}],docsUrl:"https://www.twilio.com/docs/messaging/quickstart/node",packages:["twilio"],difficulty:"medium"},"posthog-analytics":{description:"Product analytics with event tracking, feature flags, and session replay.",tags:["analytics","posthog","tracking","funnel","event","feature-flag"],envVars:[{key:"NEXT_PUBLIC_POSTHOG_KEY",description:"PostHog project API key",setupUrl:"https://app.posthog.com/project/settings"},{key:"NEXT_PUBLIC_POSTHOG_HOST",description:"PostHog instance URL (default: https://us.i.posthog.com)",setupUrl:"https://app.posthog.com/project/settings"}],docsUrl:"https://posthog.com/docs/libraries/next-js",packages:["posthog-js","posthog-node"],difficulty:"easy"},"firecrawl-scraping":{description:"Web scraping and crawling with markdown output, structured extraction, and async crawls.",tags:["scraping","crawl","firecrawl","web","extract","rag","markdown"],envVars:[{key:"FIRECRAWL_API_KEY",description:"Firecrawl API key",setupUrl:"https://firecrawl.dev"}],docsUrl:"https://docs.firecrawl.dev/sdks/node",packages:["@mendable/firecrawl-js"],difficulty:"easy"},"replicate-media":{description:"Image and video generation with 200+ AI models (Flux, Wan Video, Runway, SDXL, etc.) through one API.",tags:["image","video","replicate","flux","sdxl","generation","media","avatar","thumbnail"],envVars:[{key:"REPLICATE_API_TOKEN",description:"Replicate API token",setupUrl:"https://replicate.com/account/api-tokens"}],docsUrl:"https://replicate.com/docs/get-started/nodejs",packages:["replicate"],difficulty:"medium"}},Ye=[{id:"resend-email",name:"Resend Email",category:"communication",prompt:`## Resend Email Integration
45
45
 
46
- npx -y @mistflow-ai/cli <command> [...args]
46
+ ### File Structure
47
+ \`\`\`
48
+ lib/resend.ts \u2014 Resend client singleton
49
+ lib/email.ts \u2014 Send helper functions (sendWelcomeEmail, sendNotification, etc.)
50
+ emails/ \u2014 React Email templates directory
51
+ welcome.tsx \u2014 Welcome email template
52
+ notification.tsx \u2014 Generic notification template
53
+ app/api/webhooks/resend/route.ts \u2014 Webhook handler for delivery events
54
+ \`\`\`
47
55
 
48
- Or, if the user has installed the CLI globally:
56
+ ### Client Setup (lib/resend.ts)
57
+ \`\`\`typescript
58
+ import { Resend } from "resend";
49
59
 
50
- mist <command> [...args]
60
+ export const resend = new Resend(process.env.RESEND_API_KEY);
61
+ \`\`\`
51
62
 
52
- Never add \`@mistflow-ai/cli\` to the app's package.json. It is a standalone
53
- companion CLI, not an app dependency. Local installs create
54
- \`node_modules/.bin/mist\` shims and can drift from the machine-level CLI.
63
+ ### Send Helper (lib/email.ts)
64
+ \`\`\`typescript
65
+ import { resend } from "./resend";
66
+ import WelcomeEmail from "@/emails/welcome";
55
67
 
56
- Pass \`--json\` to any command to get a structured JSON response on stdout
57
- (progress / info goes to stderr). Errors exit non-zero with a message on
58
- stderr.
68
+ export async function sendWelcomeEmail(to: string, name: string) {
69
+ const { data, error } = await resend.emails.send({
70
+ from: "App Name <noreply@yourdomain.com>",
71
+ to,
72
+ subject: "Welcome to App Name",
73
+ react: WelcomeEmail({ name }),
74
+ });
75
+ if (error) throw new Error(error.message);
76
+ return data;
77
+ }
78
+ \`\`\`
79
+
80
+ ### React Email Template (emails/welcome.tsx)
81
+ \`\`\`tsx
82
+ import { Html, Head, Body, Container, Heading, Text, Button, Section } from "@react-email/components";
83
+
84
+ interface WelcomeEmailProps { name: string }
85
+
86
+ export default function WelcomeEmail({ name }: WelcomeEmailProps) {
87
+ return (
88
+ <Html>
89
+ <Head />
90
+ <Body style={{ backgroundColor: "#f6f9fc", fontFamily: "sans-serif" }}>
91
+ <Container style={{ maxWidth: 560, margin: "0 auto", padding: "20px 0" }}>
92
+ <Heading style={{ fontSize: 24, color: "#1a1a1a" }}>Welcome, {name}!</Heading>
93
+ <Text style={{ fontSize: 16, color: "#4a4a4a", lineHeight: 1.6 }}>
94
+ Thanks for signing up. Here's how to get started...
95
+ </Text>
96
+ <Section style={{ textAlign: "center", marginTop: 24 }}>
97
+ <Button href="https://yourapp.com/dashboard" style={{
98
+ backgroundColor: "#000", color: "#fff", padding: "12px 24px",
99
+ borderRadius: 6, fontSize: 14, fontWeight: 600,
100
+ }}>
101
+ Go to Dashboard
102
+ </Button>
103
+ </Section>
104
+ </Container>
105
+ </Body>
106
+ </Html>
107
+ );
108
+ }
109
+ \`\`\`
110
+
111
+ ### Webhook Handler (app/api/webhooks/resend/route.ts)
112
+ \`\`\`typescript
113
+ import { NextRequest, NextResponse } from "next/server";
114
+
115
+ export async function POST(req: NextRequest) {
116
+ const body = await req.json();
117
+ const { type, data } = body;
118
+ // Handle: email.sent, email.delivered, email.bounced, email.complained
119
+ switch (type) {
120
+ case "email.bounced":
121
+ console.error("Email bounced:", data.to);
122
+ // Mark user email as invalid in your DB
123
+ break;
124
+ }
125
+ return NextResponse.json({ received: true });
126
+ }
127
+ \`\`\`
128
+
129
+ ### Common Pitfalls
130
+ 1. **Domain verification required for production.** Use Resend's free onboarding@resend.dev for dev, but production requires a verified domain.
131
+ 2. **Rate limits.** Free tier: 100 emails/day, 3000/month. Queue emails for bulk sends.
132
+ 3. **From address must match a verified domain** or use onboarding@resend.dev for testing.
133
+ 4. **Webhooks need a public URL.** Use ngrok or similar for local webhook testing.
134
+ 5. **React Email templates must be Server Components.** Do not add "use client" to email templates.
135
+ 6. **Never ask the user to paste RESEND_API_KEY in chat.** Direct them to set it in the Mistflow dashboard (Project Settings > Environment Variables).`},{id:"r2-storage",name:"File Storage (R2)",category:"storage",prompt:`## File Storage Integration
136
+
137
+ Files are stored in Mistflow Cloud's managed blob storage. No extra setup needed for Mistflow-deployed apps.
138
+
139
+ ### File Structure
140
+ \`\`\`
141
+ lib/storage.ts \u2014 Upload/download helpers
142
+ app/api/upload/route.ts \u2014 Upload API route (server-side)
143
+ components/file-upload.tsx \u2014 Drag-and-drop upload component
144
+ \`\`\`
145
+
146
+ ### Upload API Route (app/api/upload/route.ts)
147
+ \`\`\`typescript
148
+ import { NextRequest, NextResponse } from "next/server";
149
+
150
+ export async function POST(req: NextRequest) {
151
+ const formData = await req.formData();
152
+ const file = formData.get("file") as File;
153
+ if (!file) return NextResponse.json({ error: "No file provided" }, { status: 400 });
154
+
155
+ // Validate file size (10MB max)
156
+ if (file.size > 10 * 1024 * 1024) {
157
+ return NextResponse.json({ error: "File too large (max 10MB)" }, { status: 400 });
158
+ }
159
+
160
+ // Validate MIME type
161
+ const allowedTypes = ["image/jpeg", "image/png", "image/webp", "application/pdf"];
162
+ if (!allowedTypes.includes(file.type)) {
163
+ return NextResponse.json({ error: "File type not allowed" }, { status: 400 });
164
+ }
165
+
166
+ const bytes = await file.arrayBuffer();
167
+ const buffer = Buffer.from(bytes);
168
+
169
+ // Upload to R2 via Mistflow's storage endpoint
170
+ const key = \`uploads/\${Date.now()}-\${file.name.replace(/[^a-zA-Z0-9.-]/g, "_")}\`;
171
+ // Store buffer with key \u2014 implementation depends on R2 binding or S3-compatible client
172
+ // For Mistflow apps: the deploy pipeline auto-configures R2 bindings
173
+
174
+ return NextResponse.json({ url: \`/api/files/\${key}\`, key });
175
+ }
176
+ \`\`\`
177
+
178
+ ### Drag-and-Drop Upload Component (components/file-upload.tsx)
179
+ \`\`\`tsx
180
+ "use client";
181
+ import { useState, useCallback } from "react";
182
+
183
+ interface FileUploadProps {
184
+ onUpload: (url: string) => void;
185
+ accept?: string;
186
+ maxSizeMB?: number;
187
+ }
188
+
189
+ export function FileUpload({ onUpload, accept = "image/*", maxSizeMB = 10 }: FileUploadProps) {
190
+ const [isDragging, setIsDragging] = useState(false);
191
+ const [uploading, setUploading] = useState(false);
192
+
193
+ const handleUpload = useCallback(async (file: File) => {
194
+ setUploading(true);
195
+ try {
196
+ const formData = new FormData();
197
+ formData.append("file", file);
198
+ const res = await fetch("/api/upload", { method: "POST", body: formData });
199
+ if (!res.ok) throw new Error("Upload failed");
200
+ const { url } = await res.json();
201
+ onUpload(url);
202
+ } finally {
203
+ setUploading(false);
204
+ }
205
+ }, [onUpload]);
206
+
207
+ return (
208
+ <div
209
+ onDragOver={(e) => { e.preventDefault(); setIsDragging(true); }}
210
+ onDragLeave={() => setIsDragging(false)}
211
+ onDrop={(e) => {
212
+ e.preventDefault();
213
+ setIsDragging(false);
214
+ const file = e.dataTransfer.files[0];
215
+ if (file) handleUpload(file);
216
+ }}
217
+ className={\`border-2 border-dashed rounded-lg p-8 text-center cursor-pointer transition-colors \${
218
+ isDragging ? "border-primary bg-primary/5" : "border-muted-foreground/25 hover:border-primary/50"
219
+ }\`}
220
+ >
221
+ <input type="file" accept={accept} className="hidden" id="file-upload"
222
+ onChange={(e) => { const f = e.target.files?.[0]; if (f) handleUpload(f); }} />
223
+ <label htmlFor="file-upload" className="cursor-pointer">
224
+ {uploading ? "Uploading..." : "Drop a file here or click to browse"}
225
+ </label>
226
+ </div>
227
+ );
228
+ }
229
+ \`\`\`
230
+
231
+ ### Common Pitfalls
232
+ 1. **Always validate file type and size server-side.** Client-side validation can be bypassed.
233
+ 2. **Sanitize filenames.** Strip special characters to avoid path traversal.
234
+ 3. **Use unique keys.** Prefix with timestamp or UUID to avoid collisions.
235
+ 4. **Set appropriate CORS headers** if the upload API is called from a different origin.
236
+ 5. **For Mistflow apps, R2 bindings are auto-configured.** No env vars needed.`},{id:"openai-ai",name:"OpenAI / AI Integration",category:"ai",prompt:`## OpenAI / AI Integration
237
+
238
+ ### File Structure
239
+ \`\`\`
240
+ lib/openai.ts \u2014 OpenAI client singleton
241
+ app/api/chat/route.ts \u2014 Streaming chat API route
242
+ components/chat.tsx \u2014 Chat UI component with streaming
243
+ \`\`\`
244
+
245
+ ### Client Setup (lib/openai.ts)
246
+ \`\`\`typescript
247
+ import OpenAI from "openai";
248
+
249
+ export const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });
250
+ \`\`\`
251
+
252
+ ### Streaming Chat Route (app/api/chat/route.ts)
253
+ \`\`\`typescript
254
+ import { openai } from "@/lib/openai";
255
+ import { NextRequest } from "next/server";
256
+
257
+ export async function POST(req: NextRequest) {
258
+ const { messages, systemPrompt } = await req.json();
259
+
260
+ const stream = await openai.chat.completions.create({
261
+ model: "gpt-4o-mini",
262
+ messages: [
263
+ { role: "system", content: systemPrompt || "You are a helpful assistant." },
264
+ ...messages,
265
+ ],
266
+ stream: true,
267
+ });
268
+
269
+ // Stream the response using ReadableStream
270
+ const encoder = new TextEncoder();
271
+ const readable = new ReadableStream({
272
+ async start(controller) {
273
+ for await (const chunk of stream) {
274
+ const text = chunk.choices[0]?.delta?.content || "";
275
+ if (text) controller.enqueue(encoder.encode(text));
276
+ }
277
+ controller.close();
278
+ },
279
+ });
280
+
281
+ return new Response(readable, {
282
+ headers: { "Content-Type": "text/plain; charset=utf-8" },
283
+ });
284
+ }
285
+ \`\`\`
286
+
287
+ ### Chat UI Component (components/chat.tsx)
288
+ \`\`\`tsx
289
+ "use client";
290
+ import { useState, useRef, useEffect } from "react";
291
+
292
+ interface Message { role: "user" | "assistant"; content: string }
293
+
294
+ export function Chat() {
295
+ const [messages, setMessages] = useState<Message[]>([]);
296
+ const [input, setInput] = useState("");
297
+ const [streaming, setStreaming] = useState(false);
298
+ const scrollRef = useRef<HTMLDivElement>(null);
299
+
300
+ useEffect(() => {
301
+ scrollRef.current?.scrollIntoView({ behavior: "smooth" });
302
+ }, [messages]);
303
+
304
+ async function handleSend() {
305
+ if (!input.trim() || streaming) return;
306
+ const userMsg: Message = { role: "user", content: input };
307
+ setMessages((prev) => [...prev, userMsg]);
308
+ setInput("");
309
+ setStreaming(true);
310
+
311
+ const res = await fetch("/api/chat", {
312
+ method: "POST",
313
+ headers: { "Content-Type": "application/json" },
314
+ body: JSON.stringify({ messages: [...messages, userMsg] }),
315
+ });
316
+
317
+ const reader = res.body?.getReader();
318
+ const decoder = new TextDecoder();
319
+ let assistantContent = "";
320
+
321
+ setMessages((prev) => [...prev, { role: "assistant", content: "" }]);
322
+
323
+ while (reader) {
324
+ const { done, value } = await reader.read();
325
+ if (done) break;
326
+ assistantContent += decoder.decode(value);
327
+ setMessages((prev) => [
328
+ ...prev.slice(0, -1),
329
+ { role: "assistant", content: assistantContent },
330
+ ]);
331
+ }
332
+ setStreaming(false);
333
+ }
334
+
335
+ return (
336
+ <div className="flex flex-col h-full">
337
+ <div className="flex-1 overflow-y-auto p-4 space-y-4">
338
+ {messages.map((m, i) => (
339
+ <div key={i} className={\`flex \${m.role === "user" ? "justify-end" : "justify-start"}\`}>
340
+ <div className={\`max-w-[80%] rounded-lg px-4 py-2 \${
341
+ m.role === "user" ? "bg-primary text-primary-foreground" : "bg-muted"
342
+ }\`}>
343
+ {m.content}
344
+ </div>
345
+ </div>
346
+ ))}
347
+ <div ref={scrollRef} />
348
+ </div>
349
+ <div className="border-t p-4 flex gap-2">
350
+ <input value={input} onChange={(e) => setInput(e.target.value)}
351
+ onKeyDown={(e) => e.key === "Enter" && handleSend()}
352
+ placeholder="Type a message..." className="flex-1 rounded-md border px-3 py-2" />
353
+ <button onClick={handleSend} disabled={streaming}
354
+ className="rounded-md bg-primary px-4 py-2 text-primary-foreground disabled:opacity-50">
355
+ Send
356
+ </button>
357
+ </div>
358
+ </div>
359
+ );
360
+ }
361
+ \`\`\`
362
+
363
+ ### Common Pitfalls
364
+ 1. **Never expose OPENAI_API_KEY to the client.** Always call OpenAI from server-side API routes.
365
+ 2. **Use gpt-4o-mini for most features** unless the user specifically asks for gpt-4o. Cost difference is 10x.
366
+ 3. **Set max_tokens to prevent runaway costs.** Default to 1000 for chat, 2000 for content generation.
367
+ 4. **Handle rate limits gracefully.** Return a user-friendly error, not a raw 429 response.
368
+ 5. **Streaming is the default UX pattern.** Non-streaming feels broken for chat interfaces.
369
+ 6. **Long streaming responses are fine on Mistflow Cloud.** Network I/O (waiting for OpenAI to generate tokens) does NOT count toward CPU time. A 2-minute streaming chat response uses only milliseconds of CPU.
370
+ 7. **Never ask the user to paste OPENAI_API_KEY in chat.** Direct them to set it in the Mistflow dashboard (Project Settings > Environment Variables).`},{id:"anthropic-ai",name:"Anthropic / Claude",category:"ai",prompt:`## Anthropic / Claude Integration
371
+
372
+ ### File Structure
373
+ \`\`\`
374
+ lib/anthropic.ts \u2014 Anthropic client singleton
375
+ app/api/chat/route.ts \u2014 Streaming chat API route
376
+ components/chat.tsx \u2014 Chat UI component with streaming
377
+ \`\`\`
378
+
379
+ ### Client Setup (lib/anthropic.ts)
380
+ \`\`\`typescript
381
+ import Anthropic from "@anthropic-ai/sdk";
382
+
383
+ export const anthropic = new Anthropic({
384
+ apiKey: process.env.ANTHROPIC_API_KEY,
385
+ });
386
+ \`\`\`
387
+
388
+ ### Streaming Chat Route (app/api/chat/route.ts)
389
+ \`\`\`typescript
390
+ import { anthropic } from "@/lib/anthropic";
391
+ import { NextRequest } from "next/server";
392
+
393
+ export async function POST(req: NextRequest) {
394
+ const { messages, systemPrompt } = await req.json();
395
+
396
+ const stream = anthropic.messages.stream({
397
+ model: "claude-sonnet-4-6",
398
+ max_tokens: 1024,
399
+ system: systemPrompt || "You are a helpful assistant.",
400
+ messages,
401
+ });
402
+
403
+ const encoder = new TextEncoder();
404
+ const readable = new ReadableStream({
405
+ async start(controller) {
406
+ for await (const event of stream) {
407
+ if (event.type === "content_block_delta" && event.delta.type === "text_delta") {
408
+ controller.enqueue(encoder.encode(event.delta.text));
409
+ }
410
+ }
411
+ controller.close();
412
+ },
413
+ });
414
+
415
+ return new Response(readable, {
416
+ headers: { "Content-Type": "text/plain; charset=utf-8" },
417
+ });
418
+ }
419
+ \`\`\`
420
+
421
+ ### Chat UI Component (components/chat.tsx)
422
+ Use the same chat component pattern as the OpenAI integration. The client-side code is identical since both stream plain text. Only the API route differs.
423
+
424
+ ### Common Pitfalls
425
+ 1. **Never expose ANTHROPIC_API_KEY to the client.** Always call Anthropic from server-side API routes.
426
+ 2. **Use claude-sonnet-4-6 for most features.** Use claude-haiku-4-5 for high-volume, cost-sensitive tasks. Claude opus is for complex reasoning.
427
+ 3. **Anthropic uses a different message format than OpenAI.** Role is "user" or "assistant" (no "system" role in messages). System prompt is a separate top-level field.
428
+ 4. **Set max_tokens explicitly.** Unlike OpenAI, Anthropic requires max_tokens on every request.
429
+ 5. **Streaming is the default UX pattern.** Use \`anthropic.messages.stream()\` not \`anthropic.messages.create()\` for chat.
430
+ 6. **Long streaming responses are fine on Mistflow Cloud.** Network I/O does NOT count toward CPU time.
431
+ 7. **Never ask the user to paste ANTHROPIC_API_KEY in chat.** Direct them to set it in the Mistflow dashboard (Project Settings > Environment Variables).`},{id:"openrouter-ai",name:"OpenRouter",category:"ai",prompt:`## OpenRouter Integration
432
+
433
+ OpenRouter provides a single API for 200+ AI models (GPT-4o, Claude, Llama, Mistral, Gemini, etc.). It uses an OpenAI-compatible API, so you use the standard OpenAI SDK with a different base URL and API key.
434
+
435
+ ### File Structure
436
+ \`\`\`
437
+ lib/openrouter.ts \u2014 OpenRouter client
438
+ app/api/chat/route.ts \u2014 Streaming chat API route
439
+ components/chat.tsx \u2014 Chat UI component with streaming
440
+ components/model-selector.tsx \u2014 Model picker dropdown
441
+ \`\`\`
442
+
443
+ ### Client Setup (lib/openrouter.ts)
444
+ \`\`\`typescript
445
+ import { OpenRouter } from "@openrouter/sdk";
446
+
447
+ export const openrouter = new OpenRouter({
448
+ apiKey: process.env.OPENROUTER_API_KEY,
449
+ });
450
+
451
+ // Popular models \u2014 use these as defaults or in a model selector
452
+ export const MODELS = {
453
+ fast: "meta-llama/llama-3.1-8b-instruct",
454
+ balanced: "anthropic/claude-sonnet-4-6",
455
+ powerful: "openai/gpt-4o",
456
+ cheap: "meta-llama/llama-3.1-8b-instruct",
457
+ } as const;
458
+ \`\`\`
459
+
460
+ ### Streaming Chat Route (app/api/chat/route.ts)
461
+ \`\`\`typescript
462
+ import { openrouter, MODELS } from "@/lib/openrouter";
463
+ import { NextRequest } from "next/server";
464
+
465
+ export async function POST(req: NextRequest) {
466
+ const { messages, systemPrompt, model } = await req.json();
467
+
468
+ const completion = await openrouter.chat.send({
469
+ model: model || MODELS.balanced,
470
+ messages: [
471
+ { role: "system", content: systemPrompt || "You are a helpful assistant." },
472
+ ...messages,
473
+ ],
474
+ stream: true,
475
+ });
476
+
477
+ const encoder = new TextEncoder();
478
+ const readable = new ReadableStream({
479
+ async start(controller) {
480
+ for await (const chunk of completion) {
481
+ const text = chunk.choices?.[0]?.delta?.content || "";
482
+ if (text) controller.enqueue(encoder.encode(text));
483
+ }
484
+ controller.close();
485
+ },
486
+ });
487
+
488
+ return new Response(readable, {
489
+ headers: { "Content-Type": "text/plain; charset=utf-8" },
490
+ });
491
+ }
492
+ \`\`\`
493
+
494
+ ### Model Selector Component (components/model-selector.tsx)
495
+ \`\`\`tsx
496
+ "use client";
497
+
498
+ const MODELS = [
499
+ { id: "anthropic/claude-sonnet-4-6", label: "Claude Sonnet", tier: "Balanced" },
500
+ { id: "openai/gpt-4o", label: "GPT-4o", tier: "Powerful" },
501
+ { id: "openai/gpt-4o-mini", label: "GPT-4o Mini", tier: "Fast" },
502
+ { id: "meta-llama/llama-3.1-70b-instruct", label: "Llama 3.1 70B", tier: "Open Source" },
503
+ { id: "google/gemini-2.0-flash-001", label: "Gemini 2.0 Flash", tier: "Fast" },
504
+ ];
505
+
506
+ interface ModelSelectorProps {
507
+ value: string;
508
+ onChange: (model: string) => void;
509
+ }
510
+
511
+ export function ModelSelector({ value, onChange }: ModelSelectorProps) {
512
+ return (
513
+ <select
514
+ value={value}
515
+ onChange={(e) => onChange(e.target.value)}
516
+ className="rounded-md border px-2 py-1 text-sm"
517
+ >
518
+ {MODELS.map((m) => (
519
+ <option key={m.id} value={m.id}>
520
+ {m.label} ({m.tier})
521
+ </option>
522
+ ))}
523
+ </select>
524
+ );
525
+ }
526
+ \`\`\`
527
+
528
+ ### Common Pitfalls
529
+ 1. **Never expose OPENROUTER_API_KEY to the client.** Always call OpenRouter from server-side API routes.
530
+ 2. **Model IDs use provider/model format.** E.g. "anthropic/claude-sonnet-4-6", not just "claude-sonnet".
531
+ 3. **Use the \`@openrouter/sdk\` package.** It wraps the OpenRouter API with proper types and streaming support.
532
+ 4. **Set max_tokens explicitly** for Anthropic models routed through OpenRouter. OpenAI models have defaults, Anthropic models don't.
533
+ 5. **Check model pricing at openrouter.ai/models.** Costs vary 100x between models. Show users which model they're using.
534
+ 6. **Long streaming responses are fine on Mistflow Cloud.** Network I/O does NOT count toward CPU time.
535
+ 7. **Never ask the user to paste OPENROUTER_API_KEY in chat.** Direct them to set it in the Mistflow dashboard (Project Settings > Environment Variables).`},{id:"stripe-payments",name:"Stripe Payments",category:"payments",prompt:`## Stripe Payments Integration
536
+
537
+ ### File Structure
538
+ \`\`\`
539
+ lib/stripe.ts \u2014 Stripe server client
540
+ db/schema/subscriptions.ts \u2014 Subscription-related DB schema
541
+ app/api/webhooks/stripe/route.ts \u2014 Webhook handler (critical path)
542
+ app/api/checkout/route.ts \u2014 Create Checkout Session
543
+ app/api/billing-portal/route.ts \u2014 Create Billing Portal session
544
+ app/(dashboard)/pricing/page.tsx \u2014 Pricing page
545
+ app/(dashboard)/billing/page.tsx \u2014 Billing/subscription management
546
+ \`\`\`
547
+
548
+ ### Server Client (lib/stripe.ts)
549
+ \`\`\`typescript
550
+ import Stripe from "stripe";
551
+
552
+ export const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);
553
+ \`\`\`
554
+
555
+ ### Webhook Handler (app/api/webhooks/stripe/route.ts)
556
+ This is the most critical file. All subscription state changes flow through here.
557
+ \`\`\`typescript
558
+ import { NextRequest, NextResponse } from "next/server";
559
+ import { stripe } from "@/lib/stripe";
560
+ import { headers } from "next/headers";
561
+
562
+ export async function POST(req: NextRequest) {
563
+ const body = await req.text();
564
+ const headersList = await headers();
565
+ const signature = headersList.get("stripe-signature")!;
566
+
567
+ let event: Stripe.Event;
568
+ try {
569
+ event = stripe.webhooks.constructEvent(body, signature, process.env.STRIPE_WEBHOOK_SECRET!);
570
+ } catch (err) {
571
+ return NextResponse.json({ error: "Invalid signature" }, { status: 400 });
572
+ }
573
+
574
+ switch (event.type) {
575
+ case "checkout.session.completed": {
576
+ const session = event.data.object as Stripe.Checkout.Session;
577
+ // Create/update subscription record in DB
578
+ // Link session.customer to your user via session.metadata.userId
579
+ break;
580
+ }
581
+ case "customer.subscription.updated":
582
+ case "customer.subscription.deleted": {
583
+ const sub = event.data.object as Stripe.Subscription;
584
+ // Update subscription status in DB (active, canceled, past_due)
585
+ break;
586
+ }
587
+ case "invoice.payment_failed": {
588
+ // Notify user about failed payment
589
+ break;
590
+ }
591
+ }
592
+
593
+ return NextResponse.json({ received: true });
594
+ }
595
+ \`\`\`
596
+
597
+ ### Checkout Route (app/api/checkout/route.ts)
598
+ \`\`\`typescript
599
+ import { stripe } from "@/lib/stripe";
600
+ import { NextRequest, NextResponse } from "next/server";
601
+
602
+ export async function POST(req: NextRequest) {
603
+ const { priceId, userId } = await req.json();
604
+
605
+ const session = await stripe.checkout.sessions.create({
606
+ mode: "subscription",
607
+ payment_method_types: ["card"],
608
+ line_items: [{ price: priceId, quantity: 1 }],
609
+ success_url: \`\${process.env.NEXT_PUBLIC_APP_URL}/billing?success=true\`,
610
+ cancel_url: \`\${process.env.NEXT_PUBLIC_APP_URL}/pricing\`,
611
+ metadata: { userId },
612
+ });
613
+
614
+ return NextResponse.json({ url: session.url });
615
+ }
616
+ \`\`\`
617
+
618
+ ### Common Pitfalls
619
+ 1. **Webhook handler must use raw body (req.text(), not req.json())** for signature verification.
620
+ 2. **Always verify webhook signatures.** Never trust unverified payloads.
621
+ 3. **Idempotency: webhooks can fire multiple times.** Use event.id to deduplicate.
622
+ 4. **Store the Stripe customer ID in your user table.** Look up by metadata.userId in webhooks.
623
+ 5. **Test with Stripe CLI locally:** \`stripe listen --forward-to localhost:3000/api/webhooks/stripe\`
624
+ 6. **Price IDs come from Stripe Dashboard.** Do not hardcode amounts. Create Products + Prices in the dashboard and reference price IDs.
625
+ 7. **Billing portal for self-service.** Use stripe.billingPortal.sessions.create() so users can manage their own subscriptions.
626
+ 8. **Never ask the user to paste Stripe keys in chat.** Direct them to set STRIPE_SECRET_KEY, STRIPE_PUBLISHABLE_KEY, and STRIPE_WEBHOOK_SECRET in the Mistflow dashboard (Project Settings > Environment Variables).`},{id:"elevenlabs-voice",name:"ElevenLabs Voice",category:"media",prompt:`## ElevenLabs Voice Integration
627
+
628
+ ### File Structure
629
+ \`\`\`
630
+ lib/elevenlabs.ts \u2014 ElevenLabs client + helpers
631
+ app/api/tts/route.ts \u2014 Text-to-speech API route (streaming)
632
+ app/api/voices/route.ts \u2014 List available voices
633
+ components/voice-player.tsx \u2014 Audio player with playback controls
634
+ components/voice-selector.tsx \u2014 Voice picker dropdown
635
+ \`\`\`
636
+
637
+ ### Client Setup (lib/elevenlabs.ts)
638
+ \`\`\`typescript
639
+ import { ElevenLabsClient } from "elevenlabs";
640
+
641
+ export const elevenlabs = new ElevenLabsClient({
642
+ apiKey: process.env.ELEVENLABS_API_KEY,
643
+ });
644
+
645
+ // Default voice ID \u2014 "Rachel" is a good general-purpose voice
646
+ export const DEFAULT_VOICE_ID = "21m00Tcm4TlvDq8ikWAM";
647
+ \`\`\`
648
+
649
+ ### Text-to-Speech Route (app/api/tts/route.ts)
650
+ \`\`\`typescript
651
+ import { elevenlabs, DEFAULT_VOICE_ID } from "@/lib/elevenlabs";
652
+ import { NextRequest, NextResponse } from "next/server";
653
+
654
+ export async function POST(req: NextRequest) {
655
+ const { text, voiceId } = await req.json();
656
+
657
+ if (!text || text.length > 5000) {
658
+ return NextResponse.json({ error: "Text is required (max 5000 chars)" }, { status: 400 });
659
+ }
660
+
661
+ const audioStream = await elevenlabs.textToSpeech.stream(
662
+ voiceId || DEFAULT_VOICE_ID,
663
+ {
664
+ text,
665
+ modelId: "eleven_multilingual_v2",
666
+ }
667
+ );
668
+
669
+ // Pipe the ElevenLabs stream directly to the client response.
670
+ // Network wait (ElevenLabs generating audio) does NOT count as CPU time
671
+ // on Mistflow Cloud, so even long text is fine.
672
+ const readable = new ReadableStream({
673
+ async start(controller) {
674
+ for await (const chunk of audioStream) {
675
+ controller.enqueue(new Uint8Array(chunk));
676
+ }
677
+ controller.close();
678
+ },
679
+ });
680
+
681
+ return new NextResponse(readable, {
682
+ headers: {
683
+ "Content-Type": "audio/mpeg",
684
+ "Transfer-Encoding": "chunked",
685
+ },
686
+ });
687
+ }
688
+ \`\`\`
689
+
690
+ ### Voice Player Component (components/voice-player.tsx)
691
+ \`\`\`tsx
692
+ "use client";
693
+ import { useState, useRef } from "react";
694
+
695
+ interface VoicePlayerProps {
696
+ text: string;
697
+ voiceId?: string;
698
+ }
699
+
700
+ export function VoicePlayer({ text, voiceId }: VoicePlayerProps) {
701
+ const [loading, setLoading] = useState(false);
702
+ const [playing, setPlaying] = useState(false);
703
+ const audioRef = useRef<HTMLAudioElement | null>(null);
704
+
705
+ async function handlePlay() {
706
+ if (playing && audioRef.current) {
707
+ audioRef.current.pause();
708
+ setPlaying(false);
709
+ return;
710
+ }
711
+
712
+ setLoading(true);
713
+ try {
714
+ const res = await fetch("/api/tts", {
715
+ method: "POST",
716
+ headers: { "Content-Type": "application/json" },
717
+ body: JSON.stringify({ text, voiceId }),
718
+ });
719
+ const blob = await res.blob();
720
+ const url = URL.createObjectURL(blob);
721
+ const audio = new Audio(url);
722
+ audioRef.current = audio;
723
+ audio.onended = () => setPlaying(false);
724
+ audio.play();
725
+ setPlaying(true);
726
+ } finally {
727
+ setLoading(false);
728
+ }
729
+ }
730
+
731
+ return (
732
+ <button onClick={handlePlay} disabled={loading}
733
+ className="inline-flex items-center gap-2 rounded-md bg-primary px-3 py-1.5 text-sm text-primary-foreground disabled:opacity-50">
734
+ {loading ? "Generating..." : playing ? "Pause" : "Listen"}
735
+ </button>
736
+ );
737
+ }
738
+ \`\`\`
739
+
740
+ ### Voice Listing Route (app/api/voices/route.ts)
741
+ \`\`\`typescript
742
+ import { elevenlabs } from "@/lib/elevenlabs";
743
+ import { NextResponse } from "next/server";
744
+
745
+ export async function GET() {
746
+ const voices = await elevenlabs.voices.getAll();
747
+ return NextResponse.json(
748
+ voices.voices.map((v) => ({ id: v.voice_id, name: v.name, category: v.category }))
749
+ );
750
+ }
751
+ \`\`\`
752
+
753
+ ### Common Pitfalls
754
+ 1. **Never expose ELEVENLABS_API_KEY to the client.** All TTS calls go through your API route.
755
+ 2. **Character limits matter for cost.** ElevenLabs charges per character. Show character count in UI and enforce limits.
756
+ 3. **Use eleven_multilingual_v2 model** for best quality across languages. Use eleven_flash_v2_5 for ultra-low latency.
757
+ 4. **Cache generated audio** when the same text + voice combination is requested multiple times. Store in Mistflow Cloud storage or local cache.
758
+ 5. **Voice cloning requires explicit user consent.** If building a voice cloning feature, add consent UI.
759
+ 6. **Free tier: 10,000 characters/month.** Display remaining quota to users to avoid surprise failures.
760
+ 7. **Long text is fine on Mistflow Cloud.** Network I/O (waiting for ElevenLabs to generate audio) does NOT count toward CPU time. Stream the response directly to the client for instant playback start.
761
+ 8. **Never ask the user to paste ELEVENLABS_API_KEY in chat.** Direct them to set it in the Mistflow dashboard (Project Settings > Environment Variables).`},{id:"google-maps",name:"Google Maps",category:"location",prompt:`## Google Maps Integration
762
+
763
+ ### File Structure
764
+ \`\`\`
765
+ lib/maps.ts \u2014 Maps loader + helper functions
766
+ components/map.tsx \u2014 Map embed component
767
+ components/places-autocomplete.tsx \u2014 Address search with autocomplete
768
+ \`\`\`
769
+
770
+ ### Map Component (components/map.tsx)
771
+ \`\`\`tsx
772
+ "use client";
773
+ import { useEffect, useRef, useState } from "react";
774
+ import { Loader } from "@googlemaps/js-api-loader";
775
+
776
+ interface MapProps {
777
+ center?: { lat: number; lng: number };
778
+ zoom?: number;
779
+ markers?: { lat: number; lng: number; title?: string }[];
780
+ className?: string;
781
+ onMarkerClick?: (marker: { lat: number; lng: number }) => void;
782
+ }
783
+
784
+ export function Map({ center = { lat: 40.7128, lng: -74.006 }, zoom = 12, markers = [], className, onMarkerClick }: MapProps) {
785
+ const mapRef = useRef<HTMLDivElement>(null);
786
+ const [map, setMap] = useState<google.maps.Map | null>(null);
787
+
788
+ useEffect(() => {
789
+ const loader = new Loader({
790
+ apiKey: process.env.NEXT_PUBLIC_GOOGLE_MAPS_API_KEY!,
791
+ version: "weekly",
792
+ libraries: ["places"],
793
+ });
794
+
795
+ loader.load().then(() => {
796
+ if (!mapRef.current) return;
797
+ const m = new google.maps.Map(mapRef.current, { center, zoom, mapId: "DEMO_MAP_ID" });
798
+ setMap(m);
799
+
800
+ for (const marker of markers) {
801
+ const m2 = new google.maps.marker.AdvancedMarkerElement({
802
+ map: m, position: marker, title: marker.title,
803
+ });
804
+ if (onMarkerClick) {
805
+ m2.addListener("click", () => onMarkerClick(marker));
806
+ }
807
+ }
808
+ });
809
+ }, []);
810
+
811
+ return <div ref={mapRef} className={className ?? "w-full h-[400px] rounded-lg"} />;
812
+ }
813
+ \`\`\`
814
+
815
+ ### Places Autocomplete (components/places-autocomplete.tsx)
816
+ \`\`\`tsx
817
+ "use client";
818
+ import { useEffect, useRef, useState } from "react";
819
+ import { Loader } from "@googlemaps/js-api-loader";
820
+
821
+ interface PlacesAutocompleteProps {
822
+ onSelect: (place: { address: string; lat: number; lng: number }) => void;
823
+ placeholder?: string;
824
+ }
825
+
826
+ export function PlacesAutocomplete({ onSelect, placeholder = "Search for an address..." }: PlacesAutocompleteProps) {
827
+ const inputRef = useRef<HTMLInputElement>(null);
828
+
829
+ useEffect(() => {
830
+ const loader = new Loader({
831
+ apiKey: process.env.NEXT_PUBLIC_GOOGLE_MAPS_API_KEY!,
832
+ version: "weekly",
833
+ libraries: ["places"],
834
+ });
835
+
836
+ loader.load().then(() => {
837
+ if (!inputRef.current) return;
838
+ const autocomplete = new google.maps.places.Autocomplete(inputRef.current, {
839
+ fields: ["formatted_address", "geometry"],
840
+ });
841
+ autocomplete.addListener("place_changed", () => {
842
+ const place = autocomplete.getPlace();
843
+ if (place.geometry?.location) {
844
+ onSelect({
845
+ address: place.formatted_address ?? "",
846
+ lat: place.geometry.location.lat(),
847
+ lng: place.geometry.location.lng(),
848
+ });
849
+ }
850
+ });
851
+ });
852
+ }, [onSelect]);
853
+
854
+ return <input ref={inputRef} placeholder={placeholder}
855
+ className="w-full rounded-md border px-3 py-2" />;
856
+ }
857
+ \`\`\`
858
+
859
+ ### Common Pitfalls
860
+ 1. **API key must be restricted in Google Cloud Console.** Restrict to your domain and specific APIs (Maps JS, Places).
861
+ 2. **Use NEXT_PUBLIC_ prefix** so the key is available client-side. This is expected for Maps JS API.
862
+ 3. **Enable the Maps JavaScript API AND Places API** in Google Cloud Console. Both are separate.
863
+ 4. **AdvancedMarkerElement requires a mapId.** Create one in Cloud Console or use "DEMO_MAP_ID" for development.
864
+ 5. **Billing is required.** Google Maps gives $200/month free credit (~28,000 map loads). Add billing alerts.
865
+ 6. **Never ask the user to paste NEXT_PUBLIC_GOOGLE_MAPS_API_KEY in chat.** Direct them to set it in the Mistflow dashboard (Project Settings > Environment Variables).`},{id:"twilio-sms",name:"Twilio SMS",category:"communication",prompt:`## Twilio SMS Integration
866
+
867
+ ### File Structure
868
+ \`\`\`
869
+ lib/twilio.ts \u2014 Twilio client singleton
870
+ lib/sms.ts \u2014 Send helper functions
871
+ app/api/sms/send/route.ts \u2014 Send SMS API route
872
+ app/api/webhooks/twilio/route.ts \u2014 Incoming SMS + delivery webhooks
873
+ \`\`\`
874
+
875
+ ### Client Setup (lib/twilio.ts)
876
+ \`\`\`typescript
877
+ import twilio from "twilio";
878
+
879
+ export const twilioClient = twilio(
880
+ process.env.TWILIO_ACCOUNT_SID,
881
+ process.env.TWILIO_AUTH_TOKEN
882
+ );
883
+ \`\`\`
884
+
885
+ ### Send Helpers (lib/sms.ts)
886
+ \`\`\`typescript
887
+ import { twilioClient } from "./twilio";
888
+
889
+ export async function sendSMS(to: string, body: string) {
890
+ return twilioClient.messages.create({
891
+ to,
892
+ from: process.env.TWILIO_PHONE_NUMBER,
893
+ body,
894
+ });
895
+ }
896
+
897
+ export async function sendOTP(to: string, code: string) {
898
+ return sendSMS(to, \`Your verification code is: \${code}. It expires in 10 minutes.\`);
899
+ }
900
+ \`\`\`
901
+
902
+ ### Send Route (app/api/sms/send/route.ts)
903
+ \`\`\`typescript
904
+ import { sendSMS } from "@/lib/sms";
905
+ import { NextRequest, NextResponse } from "next/server";
906
+
907
+ export async function POST(req: NextRequest) {
908
+ const { to, message } = await req.json();
909
+
910
+ // Validate phone number format (E.164)
911
+ if (!/^\\+[1-9]\\d{1,14}$/.test(to)) {
912
+ return NextResponse.json({ error: "Invalid phone number. Use E.164 format (+1234567890)" }, { status: 400 });
913
+ }
914
+
915
+ const result = await sendSMS(to, message);
916
+ return NextResponse.json({ sid: result.sid, status: result.status });
917
+ }
918
+ \`\`\`
919
+
920
+ ### Webhook Handler (app/api/webhooks/twilio/route.ts)
921
+ \`\`\`typescript
922
+ import { NextRequest, NextResponse } from "next/server";
923
+
924
+ export async function POST(req: NextRequest) {
925
+ const formData = await req.formData();
926
+ const from = formData.get("From") as string;
927
+ const body = formData.get("Body") as string;
928
+ const status = formData.get("MessageStatus") as string;
929
+
930
+ if (body) {
931
+ // Incoming SMS \u2014 handle auto-replies, keyword routing, etc.
932
+ console.log(\`SMS from \${from}: \${body}\`);
933
+ } else if (status) {
934
+ // Delivery status update \u2014 delivered, failed, undelivered
935
+ console.log(\`Message status: \${status}\`);
936
+ }
937
+
938
+ // Return TwiML response (empty = no auto-reply)
939
+ return new NextResponse('<?xml version="1.0" encoding="UTF-8"?><Response></Response>', {
940
+ headers: { "Content-Type": "text/xml" },
941
+ });
942
+ }
943
+ \`\`\`
944
+
945
+ ### Common Pitfalls
946
+ 1. **Phone numbers must be in E.164 format** (+1234567890). Validate before sending.
947
+ 2. **Trial accounts can only send to verified numbers.** Upgrade for production use.
948
+ 3. **Twilio webhook validation.** Use twilio.webhook() middleware to verify webhook signatures in production.
949
+ 4. **SMS rate limits vary by country.** US: 1 SMS/second per number. Use Messaging Services for higher throughput.
950
+ 5. **Never log full phone numbers.** Mask them in logs (e.g., +1***567890).
951
+ 6. **Never ask the user to paste Twilio credentials in chat.** Direct them to set TWILIO_ACCOUNT_SID, TWILIO_AUTH_TOKEN, and TWILIO_PHONE_NUMBER in the Mistflow dashboard (Project Settings > Environment Variables).`},{id:"posthog-analytics",name:"PostHog Analytics",category:"analytics",prompt:`## PostHog Analytics Integration
952
+
953
+ ### File Structure
954
+ \`\`\`
955
+ lib/posthog.ts \u2014 PostHog provider setup
956
+ app/providers.tsx \u2014 Client-side providers wrapper
957
+ components/posthog-pageview.tsx \u2014 Page view tracker component
958
+ lib/posthog-server.ts \u2014 Server-side PostHog client
959
+ \`\`\`
960
+
961
+ ### Client Provider (lib/posthog.ts)
962
+ \`\`\`typescript
963
+ "use client";
964
+ import posthog from "posthog-js";
965
+ import { PostHogProvider as PHProvider } from "posthog-js/react";
966
+ import { useEffect } from "react";
967
+
968
+ export function PostHogProvider({ children }: { children: React.ReactNode }) {
969
+ useEffect(() => {
970
+ posthog.init(process.env.NEXT_PUBLIC_POSTHOG_KEY!, {
971
+ api_host: process.env.NEXT_PUBLIC_POSTHOG_HOST || "https://us.i.posthog.com",
972
+ person_profiles: "identified_only",
973
+ capture_pageview: false, // We handle this manually for Next.js
974
+ });
975
+ }, []);
976
+
977
+ return <PHProvider client={posthog}>{children}</PHProvider>;
978
+ }
979
+ \`\`\`
980
+
981
+ ### Page View Tracker (components/posthog-pageview.tsx)
982
+ \`\`\`tsx
983
+ "use client";
984
+ import { usePathname, useSearchParams } from "next/navigation";
985
+ import { useEffect } from "react";
986
+ import { usePostHog } from "posthog-js/react";
987
+
988
+ export function PostHogPageview() {
989
+ const pathname = usePathname();
990
+ const searchParams = useSearchParams();
991
+ const posthog = usePostHog();
992
+
993
+ useEffect(() => {
994
+ if (pathname && posthog) {
995
+ let url = window.origin + pathname;
996
+ if (searchParams.toString()) url += "?" + searchParams.toString();
997
+ posthog.capture("$pageview", { "$current_url": url });
998
+ }
999
+ }, [pathname, searchParams, posthog]);
1000
+
1001
+ return null;
1002
+ }
1003
+ \`\`\`
1004
+
1005
+ ### App Providers (app/providers.tsx)
1006
+ \`\`\`tsx
1007
+ "use client";
1008
+ import { PostHogProvider } from "@/lib/posthog";
1009
+ import { PostHogPageview } from "@/components/posthog-pageview";
1010
+ import { Suspense } from "react";
1011
+
1012
+ export function Providers({ children }: { children: React.ReactNode }) {
1013
+ return (
1014
+ <PostHogProvider>
1015
+ <Suspense fallback={null}>
1016
+ <PostHogPageview />
1017
+ </Suspense>
1018
+ {children}
1019
+ </PostHogProvider>
1020
+ );
1021
+ }
1022
+ \`\`\`
1023
+
1024
+ ### Server-side Client (lib/posthog-server.ts)
1025
+ \`\`\`typescript
1026
+ import { PostHog } from "posthog-node";
1027
+
1028
+ export const posthogServer = new PostHog(process.env.NEXT_PUBLIC_POSTHOG_KEY!, {
1029
+ host: process.env.NEXT_PUBLIC_POSTHOG_HOST || "https://us.i.posthog.com",
1030
+ });
1031
+
1032
+ // Identify user after auth
1033
+ export function identifyUser(userId: string, properties?: Record<string, unknown>) {
1034
+ posthogServer.identify({ distinctId: userId, properties });
1035
+ }
1036
+
1037
+ // Track server-side events
1038
+ export function trackEvent(userId: string, event: string, properties?: Record<string, unknown>) {
1039
+ posthogServer.capture({ distinctId: userId, event, properties });
1040
+ }
1041
+ \`\`\`
1042
+
1043
+ ### Feature Flags Usage
1044
+ \`\`\`tsx
1045
+ "use client";
1046
+ import { useFeatureFlagEnabled } from "posthog-js/react";
1047
+
1048
+ export function NewFeature() {
1049
+ const isEnabled = useFeatureFlagEnabled("new-dashboard");
1050
+ if (!isEnabled) return null;
1051
+ return <div>New Dashboard Feature</div>;
1052
+ }
1053
+ \`\`\`
1054
+
1055
+ ### Common Pitfalls
1056
+ 1. **Wrap PostHogPageview in Suspense.** useSearchParams() requires a Suspense boundary in Next.js App Router.
1057
+ 2. **Set capture_pageview: false in init.** Next.js client-side navigation needs manual pageview tracking.
1058
+ 3. **Use person_profiles: "identified_only"** to save on events. Anonymous events are cheaper.
1059
+ 4. **Call posthog.identify() after login** with the user's ID to link sessions.
1060
+ 5. **Server-side: always call posthogServer.shutdown()** in serverless environments, or events may be lost.
1061
+ 6. **Never ask the user to paste PostHog keys in chat.** Direct them to set NEXT_PUBLIC_POSTHOG_KEY and NEXT_PUBLIC_POSTHOG_HOST in the Mistflow dashboard (Project Settings > Environment Variables).`},{id:"firecrawl-scraping",name:"Firecrawl Web Scraping",category:"scraping",prompt:`## Firecrawl Web Scraping Integration
1062
+
1063
+ Firecrawl handles all scraping on their servers. Your Worker just makes HTTP API calls. Single scrapes complete in 1-5 seconds. For multi-page crawls, use async mode with webhooks.
1064
+
1065
+ ### File Structure
1066
+ \`\`\`
1067
+ lib/firecrawl.ts \u2014 Firecrawl client singleton
1068
+ app/api/scrape/route.ts \u2014 Single URL scrape endpoint
1069
+ app/api/crawl/start/route.ts \u2014 Start async crawl (returns job ID)
1070
+ app/api/crawl/[id]/route.ts \u2014 Poll crawl status
1071
+ app/api/firecrawl-webhook/route.ts \u2014 Webhook for async crawl results
1072
+ \`\`\`
1073
+
1074
+ ### Client Setup (lib/firecrawl.ts)
1075
+ \`\`\`typescript
1076
+ import FirecrawlApp from "@mendable/firecrawl-js";
1077
+
1078
+ export const firecrawl = new FirecrawlApp({
1079
+ apiKey: process.env.FIRECRAWL_API_KEY!,
1080
+ });
1081
+ \`\`\`
1082
+
1083
+ ### Scrape Single URL (app/api/scrape/route.ts)
1084
+ Single scrapes complete in 1-5 seconds. Network I/O does NOT count as CPU time on Workers, so this is safe.
1085
+ \`\`\`typescript
1086
+ import { firecrawl } from "@/lib/firecrawl";
1087
+ import { NextRequest, NextResponse } from "next/server";
1088
+
1089
+ export async function POST(req: NextRequest) {
1090
+ const { url } = await req.json();
1091
+
1092
+ if (!url || typeof url !== "string") {
1093
+ return NextResponse.json({ error: "URL is required" }, { status: 400 });
1094
+ }
1095
+
1096
+ const result = await firecrawl.scrapeUrl(url, {
1097
+ formats: ["markdown"],
1098
+ onlyMainContent: true,
1099
+ timeout: 20000,
1100
+ });
1101
+
1102
+ if (!result.success) {
1103
+ return NextResponse.json({ error: "Scrape failed" }, { status: 500 });
1104
+ }
1105
+
1106
+ return NextResponse.json({
1107
+ markdown: result.markdown,
1108
+ title: result.metadata?.title,
1109
+ description: result.metadata?.description,
1110
+ sourceUrl: result.metadata?.sourceURL,
1111
+ });
1112
+ }
1113
+ \`\`\`
1114
+
1115
+ ### Start Async Crawl (app/api/crawl/start/route.ts)
1116
+ For multi-page crawls, ALWAYS use async mode with a webhook. Never use synchronous crawlUrl().
1117
+ \`\`\`typescript
1118
+ import { firecrawl } from "@/lib/firecrawl";
1119
+ import { NextRequest, NextResponse } from "next/server";
1120
+
1121
+ export async function POST(req: NextRequest) {
1122
+ const { url, maxPages = 50 } = await req.json();
1123
+
1124
+ const result = await firecrawl.asyncCrawlUrl(url, {
1125
+ limit: maxPages,
1126
+ maxDepth: 3,
1127
+ scrapeOptions: { formats: ["markdown"], onlyMainContent: true },
1128
+ webhook: {
1129
+ url: \`\${process.env.NEXT_PUBLIC_APP_URL}/api/firecrawl-webhook\`,
1130
+ },
1131
+ });
1132
+
1133
+ // Store crawl job ID in your DB for status tracking
1134
+ return NextResponse.json({ crawlId: result.id, status: "started" });
1135
+ }
1136
+ \`\`\`
1137
+
1138
+ ### Poll Crawl Status (app/api/crawl/[id]/route.ts)
1139
+ \`\`\`typescript
1140
+ import { firecrawl } from "@/lib/firecrawl";
1141
+ import { NextRequest, NextResponse } from "next/server";
1142
+
1143
+ export async function GET(
1144
+ req: NextRequest,
1145
+ { params }: { params: Promise<{ id: string }> }
1146
+ ) {
1147
+ const { id } = await params;
1148
+ const status = await firecrawl.checkCrawlStatus(id);
1149
+
1150
+ return NextResponse.json({
1151
+ status: status.status,
1152
+ completed: status.completed,
1153
+ total: status.total,
1154
+ creditsUsed: status.creditsUsed,
1155
+ pages: status.data?.map((page: { markdown?: string; metadata?: { title?: string; sourceURL?: string } }) => ({
1156
+ title: page.metadata?.title,
1157
+ url: page.metadata?.sourceURL,
1158
+ markdownLength: page.markdown?.length ?? 0,
1159
+ })),
1160
+ });
1161
+ }
1162
+ \`\`\`
1163
+
1164
+ ### Webhook Handler (app/api/firecrawl-webhook/route.ts)
1165
+ \`\`\`typescript
1166
+ import { NextRequest, NextResponse } from "next/server";
1167
+
1168
+ interface FirecrawlWebhookPayload {
1169
+ success: boolean;
1170
+ type: "crawl.started" | "crawl.page" | "crawl.completed" | "crawl.failed";
1171
+ id: string;
1172
+ data?: { markdown?: string; metadata?: { title?: string; sourceURL?: string } }[];
1173
+ error?: string;
1174
+ }
1175
+
1176
+ export async function POST(req: NextRequest) {
1177
+ const payload: FirecrawlWebhookPayload = await req.json();
1178
+
1179
+ switch (payload.type) {
1180
+ case "crawl.page":
1181
+ if (payload.data) {
1182
+ for (const page of payload.data) {
1183
+ // Store page in your database or vector store
1184
+ // page.markdown is clean, LLM-ready content
1185
+ // page.metadata.title, page.metadata.sourceURL for reference
1186
+ await storePage({
1187
+ crawlId: payload.id,
1188
+ url: page.metadata?.sourceURL ?? "",
1189
+ title: page.metadata?.title ?? "",
1190
+ markdown: page.markdown ?? "",
1191
+ });
1192
+ }
1193
+ }
1194
+ break;
1195
+ case "crawl.completed":
1196
+ // Mark crawl as done in your DB
1197
+ break;
1198
+ case "crawl.failed":
1199
+ // Log error: payload.error
1200
+ break;
1201
+ }
1202
+
1203
+ return NextResponse.json({ received: true });
1204
+ }
1205
+
1206
+ async function storePage(page: { crawlId: string; url: string; title: string; markdown: string }) {
1207
+ // TODO: Insert into your database
1208
+ // For RAG pipelines: chunk the markdown and generate embeddings
1209
+ }
1210
+ \`\`\`
1211
+
1212
+ ### Extract Structured Data (with Zod schema)
1213
+ \`\`\`typescript
1214
+ import { z } from "zod";
1215
+ import { firecrawl } from "@/lib/firecrawl";
1216
+
1217
+ const ProductSchema = z.object({
1218
+ name: z.string(),
1219
+ price: z.number(),
1220
+ description: z.string(),
1221
+ inStock: z.boolean(),
1222
+ });
1223
+
1224
+ const result = await firecrawl.extract(
1225
+ ["https://shop.example.com/products/*"],
1226
+ {
1227
+ schema: ProductSchema,
1228
+ prompt: "Extract product details from each product page",
1229
+ }
1230
+ );
1231
+ // result.data is typed as { name: string, price: number, ... }
1232
+ \`\`\`
1233
+
1234
+ ### Map a Website (discover URLs before crawling)
1235
+ \`\`\`typescript
1236
+ import { firecrawl } from "@/lib/firecrawl";
1237
+
1238
+ // Discover all URLs on a site (1 credit flat, regardless of URL count)
1239
+ const map = await firecrawl.mapUrl("https://docs.example.com", {
1240
+ search: "API reference",
1241
+ limit: 200,
1242
+ });
1243
+ // map.links: Array<{ url: string, title?: string }>
1244
+
1245
+ // Then selectively scrape only relevant URLs
1246
+ const relevantUrls = map.links.slice(0, 20).map((l: { url: string }) => l.url);
1247
+ \`\`\`
1248
+
1249
+ ### RAG Pipeline Pattern (Scrape -> Chunk -> Embed)
1250
+ Firecrawl's markdown output is already chunking-friendly. Headings are natural semantic boundaries.
1251
+ \`\`\`typescript
1252
+ function chunkMarkdown(markdown: string, maxSize = 1500): string[] {
1253
+ const sections = markdown.split(/\\n(?=#{1,3} )/);
1254
+ const chunks: string[] = [];
1255
+
1256
+ for (const section of sections) {
1257
+ if (section.length <= maxSize) {
1258
+ chunks.push(section.trim());
1259
+ } else {
1260
+ const paragraphs = section.split(/\\n\\n+/);
1261
+ let current = "";
1262
+ for (const para of paragraphs) {
1263
+ if (current.length + para.length > maxSize) {
1264
+ if (current) chunks.push(current.trim());
1265
+ current = para;
1266
+ } else {
1267
+ current += "\\n\\n" + para;
1268
+ }
1269
+ }
1270
+ if (current) chunks.push(current.trim());
1271
+ }
1272
+ }
1273
+
1274
+ return chunks.filter((c) => c.length > 50);
1275
+ }
1276
+
1277
+ // Usage: scrape a URL and prepare chunks for embedding
1278
+ const result = await firecrawl.scrapeUrl("https://docs.example.com/guide", {
1279
+ formats: ["markdown"],
1280
+ onlyMainContent: true,
1281
+ });
1282
+ const chunks = chunkMarkdown(result.markdown ?? "");
1283
+ // Now embed each chunk with OpenAI embeddings and store in your vector DB
1284
+ \`\`\`
1285
+
1286
+ ### Common Pitfalls
1287
+ 1. **Always use asyncCrawlUrl() for multi-page crawls, never crawlUrl().** The synchronous version polls until completion and will hold your Worker request open unnecessarily.
1288
+ 2. **Map first, crawl selectively.** mapUrl() costs 1 credit flat. Do not waste credits crawling irrelevant pages.
1289
+ 3. **onlyMainContent: true is the default.** It strips nav, footer, and ads. Set to false only if you need the full page structure.
1290
+ 4. **Crawl results expire in 24 hours.** Store them in your database before the expiry.
1291
+ 5. **Extract mode costs 5 credits per page** (1 scrape + 4 for JSON extraction). Use sparingly.
1292
+ 6. **Markdown is the right format for AI apps.** It strips boilerplate, preserves structure, and is what LLMs expect.
1293
+ 7. **Free tier: 500 credits (one-time).** Hobby plan is $16/month for 3,000 credits.
1294
+ 8. **Network I/O does NOT count as CPU time.** Single scrapes (1-5s of network wait) are safe on Workers.
1295
+ 9. **Never ask the user to paste FIRECRAWL_API_KEY in chat.** Direct them to set it in the Mistflow dashboard (Project Settings > Environment Variables).`},{id:"replicate-media",name:"Replicate (Image/Video Gen)",category:"media",prompt:`## Replicate Integration (Image and Video Generation)
1296
+
1297
+ Replicate is a model marketplace. One API key, 200+ models. Use it for image generation (Flux, SDXL), video generation (Wan, Runway), and other media tasks. All heavy computation runs on Replicate's GPUs. Your Worker just makes API calls.
1298
+
1299
+ ### File Structure
1300
+ \`\`\`
1301
+ lib/replicate.ts \u2014 Replicate client singleton + model helpers
1302
+ app/api/generate-image/route.ts \u2014 Image generation endpoint
1303
+ app/api/generate-video/route.ts \u2014 Video generation endpoint (async with polling)
1304
+ components/image-generator.tsx \u2014 Image generation UI
1305
+ \`\`\`
1306
+
1307
+ ### Client Setup (lib/replicate.ts)
1308
+ \`\`\`typescript
1309
+ import Replicate from "replicate";
1310
+
1311
+ export const replicate = new Replicate({
1312
+ auth: process.env.REPLICATE_API_TOKEN,
1313
+ });
1314
+
1315
+ // Recommended models \u2014 change these to use different models
1316
+ export const MODELS = {
1317
+ imageFast: "black-forest-labs/flux-schnell" as const,
1318
+ imageQuality: "black-forest-labs/flux-1.1-pro" as const,
1319
+ videoFast: "wan-video/wan-2.2-i2v-fast" as const,
1320
+ } as const;
1321
+ \`\`\`
1322
+
1323
+ ### Image Generation Route (app/api/generate-image/route.ts)
1324
+ Image generation typically completes in 2-10 seconds. Safe for Workers.
1325
+ \`\`\`typescript
1326
+ import { replicate, MODELS } from "@/lib/replicate";
1327
+ import { NextRequest, NextResponse } from "next/server";
1328
+
1329
+ export async function POST(req: NextRequest) {
1330
+ const { prompt, model } = await req.json();
1331
+
1332
+ if (!prompt || typeof prompt !== "string") {
1333
+ return NextResponse.json({ error: "Prompt is required" }, { status: 400 });
1334
+ }
1335
+
1336
+ const output = await replicate.run(model || MODELS.imageQuality, {
1337
+ input: {
1338
+ prompt,
1339
+ aspect_ratio: "1:1",
1340
+ output_format: "webp",
1341
+ },
1342
+ });
1343
+
1344
+ // Output shape depends on the model. Most image models return a URL string or array of URLs.
1345
+ const imageUrl = Array.isArray(output) ? output[0] : output;
1346
+
1347
+ return NextResponse.json({ imageUrl });
1348
+ }
1349
+ \`\`\`
1350
+
1351
+ ### Video Generation Route (app/api/generate-video/route.ts)
1352
+ Video generation takes 30s-5min. Use async predictions with polling.
1353
+ \`\`\`typescript
1354
+ import { replicate, MODELS } from "@/lib/replicate";
1355
+ import { NextRequest, NextResponse } from "next/server";
1356
+
1357
+ // Start video generation (returns immediately with prediction ID)
1358
+ export async function POST(req: NextRequest) {
1359
+ const { prompt, imageUrl } = await req.json();
1360
+
1361
+ const prediction = await replicate.predictions.create({
1362
+ model: MODELS.videoFast,
1363
+ input: {
1364
+ prompt,
1365
+ ...(imageUrl ? { image: imageUrl } : {}),
1366
+ },
1367
+ });
1368
+
1369
+ return NextResponse.json({
1370
+ predictionId: prediction.id,
1371
+ status: prediction.status,
1372
+ });
1373
+ }
1374
+
1375
+ // Poll for completion (call from client on interval)
1376
+ export async function GET(req: NextRequest) {
1377
+ const id = req.nextUrl.searchParams.get("id");
1378
+ if (!id) return NextResponse.json({ error: "Missing prediction ID" }, { status: 400 });
1379
+
1380
+ const prediction = await replicate.predictions.get(id);
1381
+
1382
+ return NextResponse.json({
1383
+ status: prediction.status,
1384
+ output: prediction.output,
1385
+ error: prediction.error,
1386
+ });
1387
+ }
1388
+ \`\`\`
1389
+
1390
+ ### Image Generator Component (components/image-generator.tsx)
1391
+ \`\`\`tsx
1392
+ "use client";
1393
+ import { useState } from "react";
1394
+
1395
+ export function ImageGenerator() {
1396
+ const [prompt, setPrompt] = useState("");
1397
+ const [imageUrl, setImageUrl] = useState<string | null>(null);
1398
+ const [loading, setLoading] = useState(false);
1399
+
1400
+ async function handleGenerate() {
1401
+ if (!prompt.trim() || loading) return;
1402
+ setLoading(true);
1403
+ setImageUrl(null);
1404
+
1405
+ try {
1406
+ const res = await fetch("/api/generate-image", {
1407
+ method: "POST",
1408
+ headers: { "Content-Type": "application/json" },
1409
+ body: JSON.stringify({ prompt }),
1410
+ });
1411
+ const { imageUrl: url } = await res.json();
1412
+ setImageUrl(url);
1413
+ } finally {
1414
+ setLoading(false);
1415
+ }
1416
+ }
1417
+
1418
+ return (
1419
+ <div className="space-y-4">
1420
+ <div className="flex gap-2">
1421
+ <input
1422
+ value={prompt}
1423
+ onChange={(e) => setPrompt(e.target.value)}
1424
+ onKeyDown={(e) => e.key === "Enter" && handleGenerate()}
1425
+ placeholder="Describe the image you want..."
1426
+ className="flex-1 rounded-md border px-3 py-2"
1427
+ />
1428
+ <button
1429
+ onClick={handleGenerate}
1430
+ disabled={loading || !prompt.trim()}
1431
+ className="rounded-md bg-primary px-4 py-2 text-primary-foreground disabled:opacity-50"
1432
+ >
1433
+ {loading ? "Generating..." : "Generate"}
1434
+ </button>
1435
+ </div>
1436
+ {imageUrl && (
1437
+ <img src={imageUrl} alt={prompt} className="max-w-md rounded-lg shadow-md" />
1438
+ )}
1439
+ </div>
1440
+ );
1441
+ }
1442
+ \`\`\`
1443
+
1444
+ ### Popular Models Reference
1445
+ **Image generation:**
1446
+ - \`black-forest-labs/flux-schnell\` \u2014 fast, good quality, cheapest
1447
+ - \`black-forest-labs/flux-1.1-pro\` \u2014 best quality, slower
1448
+ - \`black-forest-labs/flux-2-pro\` \u2014 highest fidelity, product photography, character consistency
1449
+ - \`google/nano-banana-pro\` \u2014 Google's model, strong prompt following, multilingual text rendering
1450
+
1451
+ **Video generation:**
1452
+ - \`wan-video/wan-2.2-i2v-fast\` \u2014 image-to-video, fast and affordable (10M+ runs)
1453
+ - \`google/veo-3.1-fast\` \u2014 high fidelity with audio
1454
+ - \`kwaivgi/kling-v3-video\` \u2014 multi-shot storytelling with native audio
1455
+
1456
+ ### Common Pitfalls
1457
+ 1. **Never expose REPLICATE_API_TOKEN to the client.** All generation calls go through your API routes.
1458
+ 2. **Image gen is synchronous, video gen is async.** Image models return in 2-10s (safe for Workers). Video models take 30s-5min. Use \`replicate.predictions.create()\` + polling for video.
1459
+ 3. **Output format varies by model.** Some return a URL string, some an array of URLs, some an object. Check the model's documentation on replicate.com.
1460
+ 4. **Model IDs use owner/name format.** E.g. \`black-forest-labs/flux-schnell\`, not just \`flux-schnell\`.
1461
+ 5. **Replicate charges per prediction.** Flux Schnell is ~$0.003/image. Flux Pro is ~$0.05/image. Video models are $0.05-$0.50/generation. Show generation cost to users.
1462
+ 6. **Cache generated images.** Store output URLs in your database. Replicate URLs expire after a few hours. Download and re-host on R2 for permanent storage.
1463
+ 7. **Network I/O does NOT count as CPU time on Workers.** Image generation wait time is all network I/O.
1464
+ 8. **Never ask the user to paste REPLICATE_API_TOKEN in chat.** Direct them to set it in the Mistflow dashboard (Project Settings > Environment Variables).`}];function Wi(t){return t.toLowerCase().replace(/[^a-z0-9]+/g,"-").replace(/^-|-$/g,"")}function Qe(t){let e=Ye.find(n=>n.id===t);if(e)return e;let o=t.toLowerCase().replace(/[^a-z0-9]/g,"");return Ye.find(n=>{let r=n.name.toLowerCase().replace(/[^a-z0-9]/g,"");return r===o||r.includes(o)||o.includes(r)})}function Xe(t){return Kn[t]}function Yn(t){return t?Ye.filter(e=>e.category.toLowerCase()===t.toLowerCase()):Ye}function pr(t){let e=t??Ye,o={};for(let r of e){o[r.category]||(o[r.category]=[]);let a=Kn[r.id],s=a?` \u2014 ${a.description}`:"",i=a?.packages.length?` (${a.packages.join(", ")})`:"";o[r.category].push(`${r.id} \u2014 "${r.name}"${s}${i}`)}let n=[];for(let[r,a]of Object.entries(o))n.push(`**${r}**:
1465
+ ${a.map(s=>` - ${s}`).join(`
1466
+ `)}`);return n.join(`
1467
+
1468
+ `)}function dr(t){return(t?Yn(t):Ye).map(o=>{let n=Kn[o.id];return{id:o.id,slug:Wi(o.name),name:o.name,category:o.category,description:n?.description??"",tags:n?.tags??[],envVars:n?.envVars??[],docsUrl:n?.docsUrl??"",packages:n?.packages??[],difficulty:n?.difficulty??"medium"}})}var Gi=Z.object({action:Z.enum(["get","update","share","landing-designs","integrations","errors","logs","deployments","version"]).default("get").describe("'get' reads current project state. 'update' marks steps complete or adds env vars. 'share' makes the project a shareable template. 'landing-designs' lists curated landing page hero designs. 'integrations' lists third-party service integration blueprints (Stripe, Resend, ElevenLabs, etc.) with setup guides. 'errors' fetches runtime errors from the deployed app. 'logs' fetches deploy logs for a specific deployment. 'deployments' lists deployment history. 'version' reports the installed @mistflow-ai/mcp version and whether an upgrade is available."),projectPath:Z.string().optional().describe("Path to the project directory (default: cwd)"),completedStep:Z.number().optional().describe("(update) Mark a plan step as completed by step number"),addEnvVar:Z.object({key:Z.string(),description:Z.string().optional(),setupUrl:Z.string().optional()}).optional().describe("(update) Add a required env var to the project manifest"),templateDescription:Z.string().optional().describe("(share) Short description of what this template builds"),category:Z.string().optional().describe("(landing-designs) Filter by category"),presetId:Z.string().optional().describe("(landing-designs) Get full details for a specific landing design by ID"),integrationId:Z.string().optional().describe("(integrations) Get full details for a specific integration preset by ID (e.g. 'stripe-payments', 'resend-email', 'elevenlabs-voice')"),period:Z.string().optional().describe("(errors) Time period for errors: '1h', '24h', '7d' (default: '7d')"),deploymentId:Z.string().optional().describe("(logs) Deployment ID to fetch logs for. If omitted, fetches logs for the latest deployment.")}),ur={name:"mist_project",description:So,inputSchema:Gi,handler:async t=>{let e=t;if(["share","errors","logs","deployments"].includes(e.action)&&!G())return c("You need to sign in first. Run mist_setup to connect your account.",!0);switch(e.action){case"get":case"update":return sr.handler({action:e.action,projectPath:e.projectPath,completedStep:e.completedStep,addEnvVar:e.addEnvVar});case"share":{let n=Ht(e.projectPath??process.cwd()),r=Vt(n,"mistflow.json");if(!Wt(r))return ke(n);let a;try{a=JSON.parse(Gt(r,"utf-8"))}catch{return c("Could not read mistflow.json.",!0)}let s=a.projectId;if(!s)return c("No project ID found. Deploy the project first to register it.",!0);try{let i=await Bn(s,{isTemplate:!0,description:e.templateDescription});return c(JSON.stringify({shareUrl:i.share_url,shareToken:i.share_token,message:`Your project is now a shareable template!
1469
+
1470
+ Anyone can fork it: ${i.share_url}
1471
+
1472
+ Others can use it in their AI editor:
1473
+ "build me something like ${i.share_url}"`}))}catch(i){let l=i instanceof Error?i.message:"Failed to share project";return c(l,!0)}}case"landing-designs":{if(e.presetId){let s=zt(e.presetId);if(!s)return c(`Preset '${e.presetId}' not found. Use mist_project action='presets' without presetId to list all available presets.`,!0);let i=ir(e.presetId);return c(JSON.stringify({preset:{id:s.id,title:s.title,category:s.category,description:i?.description??"",style:i?.style??"",theme:i?.theme??"dark",colors:i?.colors??[],tags:i?.tags??[],promptLength:s.prompt.length},message:`Landing design "${s.title}" (${s.category}) \u2014 ${i?.description??""}. To use it, pass landingDesign="${s.id}" when calling mist_plan.`}))}let n=lr(e.category??void 0),r=Jn(e.category),a=ar(r);return c(JSON.stringify({count:n.length,presets:n.map(s=>({id:s.id,title:s.title,category:s.category,description:s.description,style:s.style,theme:s.theme,colors:s.colors})),formatted:a,message:`${n.length} landing designs available.${e.category?` Filtered by: ${e.category}.`:""} To use one, pass landingDesign="<id>" when calling mist_plan. The design blueprint will be injected during the landing page implementation step. Browse them at ${Ge()}/designs?tab=landing-designs.`}))}case"integrations":{if(e.integrationId){let s=Qe(e.integrationId);if(!s)return c(`Integration '${e.integrationId}' not found. Use mist_project action='integrations' without integrationId to list all available integrations.`,!0);let i=Xe(s.id);return c(JSON.stringify({integration:{id:s.id,name:s.name,category:s.category,description:i?.description??"",packages:i?.packages??[],envVars:i?.envVars??[],docsUrl:i?.docsUrl??"",difficulty:i?.difficulty??"medium"},message:`Integration "${s.name}" (${s.category}) \u2014 ${i?.description??""}. This blueprint is auto-injected during implementation when your plan has a matching integration step. Required env vars: ${i?.envVars?.map(l=>l.key).join(", ")||"none"}. Docs: ${i?.docsUrl??"n/a"}.`}))}let n=dr(e.category??void 0),r=Yn(e.category??void 0),a=pr(r);return c(JSON.stringify({count:n.length,integrations:n.map(s=>({id:s.id,name:s.name,category:s.category,description:s.description,packages:s.packages,difficulty:s.difficulty,envVars:s.envVars.map(i=>i.key)})),formatted:a,message:`${n.length} integration blueprints available.${e.category?` Filtered by: ${e.category}.`:""} Integration blueprints are auto-injected during implementation when your plan includes a matching integration step. Use integrationId to see full details including env vars and setup URLs.`}))}case"errors":{let n=Ht(e.projectPath??process.cwd()),r=Vt(n,"mistflow.json");if(!Wt(r))return ke(n);let a;try{a=JSON.parse(Gt(r,"utf-8"))}catch{return c("Could not read mistflow.json.",!0)}let s=a.projectId;if(!s)return c("No project ID found. Deploy the project first.",!0);try{let i=await Dn(s,e.period??"7d");return i.total===0?c(JSON.stringify({total:0,period:i.period,message:`No runtime errors in the last ${i.period}. The app is running clean.`})):c(JSON.stringify({total:i.total,period:i.period,errors:i.errors,message:`${i.total} runtime error(s) in the last ${i.period}. Review the errors above and use mist_debug to investigate.`}))}catch(i){let l=i instanceof Error?i.message:"Failed to fetch errors";return c(l,!0)}}case"logs":{let n=Ht(e.projectPath??process.cwd()),r=Vt(n,"mistflow.json");if(!Wt(r))return ke(n);let a;try{a=JSON.parse(Gt(r,"utf-8"))}catch{return c("Could not read mistflow.json.",!0)}let s=a.projectId;if(!s)return c("No project ID found. Deploy the project first.",!0);let i=e.deploymentId;if(!i)try{let l=await Je(s);if(l.length===0)return c("No deployments found for this project.",!0);i=l[0].id}catch(l){let p=l instanceof Error?l.message:"Failed to fetch deployments";return c(p,!0)}try{let[l,p]=await Promise.all([jn(i),bt(i)]),m=l.filter(d=>d.level==="error"),u=l.filter(d=>d.level==="warn");return c(JSON.stringify({deploymentId:i,status:p.status,errorMessage:p.error??null,totalLogs:l.length,errorCount:m.length,warnCount:u.length,logs:l.map(d=>({time:d.timestamp,level:d.level,phase:d.phase,message:d.message})),message:p.status==="failed"?`Deployment failed. ${m.length} error(s) found in logs. Review the logs above to diagnose the issue.`:`Deployment status: ${p.status}. ${l.length} log entries (${m.length} errors, ${u.length} warnings).`}))}catch(l){let p=l instanceof Error?l.message:"Failed to fetch deploy logs";return c(p,!0)}}case"deployments":{let n=Ht(e.projectPath??process.cwd()),r=Vt(n,"mistflow.json");if(!Wt(r))return ke(n);let a;try{a=JSON.parse(Gt(r,"utf-8"))}catch{return c("Could not read mistflow.json.",!0)}let s=a.projectId;if(!s)return c("No project ID found. Deploy the project first.",!0);try{let i=await Je(s);return c(JSON.stringify({total:i.length,deployments:i.map(l=>({id:l.id,status:l.status,errorMessage:l.error_message,durationSeconds:l.duration_seconds,isRollback:!!l.rollback_from_id,createdAt:l.created_at})),message:`${i.length} deployment(s) found. Use mist_project action='logs' deploymentId='<id>' to see detailed logs for any deployment.`}))}catch(i){let l=i instanceof Error?i.message:"Failed to fetch deployments";return c(l,!0)}}case"version":{fn().backendSignalReceived||await Sn();let n=fn(),r=n.severity==="none",a={none:"up to date",patch:"patch update available",minor:"minor update available",major:"major update available",unsupported:"UNSUPPORTED \u2014 upgrade required"};return c(JSON.stringify({current:n.current,latest:n.latest||"unknown",minSupported:n.minSupported||"unknown",severity:n.severity,upToDate:r,upgradeCmd:n.upgradeCmd,changelogUrl:n.changelogUrl,backendSignalReceived:n.backendSignalReceived,message:n.backendSignalReceived?`Mistflow MCP ${n.current} (${a[n.severity]??n.severity}). Latest: ${n.latest}.${r?"":` Run \`${n.upgradeCmd}\` and restart your editor to upgrade.`}`:`Mistflow MCP ${n.current}. The backend hasn't replied yet \u2014 make one other API call (e.g. mist_project action='get') then retry to see the latest version.`}))}default:return c(`Unknown action: ${e.action}. Use get, update, share, landing-designs, integrations, errors, logs, deployments, or version.`,!0)}}};import{z as Le}from"zod";wn();var Vi=Le.object({action:Le.enum(["navigate","go_back","go_forward","click","type","fill","select_option","press_key","hover","screenshot","snapshot"]).describe("Action to perform. Navigation: navigate|go_back|go_forward. Interaction: click|type|fill|select_option|press_key|hover. Visual: screenshot (returns image) | snapshot (returns accessibility tree)."),url:Le.string().optional().describe("URL to navigate to. Required for 'navigate'; optional for 'screenshot' (navigates before capturing)."),selector:Le.string().optional().describe("CSS selector of the target element. Required for: click, type, fill, select_option, hover. Optional for screenshot (captures just that element)."),value:Le.string().optional().describe("Text to type/fill, option to select, or key to press (e.g. 'Enter', 'Tab'). Required for: type, fill, select_option, press_key."),fullPage:Le.boolean().default(!1).describe("For 'screenshot': capture the full scrollable page instead of just the viewport."),includeScreenshot:Le.boolean().default(!1).describe("For navigate/interact actions: also return a screenshot alongside the accessibility snapshot.")}),mr={name:"mist_browser",description:Po,inputSchema:Vi,handler:async t=>{let e=t,o=await yn();if(e.action==="navigate"){if(!e.url)return c("URL is required for 'navigate'.",!0);let a=[],s=p=>{p.type()==="error"&&a.push(p.text())};o.on("console",s),await o.goto(e.url,{waitUntil:"domcontentloaded",timeout:3e4}),await o.waitForLoadState("networkidle").catch(()=>{});let i=[],l=p=>i.push(p.message);if(o.on("pageerror",l),await o.waitForTimeout(500),o.removeListener("console",s),o.removeListener("pageerror",l),a.length>0||i.length>0){let p=await ht(o),m=[{type:"text",text:JSON.stringify({url:o.url(),title:await o.title(),snapshot:p,consoleErrors:a,pageErrors:i,hasErrors:!0})}];if(e.includeScreenshot){let u=await gt(o);m.push({type:"image",data:u.toString("base64"),mimeType:"image/png"})}return{content:m}}}else if(e.action==="go_back")await o.goBack({waitUntil:"domcontentloaded",timeout:1e4});else if(e.action==="go_forward")await o.goForward({waitUntil:"domcontentloaded",timeout:1e4});else if(e.action==="click"){if(!e.selector)return c("Selector is required for 'click'.",!0);await o.click(e.selector,{timeout:1e4}),await o.waitForLoadState("domcontentloaded").catch(()=>{}),await o.waitForTimeout(500)}else if(e.action==="type"){if(!e.selector)return c("Selector is required for 'type'.",!0);if(!e.value)return c("Value is required for 'type'.",!0);await o.type(e.selector,e.value,{delay:50})}else if(e.action==="fill"){if(!e.selector)return c("Selector is required for 'fill'.",!0);if(!e.value)return c("Value is required for 'fill'.",!0);await o.fill(e.selector,e.value)}else if(e.action==="select_option"){if(!e.selector)return c("Selector is required for 'select_option'.",!0);if(!e.value)return c("Value is required for 'select_option'.",!0);await o.selectOption(e.selector,e.value)}else if(e.action==="hover"){if(!e.selector)return c("Selector is required for 'hover'.",!0);await o.hover(e.selector,{timeout:1e4})}else if(e.action==="press_key"){if(!e.value)return c("Value is required for 'press_key' (e.g. 'Enter').",!0);await o.keyboard.press(e.value),await o.waitForLoadState("domcontentloaded").catch(()=>{}),await o.waitForTimeout(500)}else if(e.action==="screenshot"){e.url&&(await o.goto(e.url,{waitUntil:"domcontentloaded",timeout:3e4}),await o.waitForLoadState("networkidle").catch(()=>{}));let a;if(e.selector){let s=await o.$(e.selector);if(!s)return c(`Element not found: ${e.selector}`,!0);a=await s.screenshot({type:"png"})}else a=await gt(o,e.fullPage);return{content:[{type:"text",text:JSON.stringify({url:o.url(),title:await o.title(),message:`Screenshot captured (${e.fullPage?"full page":"viewport"})`})},{type:"image",data:a.toString("base64"),mimeType:"image/png"}]}}else if(e.action==="snapshot"){let a=await ht(o);return{content:[{type:"text",text:JSON.stringify({url:o.url(),title:await o.title(),snapshot:a})}]}}let n=await ht(o),r=[{type:"text",text:JSON.stringify({url:o.url(),title:await o.title(),snapshot:n})}];if(e.includeScreenshot){let a=await gt(o);r.push({type:"image",data:a.toString("base64"),mimeType:"image/png"})}return{content:r}}};import{z as hr}from"zod";var gr=`# Mistflow MCP tool reference
1474
+
1475
+ Every capability is an MCP tool. There is no companion CLI. Long-running tools
1476
+ (install, build, qa, deploy) use the fire-and-poll pattern \u2014 the first call
1477
+ returns a \`jobId\` with \`status: "running"\`; subsequent calls with that
1478
+ \`jobId\` return fresh state in <1s each. Never the 60s MCP timeout ceiling.
59
1479
 
60
1480
  ## Host AI execution rules
61
1481
 
62
- When invoking \`mist plan\` through your shell/bash tool, budget at least
63
- 2 minutes before assuming the command is stuck. Planning calls routinely take
64
- 30-150s. Heavier commands like install/build/qa/deploy may need a longer
65
- budget (up to about 5 minutes).
1482
+ When a tool returns \`status: "running"\`, call it again with the returned
1483
+ \`jobId\` in ~15-30s. Do NOT narrate "still generating" or ask the user if
1484
+ they want to keep waiting \u2014 polling is how these tools work. Each poll
1485
+ responds in under a second; the underlying work continues detached.
66
1486
 
67
- Treat the returned JSON as authoritative. If \`mist plan --describe ...\`
68
- returns \`status: "clarify"\`, the first planning pass is complete and the
69
- CLI is waiting for user answers. Do NOT re-run the same command or narrate
70
- that planning is still running. Only poll again when the status is
71
- \`"design_clarify_pending"\`, and then use \`mist plan-directions\`. Let the
72
- user answer the questions, then submit those answers exactly once.
1487
+ When \`mist_plan\` returns \`status: "clarify"\`, the first planning pass is
1488
+ complete and waiting on user answers. Render the \`questions[]\` via your
1489
+ native structured-question UI (AskUserQuestion in Claude Code, quick pick in
1490
+ Cursor). Do NOT re-call mist_plan or describe it as "still generating".
1491
+ Only re-call to submit the answers, or to poll when status is
1492
+ \`"design_clarify_pending"\`.
73
1493
 
74
1494
  Before scaffolding a NEW app, resolve the destination path explicitly. If
75
1495
  the user asked to create/build a new app and the current directory is not
76
- already that Mistflow project, ask whether to scaffold in the current
77
- folder or a new subfolder here (unless the user already gave a path). Do
78
- NOT inspect sibling directories and silently adopt one as the base just
79
- because it looks similar. Reuse an existing Mistflow project only when the
80
- user explicitly asks to continue/edit that project.
81
-
82
- \`mist init\` is all-or-nothing for normal users: it registers the cloud
83
- project first, scaffolds in a temp directory, then moves the app into place
84
- only after both steps succeed. If auth, quota, or network registration
85
- fails, stop before any app appears locally. Do NOT default to reusing some
86
- other existing app as recovery unless the user explicitly asked to continue
87
- that app.
88
-
89
- If \`mist plan\` fails with a message like \`Request to /api/plan timed out
90
- after 60s\`, suspect a stale CLI binary before blaming the backend. Check
91
- \`mist --version\`. If the installed CLI is behind the backend's advertised
92
- latest version, run \`mist upgrade\` (for the self-installed CLI in
93
- \`~/.mistflow/bin/mist\`) or retry with \`npx -y @mistflow-ai/cli@latest ...\`.
94
-
95
- ## Commands
96
-
97
- ### \`mist plan --describe "<description>" [--json]\`
98
- Start a new plan. Returns \`status: "clarify"\` with questions to relay to
99
- the user, OR \`status: "ready"\` / \`status: "design_clarify_pending"\` if
100
- the description was specific enough for the backend to skip discovery.
101
-
102
- The CLI caches ready plans locally and returns a \`planId\` for init /
103
- mockup / future modifications.
104
-
105
- ### \`mist plan --token <conversationId> --answers-stdin [--json]\`
106
- Submit the user's answers to a previous \`clarify\` round. Pipe the
107
- answers payload on stdin. Prefer an array of answer entries so duplicate
108
- \`decisionKey\` values cannot overwrite each other:
109
-
110
- echo '{"answers":[{"question":"Who will use this app?","decisionKey":"primaryActor","answer":"Individuals"}]}' | \\
111
- mist plan --token <token> --answers-stdin --json
112
-
113
- Legacy \`{"question text":"answer"}\` objects still work, but hosts must NOT
114
- collapse answers into a \`{decisionKey: answer}\` map because multiple
115
- questions can share one decision key.
116
-
117
- Returns \`status: "ready"\` or \`status: "design_clarify_pending"\`.
118
-
119
- ### \`mist plan --describe "<change request>" --existing-plan-id <planId> [--json]\`
120
- Modify an existing saved plan for a feature addition or integration
121
- addition. Reads the current plan from \`~/.mistflow/plans/<planId>.json\`
122
- or, when run inside the matching project, from \`mistflow.json\`. Returns
123
- \`status: "ready"\` with the modified plan and a diff summary.
124
-
125
- ### \`mist plan --cid <designConversationId> --pick-stdin [--json]\`
126
- Finalize the plan by submitting the user's picked design direction.
127
-
128
- echo '{"id":"modern-editorial"}' | \\
129
- mist plan --cid <cid> --pick-stdin --json
130
-
131
- ### \`mist plan-directions --cid <designConversationId> [--wait] [--json]\`
132
- Poll for asynchronously generated design directions. Without \`--wait\`,
133
- returns the current status once. With \`--wait\`, polls every 2s (up to 2
134
- min) until ready or failed.
135
-
136
- ### \`mist init --plan-id <planId> --path <absolute-path> [--json]\`
137
- Scaffold a fresh Mistflow app from a cached plan. Fast and fully CLI-driven.
138
- Registers the cloud project first, scaffolds in a temp directory, then
139
- moves the finished app into place only after both steps succeed.
140
-
141
- ### \`mist mockup --plan-id <planId> [--json]\`
142
- Generate or iterate on a grayscale HTML wireframe before scaffolding. Use
143
- \`--feedback\` to revise and \`--approved\` to lock it in.
144
-
145
- ### \`mist install <projectPath> [--json]\`
146
- Run npm install in a Mistflow project. Streams output live, no timeout.
147
-
148
- ### \`mist implement <absolute-path> [--json]\`
149
- Return the next plan step for the host AI to implement. Call repeatedly
150
- until all steps are complete.
151
-
152
- ### \`mist build <absolute-path> [--json]\`
153
- Run the local production build.
154
-
155
- ### \`mist seed <absolute-path> [--reset] [--allow-remote] [--json]\`
156
- Seed realistic sample rows from \`plan.dataModel[].sampleRows\` into the
157
- project database. Defaults to local-only seeding against PGlite; pass
158
- \`--allow-remote\` only when you intentionally want to seed a remote
159
- environment. Use before QA when acceptance criteria depend on populated
160
- tables or lists. This is for app/domain data, not admin/test account
161
- bootstrap.
162
-
163
- ### \`mist qa [live-url] [--json]\`
164
- Run browser QA against the live deployed app. If no URL is passed, Mistflow
165
- reads \`deploy.url\` from the current project's \`mistflow.json\`.
166
-
167
- ### \`mist login\`, \`mist deploy\`, \`mist status <id>\`, \`mist logs <id>\`,
168
- ### \`mist projects [id]\`, \`mist env <subcommand>\`, \`mist domains <subcommand>\`,
169
- ### \`mist rollback <id>\`
170
- Shell-native cloud/project coordination. Use \`mist env\` to set required
171
- integration keys before deploying a feature that depends on them.
172
-
173
- ## MCP-vs-CLI rule
174
-
175
- Stays MCP: \`mist_setup\`, \`mist_browser\`, \`mist_project\`, \`mist_help\`
176
- (this tool). These return structured data the AI routes on and aren't
177
- long-running.
178
-
179
- Prefers CLI: planning, install, build, seed, qa, mockup \u2014 anything that can
180
- exceed 30s or streams useful progress.
181
-
182
- ## Chaining pattern
183
-
184
- The AI reads each CLI response's \`status\` field on stdout (with \`--json\`)
185
- and decides the next command. Example end-to-end chain:
186
-
187
- # First, if the user did not specify a destination path, ask whether
188
- # the app should be created in the current folder or a new subfolder.
189
- mist plan --describe "habit tracker" --json
190
- # \u2192 {"status":"clarify","conversation_id":"...","questions":[...]}
191
- # AI asks user, gets answers, then:
192
- echo '{...}' | mist plan --token ... --answers-stdin --json
193
- # \u2192 {"status":"design_clarify_pending","design_conversation_id":"..."}
194
- mist plan-directions --cid ... --wait --json
195
- # \u2192 {"status":"ready","directions":[...], previewPath in sentinel}
196
- # CLI writes <cwd>/.mistflow/design-directions.html with per-direction
197
- # fonts/colors/hero \u2014 open it for the user, then show picker.
198
- echo '{...}' | mist plan --cid ... --pick-stdin --json
199
- # \u2192 {"status":"ready","plan":{...}}
200
- # Optional but recommended: show a wireframe before scaffolding.
201
- mist mockup --plan-id ... --json
202
- # \u2192 AI writes mockup HTML, opens it, asks user to approve.
203
- mist init --plan-id ... --path /Users/you/projects/habit-tracker --json
204
- mist install /Users/you/projects/habit-tracker --json
205
- mist implement /Users/you/projects/habit-tracker --json
206
- mist build /Users/you/projects/habit-tracker --json
207
- mist deploy /Users/you/projects/habit-tracker --json
208
- mist seed /Users/you/projects/habit-tracker --json
209
- mist qa https://your-app.mistflow.app --json
210
- `,qe={name:"mist_help",description:se,inputSchema:Ne.object({command:Ne.string().optional().describe("Optional: name of a specific command to get focused reference for. Omit to get the full catalog.")}),handler:async e=>{let{command:t}=e;if(!t)return c($e);let n=$e.split(`
211
- `),o=new RegExp(`^### \`mist ${t}`),r=-1,s=n.length;for(let i=0;i<n.length;i++)if(o.test(n[i]))r=i;else if(r>=0&&n[i].startsWith("### ")){s=i;break}else if(r>=0&&n[i].startsWith("## ")&&i>r){s=i;break}return r<0?c(`No command named '${t}' found. Call mist_help with no args to see the full catalog.`,!0):c(n.slice(r,s).join(`
212
- `).trim())}};var F=new Yt({name:"mistflow",version:"0.3.0"},{capabilities:{tools:{}},instructions:ie()}),Fe=[ke,De,Ae,qe];F.setRequestHandler(en,async()=>({tools:Fe.map(e=>({name:e.name,description:e.description,inputSchema:tn(e.inputSchema)}))}));F.setRequestHandler(Zt,async e=>{let t=Fe.find(n=>n.name===e.params.name);if(!t)return c(`Unknown tool: ${e.params.name}`,!0);try{let n=t.inputSchema.safeParse(e.params.arguments);if(!n.success){let s=n.error.issues.map(i=>`${i.path.join(".")}: ${i.message}`).join(", ");return c(`Invalid input: ${s}`,!0)}let o=e.params._meta?.progressToken,r={server:F,progressToken:o};try{return await t.handler(n.data,r)}finally{r.cleanup?.()}}catch(n){let o=n instanceof Error?n.message:"An unexpected error occurred";return console.error("Tool error:",n),c(o,!0)}});async function nn(){let e=process.argv.indexOf("--api-url");e!==-1&&process.argv[e+1]&&(process.env.MISTFLOW_API_URL=process.argv[e+1]),process.argv.includes("--local")&&!process.env.MISTFLOW_API_URL&&(process.env.MISTFLOW_API_URL="http://localhost:9100");let t=new Xt;await F.connect(t),console.error(`Mistflow MCP server running on stdio (API: ${process.env.MISTFLOW_API_URL||"https://api.mistflow.ai"})`)}nn().catch(e=>{console.error("Fatal error:",e),process.exit(1)});
1496
+ already that Mistflow project, ask whether to scaffold in the current folder
1497
+ or a new subfolder here (unless the user already gave a path). Do NOT inspect
1498
+ sibling directories and silently adopt one. Reuse an existing Mistflow
1499
+ project only when the user explicitly asks to continue/edit that project.
1500
+
1501
+ \`mist_init\` is transactional: it registers the cloud project first,
1502
+ scaffolds in a temp directory, then moves the app into place only after both
1503
+ succeed. If auth, quota, or network registration fails, stop before any app
1504
+ appears locally. Do NOT default to reusing some other existing app as
1505
+ recovery unless the user explicitly asked.
1506
+
1507
+ ## Tools
1508
+
1509
+ ### \`mist_setup({ apiKey? })\`
1510
+ Authenticate. Two-phase device code flow by default; pass \`apiKey\` for
1511
+ headless. Only call when the user has never signed in or a previous tool
1512
+ returned \`auth_missing\` / \`auth_revoked\`. Do NOT call in response to 500s,
1513
+ 404s, or network errors.
1514
+
1515
+ ### \`mist_project({ action: "get" })\`
1516
+ Load plan progress, env vars, deploy info. Call before editing an existing
1517
+ Mistflow project so you understand its shape. Other actions: \`update\`,
1518
+ \`share\`, \`landing-designs\`, \`integrations\`, \`errors\`, \`logs\`,
1519
+ \`deployments\`, \`version\`.
1520
+
1521
+ ### \`mist_plan({ description })\`
1522
+ Start a new plan. Pass the user's description EXACTLY as written \u2014 do NOT
1523
+ expand or add features. Returns \`status: "clarify"\` with \`questions[]\`
1524
+ (render via AskUserQuestion) OR \`status: "ready"\` if the description was
1525
+ specific enough.
1526
+
1527
+ Submit answers: \`mist_plan({ conversationId, answers: [{question, decisionKey, answer}, ...] })\`.
1528
+ Use the array form, not a \`{decisionKey: answer}\` map \u2014 multiple questions
1529
+ can share one decision key.
1530
+
1531
+ Modify existing plan: \`mist_plan({ description: "<change>", existingPlanId })\`.
1532
+
1533
+ Finalize after picking a design direction:
1534
+ \`mist_plan({ conversationId, designDirectionId })\`. When the tool returns
1535
+ \`status: "design_clarify_pending"\`, poll with the same conversationId until
1536
+ directions are ready, present them to the user, then submit the pick.
1537
+
1538
+ ### \`mist_mockup({ planId, feedback?, approved?, projectPath? })\`
1539
+ Generate a grayscale HTML wireframe before scaffolding. Returns a
1540
+ \`wireframePrompt\` \u2014 you write the HTML to the returned \`mockupPath\`, open
1541
+ it for the user, wait for approval. Iterate with \`feedback\`, lock with
1542
+ \`approved: true\`. Do NOT proceed to mist_init until approved.
1543
+
1544
+ ### \`mist_init({ planId, path })\`
1545
+ Scaffold a fresh Mistflow app from a plan. Transactional \u2014 registers cloud
1546
+ project first, scaffolds in temp, moves into place only after both succeed.
1547
+ Fast (~10s). Does NOT run npm install.
1548
+
1549
+ ### \`mist_install({ projectPath })\`
1550
+ Fire-and-poll \`npm install\`. First call returns a \`jobId\`; poll with
1551
+ \`mist_install({ jobId })\`. Typical duration 30-90s.
1552
+
1553
+ ### \`mist_implement({ projectPath })\`
1554
+ Execute the next plan step. Auto-marks the previous step complete on each
1555
+ call. Call repeatedly until all steps are done.
1556
+
1557
+ ### \`mist_build({ projectPath, script? })\`
1558
+ Fire-and-poll production build. Script defaults to \`build\`. On failure,
1559
+ response has \`missingModules[]\` \u2014 chain mist_install with those packages
1560
+ then mist_build again, without asking the user.
1561
+
1562
+ ### \`mist_debug({ projectPath?, buildOutput? })\`
1563
+ Parse a failed build into structured errors + suggestions. Call with
1564
+ \`projectPath\` to run \`npm run build\` and parse output; call with
1565
+ \`buildOutput\` to parse output captured elsewhere.
1566
+
1567
+ ### \`mist_deploy({ action, projectPath?, deploymentId?, environment?, adminEmail? })\`
1568
+ Fire-and-poll deploy. \`action: "deploy"\` (default) tars + uploads + starts
1569
+ backend orchestration, returns a \`deploymentId\`. Poll with
1570
+ \`mist_deploy({ action: "status", deploymentId })\`.
1571
+
1572
+ When status flips to \`complete\`, response has \`qaRequired: true\` + a
1573
+ \`url\`. Do NOT surface the URL to the user until mist_qa returns
1574
+ \`status: "complete"\`.
1575
+
1576
+ Other actions: \`promote\` (staging \u2192 prod \u2014 re-uses the preview artifact,
1577
+ ~10s), \`rollback\` (revert to a specific deploymentId).
1578
+
1579
+ ### \`mist_qa({ projectPath, baseUrl?, grep? })\`
1580
+ Fire-and-poll Playwright runner against the deployed URL. Call AFTER
1581
+ mist_deploy's \`complete\`. Only surface the deploy URL once mist_qa exits 0.
1582
+
1583
+ ### \`mist_config({ resource, action, ... })\`
1584
+ Manage env vars and custom domains. \`resource: "env"\` actions: \`set\`,
1585
+ \`list\`, \`delete\`. \`resource: "domain"\` actions: \`add\`, \`list\`,
1586
+ \`verify\`, \`remove\`. Env values are encrypted at rest and available on the
1587
+ next deployment.
1588
+
1589
+ ### \`mist_browser({ action, url?, selector?, ... })\`
1590
+ Browser automation for testing the running app. Actions: \`navigate\`,
1591
+ \`click\`, \`type\`, \`screenshot\`, \`snapshot\`. Returns screenshots inline.
1592
+ Use after mist_deploy / mist_qa to verify flows.
1593
+
1594
+ ### \`mist_help\`
1595
+ This reference. Static \u2014 safe to call frequently.
1596
+
1597
+ ## End-to-end chain for a new app
1598
+
1599
+ mist_plan({ description: "habit tracker" })
1600
+ \u2192 { status: "clarify", conversationId: "...", questions: [...] }
1601
+
1602
+ # AI renders questions via AskUserQuestion, collects answers:
1603
+ mist_plan({ conversationId, answers: [...] })
1604
+ \u2192 { status: "design_clarify_pending", conversationId: "..." }
1605
+
1606
+ # Poll until directions are ready:
1607
+ mist_plan({ conversationId })
1608
+ \u2192 { status: "ready", directions: [...], previewPath: "..." }
1609
+
1610
+ # Open previewPath for the user, ask them to pick:
1611
+ mist_plan({ conversationId, designDirectionId: "modern-editorial" })
1612
+ \u2192 { status: "ready", plan: { ... }, planId: "..." }
1613
+
1614
+ # Optional but recommended: show wireframe before scaffolding:
1615
+ mist_mockup({ planId, projectPath })
1616
+ \u2192 { wireframePrompt: "...", mockupPath: "..." }
1617
+ # AI writes the HTML, opens it, waits for user approval:
1618
+ mist_mockup({ planId, approved: true })
1619
+
1620
+ mist_init({ planId, path: "/Users/you/projects/habit-tracker" })
1621
+ \u2192 { projectId, path, status: "initialized" }
1622
+
1623
+ mist_install({ projectPath })
1624
+ \u2192 { status: "running", jobId: "job_..." }
1625
+ # Poll:
1626
+ mist_install({ jobId })
1627
+ \u2192 { status: "complete", exitCode: 0 }
1628
+
1629
+ # Implement loop \u2014 call repeatedly until all steps done:
1630
+ mist_implement({ projectPath })
1631
+ \u2192 { status: "step_completed", nextStep: { ... } } | { status: "all_done" }
1632
+
1633
+ mist_build({ projectPath })
1634
+ \u2192 { status: "running", jobId: "..." }
1635
+ mist_build({ jobId })
1636
+ \u2192 { status: "complete" } | { status: "failed", missingModules: [...] }
1637
+ # On missingModules, chain mist_install with those packages, then
1638
+ # mist_build again. Do not ask the user.
1639
+
1640
+ mist_deploy({ action: "deploy", projectPath })
1641
+ \u2192 { status: "running", deploymentId: "dep_..." }
1642
+ mist_deploy({ action: "status", deploymentId })
1643
+ \u2192 { status: "complete", url: "...", qaRequired: true }
1644
+ # URL is NOT shown to the user yet.
1645
+
1646
+ mist_qa({ projectPath })
1647
+ \u2192 { status: "running", jobId: "..." }
1648
+ mist_qa({ jobId })
1649
+ \u2192 { status: "complete", exitCode: 0 }
1650
+ # Now safe to show the URL to the user.
1651
+
1652
+ ## End-to-end chain for adding a feature (existing app)
1653
+
1654
+ **Decide the change shape first**, then pick the right chain. Ask product
1655
+ questions BEFORE any tool call \u2014 "what fields, what view, what workflow,
1656
+ what constraints" \u2014 unless the change is trivial. Getting the spec right
1657
+ is cheaper than building the wrong thing and iterating.
1658
+
1659
+ ### Cosmetic / single-file edit (change a color, fix a typo, tweak copy)
1660
+
1661
+ # No planning, no mist_plan. Edit the file directly, then:
1662
+ mist_build({ projectPath }) # fire-and-poll
1663
+ mist_deploy({ action: "deploy", projectPath })
1664
+ mist_qa({ projectPath }) # only if user-facing behavior changed
1665
+
1666
+ ### New page or feature with NO new data model (settings page, dark mode, search filter)
1667
+
1668
+ # 1. Load context first \u2014 read the current plan + schema so edits
1669
+ # respect existing conventions.
1670
+ mist_project({ action: "get", projectPath })
1671
+ \u2192 { plan, dataModel, features, envVars, ... }
1672
+
1673
+ # 2. Ask the user product questions if non-trivial.
1674
+ # 3. Edit the code directly (new routes, components, etc.). Reuse
1675
+ # existing schema \u2014 no mist_plan needed.
1676
+ # 4. Ship:
1677
+ mist_build({ projectPath })
1678
+ mist_deploy({ action: "deploy", projectPath })
1679
+ mist_qa({ projectPath })
1680
+
1681
+ ### Feature needing a new data model (add comments to posts, add teams)
1682
+
1683
+ # 1. Context + spec questions.
1684
+ mist_project({ action: "get", projectPath })
1685
+ # Ask: what fields, relationships, constraints, who can read/write.
1686
+
1687
+ # 2. Modify the plan \u2014 existingPlanId points at the current plan; the
1688
+ # backend preserves completed steps and appends new ones.
1689
+ mist_plan({ description: "<change request>", existingPlanId: "..." })
1690
+ \u2192 { status: "clarify" | "ready", ... }
1691
+
1692
+ # 3. Implement the new steps one at a time:
1693
+ mist_implement({ projectPath })
1694
+ \u2192 { status: "step_completed", nextStep }
1695
+ # Repeat until all_done.
1696
+
1697
+ # 4. Build + ship:
1698
+ mist_build({ projectPath })
1699
+ mist_deploy({ action: "deploy", projectPath })
1700
+ mist_qa({ projectPath })
1701
+
1702
+ ### Adding a third-party integration (Resend, ElevenLabs, OpenAI, Twilio)
1703
+
1704
+ # 1. Browse the catalog (or pass integrationId for full details):
1705
+ mist_project({ action: "integrations", projectPath })
1706
+
1707
+ # 2. Ask product questions \u2014 which flows use this, when triggered,
1708
+ # retry policy, etc.
1709
+
1710
+ # 3. Plan the change \u2014 the integration blueprint auto-injects when
1711
+ # the plan matches:
1712
+ mist_plan({ description: "<change request>", existingPlanId })
1713
+
1714
+ # 4. Implement:
1715
+ mist_implement({ projectPath }) # repeat until done
1716
+
1717
+ # 5. Set required secrets BEFORE deploy. mist_implement will surface
1718
+ # which keys the integration needs:
1719
+ mist_config({ resource: "env", action: "set", projectPath,
1720
+ key: "RESEND_API_KEY", value: "re_..." })
1721
+
1722
+ # 6. Ship:
1723
+ mist_build({ projectPath })
1724
+ mist_deploy({ action: "deploy", projectPath })
1725
+ mist_qa({ projectPath })
1726
+
1727
+ ### Bug fix / something broke
1728
+
1729
+ # 1. Build failure \u2192 structured errors:
1730
+ mist_debug({ projectPath })
1731
+ \u2192 { errors: [{ file, line, humanMessage, suggestion }, ...] }
1732
+
1733
+ # 2. Runtime failure in prod \u2192 runtime errors:
1734
+ mist_project({ action: "errors", projectPath, period: "24h" })
1735
+
1736
+ # 3. Fix the code directly (no mist_plan for bugs).
1737
+
1738
+ # 4. Ship:
1739
+ mist_build({ projectPath })
1740
+ mist_deploy({ action: "deploy", projectPath })
1741
+ mist_qa({ projectPath })
1742
+
1743
+ ### Promoting a staging preview to production
1744
+
1745
+ # Projects with deploy_strategy=staging auto-route to preview. After
1746
+ # mist_qa passes on the preview URL:
1747
+ mist_deploy({ action: "promote", projectPath })
1748
+ mist_deploy({ action: "status", deploymentId }) # poll
1749
+ # Promote re-runs the preview artifact on prod \u2014 ~10s.
1750
+
1751
+ ### Rolling back a bad deploy
1752
+
1753
+ mist_project({ action: "deployments", projectPath })
1754
+ \u2192 { deployments: [{ id, status, created_at }, ...] }
1755
+
1756
+ mist_deploy({ action: "rollback", deploymentId: "<last known good>" })
1757
+ mist_deploy({ action: "status", deploymentId }) # poll
1758
+ `,fr={name:"mist_help",description:Io,inputSchema:hr.object({command:hr.string().optional().describe("Optional: name of a specific tool (e.g. 'mist_plan') to get focused reference for. Omit to get the full catalog.")}),handler:async t=>{let{command:e}=t;if(!e)return c(gr);let o=e.startsWith("mist_")?e:`mist_${e}`,n=gr.split(`
1759
+ `),r=new RegExp(`^### \`${o}`),a=-1,s=n.length;for(let i=0;i<n.length;i++)if(r.test(n[i]))a=i;else if(a>=0&&n[i].startsWith("### ")){s=i;break}else if(a>=0&&n[i].startsWith("## ")&&i>a){s=i;break}return a<0?c(`No tool named '${o}' found. Call mist_help with no args to see the full catalog.`,!0):c(n.slice(a,s).join(`
1760
+ `).trim())}};import{existsSync as Jt,mkdirSync as wr,readFileSync as vr,readdirSync as xr,writeFileSync as Ji}from"fs";import{join as Ae,resolve as Ki}from"path";import{homedir as Xn}from"os";import{z as kt}from"zod";function Ze(t){return t.entity??t.name??"Unknown"}function et(t){return typeof t=="string"?t:t.name}function Qn(t){return typeof t=="string"?"text":t.type}function yr(t){let e=t.sampleRows;if(Array.isArray(e)&&e.length>0)return e.slice(0,3).map(r=>{let a={};for(let[s,i]of Object.entries(r))a[s]=i==null?"":String(i);return a});let o=t.fields||[],n=[];for(let r=0;r<3;r++){let a={};for(let s of o)a[et(s)]=Yi(et(s),Qn(s),Ze(t),r);n.push(a)}return n}function Yi(t,e,o,n){let r=t.toLowerCase(),a=(e||"text").toLowerCase();return r==="name"||r==="title"?[`${o} Alpha`,`${o} Beta`,`${o} Gamma`][n]:r==="email"?["alice@example.com","bob@example.com","carol@example.com"][n]:r==="status"?["Active","Pending","Completed"][n]:r==="priority"?["High","Medium","Low"][n]:r.includes("date")||a==="date"?["Jan 15, 2024","Feb 20, 2024","Mar 10, 2024"][n]:r.includes("price")||r.includes("amount")||r.includes("cost")?["$29","$49","$99"][n]:r.includes("count")||r.includes("quantity")||a==="number"||a==="integer"?["12","34","56"][n]:a==="boolean"||a==="bool"?["Yes","No","Yes"][n]:r.includes("description")||a==="textarea"?["Brief description here","Another example entry","Third sample item"][n]:`Sample ${n+1}`}function Qi(t){let e=t.dataModel??[],o=t.pages??[],n=t.design??{},r=o.map(u=>({label:u.name??u.path??"Page",route:u.path??u.route??"/"})),a=[],s=e.slice(0,3).map(u=>({name:Ze(u),fields:(u.fields||[]).map(d=>({name:et(d),type:Qn(d)})),sampleData:yr(u)})),i=[];t.primaryAction&&(i.push(`PRIMARY: ${t.primaryAction.action} \u2014 this is the first thing the user sees and does`),i.push(`SURFACE: ${t.primaryAction.dashboardSurface}`)),i.push(`METRICS: Key counts for ${e.map(u=>Ze(u)).join(", ")}`),i.push("RECENT: Latest activity or items"),a.push({name:"Dashboard",type:"dashboard",route:"/dashboard",purpose:t.primaryAction?`Action surface \u2014 user comes here to ${t.primaryAction.action.toLowerCase()}. Not a stats display.`:`Overview of ${e.map(u=>Ze(u)).join(", ")} with key metrics.`,informationHierarchy:i,interactionStates:["Empty state: new user, no data yet \u2014 show onboarding prompt","Loading state: skeleton placeholders for metrics and table","Populated state: real data with metrics, recent items, quick actions"],entities:s});let l=e[0];if(l){let u=Ze(l),d=u.toLowerCase().endsWith("s")?u:`${u}s`;a.push({name:`${u} List`,type:"detail",route:`/${d.toLowerCase()}`,purpose:`Browse, search, and manage ${d.toLowerCase()}. Create new ${u.toLowerCase()}s.`,informationHierarchy:[`HEADER: "${d}" title + "Add ${u}" button`,"SEARCH: Filter/search bar \u2014 users will have many items",`TABLE: ${(l.fields||[]).slice(0,5).map(b=>et(b)).join(", ")} columns`,"ROW ACTIONS: Edit, delete on each row"],interactionStates:[`Empty state: "No ${d.toLowerCase()} yet" with create CTA`,"Loading state: skeleton table rows",`Search with no results: "No ${d.toLowerCase()} matching..." with clear filter`],entities:[{name:u,fields:(l.fields||[]).map(b=>({name:et(b),type:Qn(b)})),sampleData:yr(l)}]})}t.steps.some(u=>{let d=`${u.name??u.title??""} ${u.description??""}`.toLowerCase();return d.includes("landing")||d.includes("hero")||d.includes("marketing")||d.includes("homepage")})&&a.push({name:"Landing Page",type:"landing",route:"/",purpose:`Convince visitors to sign up for ${t.name}. Answer: what is this, who is it for, why should I care.`,informationHierarchy:[`HERO: One sentence about what ${t.name} does \u2014 not "Transform your X", be specific`,"CTA: Sign up / Get started \u2014 one clear action","PROOF: What makes this valuable (features, not buzzwords)","SECONDARY CTA: Repeat the sign up prompt"],interactionStates:["Mobile: hero stacks vertically, nav collapses to hamburger","Desktop: hero side-by-side or centered, full nav"],entities:[]});let m=[];for(let u of e.slice(0,3)){let d=Ze(u);(u.fields||[]).find(T=>{let w=et(T).toLowerCase();return w==="name"||w==="title"})&&m.push(`What if a ${d.toLowerCase()}'s name is 47 characters? Does the layout break?`),m.push(`What if there are 0 ${d.toLowerCase()}s? 1? 500?`)}return t.authModel&&t.authModel!=="none"&&m.push("What does a brand-new user see? (no data, no setup)"),m.push("What if the network is slow? What loads first?"),{appName:t.name,summary:t.summary??"",screens:a,navigation:{style:t.navStyle??"sidebar",items:r},primaryAction:t.primaryAction??null,designDirection:{tone:n.tone??"professional",accentColor:n.accentColor??"blue",navStyle:t.navStyle??"sidebar",fonts:n.fonts??{heading:"Inter",body:"Inter"}},edgeCases:m}}function Xi(t,e,o){let n=[];n.push(`# Wireframe sketch for ${t.appName}`),n.push(""),n.push(`**${t.appName}** \u2014 ${t.summary}`),n.push(""),e&&(n.push("## Feedback to apply"),n.push(e),n.push("")),n.push("## Design principles"),n.push(""),n.push("Apply these when deciding layout and hierarchy:"),n.push("1. **Information hierarchy** \u2014 What does the user see first, second, third? The primary action is first. Metrics are second. Everything else is supporting."),n.push("2. **Interaction states** \u2014 Every screen has at least: empty, loading, populated. Show the populated state but add HTML comments noting the others."),n.push("3. **Edge case paranoia** \u2014 What if there are 0 items? 500 items? A 47-character name? Think about these and comment where they matter."),n.push(`4. **Subtraction** \u2014 "As little design as possible" (Dieter Rams). Every element earns its pixels. If removing something doesn't hurt, remove it.`),n.push("5. **Design for trust** \u2014 Clear labels, predictable layout, obvious actions. No mystery meat navigation."),n.push(""),n.push("## Wireframe rules (strict)"),n.push(""),n.push(`Write a **single self-contained HTML file** saved to \`${o}\`.`),n.push(""),n.push("The wireframe must:"),n.push("- Use **system fonts only** (`-apple-system, system-ui, sans-serif`) \u2014 no Google Fonts, no CDN"),n.push("- Use **inline CSS only** \u2014 no external stylesheets, no Tailwind CDN"),n.push("- Look **intentionally rough** \u2014 thin gray borders (#ddd), light backgrounds (#f8f8f8), no color, no shadows"),n.push("- Use **realistic placeholder content** that matches this specific app (sample data provided below) \u2014 NOT lorem ipsum"),n.push("- Include **HTML comments** explaining design decisions"),n.push("- Show **all screens in a single page** using tabs/sections that the user can click through"),n.push("- Be **responsive** \u2014 test that it looks reasonable at both 1200px and 375px widths"),n.push("- Include a small header bar showing: screen name tabs + the design direction summary"),n.push(""),n.push("The wireframe must NOT:"),n.push("- Use any color except grayscale (#333, #666, #999, #ddd, #f8f8f8, white)"),n.push("- Use any external dependencies \u2014 no CDN, no imports, no build step"),n.push("- Look polished \u2014 it should feel like a sketch on a whiteboard, not a finished product"),n.push("- Include decorative elements \u2014 no icons (use text labels), no illustrations, no gradients"),n.push(""),n.push("## Screens to wireframe"),n.push("");for(let r of t.screens){n.push(`### ${r.name} (\`${r.route}\`)`),n.push(`**Purpose**: ${r.purpose}`),n.push(""),n.push("**Information hierarchy** (render in this order, top to bottom):");for(let a of r.informationHierarchy)n.push(`- ${a}`);n.push(""),n.push("**Interaction states** (add HTML comments for non-visible states):");for(let a of r.interactionStates)n.push(`- ${a}`);if(n.push(""),r.entities.length>0){n.push("**Data model and sample content** (use this real data, not lorem ipsum):");for(let a of r.entities)n.push(`
1761
+ **${a.name}** \u2014 fields: ${a.fields.map(s=>`${s.name} (${s.type})`).join(", ")}`),n.push("```json"),n.push(JSON.stringify(a.sampleData,null,2)),n.push("```");n.push("")}}n.push("## Navigation"),n.push(`**Style**: ${t.navigation.style} (use this layout)`),n.push("**Items**:");for(let r of t.navigation.items)n.push(`- ${r.label} \u2192 \`${r.route}\``);if(n.push(""),t.primaryAction&&(n.push("## Primary action (this drives the layout)"),n.push(`- **Action**: ${t.primaryAction.action}`),n.push(`- **Flow**: ${t.primaryAction.flow}`),n.push(`- **Dashboard must show**: ${t.primaryAction.dashboardSurface}`),n.push(""),n.push("The dashboard is an ACTION surface. The primary action should be the most prominent thing on the page \u2014 above the metrics, above the recent items. Users came here to DO something, not to look at numbers."),n.push("")),t.edgeCases.length>0){n.push("## Edge cases to consider"),n.push("Add HTML comments in the wireframe where these matter:");for(let r of t.edgeCases)n.push(`- ${r}`);n.push("")}return n.push("## Design direction (DO NOT apply to wireframe \u2014 this is for reference only)"),n.push(`The final app will use: ${t.designDirection.tone} tone, ${t.designDirection.accentColor} accent, ${t.designDirection.navStyle} nav, ${t.designDirection.fonts.heading} / ${t.designDirection.fonts.body} fonts.`),n.push("The wireframe is grayscale and rough. These tokens will be applied during the actual build."),n.push(""),n.push("## After writing the wireframe"),n.push(`1. Write the file to \`${o}\``),n.push(`2. Open it for the user: \`open "${o}"\``),n.push(`3. Tell the user: "Here's a rough wireframe of your app. Does the layout feel right? Let me know what to change, or I can start building if it's close."`),n.push("4. WAIT for the user's response. Do NOT call mist_init until they approve the layout."),n.join(`
1762
+ `)}function kr(t){return Ae(Xn(),".mistflow","mockup-state",`${t}.json`)}function Zi(t){let e=kr(t);if(!Jt(e))return null;try{return JSON.parse(vr(e,"utf-8"))}catch{return null}}function br(t,e){let o=Ae(Xn(),".mistflow","mockup-state");wr(o,{recursive:!0}),Ji(kr(t),JSON.stringify(e,null,2))}var ea=kt.object({planId:kt.string().min(1).describe("Plan ID from mist_plan. Required."),projectPath:kt.string().optional().describe("Project directory (default: cwd). The mockup is written to <projectPath>/.mistflow/mockups/."),feedback:kt.string().optional().describe("User feedback to apply to the next iteration."),approved:kt.boolean().optional().describe("Mark the wireframe as approved (terminal \u2014 unlocks scaffolding).")}).refine(t=>!(t.feedback&&t.approved),{message:"Pass either 'feedback' or 'approved' \u2014 not both. Feedback iterates the design; approved locks it in."}),Sr={name:"mist_mockup",description:To,inputSchema:ea,handler:async t=>{let e=t,{planId:o,feedback:n,approved:r}=e,a=Ki(e.projectPath??process.cwd()),s=Ae(Xn(),".mistflow","plans",`${o}.json`);if(!Jt(s))return c(`Plan not found for planId '${o}'. Call mist_plan to generate a plan first.`,!0);let i;try{i=JSON.parse(vr(s,"utf-8"))}catch{return c("Failed to read plan file. Call mist_plan again.",!0)}let l=i.plan;if(!l)return c("Plan data is empty. Call mist_plan again.",!0);let p=Zi(o);p||(p={planId:o,iterationCount:0,approved:!1,screens:[],feedback:[]});let m=Ae(a,".mistflow","mockups");wr(m,{recursive:!0});let u=`mockup-${o}.html`,d=Ae(m,u);if(r){p.approved=!0,br(o,p);let x=Jt(m)?xr(m).filter(M=>M.endsWith(".html")).map(M=>Ae(".mistflow","mockups",M)):[];return c(JSON.stringify({status:"approved",message:`Wireframe approved after ${p.iterationCount} iteration(s). Layout direction is locked in.`,mockupFiles:x,nextAction:`Call mist_init with planId='${o}' and path='<absolute project path>' to scaffold the project. Mockups in .mistflow/mockups/ will be used as layout reference during implementation.`}))}p.iterationCount++,n&&p.feedback.push(n);let b=Qi(l);p.screens=b.screens.map(x=>x.name),br(o,p);let T=Xi(b,n??void 0,d),w=p.iterationCount>=3?"The wireframe is shaping up \u2014 want to keep refining the layout, or start building?":void 0,y=n?`Apply the user's feedback to ${d}. Rewrite the file, open it for review, then ask if they want more changes or are ready to build.`:`Generate the wireframe HTML following the wireframePrompt, write it to ${d}, then open it in the browser. Ask the user if the layout feels right.`;return c(JSON.stringify({status:"wireframe",iterationCount:p.iterationCount,screens:b.screens.map(x=>({name:x.name,type:x.type,route:x.route})),wireframePrompt:T,designDirection:b.designDirection,...w?{nudge:w}:{},mockupFile:u,mockupPath:d,nextAction:y}))}};function Pr(t){let e=Ae(t,".mistflow","mockups");return Jt(e)?xr(e).filter(o=>o.endsWith(".html")).map(o=>Ae(e,o)):[]}import{z as Zn}from"zod";import{resolve as ta}from"path";import{spawn as na}from"child_process";function Kt(t){let e=[],o=/([^\s(]+)\((\d+),(\d+)\):\s*error\s+(TS\d+):\s*(.+)/g;for(let i of t.matchAll(o)){let[,l,p,m,u,d]=i;e.push({file:l,line:parseInt(p,10),column:parseInt(m,10),message:`${u}: ${d}`,humanMessage:`There is a type error in ${l} on line ${p}: ${d}`,suggestion:`Check line ${p} in ${l}. ${u==="TS2345"?"The types of the arguments do not match.":`Fix the ${u} error.`}`})}let n=/(?:Error:\s*)?\.\/([^\s:]+):(\d+):(\d+)\s*\n\s*(.+)/g;for(let i of t.matchAll(n)){let[,l,p,m,u]=i;e.some(d=>d.file===l&&d.line===parseInt(p,10))||e.push({file:l,line:parseInt(p,10),column:parseInt(m,10),message:u,humanMessage:`There is an error in ${l} on line ${p}: ${u.trim()}`,suggestion:`Check line ${p} in ${l} and fix the issue.`})}let r=/Module not found:\s*(?:Error:\s*)?Can't resolve ['"]([^'"]+)['"]\s*(?:in\s*['"]?([^'"]+)['"]?)?/g;for(let i of t.matchAll(r)){let[,l,p]=i;e.push({file:p,message:`Module not found: ${l}`,humanMessage:`The file ${p??"your project"} is trying to import '${l}' which is not installed.`,suggestion:`Run npm install ${l}`})}let a=/Package subpath ['"]([^'"]+)['"] is not defined by "exports" in .*?node_modules\/([^/]+(?:\/[^/]+)?)\//g;for(let i of t.matchAll(a)){let[,l,p]=i;e.push({message:`ERR_PACKAGE_PATH_NOT_EXPORTED: ${p}${l}`,humanMessage:`The package '${p}' does not export the subpath '${l}'. This is usually caused by a version conflict between packages that depend on different major versions of '${p}'.`,suggestion:`Add '${p}' at the version that exports '${l}' as an optionalDependency in package.json (this pins it at root level and lets the other version nest). Then delete node_modules and package-lock.json, and run npm install.`})}let s=/SyntaxError:\s*([^\s:]+):\s*(.+?)\s*\((\d+):(\d+)\)/g;for(let i of t.matchAll(s)){let[,l,p,m,u]=i;e.some(d=>d.file===l&&d.line===parseInt(m,10))||e.push({file:l,line:parseInt(m,10),column:parseInt(u,10),message:`SyntaxError: ${p}`,humanMessage:`There is a syntax error in ${l} on line ${m}.`,suggestion:`Check line ${m} in ${l} for a missing closing bracket or unexpected token.`})}return e}var oa=Zn.object({projectPath:Zn.string().optional().describe("Absolute path to the project directory. Defaults to cwd."),buildOutput:Zn.string().optional().describe("Build output to parse. If omitted, the tool runs `npm run build` in projectPath.")});function ra(t){return new Promise(e=>{let o=na("npm",["run","build"],{cwd:t,stdio:["ignore","pipe","pipe"]}),n="";o.stdout?.on("data",r=>{n+=r.toString()}),o.stderr?.on("data",r=>{n+=r.toString()}),o.on("error",r=>{e({exitCode:127,combined:`${n}
1763
+ ${r.message}`})}),o.on("close",r=>{e({exitCode:r??1,combined:n})})})}var Ir={name:"mist_debug",description:_o,inputSchema:oa,handler:async t=>{let e=t,o=ta(e.projectPath??process.cwd()),n=e.buildOutput??"";if(!e.buildOutput){let s=await ra(o);if(s.exitCode===0)return c(JSON.stringify({errors:[],rawOutput:"",message:"Build succeeded with no errors."}));n=s.combined}let r=Kt(n),a=n.slice(0,2e3);return r.length===0?c(JSON.stringify({errors:r,rawOutput:a,message:"Build output could not be parsed into structured errors. See rawOutput for the first 2KB."})):c(JSON.stringify({errors:r,rawOutput:a,message:`Found ${r.length} error${r.length===1?"":"s"} in the build output.`}))}};import{z as I}from"zod";import{existsSync as St,mkdirSync as Xt,readFileSync as Er,readdirSync as ha,statSync as ga,unlinkSync as fa,writeFileSync as Zt}from"fs";import{dirname as ya,isAbsolute as ba,join as te}from"path";import{homedir as Ce}from"os";import{createHash as wa,createHmac as Nr,randomBytes as va,randomUUID as Cr,timingSafeEqual as xa}from"crypto";ie();function Yt(t,e,o,n=3e3){if(e===void 0)return{stop:()=>{}};let r=0,a=!1,i=setInterval(()=>{a||(r++,t.notification({method:"notifications/progress",params:{progressToken:e,progress:r,message:o()}}).catch(()=>{}))},n);return{stop:()=>{a||(a=!0,clearInterval(i))}}}var sa=[{name:"Dashboard",description:"Overview with key stats and today's activity",condition:t=>t.surfaceType==="internal-tool"||t.surfaceType==="customer-app",keywords:/\b(dashboard|overview|home.?page|stats)\b/i},{name:"Landing Page",description:"Public page explaining what this does",condition:t=>t.publicLanding===!0,keywords:/\b(landing|marketing|hero|homepage)\b/i},{name:"Scheduling / Booking",description:"Calendar, time slots, reservations",condition:t=>t.scheduling===!0,keywords:/\b(schedul|book|reserv|appointment|calendar|slot)\b/i},{name:"Payments / Billing",description:"Charge users, invoices, subscriptions (experimental \u2014 Stripe integration is early-stage)",condition:()=>!1,keywords:/\b(payment|billing|invoice|subscription|checkout|stripe)\b/i},{name:"Admin Panel",description:"Manage users, roles, and content",condition:t=>t.multiRole===!0||t.primaryActor==="both",keywords:/\b(admin|panel|manage.?user|moderat)\b/i},{name:"User Profiles",description:"Account pages, settings, preferences",condition:t=>t.primaryActor==="customers"||t.primaryActor==="both",keywords:/\b(profile|account|settings|preferences)\b/i},{name:"Search / Browse",description:"Find and filter content or listings",condition:t=>t.surfaceType==="marketplace",keywords:/\b(search|browse|filter|discover|explore)\b/i},{name:"Email Notifications",description:"Welcome emails, alerts, reminders",condition:t=>t.integrations?.includes("email")===!0,keywords:/\b(notification|alert|reminder|email.?notif|sms|welcome.?email)\b/i},{name:"Analytics / Reports",description:"Usage stats, trends, data exports",condition:()=>!1,keywords:/\b(analytics|report|chart|trend|insight|metric)\b/i},{name:"File Uploads",description:"Images, documents, attachments",condition:t=>t.integrations?.includes("file-uploads")===!0,keywords:/\b(upload|image|photo|attachment|document|gallery|file)\b/i},{name:"AI Features",description:"Chatbot, content generation, AI assistant",condition:t=>t.integrations?.includes("ai")===!0,keywords:/\b(ai|chatbot|gpt|llm|generat|assistant)\b/i},{name:"Maps / Location",description:"Google Maps, location search, geolocation",condition:t=>t.integrations?.includes("maps")===!0,keywords:/\b(map|location|address|geo|places)\b/i},{name:"Voice / TTS",description:"Text-to-speech, voice notes, audio generation",condition:()=>!1,keywords:/\b(voice|tts|text.?to.?speech|audio|speak|narrat|podcast|elevenlabs)\b/i},{name:"SMS / Text Messages",description:"Send SMS notifications, OTP verification",condition:t=>t.integrations?.includes("sms")===!0,keywords:/\b(sms|text.?message|twilio|otp|verification.?code|phone.?verif)\b/i},{name:"Web Scraping",description:"Scrape URLs, crawl websites, extract structured data",condition:()=>!1,keywords:/\b(scrape|crawl|web.?scrap|extract.?from.?url|read.?url|ingest.?web|firecrawl)\b/i},{name:"Chat / Messaging",description:"Real-time messaging between users",condition:()=>!1,keywords:/\b(chat|messag|inbox|conversation|dm)\b/i},{name:"Events / Tournaments",description:"Create and manage events, registrations",condition:()=>!1,keywords:/\b(event|tournament|competition|league|registration)\b/i},{name:"High Scores",description:"Track and display top scores with a leaderboard",condition:t=>t.surfaceType==="game",keywords:/\b(high.?score|leaderboard|top.?score|ranking|scoreboard)\b/i},{name:"Save Progress",description:"Save and resume game state between sessions",condition:t=>t.surfaceType==="game",keywords:/\b(save|progress|resume|checkpoint|continue)\b/i},{name:"Guest Play",description:"Play immediately without signing up, optionally link account later",condition:t=>t.surfaceType==="game",keywords:/\b(guest|anonymous|no.?login|play.?now|instant.?play)\b/i},{name:"Levels / Stages",description:"Progressive difficulty with unlockable stages",condition:()=>!1,keywords:/\b(level|stage|unlock|difficult|progress|world)\b/i},{name:"Achievements",description:"Badges, trophies, and milestones for player accomplishments",condition:()=>!1,keywords:/\b(achieve|badge|trophy|milestone|reward|unlock)\b/i},{name:"Daily Challenge",description:"New puzzle or challenge every day to keep players coming back",condition:()=>!1,keywords:/\b(daily|challenge|streak|word.?of.?the.?day|puzzle.?of)\b/i}];function _r(t,e,o){let n=o?.suggestedName||ia(t),r=e.primaryActor==="both"?"Staff + Customers":e.primaryActor==="staff"?"Staff / Admin":e.primaryActor==="customers"?"End Users":"Users",a=e.audienceType??(e.surfaceType==="internal-tool"?"internal":(e.primaryActor==="customers"||e.primaryActor==="both","b2c")),s=e.surfaceType==="internal-tool"?"Internal tool":e.surfaceType==="marketplace"?"Marketplace":e.surfaceType==="content-site"?"Content site":e.surfaceType==="game"?"Game":"App",i;if(o?.suggestedFeatures&&o.suggestedFeatures.length>0)i=o.suggestedFeatures.map(l=>({name:l.name,description:l.description,checked:l.recommended,source:l.recommended?"explicit":"suggested"}));else{let l=`${t} ${e.primaryAction||""}`;i=[];for(let p of sa){let m=p.keywords.test(l),u=p.condition(e);(m||u)&&i.push({name:p.name,description:p.description,checked:m,source:m?"explicit":"suggested"})}}return{name:n,audience:r,audienceType:a,surfaceType:s,primaryAction:e.primaryAction||"manage items",features:i,publicLanding:e.publicLanding??!0,authModel:e.authModel??"email",dbProvider:e.dbProvider??"neon",integrations:e.integrations??[],language:o?.language||"English"}}function Tr(t){let e=t.features.filter(i=>i.checked),o=t.features.filter(i=>!i.checked),n={email:"Email sign-up",none:"No login (public)",social:"Social login","invite-only":"Invite-only"},r={neon:"Postgres",turso:"SQLite (legacy)"},a={b2c:"Your customers use this app (business-to-customer)",b2b:"Other businesses sign up for this (SaaS platform)",internal:"Internal team tool (staff only)"},s=[`**${t.name}** \u2014 ${t.surfaceType} for ${t.audience}`,`Audience: ${a[t.audienceType]??t.audienceType}`,`Primary action: ${t.primaryAction}`,`Access: ${n[t.authModel]??t.authModel} | Database: ${r[t.dbProvider]??t.dbProvider}${t.publicLanding?" | Landing page: Yes":""}${t.language&&t.language!=="English"?` | Language: ${t.language}`:""}`,""];if(e.length>0){s.push("**Included:**");for(let i of e)s.push(` \u2713 ${i.name} \u2014 ${i.description}`)}if(t.integrations.length>0&&(s.push(""),s.push(`**Integrations:** ${t.integrations.join(", ")}`)),o.length>0){s.push(""),s.push("**Available to add:**");for(let i of o)s.push(` \u25CB ${i.name} \u2014 ${i.description}`)}return s.join(`
1764
+ `)}function ia(t){let e=t.match(/\b(?:called|named)\s+["']?([A-Za-z][A-Za-z0-9 ]{1,30})["']?/i);if(e)return eo(e[1]);let o=new Set(["build","create","make","a","an","the","for","me","my","app","application","website","web","tool","system","platform","using","with","and","that","this","want","need","please","can","you","i","mist","mistflow"]),r=t.toLowerCase().replace(/[^a-z0-9\s]/g,"").split(/\s+/).filter(a=>a.length>2&&!o.has(a)).slice(0,3);return r.length===0?"my-app":r.join("-")}function eo(t){return t.toLowerCase().replace(/[^a-z0-9\s]/g,"").trim().replace(/\s+/g,"-")}var aa=["typographic","split-panel","terminal","full-bleed-photo","magazine-hero"],la=["sharp","soft","pill","organic"],ca=["flat","paper-grain","film-grain","scanlines","gradient-mesh","noise","glassmorphic"];function to(t,e,o){return e.includes(t)?t:o}function ee(t){return String(t??"").replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;").replace(/'/g,"&#39;")}function tt(t,e){return typeof t!="string"?e:/^#[0-9a-fA-F]{3}([0-9a-fA-F]{3})?$/.test(t.trim())?t.trim():e}function pa(t){let e=new Set;for(let n of t)n.fonts?.display&&e.add(n.fonts.display),n.fonts?.body&&e.add(n.fonts.body);return e.size===0?"":`https://fonts.googleapis.com/css2?${[...e].map(n=>`family=${n.trim().replace(/\s+/g,"+").replace(/[^A-Za-z0-9+]/g,"")}:wght@400;700`).join("&")}&display=swap`}function Rr(t){let e=(t||"").trim(),o=e.toLowerCase(),n=/mono|courier|code/.test(o)?"ui-monospace, monospace":/serif|garamond|bodoni|fraunces|playfair|lora|sentient|migra|sectra|cormorant|abril|crimson/.test(o)?"Georgia, serif":"system-ui, sans-serif";return e?`"${e.replace(/"/g,"")}", ${n}`:n}function da(t){switch(t){case"sharp":return{card:"0px",button:"0px",chip:"0px"};case"pill":return{card:"20px",button:"999px",chip:"999px"};case"organic":return{card:"4px 24px 4px 24px",button:"24px 4px 24px 4px",chip:"12px 2px 12px 2px"};default:return{card:"10px",button:"8px",chip:"6px"}}}function ua(){let t=`url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='180' height='180'><filter id='n'><feTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='2' stitchTiles='stitch'/><feColorMatrix values='0 0 0 0 0.3 0 0 0 0 0.3 0 0 0 0 0.3 0 0 0 0.55 0'/></filter><rect width='100%25' height='100%25' filter='url(%23n)'/></svg>")`;return`
1765
+ .card-hero { position: relative; overflow: hidden; isolation: isolate; }
1766
+ .card-hero::before {
1767
+ content: "";
1768
+ position: absolute;
1769
+ inset: 0;
1770
+ pointer-events: none;
1771
+ z-index: 0;
1772
+ opacity: 0;
1773
+ mix-blend-mode: overlay;
1774
+ }
1775
+ .card-hero > * { position: relative; z-index: 1; }
1776
+
1777
+ .card-hero.texture-flat::before { opacity: 0; }
1778
+
1779
+ .card-hero.texture-paper-grain::before {
1780
+ opacity: 0.35;
1781
+ background-image: ${t};
1782
+ mix-blend-mode: multiply;
1783
+ }
1784
+ .card-hero.texture-film-grain::before {
1785
+ opacity: 0.5;
1786
+ background-image: ${t};
1787
+ mix-blend-mode: overlay;
1788
+ }
1789
+ .card-hero.texture-noise::before {
1790
+ opacity: 0.25;
1791
+ background-image: ${t};
1792
+ mix-blend-mode: overlay;
1793
+ }
1794
+ .card-hero.texture-scanlines::before {
1795
+ opacity: 0.4;
1796
+ background: repeating-linear-gradient(
1797
+ to bottom,
1798
+ rgba(255,255,255,0.08) 0 1px,
1799
+ transparent 1px 3px
1800
+ );
1801
+ mix-blend-mode: screen;
1802
+ }
1803
+ .card-hero.texture-gradient-mesh::before {
1804
+ opacity: 1;
1805
+ background:
1806
+ radial-gradient(circle at 15% 20%, rgba(255,255,255,0.18), transparent 48%),
1807
+ radial-gradient(circle at 85% 80%, rgba(0,0,0,0.22), transparent 55%),
1808
+ radial-gradient(circle at 60% 10%, rgba(255,255,255,0.12), transparent 40%);
1809
+ mix-blend-mode: normal;
1810
+ }
1811
+ .card-hero.texture-glassmorphic {
1812
+ position: relative;
1813
+ }
1814
+ .card-hero.texture-glassmorphic::before {
1815
+ opacity: 1;
1816
+ background:
1817
+ radial-gradient(circle at 20% 30%, rgba(255,255,255,0.25), transparent 42%),
1818
+ radial-gradient(circle at 80% 70%, rgba(255,255,255,0.15), transparent 45%);
1819
+ backdrop-filter: blur(14px);
1820
+ mix-blend-mode: normal;
1821
+ }
1822
+ `}function ma(t,e,o,n){let r=tt(t.colors?.bg??"","#0f0f0f"),a=tt(t.colors?.fg??"","#f5f5f5"),s=tt(t.colors?.accent??"","#7c9cff"),i=ee(t.name),l=ee(t.hero_headline||t.name),p=ee(t.body_sample||t.summary||""),m=ee(t.cta_text||"Continue"),u=`<div class="hero-eyebrow" style="font-family:${o}">${i.toUpperCase()}</div>`,d=`<h2 class="hero-headline" style="font-family:${e}">${l}</h2>`,b=`<p class="hero-body" style="font-family:${o}">${p}</p>`,T=`<button class="hero-cta" style="background:${s};color:${r};font-family:${o};border-radius:${n.button}">${m}</button>`,w=to(t.hero_treatment,aa,"typographic");if(w==="terminal"){let y=`"${(t.fonts?.display??"JetBrains Mono").replace(/"/g,"")}", ui-monospace, monospace`;return`
1823
+ ${u}
1824
+ <div class="hero-terminal-lines" style="font-family:${y};color:${a}">
1825
+ <div>$ status --all</div>
1826
+ <div>api.service.......<span style="color:${s}">OK</span></div>
1827
+ <div>worker.queue......<span style="color:${s}">OK</span></div>
1828
+ <div>db.primary.......<span style="color:${s}">OK</span></div>
1829
+ </div>
1830
+ ${d}
1831
+ ${b}
1832
+ <div class="hero-cta-row">${T}</div>
1833
+ `}return w==="split-panel"?`
1834
+ <div class="hero-split">
1835
+ <div class="hero-split-left" style="background:${s};color:${r};font-family:${e}">
1836
+ <div class="hero-split-mark" style="font-family:${o};color:${r}">${i[0]??"\xB7"}</div>
1837
+ </div>
1838
+ <div class="hero-split-right">
1839
+ ${u}
1840
+ ${d}
1841
+ ${b}
1842
+ <div class="hero-cta-row">${T}</div>
1843
+ </div>
1844
+ </div>
1845
+ `:w==="full-bleed-photo"?`
1846
+ <div class="hero-photo" style="background: linear-gradient(160deg, ${s}30 0%, ${a}08 35%, ${r} 100%), ${r}">
1847
+ <span class="hero-photo-label" style="font-family:${o};color:${a}">photo slot</span>
1848
+ </div>
1849
+ ${u}
1850
+ ${d}
1851
+ ${b}
1852
+ <div class="hero-cta-row">${T}</div>
1853
+ `:w==="magazine-hero"?`
1854
+ ${u}
1855
+ <h2 class="hero-headline hero-headline-mag" style="font-family:${e}">${l}</h2>
1856
+ <div class="hero-rule" style="background:${a}"></div>
1857
+ <p class="hero-body hero-body-mag" style="font-family:${o}">${p}</p>
1858
+ <div class="hero-byline" style="font-family:${o};color:${a}">\u2014 Volume 01 \xB7 Issue 01</div>
1859
+ <div class="hero-cta-row">${T}</div>
1860
+ `:`
1861
+ ${u}
1862
+ ${d}
1863
+ ${b}
1864
+ <div class="hero-cta-row">${T}</div>
1865
+ `}function Ar(t,e){let o=pa(e),n=e.map(r=>{let a=tt(r.colors?.bg??"","#0f0f0f"),s=tt(r.colors?.fg??"","#f5f5f5"),i=tt(r.colors?.accent??"","#7c9cff"),l=Rr(r.fonts?.display??""),p=Rr(r.fonts?.body??""),m=to(r.shape_lang,la,"soft"),u=to(r.texture,ca,"flat"),d=da(m),b=ma(r,l,p,d),T=ee(r.name),w=ee(r.id),y=ee(r.fonts?.display??""),x=ee(r.fonts?.body??""),M=ee(r.summary||""),J=ee(r.decoration_hint||""),H=(re,q)=>`<div class="card-meta-row"><span class="card-meta-label">${re}</span><span class="card-meta-value">${q}</span></div>`;return` <article class="card" data-direction-id="${w}" style="border-radius:${d.card}">
1866
+ <div class="card-hero texture-${u}" style="background:${a};color:${s}">${b}
1867
+ </div>
1868
+ <div class="card-meta" style="border-top-color:${s}14">
1869
+ <div class="card-meta-title">${T}</div>
1870
+ <p class="card-meta-summary">${M}</p>
1871
+ ${H("Fonts",`${y||"\u2014"} <span class="sep">\xB7</span> ${x||"\u2014"}`)}
1872
+ ${H("Palette",`
1873
+ <span class="card-meta-swatches">
1874
+ <span class="card-meta-swatch" title="${a}" style="background:${a}"></span>
1875
+ <span class="card-meta-swatch" title="${s}" style="background:${s}"></span>
1876
+ <span class="card-meta-swatch" title="${i}" style="background:${i}"></span>
1877
+ </span>
1878
+ `)}
1879
+ ${H("Shape",`<span class="chip" style="border-radius:${d.chip}">${m}</span>`)}
1880
+ ${H("Texture",`<span class="chip" style="border-radius:${d.chip}">${ee(u)}</span>`)}
1881
+ ${J?H("Decoration",ee(J)):""}
1882
+ ${H("Pick with",`<code>${T}</code>`)}
1883
+ </div>
1884
+ </article>`}).join(`
1885
+ `);return`<!DOCTYPE html>
1886
+ <html lang="en">
1887
+ <head>
1888
+ <meta charset="UTF-8" />
1889
+ <meta name="viewport" content="width=device-width,initial-scale=1" />
1890
+ <title>Design directions \u2014 ${ee(t)}</title>
1891
+ <link rel="preconnect" href="https://fonts.googleapis.com" />
1892
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
1893
+ ${o?`<link rel="stylesheet" href="${o}" />`:""}
1894
+ <style>
1895
+ :root {
1896
+ --chrome-bg: #f4f3ef;
1897
+ --chrome-ink: #1a1a1a;
1898
+ --chrome-muted: #6b6b6b;
1899
+ --chrome-rule: #e4e2dc;
1900
+ --card-shadow: 0 1px 2px rgba(0,0,0,0.04), 0 20px 40px -20px rgba(0,0,0,0.12);
1901
+ --chrome-font: "S\xF6hne", ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
1902
+ }
1903
+ * { box-sizing: border-box; }
1904
+ html, body { margin: 0; padding: 0; }
1905
+ body {
1906
+ background: var(--chrome-bg);
1907
+ color: var(--chrome-ink);
1908
+ font-family: var(--chrome-font);
1909
+ -webkit-font-smoothing: antialiased;
1910
+ line-height: 1.5;
1911
+ min-height: 100vh;
1912
+ }
1913
+ .page { max-width: 1280px; margin: 0 auto; padding: 56px 28px 96px; }
1914
+ header.page-header { margin-bottom: 48px; max-width: 820px; }
1915
+ .eyebrow {
1916
+ font-size: 12px; letter-spacing: 0.14em; text-transform: uppercase;
1917
+ color: var(--chrome-muted); font-weight: 500;
1918
+ display: inline-flex; align-items: center; gap: 10px;
1919
+ }
1920
+ .eyebrow-dot { width: 6px; height: 6px; border-radius: 999px; background: #111; display: inline-block; }
1921
+ h1.page-title {
1922
+ font-size: clamp(28px, 3.6vw, 42px); font-weight: 600; letter-spacing: -0.02em;
1923
+ line-height: 1.1; margin: 16px 0 14px;
1924
+ }
1925
+ p.page-sub { color: var(--chrome-muted); font-size: 16px; margin: 0; max-width: 680px; }
1926
+ p.page-sub strong { color: var(--chrome-ink); font-weight: 600; }
1927
+ .grid {
1928
+ display: grid;
1929
+ grid-template-columns: repeat(2, minmax(0, 1fr));
1930
+ gap: 28px;
1931
+ }
1932
+ @media (max-width: 920px) {
1933
+ .grid { grid-template-columns: 1fr; gap: 20px; }
1934
+ .page { padding: 36px 18px 72px; }
1935
+ }
1936
+
1937
+ /* \u2500\u2500 Card shell \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
1938
+ .card {
1939
+ background: #fff;
1940
+ box-shadow: var(--card-shadow);
1941
+ overflow: hidden;
1942
+ display: flex;
1943
+ flex-direction: column;
1944
+ transition: transform 180ms ease, box-shadow 180ms ease;
1945
+ }
1946
+ .card:hover {
1947
+ transform: translateY(-2px);
1948
+ box-shadow: 0 1px 2px rgba(0,0,0,0.05), 0 28px 56px -20px rgba(0,0,0,0.18);
1949
+ }
1950
+
1951
+ /* \u2500\u2500 Hero block \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
1952
+ .card-hero {
1953
+ padding: 40px 36px 32px;
1954
+ min-height: 380px;
1955
+ display: flex;
1956
+ flex-direction: column;
1957
+ gap: 12px;
1958
+ }
1959
+ .hero-eyebrow {
1960
+ font-size: 11px;
1961
+ letter-spacing: 0.18em;
1962
+ opacity: 0.7;
1963
+ font-weight: 500;
1964
+ }
1965
+ .hero-headline {
1966
+ font-size: clamp(30px, 2.8vw, 42px);
1967
+ line-height: 1.05;
1968
+ letter-spacing: -0.01em;
1969
+ margin: 0;
1970
+ font-weight: 700;
1971
+ }
1972
+ .hero-body {
1973
+ font-size: 15px;
1974
+ line-height: 1.55;
1975
+ opacity: 0.85;
1976
+ margin: 0;
1977
+ max-width: 44ch;
1978
+ }
1979
+ .hero-cta-row {
1980
+ margin-top: auto;
1981
+ padding-top: 20px;
1982
+ display: flex;
1983
+ align-items: center;
1984
+ gap: 12px;
1985
+ flex-wrap: wrap;
1986
+ }
1987
+ .hero-cta {
1988
+ border: 0;
1989
+ padding: 12px 22px;
1990
+ font-size: 14px;
1991
+ font-weight: 600;
1992
+ cursor: default;
1993
+ letter-spacing: 0.01em;
1994
+ }
1995
+
1996
+ /* \u2500\u2500 Hero treatment: terminal \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
1997
+ .hero-terminal-lines {
1998
+ font-size: 13px;
1999
+ line-height: 1.7;
2000
+ opacity: 0.85;
2001
+ padding: 12px 14px;
2002
+ border: 1px solid currentColor;
2003
+ border-opacity: 0.3;
2004
+ background: rgba(0,0,0,0.18);
2005
+ margin-bottom: 6px;
2006
+ }
2007
+ .hero-terminal-lines > div { white-space: pre; }
2008
+
2009
+ /* \u2500\u2500 Hero treatment: split-panel \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
2010
+ .hero-split {
2011
+ display: flex;
2012
+ flex: 1;
2013
+ margin: -40px -36px -32px;
2014
+ min-height: 380px;
2015
+ }
2016
+ .hero-split-left {
2017
+ width: 32%;
2018
+ display: flex;
2019
+ align-items: center;
2020
+ justify-content: center;
2021
+ padding: 24px;
2022
+ }
2023
+ .hero-split-mark {
2024
+ font-size: 96px;
2025
+ font-weight: 800;
2026
+ line-height: 1;
2027
+ }
2028
+ .hero-split-right {
2029
+ flex: 1;
2030
+ padding: 40px 36px 32px;
2031
+ display: flex;
2032
+ flex-direction: column;
2033
+ gap: 12px;
2034
+ }
2035
+
2036
+ /* \u2500\u2500 Hero treatment: full-bleed-photo \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
2037
+ .hero-photo {
2038
+ height: 160px;
2039
+ margin: -40px -36px 20px;
2040
+ position: relative;
2041
+ display: flex;
2042
+ align-items: flex-end;
2043
+ justify-content: flex-start;
2044
+ padding: 18px;
2045
+ }
2046
+ .hero-photo-label {
2047
+ font-size: 10px;
2048
+ letter-spacing: 0.2em;
2049
+ text-transform: uppercase;
2050
+ opacity: 0.4;
2051
+ }
2052
+
2053
+ /* \u2500\u2500 Hero treatment: magazine-hero \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
2054
+ .hero-headline-mag {
2055
+ font-size: clamp(34px, 3.2vw, 48px);
2056
+ font-weight: 700;
2057
+ letter-spacing: -0.02em;
2058
+ }
2059
+ .hero-rule {
2060
+ height: 2px;
2061
+ width: 48px;
2062
+ margin: 8px 0;
2063
+ opacity: 0.8;
2064
+ }
2065
+ .hero-body-mag {
2066
+ font-style: italic;
2067
+ max-width: 40ch;
2068
+ }
2069
+ .hero-byline {
2070
+ font-size: 11px;
2071
+ letter-spacing: 0.16em;
2072
+ text-transform: uppercase;
2073
+ opacity: 0.6;
2074
+ margin-top: 4px;
2075
+ }
2076
+
2077
+ ${ua()}
2078
+
2079
+ /* \u2500\u2500 Card meta footer \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
2080
+ .card-meta {
2081
+ padding: 22px 28px 26px;
2082
+ border-top: 1px solid var(--chrome-rule);
2083
+ display: flex;
2084
+ flex-direction: column;
2085
+ gap: 10px;
2086
+ background: #fff;
2087
+ font-family: var(--chrome-font);
2088
+ }
2089
+ .card-meta-title {
2090
+ font-size: 15px;
2091
+ font-weight: 600;
2092
+ color: var(--chrome-ink);
2093
+ letter-spacing: -0.01em;
2094
+ }
2095
+ .card-meta-summary {
2096
+ font-size: 13px;
2097
+ color: var(--chrome-muted);
2098
+ line-height: 1.5;
2099
+ margin: -2px 0 6px;
2100
+ }
2101
+ .card-meta-row {
2102
+ display: flex;
2103
+ align-items: center;
2104
+ gap: 14px;
2105
+ font-size: 13px;
2106
+ }
2107
+ .card-meta-label {
2108
+ flex-shrink: 0;
2109
+ width: 82px;
2110
+ color: var(--chrome-muted);
2111
+ font-size: 11px;
2112
+ letter-spacing: 0.1em;
2113
+ text-transform: uppercase;
2114
+ font-weight: 500;
2115
+ }
2116
+ .card-meta-value {
2117
+ color: var(--chrome-ink);
2118
+ font-size: 13px;
2119
+ display: inline-flex;
2120
+ align-items: center;
2121
+ gap: 8px;
2122
+ }
2123
+ .card-meta-value .sep { color: var(--chrome-muted); margin: 0 2px; }
2124
+ .card-meta-swatches {
2125
+ display: inline-flex;
2126
+ gap: 6px;
2127
+ }
2128
+ .card-meta-swatch {
2129
+ width: 20px;
2130
+ height: 20px;
2131
+ border-radius: 999px;
2132
+ display: inline-block;
2133
+ box-shadow: inset 0 0 0 1px rgba(0,0,0,0.08);
2134
+ }
2135
+ .chip {
2136
+ display: inline-block;
2137
+ background: #f0efe9;
2138
+ padding: 3px 10px;
2139
+ font-size: 11px;
2140
+ letter-spacing: 0.04em;
2141
+ font-weight: 500;
2142
+ color: var(--chrome-ink);
2143
+ }
2144
+ .card-meta code {
2145
+ font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
2146
+ background: #f0efe9;
2147
+ padding: 2px 8px;
2148
+ border-radius: 6px;
2149
+ font-size: 12px;
2150
+ color: #222;
2151
+ }
2152
+ footer.page-footer {
2153
+ margin-top: 56px;
2154
+ padding-top: 28px;
2155
+ border-top: 1px solid var(--chrome-rule);
2156
+ color: var(--chrome-muted);
2157
+ font-size: 14px;
2158
+ line-height: 1.6;
2159
+ display: flex;
2160
+ gap: 28px;
2161
+ flex-wrap: wrap;
2162
+ align-items: flex-start;
2163
+ justify-content: space-between;
2164
+ }
2165
+ footer.page-footer .escape {
2166
+ padding: 10px 16px;
2167
+ border: 1px dashed var(--chrome-rule);
2168
+ border-radius: 10px;
2169
+ background: #fff;
2170
+ color: var(--chrome-ink);
2171
+ font-size: 13px;
2172
+ }
2173
+ footer .escape code {
2174
+ font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
2175
+ font-size: 12px;
2176
+ background: #f0efe9;
2177
+ padding: 2px 6px;
2178
+ border-radius: 4px;
2179
+ }
2180
+ </style>
2181
+ </head>
2182
+ <body>
2183
+ <div class="page">
2184
+ <header class="page-header">
2185
+ <div class="eyebrow"><span class="eyebrow-dot"></span>Mistflow \xB7 design direction</div>
2186
+ <h1 class="page-title">How should <strong>${ee(t)}</strong> feel?</h1>
2187
+ <p class="page-sub">Each card commits to a different extreme \u2014 different fonts, colors, hero layout, corner radius, texture, and voice. Pick one by telling your coding assistant its name, or describe your own.</p>
2188
+ </header>
2189
+
2190
+ <div class="grid">
2191
+ ${n}
2192
+ </div>
2193
+
2194
+ <footer class="page-footer">
2195
+ <div>
2196
+ <div><strong>How to pick</strong></div>
2197
+ <div>Reply to your assistant with the direction name \u2014 e.g. &ldquo;pick <em>Morning Paper</em>.&rdquo; The final DESIGN.md will honor the fonts, palette, layout, and voice you see here.</div>
2198
+ </div>
2199
+ <div class="escape">
2200
+ <strong>None fit?</strong> Tell your assistant <code>describe your own</code> and give a short description.
2201
+ </div>
2202
+ </footer>
2203
+ </div>
2204
+ </body>
2205
+ </html>
2206
+ `}var Qt="__mistflow_url_choice__",ka=600*1e3;function jr(){let t=te(Ce(),".mistflow","confirm-secret");if(St(t))try{return Buffer.from(Er(t,"utf-8").trim(),"hex")}catch{}let e=va(32);return Xt(te(Ce(),".mistflow"),{recursive:!0}),Zt(t,e.toString("hex"),{mode:384}),e}function Dr(t){return wa("sha256").update(t.trim().toLowerCase()).digest("hex").slice(0,16)}function Sa(t,e){let o={cwd:t,d:Dr(e),exp:Date.now()+ka},n=Buffer.from(JSON.stringify(o)).toString("base64url"),r=Nr("sha256",jr()).update(n).digest("base64url");return`${n}.${r}`}function Pa(t,e,o){let n=t.split(".");if(n.length!==2)return!1;let[r,a]=n,s=Nr("sha256",jr()).update(r).digest("base64url"),i=Buffer.from(a),l=Buffer.from(s);if(i.length!==l.length||!xa(i,l))return!1;try{let p=JSON.parse(Buffer.from(r,"base64url").toString("utf-8"));return!(typeof p.exp!="number"||Date.now()>p.exp||p.cwd!==e||p.d!==Dr(o))}catch{return!1}}function Ia(t){let e=t,o=Ce(),n=!1;for(let r=0;r<64;r++){if(St(te(e,"mistflow.json")))return"mistflow";if(!n&&St(te(e,"package.json"))&&(n=!0),e===o)break;let a=ya(e);if(a===e)break;e=a}return n?"foreign":"none"}function _a(t){let e=Ce(),o=t.replace(/\/+$/,"");if(o===e||o==="/"||o===""||o==="/tmp"||o==="/private/tmp")return!0;let n=["Desktop","Documents","Downloads"];for(let r of n)if(o===te(e,r))return!0;return!1}var Ta=I.object({description:I.string().optional().describe("App description or modification request. Required for the first call; omit on follow-up polls where only conversationId is passed. "),projectPath:I.string().min(1).describe("REQUIRED. Absolute path to the user's current working directory \u2014 where the Mistflow app will be scaffolded. Pass the directory the user is actually working in (e.g. /Users/alice/projects). Do NOT pass '/', '~', $HOME, Desktop, Documents, Downloads, or /tmp \u2014 the tool will refuse to scaffold at those locations. If you are unsure of the user's working directory, ask them before calling this tool."),conversationId:I.string().optional().describe("Returned by a previous mist_plan call with status 'clarify' or 'running'. Pass it back alone (no description) to poll an in-flight discovery call; pass with answers to submit responses."),answers:I.record(I.string()).optional().describe("User's answers to the clarifying questions from the previous round. Keys are the questions, values are the answers."),existingPlan:I.record(I.unknown()).optional().describe("If provided, modifies this existing plan instead of creating a new one. Pass the current plan object from mistflow.json."),existingPlanId:I.string().optional().describe("Alternative to existingPlan \u2014 pass the planId from a previous mist_plan call to modify that plan."),templateToken:I.string().optional().describe("Fork from a shared template. Pass the share token (from a mistflow.ai/t/... URL) to clone that project's plan into your workspace."),remixDescription:I.string().optional().describe("Optional remix request when forking a template. Describes how you want the template to be different. E.g. 'Make it for tracking books instead of habits, add a search feature.' Only used with templateToken."),autonomous:I.boolean().optional().describe("Skip clarifying questions and generate the plan immediately"),landingDesign:I.string().optional().describe("ID of a curated landing page design for the hero section. When set, the design's detailed blueprint (colors, fonts, layout, animations) is injected during the landing page implementation step. Use mist_project with action='landing-designs' to browse available landing designs."),appStyle:I.string().optional().describe("ID of a full-app style (e.g. 'stripe', 'linear', 'vercel', 'notion'). When set, the style's color palette, typography, component specs, shadows, and layout rules are injected during ALL implementation steps for consistent brand-quality design across every page. Use mist_project with action='app-styles' to browse available app styles."),language:I.string().optional().describe("UI language for the app. All user-facing text, labels, buttons, and content will be generated in this language. Use the language name in English (e.g. 'Spanish', 'French', 'Arabic', 'Japanese'). Defaults to English if not specified."),brandMentioned:I.boolean().optional().describe("Set to true ONLY when the user's original request explicitly invoked Mistflow by name (e.g. 'build me a CRM using mist', 'make a todo app with mistflow'). Skips the existing-project confirmation gate because the user clearly wants Mistflow. Do NOT set this for generic 'build me X' requests. Do NOT infer this \u2014 only set it when the user literally typed 'mist' or 'mistflow'."),confirmToken:I.string().optional().describe("The token returned in a previous mist_plan response with status 'confirm_new_project'. Only pass this AFTER asking the user via AskUserQuestion and they chose to scaffold a new Mistflow app in an existing-project directory. The token is bound to the projectPath and description \u2014 you must pass the SAME description AND projectPath on the retry."),urlChoice:I.string().optional().describe("The user's answer to the 'Your app URL' question from a previous mist_plan response. Pass JUST the subdomain (e.g. 'nutrition-tracker'), not the full URL or the option label. If the user kept the default suggestion, pass the suggested subdomain verbatim. If they typed a custom URL like 'myapp.mistflow.app', pass just 'myapp'. Pass this as a top-level parameter \u2014 do NOT nest it inside answers. The answers-dict magic key is deprecated and unreliable."),designConversationId:I.string().optional().describe("Returned by a previous mist_plan call with status 'design_clarify'. Pass it back on the follow-up call together with designDirection to finalize the plan's DESIGN.md."),designDirection:I.object({id:I.string().optional(),name:I.string().optional(),summary:I.string().optional(),heroHeadline:I.string().optional(),ctaText:I.string().optional(),bodySample:I.string().optional(),fontsHint:I.string().optional(),fonts:I.object({display:I.string(),body:I.string()}).partial().optional(),colorMood:I.string().optional(),colors:I.object({bg:I.string(),fg:I.string(),accent:I.string()}).partial().optional(),heroTreatment:I.string().optional(),shapeLang:I.string().optional(),texture:I.string().optional(),decorationHint:I.string().optional(),custom:I.string().optional()}).passthrough().optional().describe("The creative direction the user picked from a previous 'design_clarify' response. Pass the FULL direction object the user chose (all fields from the 'directions' array). If the user wrote their own description instead of picking one, pass { custom: '<their description>' } and omit the other fields.")});function Ra(t){let e=[[/payment/i,"Payments"],[/database/i,"Database"],[/auth|sign.?up|login|access/i,"Access"],[/landing.?page/i,"Landing page"],[/who.*using|user|role/i,"Users"],[/design|theme|style/i,"Design"],[/deploy/i,"Deploy"],[/domain/i,"Domain"],[/notification/i,"Notify"],[/email/i,"Email"],[/mobile|responsive/i,"Mobile"],[/integrat/i,"Integration"],[/field|info|propert|detail|contain/i,"Item shape"],[/view|layout|board|grid|list|timeline/i,"View"],[/scope|how many|one.*or.*many|multi/i,"Scope"],[/share|read.?only|viewer|stakeholder/i,"Sharing"],[/workflow|status|state|move|stage|pipeline/i,"Workflow"],[/avoid|bloat|simple|complex|minimal/i,"Constraints"],[/time.*period|quarter|month|sprint/i,"Time periods"],[/swimlane|column|group|categor/i,"Structure"]];for(let[n,r]of e)if(n.test(t))return r;return t.replace(/[?.,!]/g,"").split(/\s+/).filter(n=>!["what","how","do","does","is","are","the","a","an","would","should","you","your","for","this","that","to","of","or","and","want","like","prefer"].includes(n.toLowerCase())).slice(0,2).join(" ").slice(0,12)||"Option"}function Aa(t){let e=te(Ce(),".mistflow","plans",`${t}.json`);if(!St(e))return null;try{return JSON.parse(Er(e,"utf-8")).plan??null}catch{return null}}async function Ca(t){for(let n=0;n<60;n++){try{let r=await _n(t.design_conversation_id);if(r.status==="ready")return{status:"design_clarify",design_conversation_id:t.design_conversation_id,directions:r.directions,plan:r.plan,methodology:r.methodology};if(r.status==="failed")return{status:"ready",plan:r.plan,methodology:r.methodology}}catch(r){let a=r instanceof Error?r.message:String(r);if(a.toLowerCase().includes("not found"))return{status:"ready",plan:t.plan,methodology:t.methodology};console.error(`[plan] directions poll attempt ${n+1} failed: ${a}`)}await new Promise(r=>setTimeout(r,2e3))}return console.error("[plan] directions poll exhausted, falling back to ready"),{status:"ready",plan:t.plan,methodology:t.methodology}}async function Ea(t,e){let{description:o,projectPath:n,conversationId:r,answers:a,existingPlan:s,existingPlanId:i,templateToken:l,remixDescription:p,autonomous:m,language:u,landingDesign:d,appStyle:b,brandMentioned:T,confirmToken:w,urlChoice:y,designConversationId:x,designDirection:M}=t;if(r&&!o&&!a&&!x&&!M&&!s&&!i&&!l)try{let h=await $t(r);return h.status==="clarify_pending"?c(JSON.stringify({status:"running",conversationId:r,phase:"generating_questions",nextAction:`Still generating. Call mist_plan with { projectPath, conversationId: "${r}" } again in ~10-15s.`})):c(JSON.stringify(h))}catch(h){let g=h instanceof Error?h.message:String(h);return c(`Could not poll plan conversation '${r}': ${g}`,!0)}let J=o??"";if(!J.trim()&&!r&&!s&&!i&&!l)return c("mist_plan requires a `description` for the first call (the user's app idea). Pass { conversationId } alone to poll an in-flight call, or along with { answers } to submit responses.",!0);let H=s;if(!H&&i&&(H=Aa(i)??void 0,!H))return c("Your previous plan is no longer available. Please describe your app again to generate a new plan.",!0);let re=r;if(!G())return c("No Mistflow credentials found. Run mist_setup to connect your account.",!0);let q;if(!re&&!H&&!l){if(!ba(n))return c(`projectPath must be an absolute path \u2014 received '${n}'. Pass the full absolute path to the user's project directory (e.g. /Users/alice/projects/my-app).`,!0);let h=Ia(n);if(h!=="mistflow"&&_a(n))return c(JSON.stringify({status:"unsafe_cwd",projectPath:n,instruction:[`The projectPath you passed (${n}) is a system-level directory (home root, Desktop, Documents, Downloads, or /tmp).`,"Scaffolding here would drop node_modules and a git repo directly at that location, which is messy and hard to clean up.","MANDATORY: Before calling mist_plan again:"," 1. Pick a subfolder name based on the app description (e.g. 'nutrition-tracker', 'habit-app').",` 2. Create the subfolder at ${n}/<subfolder-name>.`," 3. Call mist_plan again with the SAME description and projectPath set to the new subfolder.","Do NOT ask the user where to put the project \u2014 just pick a sensible subfolder name from their description."].join(`
2207
+ `)}),!0);if(h==="foreign"&&!T){if(!(w?Pa(w,n,J):!1)){let S=Sa(n,J);return c(JSON.stringify({status:"confirm_new_project",projectPath:n,description:J,confirmToken:S,askUserQuestion:{question:"You're inside an existing project directory. Do you want to scaffold a new Mistflow app here, or edit the existing codebase directly?",header:"Scope",options:[{label:"Scaffold a new Mistflow app in a subdirectory",description:"Creates a fresh project in this folder without touching the existing code."},{label:"Edit this existing codebase directly",description:"Cancel Mistflow. Handle the request by editing the current project's files."}],multiSelect:!1},instruction:["The user is inside an existing project (package.json found up the directory tree, no mistflow.json) and did NOT explicitly invoke Mistflow by name.","MANDATORY: Use the AskUserQuestion tool with the provided askUserQuestion to confirm their intent before calling mist_plan again.","If they pick 'Scaffold a new Mistflow app in a subdirectory', call mist_plan again with the SAME description and confirmToken set to the token returned above.","If they pick 'Edit this existing codebase directly', DO NOT call mist_plan again. Fulfill their request by editing files directly in the current project.",w?"The previous confirmToken was invalid, expired, or did not match the current directory/description. Use the fresh token above.":""].filter(Boolean).join(`
2208
+ `)}))}q="Note: You're inside an existing project. Mistflow will create the new app in a subdirectory. It won't modify this codebase."}else h==="foreign"&&T&&(q="Note: You're inside an existing project. Mistflow will create the new app in a subdirectory. It won't modify this codebase.")}if(l)try{if(!(await Fn(l)).plan)return c("This template has no plan to fork. Try a different template.",!0);let g=await qn(l),S=g.plan,P="";if(p&&g.has_source)try{let X=await Lt(g.plan,p),U=X.plan??X,Et=X.diff,F=U?.steps??[],un=new Set([...(Et?.added??[]).map(le=>le.number),...(Et?.modified??[]).map(le=>le.number)]),Pe=F.map(le=>{let Cs=le.number;return un.has(Cs)?{...le,status:"pending"}:{...le,status:"completed",source:"forked"}});U.steps=Pe,S=U;let Me=Pe.filter(le=>le.status==="pending").length;P=` Remixed: ${Pe.filter(le=>le.status==="completed").length} steps unchanged, ${Me} steps need re-implementation.`}catch(X){console.error("[plan] Remix failed, using original plan:",X),P=" (Remix failed \u2014 using original plan. You can modify it later.)"}let W=Cr(),L=te(Ce(),".mistflow","plans");Xt(L,{recursive:!0}),Zt(te(L,`${W}.json`),JSON.stringify({plan:S,projectId:g.id,sourceDeploymentId:g.source_deployment_id,forkToken:g.fork_token,requiredEnvVars:g.required_env_vars,dbProvider:g.db_provider}));let Y=S?.name??"forked-app",Q=g.has_source,dt=Q?"Source code will be restored during init. Run init promptly \u2014 the download token expires in 1 hour.":"",Oe=g.deploy_url?` Instant deploy started \u2014 your app will be live at ${g.deploy_url} in under a minute.`:"";return c(JSON.stringify({planId:W,forkedFrom:g.forked_from,projectId:g.id,hasSource:Q,deployUrl:g.deploy_url,message:`Forked "${g.forked_from}" into your workspace.${P}${Oe} ${dt} NEXT: Call mist_init, name='${Y}', and planId='${W}' to create the project now.`}))}catch(h){let g=h instanceof Error?h.message:"Failed to fork template";return c(g,!0)}if(H){let h;try{h=await Lt(H,J)}catch(L){let Y=L instanceof Error?L.message:"Failed to modify plan";return c(Y,!0)}let g=h.plan,S=h.diff,P=[];if(S?.added?.length){let L=S.added.map(Y=>Y.title);P.push(`Added ${L.length} step(s): ${L.join(", ")}`)}if(S?.removed?.length){let L=S.removed.map(Y=>Y.title);P.push(`Removed ${L.length} step(s): ${L.join(", ")}`)}if(S?.modified?.length){let L=S.modified.map(Y=>Y.title);P.push(`Modified ${L.length} step(s): ${L.join(", ")}`)}let W=P.length>0?P.join(". "):"No changes detected.";return c(JSON.stringify({plan:g,diff:S,message:`Plan modified. ${W}. Update mistflow.json with the new plan, then continue with mist_implement.`}))}let v=y?.trim()||void 0,$=a;if(!v&&a&&Qt in a&&(v=a[Qt]),a&&Qt in a){let{[Qt]:h,...g}=a;$=Object.keys(g).length>0?g:void 0}if(v&&(v=v.replace(/^Keep\s+/i,"").replace(/\s*\(Recommended\)\s*$/i,"").replace(/\.mistflow\.app.*$/i,"").trim()||void 0),v){let h=v.toLowerCase().replace(/\s+/g,"-");/^[a-z0-9][a-z0-9-]{1,30}[a-z0-9]$/.test(h)?v=h:(console.error(`[mist_plan] Discarding urlChoice '${v}' \u2014 does not look like a subdomain. Backend will auto-generate.`),v=void 0)}let K;if(M){K={...M};let h={fontsHint:"fonts_hint",colorMood:"color_mood",heroHeadline:"hero_headline",ctaText:"cta_text",bodySample:"body_sample",heroTreatment:"hero_treatment",shapeLang:"shape_lang",decorationHint:"decoration_hint"};for(let[g,S]of Object.entries(h))M[g]!==void 0&&K[S]===void 0&&(K[S]=M[g])}let ne=a?"Generating plan with your answers (LLM call)":x?"Finalizing design direction":"Thinking through discovery questions",we=e?Yt(e.server,e.progressToken,()=>ne):{stop:()=>{}};e&&(e.cleanup=()=>we.stop());let C;try{re&&!$&&!x&&!H&&!i?C=await $t(re):C=await Tn(J,{conversationId:re,answers:$,autonomous:m,language:u,designConversationId:x,designDirection:K})}catch(h){we.stop();let g=h instanceof Error?h.message:"Failed to generate plan";return c(g,!0)}if(C.status==="clarify_pending"){we.stop();let h=C;return c(JSON.stringify({status:"running",conversationId:h.conversation_id,phase:"generating_questions",nextAction:`Discovery questions are generating (backend Sonnet call). Call mist_plan with { projectPath, description: "", conversationId: "${h.conversation_id}" } in ~10-15s to poll. Do NOT re-send description or answers \u2014 just the conversationId.`}))}if(C.status==="design_clarify_pending"&&(ne="Generating creative design directions",C=await Ca(C)),we.stop(),C.status==="clarify"){let h=C.reflection||"",g=C.suggestedName||"",S=C.suggestedFeatures??[],P=C.questions??[],W=P.some(F=>Array.isArray(F.options)&&typeof F.options[0]=="object"&&F.options[0]?.label),L={primaryActor:"Users",primaryAction:"Core action",surfaceType:"App type",audienceType:"Audience",multiRole:"Roles",publicLanding:"Landing page",realMoney:"Payments",scheduling:"Scheduling",authModel:"Access",dbProvider:"Database",integrations:"Integration",entityShape:"Item shape",coreView:"View",scope:"Scope",sharing:"Sharing",workflow:"Workflow",constraints:"Constraints",domain:"Product"},Y=P.map(F=>{let un=F.decisionKey&&L[F.decisionKey]||Ra(F.question),Pe;return W&&Array.isArray(F.options)?Pe=F.options.map(Me=>({label:Me.label,description:Me.description??""})):Array.isArray(F.options)?Pe=F.options.map((Me,xo)=>({label:xo===0?`${Me} (Recommended)`:String(Me),description:F.why??""})):Pe=[{label:"Yes (Recommended)",description:F.why??""},{label:"No",description:""}],{question:F.question,header:un,options:Pe,multiSelect:!1}}),dt=C.decisions?.audienceType??null,Oe=S.length>0?_r(J,{primaryActor:null,primaryAction:null,surfaceType:null,audienceType:dt,multiRole:null,publicLanding:null,realMoney:null,scheduling:null,authModel:null,dbProvider:null,integrations:null},{suggestedName:g,suggestedFeatures:S,language:u}):null,X=Oe?Tr(Oe):"",U=eo(g||"my-app").slice(0,32);try{let F=await Pn(U);!F.available&&F.suggestion&&(U=F.suggestion)}catch{}X&&(X+=`
2209
+
2210
+ **Your app URL:** https://${U}.mistflow.app`);let Et={question:`Your app will be at ${U}.mistflow.app \u2014 want to customize the URL?`,header:"URL",options:[{label:`Keep ${U}.mistflow.app (Recommended)`,description:"This URL is available"},{label:"Choose a different URL",description:"Type your preferred subdomain"}],multiSelect:!1};return Y.push(Et),c(JSON.stringify({status:"clarify",conversation_id:C.conversation_id,questions:P,questionCount:P.length,suggestedFeatures:S,suggestedName:g,suggestedSubdomain:U,reflection:h,briefText:X,askUserQuestions:Y,planTimingHint:"After the user answers all questions, generating the actual plan takes about 60-90 seconds (backend LLM). Narrate this explicitly before the next mist_plan call.",instruction:[...q?[q,""]:[],h?`${h}
2211
+ `:"",X?`Here's what I'd build:
2212
+
2213
+ ${X}
2214
+ `:"",`I have ${P.length} quick question${P.length===1?"":"s"} to pin down the details.`,"","MANDATORY: Use the AskUserQuestion tool to present these questions. Do NOT present them as text.","","IF you have access to the AskUserQuestion tool (Claude Code):"," Use it to present questions with selectable options. Pass the 'askUserQuestions' array directly.","","OTHERWISE (Cursor, Codex, or other hosts):"," Present each question conversationally with the options listed.","","BEFORE calling mist_plan for the final plan generation, tell the user:"," 'Generating your plan now. This usually takes about 60\u201390 seconds \u2014 I'm designing the data model, laying out pages, and writing build steps.'","This primes the user for the silent wait. Do NOT call mist_plan without saying this first.","","Once you have all answers, call mist_plan again with:",` conversationId: "${C.conversation_id}"`,' answers: { "<question text>": "<user answer>", ... }'," description: (same description as before)",' urlChoice: "<the URL subdomain the user picked>" \u2190 pass as a top-level param, NOT inside answers',"","NOTE: You may receive another 'clarify' response with follow-up questions if the user's answers","revealed new ambiguity. This is normal \u2014 relay those questions too. Keep going until you get 'ready'.","","IMPORTANT: For the URL question, pass the answer as the top-level 'urlChoice' parameter (not inside answers).",`If the user keeps the default, set urlChoice: "${U}".`,'If they type a custom URL, set urlChoice to just the subdomain part (e.g. "myapp" for "myapp.mistflow.app"). Do not include ".mistflow.app" or any "Keep X" label text \u2014 the tool strips those but passing just the subdomain is cleanest.'].join(`
2215
+ `)}))}if(C.status==="design_clarify"){let h=C.directions??[],g=C.plan.name??"your app",S;try{let Q=h.map(U=>({id:U.id,name:U.name,summary:U.summary,hero_headline:U.hero_headline,cta_text:U.cta_text,body_sample:U.body_sample,fonts:U.fonts,colors:U.colors,hero_treatment:U.hero_treatment,shape_lang:U.shape_lang,texture:U.texture,decoration_hint:U.decoration_hint})),dt=Ar(g,Q),Oe=te(n,".mistflow");Xt(Oe,{recursive:!0});let X=te(Oe,"design-directions.html");Zt(X,dt,"utf-8"),S=X}catch(Q){console.error(`[mist_plan] design-directions preview render failed: ${Q instanceof Error?Q.message:String(Q)}`)}let P=h.map(Q=>({label:Q.name,description:`${Q.summary} \u2014 ${Q.fonts?.display??""} + ${Q.fonts?.body??""}`}));P.push({label:"Describe your own direction",description:"Skip the proposed options and give me a short description of how the app should feel."});let W={question:`${g} is planned. Now pick the creative direction \u2014 this shapes fonts, colors, and overall feel.`,header:"Design",options:P,multiSelect:!1},L=S?`A visual preview of all ${h.length} directions has been written to ${S}. Open it in the browser \u2014 each card is rendered in its direction's own fonts + palette so the user can see what they're picking.`:"No visual preview rendered (see server logs). The AskUserQuestion options below carry fonts + mood so the user can still pick.",Y=S?`open "${S}"`:"";return c(JSON.stringify({status:"design_clarify",designConversationId:C.design_conversation_id,directions:h,previewPath:S,askUserQuestion:W,instruction:[`The plan for "${g}" is ready. I've proposed ${h.length} creative directions \u2014 each commits to a specific aesthetic (fonts, colors, voice).`,"",L,Y?`Run this command for the user: ${Y}`:"","","MANDATORY: After opening the preview, use the AskUserQuestion tool to present the directionQuestion above. Do NOT present it as text.","","Once the user picks a direction, call mist_plan again with:"," description: (same description as before)",` designConversationId: "${C.design_conversation_id}"`," designDirection: <the full direction object from the 'directions' array that the user picked>","","IF the user picks 'Describe your own direction':"," Ask them a short open question ('How should the app feel? Any fonts or colors in mind?')."," Then call mist_plan with designDirection: { custom: '<their description>' } and the same designConversationId.","","The next mist_plan call takes ~10-20s \u2014 that's the LLM generating the final DESIGN.md with the picked direction. Tell the user 'Locking in the direction now \u2014 this takes about 15 seconds.' before calling."].filter(Boolean).join(`
2216
+ `)}))}let E=C.plan,he=E.name??"Untitled App",Se=C.methodology,j=E.steps;if(!Array.isArray(j)||j.length===0)return c("Plan generation incomplete \u2014 the plan is missing implementation steps. Please call mist_plan again with the same description to retry.",!0);let oe=E.publicPages;if(!oe||Array.isArray(oe)&&oe.length===0){let h=E.pages,g=j.some(P=>typeof P.name=="string"&&P.name.toLowerCase().includes("landing")||typeof P.title=="string"&&P.title.toLowerCase().includes("landing")),S=Array.isArray(h)&&h.some(P=>P.path==="/"||P.route==="/");g||S?oe=["/","/pricing"]:oe=["/"]}let lt=E.primaryAction;if(!lt){let h=E.features;if(Array.isArray(h)&&h.length>0){let S=h.find(W=>typeof W.priority=="string"&&W.priority.toLowerCase()==="must-have")??h[0];lt={entity:S.name??S.title??"item",action:"create",fromPage:"/dashboard"}}}let Ne=E.nonNegotiables;(!Ne||Array.isArray(Ne)&&Ne.length===0)&&(Ne=["Landing page renders correctly at / with content (not a redirect)","Core user action works end-to-end (create entity, see it in list)"]);let B=Cr(),Be=te(Ce(),".mistflow","plans");Xt(Be,{recursive:!0});try{let g=Date.now();for(let S of[Be,te(Ce(),".mistflow","mockup-state")])if(St(S))for(let P of ha(S))try{let W=te(S,P),L=ga(W).mtimeMs;g-L>6048e5&&fa(W)}catch{}}catch{}let se;if(d){let h=zt(d);h?se=h.id:console.error(`Landing design '${d}' not found \u2014 ignoring. Use mist_project action='landing-designs' to browse available landing designs.`)}let je=b||void 0,ae=!!se,ct=j.some(h=>{let g=`${h.name??h.title} ${h.description??""}`.toLowerCase();return g.includes("landing")||g.includes("hero")||g.includes("marketing")||g.includes("homepage")}),ge=!ae&&ct?cr(J,{maxResults:2}):[];ge.length>0&&(se=ge[0].id,console.error(`Auto-assigned landing layout preset (default): ${ge[0].title} (${se})`));let pn={name:E.name,summary:E.summary,dataModel:E.dataModel,pages:E.pages,features:E.features,steps:j.map(h=>({...h,name:h.name??h.title})),design:E.design,landingDesign:se,appStyle:je,dbProvider:E.dbProvider??"neon",authModel:E.authModel,audienceType:E.audienceType??"b2c",roles:E.roles,defaultRole:E.defaultRole,publicPages:oe,navStyle:E.navStyle,multiTenant:E.multiTenant,primaryAction:lt,nonNegotiables:Ne,requestedSubdomain:v,...u&&u.toLowerCase()!=="english"?{language:u}:{}};Zt(te(Be,`${B}.json`),JSON.stringify({plan:pn,methodology:Se}));let dn=j.map(h=>`${h.number}. ${h.name??h.title}`),ve="",pt=[],De;!ae&&ge.length>0&&(pt=ge.map(g=>({id:g.id,slug:g.slug,title:g.title,description:g.description,url:`${Ge()}/designs?tab=landing-designs`})),De={question:"What landing page style fits this app?",header:"Landing Design",options:[...ge.map((g,S)=>({label:S===0?`${g.title} (Recommended)`:g.title,description:g.description})),{label:"Design from scratch (Creative)",description:"No template \u2014 the AI designs a bespoke hero from your app's concept using 3D, scroll-driven animation, or particle effects. Higher creative ceiling, takes ~10 min longer, occasionally needs a second pass."},{label:"Browse all landing designs",description:`Not sure? See all landing designs at ${Ge()}/designs?tab=landing-designs and pass the ID back.`}],multiSelect:!1},ve=` REQUIRED: ask the user which landing design they want using the AskUserQuestion tool with the 'landingDesignQuestion' object before calling mist_init. Do NOT assume the recommended option \u2014 the user may want a different style than we inferred. Recommended: ${ge.map(g=>g.title).join(" or ")}. If they pick "Design from scratch", pass landingDesign='freeform' to mist_init. If they pick a specific preset title, look up its ID via mist_project action='landing-designs' and pass that. If they pick the recommended option, pass landingDesign='${ge[0].id}' explicitly.`);let Ct="",f=[],k=void 0,z=(E.audienceType??"b2c")==="b2c",fe={question:"Include a lifestyle photo in your landing page hero? Photos add warmth and human context; pure CSS stays cleaner.",header:"Hero",options:[{label:z?"Yes, add a photo (Recommended)":"Yes, add a photo",description:"Lifestyle photography background with your product preview overlaid \u2014 feels warm and human (like HabitFlow, Airbnb)."},{label:z?"No, CSS only":"No, CSS only (Recommended)",description:"Animated gradients + glassmorphism, no photo \u2014 cleaner and more technical (like Stripe, Linear, Vercel)."}],multiSelect:!1},ye=" Also ask the user about the 'heroPhotoQuestion' provided. Once they pick, pass heroPhoto=true (photo) or heroPhoto=false (CSS only) to the mist_init call.",_="",O=[];for(let h of j){let g=h.name??h.title,S=h.integrationId;if(S){let P=Qe(S);if(P){let W=Xe(P.id);O.push({step:g,presetId:P.id,presetName:P.name,envVars:W?.envVars??[]})}}}if(O.length>0){let h=O.flatMap(P=>P.envVars),g=[...new Set(h.map(P=>P.key))];_=` This plan uses integrations (${O.map(P=>P.presetName).join(", ")}). Detailed blueprints will be auto-injected during each integration step.${g.length>0?` The user will need these API keys: ${g.join(", ")}.`:""}`}return c(JSON.stringify({planId:B,name:E.name,summary:E.summary,stepCount:j.length,steps:dn,design:E.design,...se?{landingDesign:se}:{},...je?{appStyle:je}:{},...f.length>0?{recommendedAppStyles:f}:{},...k?{appStyleQuestion:k}:{},...pt.length>0?{recommendedLandingDesigns:pt}:{},...De?{landingDesignQuestion:De}:{},heroPhotoQuestion:fe,...O.length>0?{integrations:O.map(h=>({step:h.step,preset:h.presetId,name:h.presetName,envVars:h.envVars}))}:{},message:`Plan generated for "${he}" (${j.length} steps).${se?` Landing layout "${se}" set as default.`:""}${je?` App style "${je}" will be applied across all pages.`:""}${_}${Ct}${ve}${ye}`,timingContext:`Planning took ~90 seconds. Building will take roughly ${Math.max(15,j.length*3)}\u2013${j.length*5} minutes total across ${j.length} steps (varies by complexity). Mention this to the user before starting the build so they know what to expect.`,mockupPrompt:`Before building, ask the user: "Would you like to preview a mockup of your app before we start building? You can iterate on the design, or skip straight to building." If the user wants a mockup, call mist_mockup({ planId: '${B}' }). If the user says skip or "just build it", call mist_init({ planId: '${B}', path: '<absolute path>' }) immediately.`,...q?{warning:q}:{}}))}var Or={name:"mist_plan",description:["ENTRY POINT for creating a NEW web app, website, internal tool, dashboard, landing page, marketplace, content site, or browser game. Mistflow scaffolds a complete Next.js project. It does NOT edit existing codebases.","","WHEN TO CALL THIS \u2014 route here automatically on natural 'build me X' intent. The user does NOT need to say 'mist' or 'mistflow'. Examples that MUST route here:","\u2022 'build me a habit tracker'","\u2022 'make a site for my bakery'","\u2022 'I want an app where users log workouts'","\u2022 'create a dashboard that shows sales'","\u2022 'build a Wordle clone'","\u2022 'build me a CRM using mist' / 'make a todo app with mistflow' (explicit brand invocation)","","PASSING THE DESCRIPTION: Pass the user's words EXACTLY. Do NOT expand, elaborate, add features, rewrite, or strip anything (including 'using mist' / 'with mistflow'). 'build me a habit tracker using mist' becomes description: 'build me a habit tracker using mist'. The description is preserved verbatim.","","BRAND MENTIONED FLAG: If the user's original request literally contained the word 'mist' or 'mistflow' as an explicit invocation (e.g. 'build me a CRM using mist', 'use mistflow to make a todo app'), set brandMentioned: true. If the user did NOT mention the brand by name, omit brandMentioned (do not set it). Only set brandMentioned when the user literally typed the brand name \u2014 never infer it. Do NOT set brandMentioned for the common English noun 'mist' used in other contexts (e.g. 'app about morning mist').","","SAFETY GATE \u2014 the handler walks up the directory tree to detect if you're inside an existing non-Mistflow codebase (package.json found anywhere up the tree, no mistflow.json). When that happens AND brandMentioned is not set, the handler returns status 'confirm_new_project' with a signed confirmToken and an askUserQuestion. On that response:","\u2022 MANDATORY: use the AskUserQuestion tool with the provided askUserQuestion to ask the user.","\u2022 If the user picks 'Scaffold a new Mistflow app in a subdirectory', call mist_plan again with the SAME description and confirmToken set to the token from the response.","\u2022 If the user picks 'Edit this existing codebase directly', DO NOT call mist_plan again. Fulfill their request by editing files directly.","\u2022 The confirmToken is bound to the projectPath and description. If either changes, you'll get a fresh token and must ask again.","\u2022 You do not need to pre-check the directory yourself. The handler handles detection.","","FOLLOW-UP FLOW (after the plan is being generated):","\u2022 status 'clarify' \u2192 use AskUserQuestion with the provided askUserQuestions, then call mist_plan again with conversationId + answers + the same description.","\u2022 You may receive MULTIPLE 'clarify' rounds \u2014 if the user's answers reveal new ambiguity, the planner will ask follow-up questions. Keep relaying questions until you get status 'ready'. This is normal and produces better plans.","\u2022 status 'ready' \u2192 IMMEDIATELY call mist_init with the returned planId. Do not ask permission.","\u2022 NEVER skip the clarifying questions. The discovery process ensures the right thing gets built.","","EXISTING MISTFLOW PROJECTS (mistflow.json present anywhere up the tree): call this for changes that need a new data model, third-party integration, or multi-step structural change (pass existingPlan or existingPlanId). For simpler features (new pages, UI additions), do NOT call mist_plan, but DO ask the user product questions before building (what fields, what layout, what constraints). Get the spec right, build once. For cosmetic changes and bug fixes, skip questions and edit directly. Use mist_project action='get' for context.","","OTHER MODES: Pass templateToken to fork from a mistflow.ai/t/... shared template. Pass appStyle (53 full-app design systems like 'stripe', 'linear') to apply a design system. Browse via mist_project action='app-styles'. Landing layout presets are auto-assigned based on app description."].join(`
2217
+ `),inputSchema:Ta,handler:Ea};import{z as Pt}from"zod";import{existsSync as Fe,mkdirSync as ao,writeFileSync as lo,readFileSync as tn,readdirSync as qr,copyFileSync as Da}from"fs";import{join as ue,resolve as Br,dirname as It,isAbsolute as Oa}from"path";import{homedir as Ma}from"os";import{spawn as Pd}from"child_process";import{randomBytes as Ua}from"crypto";import{simpleGit as La}from"simple-git";ie();$e();function Mr(t){if(!t)return"Item";let e=t.replace(/([a-z0-9])([A-Z])/g,"$1 $2").split(/[^A-Za-z0-9]+/).filter(Boolean);return e.length===0?"Item":e.map(o=>o.charAt(0).toUpperCase()+o.slice(1).toLowerCase()).join("")}function Na(t){return t&&t.replace(/([a-z0-9])([A-Z])/g,"$1-$2").replace(/[^A-Za-z0-9]+/g,"-").toLowerCase().replace(/^-+|-+$/g,"")||"item"}function ja(t){let e=Mr(t);return e.charAt(0).toLowerCase()+e.slice(1)}function oo(){return["# Integration contracts","","This directory holds the single source of truth for every API shape in","this app. Frontend code and backend routes both import from here, so","drift becomes a compile error instead of a silent runtime bug.","","## The convention","","Every entity defined in `db/schema.ts` MUST have a matching contract","file in this directory. Each contract file exports three things:","","- `<Entity>Schema` \u2014 the Zod schema for reading the entity (derived from"," the Drizzle table via `createSelectSchema`).","- `Create<Entity>Input` \u2014 the Zod schema for creating a new row"," (derived via `createInsertSchema`, typically with `id` and"," `createdAt` omitted).","- `<Entity>` \u2014 the inferred TypeScript type from `<Entity>Schema`.","","File name is the kebab-case singular of the entity, e.g. `habit.ts`,","`blog-post.ts`. One entity per file.","","## Example","","```ts","// contracts/habit.ts",'import { createSelectSchema, createInsertSchema } from "drizzle-zod";','import { z } from "zod";','import { habit } from "@/db/schema";',"","export const HabitSchema = createSelectSchema(habit);","export type Habit = z.infer<typeof HabitSchema>;","","export const CreateHabitInput = createInsertSchema(habit).omit({"," id: true,"," createdAt: true,","});","export type CreateHabitInput = z.infer<typeof CreateHabitInput>;","```","","## Usage","","Frontend fetch and backend route should BOTH import from `contracts/`:","","```ts","// app/api/habits/route.ts",'import { CreateHabitInput, HabitSchema } from "@/contracts/habit";',"","export async function POST(request: Request) {"," const body = CreateHabitInput.parse(await request.json());"," const row = await db.insert(habit).values(body).returning();"," return Response.json(HabitSchema.parse(row[0]));","}","```","","```ts","// app/habits/page.tsx",'import type { Habit } from "@/contracts/habit";',"",'const res = await fetch("/api/habits");',"const habits: Habit[] = await res.json();","```","","## Adding a new entity","","1. Add the Drizzle table to `db/schema.ts`.","2. Create the matching `contracts/<entity>.ts` file using the template above.","3. Import from `contracts/` in every route, server action, and client"," component that touches the entity. Never inline the type.","","Run `mist_doctor` to check for entities that are missing a contract.",""].join(`
2218
+ `)}function Ur(){return["<!-- mist:contracts:start -->","## Integration contracts","","Every API shape in this app lives in `contracts/`. The directory holds","Zod schemas derived from `db/schema.ts` via `drizzle-zod`. Frontend and","backend both import from `contracts/`, so type drift becomes a compile","error instead of a runtime bug.","","### Rules for AI agents","","1. Every API route (`app/api/**/route.ts`) and server action MUST"," import its request + response types from `contracts/<entity>.ts`."," Never inline a Zod schema or a TypeScript type for an entity that"," already has a contract. Never `z.object({ ... })` inside a route"," handler for a known entity.","2. When you add a new entity to `db/schema.ts`, you MUST create the"," matching contract file BEFORE writing any route that uses it. Order"," matters: schema -> contract -> route.","3. Validate every request body with the contract's `.parse()` method."," If validation fails, return a 400 with the Zod error message \u2014 the"," contract is the boundary, not a decoration.","4. Validate every response body before returning it. Use the select"," schema's `.parse()` on the row(s) you read from the DB. This catches"," the case where the DB shape has drifted from the contract.","5. Client components that fetch an entity MUST import the inferred",' TypeScript type (`import type { Habit } from "@/contracts/habit"`)'," \u2014 never hand-write a duplicate type.","","### Contract file shape","","```ts","// contracts/<entity>.ts",'import { createSelectSchema, createInsertSchema } from "drizzle-zod";','import { z } from "zod";','import { entityTable } from "@/db/schema";',"","export const EntitySchema = createSelectSchema(entityTable);","export type Entity = z.infer<typeof EntitySchema>;","","export const CreateEntityInput = createInsertSchema(entityTable).omit({"," id: true,"," createdAt: true,","});","export type CreateEntityInput = z.infer<typeof CreateEntityInput>;","```","","### Example route using a contract","","```ts","// app/api/entities/route.ts",'import { db } from "@/lib/db";','import { entityTable } from "@/db/schema";','import { CreateEntityInput, EntitySchema } from "@/contracts/entity";',"","export async function POST(request: Request) {"," const body = CreateEntityInput.parse(await request.json());"," const [row] = await db.insert(entityTable).values(body).returning();"," return Response.json(EntitySchema.parse(row));","}","```","","Run `mist_doctor` to check every entity in `db/schema.ts` has a","contract file. Missing contracts are reported as warnings.","<!-- mist:contracts:end -->",""].join(`
2219
+ `)}var no="<!-- mist:contracts:start -->",$r="<!-- mist:contracts:end -->";function ro(t){let e=Ur();if(t.includes(no)){let n=t.indexOf(no),r=$r,a=t.indexOf(r,n);if(a===-1)return t.slice(0,n)+e;let s=t.slice(a+r.length);return t.slice(0,n)+e+s.replace(/^\n+/,"")}return t.replace(/\s+$/,"")+`
2220
+
2221
+ `+e}function so(t,e){let o=Mr(t),n=e??ja(t);return['import { createSelectSchema, createInsertSchema } from "drizzle-zod";','import { z } from "zod";',`import { ${n} } from "@/db/schema";`,"","// Select schema \u2014 shape of a row read from the DB. Use this on both","// sides of the wire: backend validates responses, frontend gets types.",`export const ${o}Schema = createSelectSchema(${n});`,`export type ${o} = z.infer<typeof ${o}Schema>;`,"","// Insert schema \u2014 shape accepted when creating a new row. id +","// createdAt are generated server-side, so we strip them from the","// input contract. Adjust if your schema uses different column names.",`export const Create${o}Input = createInsertSchema(${n}).omit({`," id: true,"," createdAt: true,","});",`export type Create${o}Input = z.infer<typeof Create${o}Input>;`,""].join(`
2222
+ `)}function io(t){return`contracts/${Na(t)}.ts`}function $a(t){let e=ue(Ma(),".mistflow","plans",`${t}.json`);if(!Fe(e))return null;try{let o=JSON.parse(tn(e,"utf-8"));return o.plan?{plan:o.plan}:null}catch{return null}}function Fa(t){let e=It(Br(t)),o=10,n=0;for(;n<o&&e!==It(e);){if(Fe(ue(e,"pnpm-workspace.yaml"))||Fe(ue(e,"lerna.json")))return e;let r=ue(e,"package.json");if(Fe(r))try{if(JSON.parse(tn(r,"utf-8")).workspaces)return e}catch{}e=It(e),n++}return null}var qa=Pt.object({name:Pt.string().min(1).describe("Human-readable app name (e.g. 'Task Flow')."),plan:Pt.any().optional().describe("Full plan object. Optional when planId is provided \u2014 the tool loads the cached plan from ~/.mistflow/plans/<planId>.json."),path:Pt.string().optional().describe("Absolute path where the project should be scaffolded."),planId:Pt.string().optional().describe("Plan ID from a prior mist_plan call. When present, the full plan is loaded from disk if 'plan' is omitted.")});function R(t,e,o){let n=ue(t,e);ao(It(n),{recursive:!0}),lo(n,o)}function Ba(t){if(!Fe(t))return!0;let e;try{e=qr(t)}catch{return!1}return e.filter(n=>n!==".mistflow").length===0}var za={sharp:"0.125rem",subtle:"0.375rem",rounded:"0.75rem",pill:"9999px"},Ha=[["--color-background","#ffffff"],["--color-foreground","#0a0a0a"],["--color-card","#ffffff"],["--color-card-foreground","#0a0a0a"],["--color-popover","#ffffff"],["--color-popover-foreground","#0a0a0a"],["--color-muted","#f5f5f5"],["--color-muted-foreground","#525252"],["--color-border","#e5e5e5"],["--color-input","#e5e5e5"],["--color-primary","#0a0a0a"],["--color-primary-foreground","#ffffff"],["--color-ring","#0a0a0a"],["--color-secondary","#f5f5f5"],["--color-secondary-foreground","#0a0a0a"],["--color-accent","#f5f5f5"],["--color-accent-foreground","#0a0a0a"],["--color-destructive","#b91c1c"],["--color-destructive-foreground","#ffffff"],["--color-success","#15803d"],["--color-success-foreground","#ffffff"],["--color-warning","#a16207"],["--color-warning-foreground","#ffffff"],["--color-info","#0369a1"],["--color-info-foreground","#ffffff"]];function Wa(t){let e=t.match(/^---\n([\s\S]*?)\n---/);if(!e)return null;let o=e[1],n={theme:"light",colors:{},typography:{},rounded:{},spacing:{}},r=o.split(`
2223
+ `),a=null,s=null,i=l=>l.replace(/^["']|["']$/g,"").trim();for(let l of r){let p=l.replace(/\r$/,"");if(!p.trim()||p.trim().startsWith("#"))continue;if(p.startsWith("theme:")){let d=i(p.slice(6).trim());(d==="dark"||d==="light")&&(n.theme=d);continue}let m=p.match(/^(colors|typography|rounded|spacing):\s*$/);if(m){a=m[1],s=null;continue}if(a==="typography"){let d=p.match(/^ {2}([a-zA-Z0-9_-]+):\s*$/);if(d){s=d[1].replace(/-/g,"_"),n.typography[s]={};continue}let b=p.match(/^ {4}([a-zA-Z0-9_-]+):\s*(.+)$/);if(b&&s){n.typography[s][b[1]]=i(b[2]);continue}}let u=p.match(/^ {2}([a-zA-Z0-9_-]+):\s*(.+)$/);if(u&&a){let d=u[1].replace(/-/g,"_"),b=i(u[2]);a==="colors"?n.colors[d]=b:a==="rounded"?n.rounded[d]=b:a==="spacing"&&(n.spacing[d]=b)}}return n}function Ga(t,e){let o=t.colors,r=[["--color-background",o.background],["--color-foreground",o.on_background??o.on_surface],["--color-card",o.surface??o.background],["--color-card-foreground",o.on_surface??o.on_background],["--color-popover",o.surface??o.background],["--color-popover-foreground",o.on_surface??o.on_background],["--color-muted",o.surface_variant??o.outline_variant??o.outline],["--color-muted-foreground",o.on_surface_variant??o.outline],["--color-border",o.outline_variant??o.outline],["--color-input",o.outline_variant??o.outline],["--color-primary",o.primary],["--color-primary-foreground",o.on_primary],["--color-ring",o.primary],["--color-secondary",o.secondary],["--color-secondary-foreground",o.on_secondary],["--color-accent",o.secondary],["--color-accent-foreground",o.on_secondary],["--color-destructive",o.error],["--color-destructive-foreground",o.on_error],["--color-success",o.success],["--color-success-foreground",o.on_success],["--color-warning",o.warning],["--color-warning-foreground",o.on_warning],["--color-info",o.info??o.primary],["--color-info-foreground",o.on_info??o.on_primary]].filter(i=>typeof i[1]=="string").map(([i,l])=>` ${i}: ${l};`).join(`
2224
+ `),a=[` --radius-sm: ${t.rounded.sm??"0.25rem"};`,` --radius-md: ${t.rounded.md??e};`,` --radius-lg: ${t.rounded.lg??"0.5rem"};`,` --radius-xl: ${t.rounded.xl??"0.75rem"};`].join(`
2225
+ `);return`@import "tailwindcss";
2226
+ @import "tw-animate-css";
2227
+
2228
+ @theme {
2229
+ ${r}
2230
+ ${a}
2231
+ }
2232
+
2233
+
2234
+ @layer base {
2235
+ * { border-color: var(--color-border); }
2236
+ body { background-color: var(--color-background); color: var(--color-foreground); }
2237
+ }
2238
+
2239
+ :root {
2240
+ --ease-quart-out: cubic-bezier(0.25, 1, 0.5, 1);
2241
+ --ease-expo-out: cubic-bezier(0.16, 1, 0.3, 1);
2242
+ --ease-back-out: cubic-bezier(0.34, 1.56, 0.64, 1);
2243
+ --duration-micro: 80ms;
2244
+ --duration-short: 150ms;
2245
+ --duration-medium: 250ms;
2246
+ --duration-long: 400ms;
2247
+ --z-dropdown: 10;
2248
+ --z-sticky: 20;
2249
+ --z-modal-backdrop: 30;
2250
+ --z-modal: 40;
2251
+ --z-toast: 50;
2252
+ --z-tooltip: 60;
2253
+ }
2254
+
2255
+ @keyframes fade-up { from { opacity: 0; transform: translateY(12px); } to { opacity: 1; transform: translateY(0); } }
2256
+ @keyframes fade-in { from { opacity: 0; } to { opacity: 1; } }
2257
+ .animate-fade-up { animation: fade-up 0.5s var(--ease-expo-out) both; }
2258
+ .animate-fade-in { animation: fade-in 0.3s var(--ease-quart-out) both; }
2259
+
2260
+ .stagger > :nth-child(1) { animation-delay: 0ms; }
2261
+ .stagger > :nth-child(2) { animation-delay: 50ms; }
2262
+ .stagger > :nth-child(3) { animation-delay: 100ms; }
2263
+ .stagger > :nth-child(4) { animation-delay: 150ms; }
2264
+ .stagger > :nth-child(5) { animation-delay: 200ms; }
2265
+ .stagger > :nth-child(6) { animation-delay: 250ms; }
2266
+
2267
+ @media (prefers-reduced-motion: reduce) {
2268
+ *, *::before, *::after {
2269
+ animation-duration: 0.01ms !important;
2270
+ animation-iteration-count: 1 !important;
2271
+ transition-duration: 0.01ms !important;
2272
+ }
2273
+ }
2274
+
2275
+ :focus-visible { outline: 2px solid var(--color-primary); outline-offset: 2px; }
2276
+ :focus:not(:focus-visible) { outline: none; }
2277
+
2278
+ button, [role="button"] { transition: transform 100ms var(--ease-quart-out); }
2279
+ button:active:not(:disabled), [role="button"]:active:not(:disabled) { transform: scale(0.97); }
2280
+ `}function Va(t,e,o){let n=t?.borderRadius??"subtle",r=za[n]??"0.375rem";if(o){let s=Wa(o);if(s)return Ga(s,r)}return`@import "tailwindcss";
2281
+ @import "tw-animate-css";
2282
+
2283
+ @theme {
2284
+ ${[...Ha.map(([s,i])=>` ${s}: ${i};`)," --radius-sm: 0.25rem;",` --radius-md: ${r};`," --radius-lg: 0.5rem;"," --radius-xl: 0.75rem;"].join(`
2285
+ `)}
2286
+ }
2287
+
2288
+ @layer base {
2289
+ * { border-color: var(--color-border); }
2290
+ body { background-color: var(--color-background); color: var(--color-foreground); }
2291
+ }
2292
+
2293
+ :root {
2294
+ --ease-quart-out: cubic-bezier(0.25, 1, 0.5, 1);
2295
+ --ease-expo-out: cubic-bezier(0.16, 1, 0.3, 1);
2296
+ --ease-back-out: cubic-bezier(0.34, 1.56, 0.64, 1);
2297
+ --duration-micro: 80ms;
2298
+ --duration-short: 150ms;
2299
+ --duration-medium: 250ms;
2300
+ --duration-long: 400ms;
2301
+ --z-dropdown: 10;
2302
+ --z-sticky: 20;
2303
+ --z-modal-backdrop: 30;
2304
+ --z-modal: 40;
2305
+ --z-toast: 50;
2306
+ --z-tooltip: 60;
2307
+ }
2308
+
2309
+ @keyframes fade-up { from { opacity: 0; transform: translateY(12px); } to { opacity: 1; transform: translateY(0); } }
2310
+ @keyframes fade-in { from { opacity: 0; } to { opacity: 1; } }
2311
+ .animate-fade-up { animation: fade-up 0.5s var(--ease-expo-out) both; }
2312
+ .animate-fade-in { animation: fade-in 0.3s var(--ease-quart-out) both; }
2313
+
2314
+ .stagger > :nth-child(1) { animation-delay: 0ms; }
2315
+ .stagger > :nth-child(2) { animation-delay: 50ms; }
2316
+ .stagger > :nth-child(3) { animation-delay: 100ms; }
2317
+ .stagger > :nth-child(4) { animation-delay: 150ms; }
2318
+ .stagger > :nth-child(5) { animation-delay: 200ms; }
2319
+ .stagger > :nth-child(6) { animation-delay: 250ms; }
2320
+
2321
+ @media (prefers-reduced-motion: reduce) {
2322
+ *, *::before, *::after {
2323
+ animation-duration: 0.01ms !important;
2324
+ animation-iteration-count: 1 !important;
2325
+ transition-duration: 0.01ms !important;
2326
+ }
2327
+ }
2328
+
2329
+ :focus-visible { outline: 2px solid var(--color-primary); outline-offset: 2px; }
2330
+ :focus:not(:focus-visible) { outline: none; }
2331
+
2332
+ button, [role="button"] { transition: transform 100ms var(--ease-quart-out); }
2333
+ button:active:not(:disabled), [role="button"]:active:not(:disabled) { transform: scale(0.97); }
2334
+ `}var Lr={"Fredoka One":"Fredoka","Source Sans Pro":"Source_Sans_3","Source Serif Pro":"Source_Serif_4","Open Sans Condensed":"Open_Sans","Baloo 2":"Baloo_2","DM Serif Display":"DM_Serif_Display","DM Serif Text":"DM_Serif_Text","IBM Plex Mono":"IBM_Plex_Mono","IBM Plex Sans":"IBM_Plex_Sans","IBM Plex Serif":"IBM_Plex_Serif","Fira Code":"Fira_Code","Fira Sans":"Fira_Sans","Noto Sans JP":"Noto_Sans_JP","PT Sans":"PT_Sans","PT Serif":"PT_Serif","Work Sans":"Work_Sans","Space Mono":"Space_Mono","Space Grotesk":"Space_Grotesk","Plus Jakarta Sans":"Plus_Jakarta_Sans"};function Fr(t){let e=t.replace(/[^A-Za-z0-9_ -]/g,"");return Lr[e]?Lr[e]:e.replace(/\s+/g,"_")}function Ja(t){return t?{english:"en",spanish:"es",french:"fr",german:"de",italian:"it",portuguese:"pt",dutch:"nl",russian:"ru",japanese:"ja",chinese:"zh",korean:"ko",arabic:"ar",hebrew:"he",hindi:"hi",turkish:"tr",polish:"pl",swedish:"sv",norwegian:"no",danish:"da",finnish:"fi",thai:"th",vietnamese:"vi",indonesian:"id",malay:"ms",farsi:"fa",persian:"fa",czech:"cs",greek:"el",romanian:"ro",hungarian:"hu",ukrainian:"uk",bengali:"bn",tamil:"ta",telugu:"te",urdu:"ur"}[t.toLowerCase()]??"en":"en"}var Ka=new Set(["ar","he","fa","ur"]);function Ya(t,e,o){let n=t.replace(/[\\"`$]/g,""),r=Ja(o),s=Ka.has(r)?`lang="${r}" dir="rtl"`:`lang="${r}"`,i=e?.fonts?.heading,l=e?.fonts?.body;if(!i&&!l)return`import type { Metadata } from "next";
2335
+ import { DM_Sans } from "next/font/google";
2336
+ import { Toaster } from "sonner";
2337
+ import "./globals.css";
2338
+
2339
+ const body = DM_Sans({ subsets: ["latin"], variable: "--font-body" });
2340
+
2341
+ export const metadata: Metadata = { title: "${n}", description: "Built with Mistflow" };
2342
+
2343
+ export default function RootLayout({ children }: { children: React.ReactNode }) {
2344
+ return (
2345
+ <html ${s}>
2346
+ <body className={body.variable}>{children}<Toaster richColors /></body>
2347
+ </html>
2348
+ );
2349
+ }
2350
+ `;let p=Fr(i??l),m=Fr(l??i);return p===m?`import type { Metadata } from "next";
2351
+ import { ${p} } from "next/font/google";
2352
+ import { Toaster } from "sonner";
2353
+ import "./globals.css";
2354
+
2355
+ const font = ${p}({ subsets: ["latin"], variable: "--font-body" });
2356
+
2357
+ export const metadata: Metadata = { title: "${n}", description: "Built with Mistflow" };
2358
+
2359
+ export default function RootLayout({ children }: { children: React.ReactNode }) {
2360
+ return (
2361
+ <html ${s}>
2362
+ <body className={font.variable}>{children}<Toaster richColors /></body>
2363
+ </html>
2364
+ );
2365
+ }
2366
+ `:`import type { Metadata } from "next";
2367
+ import { ${p}, ${m} } from "next/font/google";
2368
+ import { Toaster } from "sonner";
2369
+ import "./globals.css";
2370
+
2371
+ const heading = ${p}({ subsets: ["latin"], variable: "--font-heading" });
2372
+ const body = ${m}({ subsets: ["latin"], variable: "--font-body" });
2373
+
2374
+ export const metadata: Metadata = { title: "${n}", description: "Built with Mistflow" };
2375
+
2376
+ export default function RootLayout({ children }: { children: React.ReactNode }) {
2377
+ return (
2378
+ <html ${s}>
2379
+ <body className={\`\${heading.variable} \${body.variable}\`}>{children}<Toaster richColors /></body>
2380
+ </html>
2381
+ );
2382
+ }
2383
+ `}function en(t,...e){let o=JSON.stringify(t).toLowerCase();return e.some(n=>o.includes(n.toLowerCase()))}var Qa={dashboard:"Home",home:"Home",overview:"Home",patient:"Users",member:"Users",user:"Users",people:"Users",team:"Users",client:"Users",contact:"Users",customer:"Users",appointment:"Calendar",schedule:"Calendar",booking:"Calendar",event:"Calendar",billing:"CreditCard",invoice:"CreditCard",payment:"CreditCard",pricing:"CreditCard",treatment:"ClipboardList",plan:"ClipboardList",task:"CheckSquare",exercise:"Dumbbell",workout:"Dumbbell",report:"BarChart3",analytics:"BarChart3",stats:"BarChart3",setting:"Settings",config:"Settings",profile:"User",account:"User",message:"MessageSquare",chat:"MessageSquare",inbox:"MessageSquare",product:"Package",item:"Package",catalog:"Package",order:"ShoppingCart",cart:"ShoppingCart",file:"FileText",document:"FileText",upload:"Upload",notification:"Bell",alert:"Bell",project:"FolderKanban",board:"FolderKanban",post:"PenSquare",blog:"PenSquare",article:"PenSquare",course:"GraduationCap",lesson:"GraduationCap",class:"GraduationCap",habit:"Target",goal:"Target",streak:"Flame",progress:"TrendingUp",feature:"Sparkles",subscription:"CreditCard",price:"CreditCard",recipe:"ChefHat",food:"UtensilsCrossed",meal:"UtensilsCrossed",pet:"PawPrint",animal:"PawPrint",music:"Music",playlist:"ListMusic",song:"Music",photo:"Image",image:"Image",gallery:"Images",video:"Video",movie:"Film",map:"MapPin",location:"MapPin",place:"MapPin",search:"Search",explore:"Compass",inventory:"Boxes",stock:"Boxes",warehouse:"Warehouse",review:"Star",rating:"Star",feedback:"Star",log:"ScrollText",history:"Clock",activity:"Activity"};function co(t){let e=t.toLowerCase().replace(/[^a-z]/g,"");for(let[o,n]of Object.entries(Qa))if(e.includes(o))return n;return"Circle"}function _t(t){return t.split("-").map(e=>e.charAt(0).toUpperCase()+e.slice(1)).join(" ")}function Xa(t){if(t.authModel==="none")return null;let e=["/login","/register","/forgot-password","/reset-password","/api/auth","/api/health","/api/webhooks","/api/admin/seed"];if(t.publicPages&&Array.isArray(t.publicPages))for(let a of t.publicPages){if(typeof a!="string"||a.length<1)continue;let s=a.replace(/[\u201C\u201D\u201E\u201F\u2018\u2019\u2033\u2036]/g,"").trim();if(!s)continue;let i=s.startsWith("/")?s:"/"+s;e.includes(i)||e.push(i)}let o=e.filter(a=>a==="/"),n=e.filter(a=>a!=="/"),r=[];r.push('import { NextRequest, NextResponse } from "next/server";'),r.push(""),r.push("const PUBLIC_PREFIXES = [");for(let a of n)r.push(' "'+a+'",');return r.push("];"),r.push(""),o.length>0&&(r.push('const PUBLIC_EXACT = ["'+o.join('", "')+'"];'),r.push("")),r.push("export function middleware(req: NextRequest) {"),r.push(" const { pathname, search } = req.nextUrl;"),r.push(""),o.length>0&&r.push(" if (PUBLIC_EXACT.includes(pathname)) return NextResponse.next();"),r.push(" if (PUBLIC_PREFIXES.some((p) => pathname.startsWith(p))) return NextResponse.next();"),r.push(""),r.push(' const token = req.cookies.get("better-auth.session_token")?.value || req.cookies.get("__Secure-better-auth.session_token")?.value;'),r.push(" if (!token) {"),r.push(' const loginUrl = new URL("/login", req.url);'),r.push(" const params = new URLSearchParams(search);"),r.push(' for (const key of ["verified", "error"]) {'),r.push(" const v = params.get(key);"),r.push(" if (v) loginUrl.searchParams.set(key, v);"),r.push(" }"),r.push(" return NextResponse.redirect(loginUrl);"),r.push(" }"),r.push(""),r.push(" return NextResponse.next();"),r.push("}"),r.push(""),r.push("export const config = {"),r.push(' matcher: ["/((?!_next|static|favicon\\\\.ico).*)"],'),r.push("};"),r.push(""),r.join(`
2384
+ `)}function Za(t){if(t.navStyle==="none")return null;let e=["/api","/login","/register","/sign-in","/sign-up","/admin","/pricing","/about","/contact","/terms","/privacy","/onboarding","/join","/forgot-password","/reset-password"],n=(t.pages??[]).filter(l=>{let p=l.path??l.route??"";return p==="/"||p===""||p.includes("[")||p.replace(/^\//,"").split("/").length>1?!1:!e.some(u=>p.startsWith(u))}).map(l=>{let p=l.path??l.route??"",m=p.startsWith("/")?p:"/"+p,u=l.name??_t(p.replace(/^\//,"")),d=co(u);return{label:u,href:m,icon:d}});n.some(l=>l.href==="/dashboard")||n.unshift({label:"Dashboard",href:"/dashboard",icon:"Home"});let r=t.authModel==="none",a=[...new Set(n.map(l=>l.icon))];r||a.push("LogOut");let s=_t(t.name);if(t.navStyle==="topbar"){let l=[];l.push('"use client";'),l.push(""),l.push('import Link from "next/link";'),l.push('import { usePathname } from "next/navigation";'),r||l.push('import { authClient } from "@/lib/auth-client";'),l.push('import { Button } from "@/components/ui/button";'),l.push('import { cn } from "@/lib/utils";'),l.push("import { "+a.join(", ")+' } from "lucide-react";'),l.push(""),l.push("interface TopNavProps {"),l.push(" user: { name: string | null; email: string; role?: string | undefined };"),l.push("}"),l.push(""),l.push("const NAV_ITEMS = [");for(let p of n)l.push(' { label: "'+p.label+'", href: "'+p.href+'", icon: '+p.icon+" },");return l.push("];"),l.push(""),l.push("export default function TopNav({ user }: TopNavProps) {"),l.push(" const pathname = usePathname();"),l.push(""),l.push(" return ("),l.push(' <nav className="border-b bg-card">'),l.push(' <div className="mx-auto flex h-14 max-w-7xl items-center justify-between px-4">'),l.push(' <div className="flex items-center gap-6">'),l.push(' <span className="text-lg font-semibold">'+s+"</span>"),l.push(' <div className="flex items-center gap-1">'),l.push(" {NAV_ITEMS.map((item) => ("),l.push(" <Link"),l.push(" key={item.href}"),l.push(" href={item.href}"),l.push(' aria-current={pathname === item.href ? "page" : undefined}'),l.push(" className={cn("),l.push(' "flex items-center gap-1.5 rounded-md px-3 py-1.5 text-sm font-medium transition-colors",'),l.push(' pathname === item.href ? "bg-primary/10 text-primary" : "text-muted-foreground hover:text-foreground"'),l.push(" )}"),l.push(" >"),l.push(' <item.icon className="h-4 w-4" />'),l.push(" {item.label}"),l.push(" </Link>"),l.push(" ))}"),l.push(" </div>"),l.push(" </div>"),r?l.push(' <span className="text-sm text-muted-foreground">{user.name}</span>'):(l.push(' <div className="flex items-center gap-2">'),l.push(' <span className="text-sm text-muted-foreground">{user.email}</span>'),l.push(" <Button"),l.push(' variant="ghost"'),l.push(' size="sm"'),l.push(' onClick={() => authClient.signOut({ fetchOptions: { onSuccess: () => { window.location.href = "/login"; } } })}'),l.push(" >"),l.push(' <LogOut className="h-4 w-4" />'),l.push(" </Button>"),l.push(" </div>")),l.push(" </div>"),l.push(" </nav>"),l.push(" );"),l.push("}"),l.push(""),{path:"components/topnav.tsx",content:l.join(`
2385
+ `)}}let i=[];i.push('"use client";'),i.push(""),i.push('import { useState } from "react";'),i.push('import Link from "next/link";'),i.push('import { usePathname } from "next/navigation";'),r||i.push('import { authClient } from "@/lib/auth-client";'),i.push('import { Button } from "@/components/ui/button";'),i.push('import { Sheet, SheetContent, SheetTrigger, SheetTitle } from "@/components/ui/sheet";'),i.push('import { cn } from "@/lib/utils";'),i.push("import { Menu, "+a.join(", ")+' } from "lucide-react";'),i.push(""),i.push("interface SidebarProps {"),i.push(" user: { name: string | null; email: string; role?: string | undefined };"),i.push("}"),i.push(""),i.push("const NAV_ITEMS = [");for(let l of n)i.push(' { label: "'+l.label+'", href: "'+l.href+'", icon: '+l.icon+" },");return i.push("];"),i.push(""),i.push('function NavContent({ pathname, user, onNavigate }: { pathname: string; user: SidebarProps["user"]; onNavigate?: () => void }) {'),i.push(" return ("),i.push(" <>"),i.push(' <div className="flex h-14 items-center border-b px-4">'),i.push(' <span className="text-lg font-semibold">'+s+"</span>"),i.push(" </div>"),i.push(' <nav className="flex-1 space-y-1 p-2">'),i.push(" {NAV_ITEMS.map((item) => ("),i.push(" <Link"),i.push(" key={item.href}"),i.push(" href={item.href}"),i.push(" onClick={onNavigate}"),i.push(' aria-current={pathname === item.href ? "page" : undefined}'),i.push(" className={cn("),i.push(' "flex items-center gap-3 rounded-md px-3 py-2.5 text-sm font-medium transition-colors",'),i.push(' pathname === item.href ? "bg-primary/10 text-primary" : "text-muted-foreground hover:bg-muted hover:text-foreground"'),i.push(" )}"),i.push(" >"),i.push(' <item.icon className="h-4 w-4" />'),i.push(" {item.label}"),i.push(" </Link>"),i.push(" ))}"),i.push(" </nav>"),i.push(' <div className="border-t p-4">'),i.push(' <div className="flex items-center gap-3">'),i.push(' <div className="flex-1 truncate">'),i.push(' <p className="truncate text-sm font-medium">{user.name ?? "User"}</p>'),i.push(' <p className="truncate text-xs text-muted-foreground">{user.email}</p>'),i.push(" </div>"),r||(i.push(" <Button"),i.push(' variant="ghost"'),i.push(' size="icon"'),i.push(' onClick={() => authClient.signOut({ fetchOptions: { onSuccess: () => { window.location.href = "/login"; } } })}'),i.push(" >"),i.push(' <LogOut className="h-4 w-4" />'),i.push(" </Button>")),i.push(" </div>"),i.push(" </div>"),i.push(" </>"),i.push(" );"),i.push("}"),i.push(""),i.push("export default function Sidebar({ user }: SidebarProps) {"),i.push(" const pathname = usePathname();"),i.push(" const [open, setOpen] = useState(false);"),i.push(""),i.push(" return ("),i.push(" <>"),i.push(' <aside className="hidden md:flex h-screen w-64 flex-col border-r bg-card">'),i.push(" <NavContent pathname={pathname} user={user} />"),i.push(" </aside>"),i.push(' <div className="sticky top-0 z-[var(--z-sticky)] flex h-14 items-center gap-3 border-b bg-card px-4 md:hidden">'),i.push(" <Sheet open={open} onOpenChange={setOpen}>"),i.push(" <SheetTrigger asChild>"),i.push(' <Button variant="ghost" size="icon" className="-ml-2">'),i.push(' <Menu className="h-5 w-5" />'),i.push(" </Button>"),i.push(" </SheetTrigger>"),i.push(' <SheetContent side="left" className="w-64 p-0">'),i.push(' <SheetTitle className="sr-only">Navigation</SheetTitle>'),i.push(' <div className="flex h-full flex-col">'),i.push(" <NavContent pathname={pathname} user={user} onNavigate={() => setOpen(false)} />"),i.push(" </div>"),i.push(" </SheetContent>"),i.push(" </Sheet>"),i.push(' <span className="text-lg font-semibold">'+s+"</span>"),i.push(" </div>"),i.push(" </>"),i.push(" );"),i.push("}"),i.push(""),{path:"components/sidebar.tsx",content:i.join(`
2386
+ `)}}function el(t){if(!t.roles||t.roles.length===0)return null;let e=t.roles,o=t.defaultRole??e[0],n=[];n.push("export type Role = "+e.map(r=>'"'+r+'"').join(" | ")+";"),n.push(""),n.push("export const ROLES = ["+e.map(r=>'"'+r+'"').join(", ")+"] as const;"),n.push(""),n.push('export const DEFAULT_ROLE: Role = "'+o+'";'),n.push(""),n.push("export const ROLE_LABELS: Record<Role, string> = {");for(let r of e){let a=r.charAt(0).toUpperCase()+r.slice(1);n.push(' "'+r+'": "'+a+'",')}return n.push("};"),n.push(""),n.push("export function getUserRole(user: Record<string, unknown>): Role {"),n.push(" const role = (user.role as string) ?? DEFAULT_ROLE;"),n.push(" if (ROLES.includes(role as Role)) return role as Role;"),n.push(" return DEFAULT_ROLE;"),n.push("}"),n.push(""),n.push("export function hasRole(userRole: string | undefined, required: Role | Role[]): boolean {"),n.push(" if (!userRole) return false;"),n.push(" const allowed = Array.isArray(required) ? required : [required];"),n.push(" return allowed.includes(userRole as Role);"),n.push("}"),n.push(""),n.join(`
2387
+ `)}function tl(t){let e=_t(t.name);if(t.authModel==="none"){let a=[];return a.push("export default function HomePage() {"),a.push(" return ("),a.push(' <main className="flex min-h-screen flex-col items-center justify-center p-8">'),a.push(' <h1 className="text-4xl font-bold">'+e+"</h1>"),t.summary&&a.push(' <p className="mt-4 text-lg text-muted-foreground">'+t.summary+"</p>"),a.push(" </main>"),a.push(" );"),a.push("}"),a.push(""),a.join(`
2388
+ `)}let o=t.publicPages?.includes("/"),n=t.design?.landingTone;if(o&&n){let a=[];return a.push('import Link from "next/link";'),a.push(""),a.push("export default function HomePage() {"),a.push(" return ("),a.push(' <main className="flex min-h-screen flex-col">'),a.push(' <section className="flex flex-1 flex-col items-center justify-center gap-6 px-4 py-24 text-center">'),a.push(' <h1 className="text-5xl font-bold tracking-tight">'+e+"</h1>"),t.summary&&a.push(' <p className="max-w-2xl text-xl text-muted-foreground">'+t.summary+"</p>"),a.push(' <div className="flex gap-4">'),a.push(' <Link href="/register" className="inline-flex h-11 items-center rounded-md bg-primary px-8 text-sm font-medium text-primary-foreground hover:bg-primary/90">'),a.push(" Get Started"),a.push(" </Link>"),a.push(' <Link href="/login" className="inline-flex h-11 items-center rounded-md border px-8 text-sm font-medium hover:bg-muted">'),a.push(" Sign In"),a.push(" </Link>"),a.push(" </div>"),a.push(" </section>"),a.push(" </main>"),a.push(" );"),a.push("}"),a.push(""),a.join(`
2389
+ `)}if(o){let a=[];return a.push('import Link from "next/link";'),a.push(""),a.push("export default function HomePage() {"),a.push(" return ("),a.push(' <main className="flex min-h-screen flex-col items-center justify-center gap-6 p-8 text-center">'),a.push(' <h1 className="text-4xl font-bold">'+e+"</h1>"),t.summary&&a.push(' <p className="text-lg text-muted-foreground">'+t.summary+"</p>"),a.push(' <Link href="/login" className="inline-flex h-10 items-center rounded-md bg-primary px-6 text-sm font-medium text-primary-foreground hover:bg-primary/90">'),a.push(" Sign In"),a.push(" </Link>"),a.push(" </main>"),a.push(" );"),a.push("}"),a.push(""),a.join(`
2390
+ `)}let r=[];return r.push('import { headers } from "next/headers";'),r.push('import { redirect } from "next/navigation";'),r.push('import { auth } from "@/lib/auth";'),r.push(""),r.push("export default async function HomePage() {"),r.push(" const session = await auth.api.getSession({ headers: await headers() });"),r.push(' if (session) redirect("/dashboard");'),r.push(' redirect("/login");'),r.push("}"),r.push(""),r.join(`
2391
+ `)}function nl(t,e){let o=t.authModel==="none",n=[];o||(n.push('import { Suspense } from "react";'),n.push('import { headers } from "next/headers";'),n.push('import { redirect } from "next/navigation";'),n.push('import { auth } from "@/lib/auth";'),n.push('import { VerifiedBanner } from "@/components/auth/verified-banner";')),t.navStyle==="topbar"?n.push('import TopNav from "@/components/topnav";'):t.navStyle!=="none"&&n.push('import Sidebar from "@/components/sidebar";'),!o&&t.roles&&t.roles.length>0&&n.push('import { getUserRole } from "@/lib/roles";'),n.push(""),n.push("export default async function DashboardLayout({ children }: { children: React.ReactNode }) {"),o?n.push(' const user = { name: "Guest", email: "" };'):(n.push(" const session = await auth.api.getSession({ headers: await headers() });"),n.push(' if (!session) redirect("/login");'),n.push(""),t.roles&&t.roles.length>0?(n.push(" const role = getUserRole(session.user as Record<string, unknown>);"),n.push(" const user = { name: session.user.name, email: session.user.email, role };")):(n.push(" const user = {"),n.push(" name: session.user.name,"),n.push(" email: session.user.email,"),n.push(" role: (session.user as Record<string, unknown>).role as string | undefined,"),n.push(" };"))),n.push("");let r=o?"":"<Suspense fallback={null}><VerifiedBanner /></Suspense>";return t.navStyle==="topbar"?(n.push(" return ("),n.push(' <div className="min-h-screen">'),n.push(" <TopNav user={user} />"),n.push(' <main className="mx-auto max-w-7xl p-6">'+r+"{children}</main>"),n.push(" </div>"),n.push(" );")):t.navStyle==="none"?(n.push(" return ("),n.push(' <div className="min-h-screen">'),n.push(' <main className="mx-auto max-w-5xl p-6">'+r+"{children}</main>"),n.push(" </div>"),n.push(" );")):(n.push(" return ("),n.push(' <div className="flex flex-col md:flex-row min-h-screen">'),n.push(" <Sidebar user={user} />"),n.push(' <main className="flex-1 overflow-x-hidden p-4 md:p-6">'+r+"{children}</main>"),n.push(" </div>"),n.push(" );")),n.push("}"),n.push(""),n.join(`
2392
+ `)}function ol(t){let e=_t(t.name),o=t.dataModel??[],n=[];if(o.length>0){let r=o.map(s=>co(s.entity??s.name??"item")),a=[...new Set(r)];n.push('import { Card, CardContent } from "@/components/ui/card";'),n.push("import { "+a.join(", ")+' } from "lucide-react";'),n.push("")}if(n.push("export default function DashboardPage() {"),n.push(" return ("),n.push(' <div className="space-y-6">'),n.push(" <div>"),n.push(' <h1 className="text-3xl font-bold">'+e+"</h1>"),t.summary&&n.push(' <p className="mt-1 text-muted-foreground">'+t.summary+"</p>"),n.push(" </div>"),o.length>0){n.push(' <div className="rounded-lg border p-8 text-center">'),n.push(' <h2 className="text-lg font-semibold">Get started</h2>'),n.push(` <p className="mt-1 text-sm text-muted-foreground">Here's what you can do</p>`),n.push(' <div className="mt-6 grid gap-3 sm:grid-cols-2 text-left">');for(let r of o){let a=r.entity??r.name??"Item",s=co(a),i=_t(a.replace(/_/g,"-"));n.push(' <div className="flex items-center gap-3 rounded-md border p-3">'),n.push(" <"+s+' className="h-5 w-5 text-muted-foreground" />'),n.push(' <span className="text-sm font-medium">Add your first '+i+"</span>"),n.push(" </div>")}n.push(" </div>"),n.push(" </div>")}return n.push(" </div>"),n.push(" );"),n.push("}"),n.push(""),n.join(`
2393
+ `)}function rl(t,e=!1){if(!t.multiTenant)return null;let o=[];return e?(o.push('import { pgTable, text, timestamp, index } from "drizzle-orm/pg-core";'),o.push('import { user } from "./auth";'),o.push(""),o.push('export const organization = pgTable("organization", {'),o.push(' id: text("id").primaryKey(),'),o.push(' name: text("name").notNull(),'),o.push(' slug: text("slug").unique().notNull(),'),o.push(' createdAt: timestamp("created_at").defaultNow().notNull(),'),o.push(' updatedAt: timestamp("updated_at").defaultNow().notNull(),'),o.push("});"),o.push(""),o.push("export const orgMember = pgTable(")):(o.push('import { sqliteTable, text, index } from "drizzle-orm/sqlite-core";'),o.push('import { sql } from "drizzle-orm";'),o.push('import { user } from "./auth";'),o.push(""),o.push('export const organization = sqliteTable("organization", {'),o.push(' id: text("id").primaryKey(),'),o.push(' name: text("name").notNull(),'),o.push(' slug: text("slug").unique().notNull(),'),o.push(' createdAt: text("created_at").default(sql`CURRENT_TIMESTAMP`).notNull(),'),o.push(' updatedAt: text("updated_at").default(sql`CURRENT_TIMESTAMP`).notNull(),'),o.push("});"),o.push(""),o.push("export const orgMember = sqliteTable(")),o.push(' "org_member",'),o.push(" {"),o.push(' id: text("id").primaryKey(),'),o.push(' orgId: text("org_id").notNull().references(() => organization.id),'),o.push(' userId: text("user_id").notNull().references(() => user.id),'),o.push(' role: text("role").notNull(),'),e?o.push(' joinedAt: timestamp("joined_at").defaultNow().notNull(),'):o.push(' joinedAt: text("joined_at").default(sql`CURRENT_TIMESTAMP`).notNull(),'),o.push(" },"),o.push(" (table) => ({"),o.push(' orgIdx: index("org_member_org_idx").on(table.orgId),'),o.push(' userIdx: index("org_member_user_idx").on(table.userId),'),o.push(" }),"),o.push(");"),o.push(""),o.join(`
2394
+ `)}function sl(t){if(!t.multiTenant)return null;let e=[];return e.push('import { db } from "./db";'),e.push('import { organization, orgMember } from "@/db/schema/organization";'),e.push('import { eq } from "drizzle-orm";'),e.push(""),e.push("export async function getCurrentOrg(userId: string) {"),e.push(" const membership = await db"),e.push(" .select()"),e.push(" .from(orgMember)"),e.push(" .where(eq(orgMember.userId, userId))"),e.push(" .limit(1);"),e.push(" if (membership.length === 0) return null;"),e.push(" const org = await db"),e.push(" .select()"),e.push(" .from(organization)"),e.push(" .where(eq(organization.id, membership[0].orgId))"),e.push(" .limit(1);"),e.push(" return org[0] ?? null;"),e.push("}"),e.push(""),e.push("export async function getOrgMembers(orgId: string) {"),e.push(" return db"),e.push(" .select()"),e.push(" .from(orgMember)"),e.push(" .where(eq(orgMember.orgId, orgId));"),e.push("}"),e.push(""),e.push("export async function inviteToOrg(orgId: string, email: string, role: string) {"),e.push(" const id = crypto.randomUUID();"),e.push(" await db.insert(orgMember).values({"),e.push(" id,"),e.push(" orgId,"),e.push(" userId: email,"),e.push(" role,"),e.push(" });"),e.push(" return { id, orgId, email, role };"),e.push("}"),e.push(""),e.join(`
2395
+ `)}function il(t){if(!t.multiTenant)return null;let e=[];return e.push('"use client";'),e.push(""),e.push("import {"),e.push(" DropdownMenu,"),e.push(" DropdownMenuContent,"),e.push(" DropdownMenuItem,"),e.push(" DropdownMenuTrigger,"),e.push('} from "@/components/ui/dropdown-menu";'),e.push('import { Button } from "@/components/ui/button";'),e.push('import { ChevronsUpDown } from "lucide-react";'),e.push(""),e.push("interface OrgSwitcherProps {"),e.push(" orgs: Array<{ id: string; name: string }>;"),e.push(" currentOrgId: string;"),e.push("}"),e.push(""),e.push("export default function OrgSwitcher({ orgs, currentOrgId }: OrgSwitcherProps) {"),e.push(" const currentOrg = orgs.find((o) => o.id === currentOrgId);"),e.push(""),e.push(" return ("),e.push(" <DropdownMenu>"),e.push(" <DropdownMenuTrigger asChild>"),e.push(' <Button variant="outline" className="w-full justify-between">'),e.push(' <span className="truncate">{currentOrg?.name ?? "Select org"}</span>'),e.push(' <ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />'),e.push(" </Button>"),e.push(" </DropdownMenuTrigger>"),e.push(' <DropdownMenuContent className="w-56">'),e.push(" {orgs.map((org) => ("),e.push(" <DropdownMenuItem key={org.id}>"),e.push(" {org.name}"),e.push(" </DropdownMenuItem>"),e.push(" ))}"),e.push(" </DropdownMenuContent>"),e.push(" </DropdownMenu>"),e.push(" );"),e.push("}"),e.push(""),e.join(`
2396
+ `)}function al(t,e,o){let n=[],r=t.split("-").map(l=>l.charAt(0).toUpperCase()+l.slice(1)).join(" ");n.push(`# ${r}`),n.push(""),e?.summary&&(n.push(e.summary),n.push(""));let a=e?.features??[];if(a.length>0){n.push("## Features"),n.push("");for(let l of a){let p=l.description?` \u2014 ${l.description}`:"";n.push(`- **${l.name}**${p}`)}n.push("")}n.push("## Tech Stack"),n.push(""),n.push("| Layer | Technology |"),n.push("|-------|------------|"),n.push("| Framework | Next.js 15 (App Router) |"),n.push("| Database | Mistflow Cloud (Postgres) + Drizzle ORM |"),n.push("| Auth | Better Auth (email/password, social login) |"),n.push("| Styling | Tailwind CSS + shadcn/ui |"),n.push("| Deployment | Mistflow Cloud |"),o.hasStripe&&n.push("| Payments | Stripe |"),o.hasResend&&n.push("| Email | Resend + React Email |"),o.hasStorage&&n.push("| File Storage | Mistflow Cloud (managed blob storage) |"),o.hasAdmin&&n.push("| Admin | Better Auth admin plugin |"),o.hasAI&&n.push("| AI | Vercel AI SDK + OpenAI |"),n.push("");let s=e?.pages??[];if(s.length>0){n.push("## Pages"),n.push(""),n.push("| Route | Description |"),n.push("|-------|-------------|");for(let l of s){let p=l.path??l.route??l.name??"",m=l.description??"";n.push(`| \`${p.startsWith("/")?p:"/"+p}\` | ${m} |`)}n.push("")}let i=e?.dataModel??[];if(i.length>0){n.push("## Data Model"),n.push("");for(let l of i){let p=l.entity??l.name??"Unknown";if(n.push(`### ${p}`),n.push(""),l.fields.length>0){if(typeof l.fields[0]=="string")n.push(`Fields: ${l.fields.join(", ")}`);else{n.push("| Field | Type |"),n.push("|-------|------|");for(let m of l.fields)n.push(`| ${m.name} | ${m.type} |`)}n.push("")}}}return n.push("## Getting Started"),n.push(""),n.push("### Prerequisites"),n.push(""),n.push("- Node.js 20+"),n.push("- npm"),n.push(""),n.push("### Install"),n.push(""),n.push("The host AI calls mist_install (fire-and-poll) which handles this for you. Run manually with plain npm if needed:"),n.push(""),n.push("```bash"),n.push("npm install"),n.push("```"),n.push(""),n.push("### Set up environment"),n.push(""),n.push("Copy `.env.example` to `.env.local` and fill in the values:"),n.push(""),n.push("```bash"),n.push("cp .env.example .env.local"),n.push("```"),n.push(""),n.push("| Variable | Description | Required |"),n.push("|----------|-------------|----------|"),o.isNeon?n.push("| `DATABASE_URL` | Postgres connection URL | Yes |"):(n.push("| `TURSO_URL` | Database connection URL | Yes |"),n.push("| `TURSO_AUTH_TOKEN` | Database auth token | Yes |")),n.push("| `AUTH_SECRET` | Auth encryption secret (auto-generated) | Yes |"),o.hasStripe&&(n.push("| `STRIPE_SECRET_KEY` | Stripe secret key | Yes |"),n.push("| `STRIPE_WEBHOOK_SECRET` | Stripe webhook signing secret | Yes |"),n.push("| `NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY` | Stripe publishable key | Yes |")),o.hasResend&&(n.push("| `RESEND_API_KEY` | Resend API key | Yes |"),n.push("| `EMAIL_FROM` | Sender email address | Yes (production) |")),o.hasStorage&&(n.push("| `MISTFLOW_API_KEY` | Mistflow API key for file storage | Yes |"),n.push("| `MISTFLOW_PROJECT_ID` | Mistflow project ID | Yes |")),o.hasAI&&n.push("| `OPENAI_API_KEY` | OpenAI API key | Yes |"),n.push(""),n.push("### Local database"),n.push(""),o.isNeon?(n.push("For local development, start a local Postgres server:"),n.push(""),n.push("```bash"),n.push("# Using Docker:"),n.push("docker run -d --name postgres -e POSTGRES_PASSWORD=postgres -p 5432:5432 postgres:17"),n.push("# Or install via Homebrew: brew install postgresql@17 && brew services start postgresql@17"),n.push("```")):(n.push("For local development, start a local Turso server:"),n.push(""),n.push("```bash"),n.push("npx turso dev"),n.push("```")),n.push(""),n.push("Then set up the database:"),n.push(""),n.push("```bash"),n.push("npm run db:push"),n.push("```"),n.push(""),n.push("### Run"),n.push(""),n.push("```bash"),n.push("npm run dev"),n.push("```"),n.push(""),n.push("Open [http://localhost:3000](http://localhost:3000)."),n.push(""),n.push("## Project Structure"),n.push(""),n.push("```"),n.push("app/"),n.push(" (auth)/ Login and registration pages"),n.push(" (dashboard)/ Authenticated app pages"),o.hasAdmin&&n.push(" (admin)/ Admin panel pages"),n.push(" api/ API routes (auth, health, webhooks)"),n.push(" layout.tsx Root layout with fonts and providers"),n.push(" globals.css Design tokens and Tailwind config"),n.push("components/ Reusable UI components"),n.push("db/"),n.push(" schema/ Database table definitions"),n.push(" index.ts Schema exports"),n.push("lib/"),n.push(" auth.ts Better Auth server config"),n.push(" auth-client.ts Better Auth client config"),n.push(` db.ts ${o.isNeon?"Postgres":"SQLite"} database connection`),o.hasStripe&&n.push(" stripe.ts Stripe client"),o.hasResend&&(n.push(" resend.ts Resend client"),n.push(" email.ts Email send helpers")),o.hasStorage&&n.push(" storage.ts File upload/download helpers"),o.hasAI&&n.push(" ai.ts AI client (Vercel AI SDK + OpenAI)"),o.hasResend&&n.push("emails/ React Email templates"),n.push("```"),n.push(""),n.push("## Deploy"),n.push(""),n.push("Deploy to production with Mistflow:"),n.push(""),n.push("```"),n.push("# In your AI editor (Claude Code, Cursor, etc.):"),n.push("mist_deploy action='deploy'"),n.push("```"),n.push(""),n.push("Your app will be live at `https://<app-name>.mistflow.app`."),n.push(""),e?.design&&(n.push("## Design"),n.push(""),e.design.tone&&n.push(`- **Tone**: ${e.design.tone}`),e.design.fonts&&(n.push(`- **Heading font**: ${e.design.fonts.heading}`),n.push(`- **Body font**: ${e.design.fonts.body}`)),e.design.accentColor&&n.push(`- **Accent color**: ${e.design.accentColor}`),e.design.borderRadius&&n.push(`- **Border radius**: ${e.design.borderRadius}`),n.push("")),n.push("---"),n.push(""),n.push("Built with [Mistflow](https://mistflow.ai)"),n.push(""),n.join(`
2397
+ `)}async function ll(t,e){let{name:o,plan:n,path:r,planId:a}=t;if(!r)return c("mist_init requires an explicit 'path' \u2014 the absolute directory where the project should be scaffolded. Pass the user's project directory (e.g. /Users/alice/projects/my-app). Do not rely on a default.",!0);if(!Oa(r))return c(`mist_init 'path' must be an absolute path \u2014 received '${r}'. Pass the full absolute path to the target directory.`,!0);let s=Br(r),i=n;if(!i&&a){let f=$a(a);if(!f)return c(`No plan found for planId '${a}'. Call mist_plan first, or pass the plan object inline.`,!0);i=f.plan}let l=i?.design,p=i?.appStyle,m=i?en(i,"stripe","payment","billing","subscription","checkout","pricing"):!1,u=!0,d=i?en(i,"upload","file storage","image upload","profile picture","attachment","gallery","media","blob"):!1,b=i?en(i,"admin panel","admin dashboard","admin management"):!1,T=i?en(i,"ai integration","openai","llm","ai chat","chatbot","gpt"):!1,w=i,y=!0;if(!Ba(s))return c(`A project already exists at this location (${s}). Choose a different name, or delete the existing folder first. (A .mistflow/ folder left over from planning is fine \u2014 init will preserve it.)`,!0);ao(s,{recursive:!0});try{let f=ue(It(s),".mistflow","mockups");if(Fe(f)){let k=qr(f).filter(D=>D.endsWith(".html"));if(k.length>0){let D=ue(s,".mistflow","mockups");ao(D,{recursive:!0});for(let z of k)Da(ue(f,z),ue(D,z));console.error(`Copied ${k.length} mockup file(s) into project`)}}}catch(f){console.error("Could not copy mockup files:",f instanceof Error?f.message:f)}let x=null;try{x=await Un("nextjs")}catch(f){console.error("Could not fetch scaffold from API, using minimal scaffold:",f instanceof Error?f.message:f)}if(x){let f=o.toLowerCase().replace(/[^a-z0-9-]/g,"-");for(let _ of x.files){if(_.path==="package.json"||_.path==="middleware.ts"||_.path==="components/sidebar.tsx"||_.path==="components/topnav.tsx"||_.path==="app/(dashboard)/layout.tsx"||_.path==="app/(dashboard)/page.tsx"||_.path==="app/(dashboard)/dashboard/page.tsx"||!m&&(_.path.includes("stripe")||_.path.includes("webhook/stripe"))||!u&&(_.path.includes("resend")||_.path.includes("emails/"))||!b&&(_.path.includes("(admin)")||_.path.includes("admin-sidebar"))||y&&(_.path==="lib/db.ts"||_.path==="lib/auth.ts"||_.path==="drizzle.config.ts"||_.path==="db/schema/auth.ts"))continue;let O=_.content.replace(/\{\{APP_NAME\}\}/g,o).replace(/\{\{WORKER_NAME\}\}/g,f);if(y&&_.path==="next.config.ts"&&(O=O.replace(/serverExternalPackages:\s*\[[^\]]*\],?/g,'serverExternalPackages: ["@electric-sql/pglite"],')),_.path==="next.config.ts"){let h=Fa(s);h&&(console.error(`[init] Project is inside monorepo at ${h} \u2014 adding outputFileTracingRoot`),O.includes("outputFileTracingRoot")||(O=O.replace('import type { NextConfig } from "next";',`import type { NextConfig } from "next";
2398
+ import { dirname } from "path";
2399
+ import { fileURLToPath } from "url";
2400
+
2401
+ const __dirname = dirname(fileURLToPath(import.meta.url));`),O=O.replace("images: {",`outputFileTracingRoot: __dirname,
2402
+ images: {`)))}!b&&_.path.includes("sidebar")&&(O=O.replace(/\{user\.role === "admin"[\s\S]*?<\/Link>\s*\)\}/m,""),O=O.replace(/, Shield/g,"")),R(s,_.path,O)}let k={...x.dependencies},D={...x.devDependencies};if(k["drizzle-zod"]||(k["drizzle-zod"]="^0.5.1"),y&&(delete k["@libsql/client"],k["@neondatabase/serverless"]="^0.10.0",D["@electric-sql/pglite"]="^0.2.0"),m&&(k.stripe="^17.0.0"),u&&(k.resend="^4.0.0",k["@react-email/components"]="^0.0.31"),T&&(k.ai="^4.0.0",k["@ai-sdk/openai"]="^1.0.0",k.openai="^4.0.0"),R(s,"package.json",JSON.stringify({name:o,version:"0.1.0",private:!0,scripts:{dev:"next dev",build:"next build","build:cf":"opennextjs-cloudflare build",start:"next start",lint:"next lint","db:push":"drizzle-kit push","db:studio":"drizzle-kit studio"},dependencies:k,devDependencies:D,optionalDependencies:{"@noble/ciphers":"^1.3.0"},overrides:{react:"19.1.0","react-dom":"19.1.0",punycode:"^2.3.1"}},null,2)),x.methodology){let _=x.methodology;y&&(_=_.replace(/sqliteTable/g,"pgTable").replace(/drizzle-orm\/sqlite-core/g,"drizzle-orm/pg-core").replace(/Use `text` for dates \(SQLite stores dates as text\)/g,"Use `timestamp` for dates and `boolean` for booleans (native Postgres types)").replace(/text\("created_at"\)\.notNull\(\)\.default\(sql`\(CURRENT_TIMESTAMP\)`\)/g,'timestamp("created_at").notNull().defaultNow()').replace(/text\("updated_at"\)\.notNull\(\)\.default\(sql`\(CURRENT_TIMESTAMP\)`\)/g,'timestamp("updated_at").notNull().defaultNow()').replace(/import { sqliteTable, text, integer } from "drizzle-orm\/sqlite-core";\nimport { sql } from "drizzle-orm";/g,'import { pgTable, text, timestamp, boolean } from "drizzle-orm/pg-core";').replace(/`drizzle-kit push` for SQLite\/Turso/g,"`drizzle-kit push` for Postgres")),_=ro(_),R(s,"AGENTS.md",_),R(s,"CLAUDE.md",_)}let ye=i?.designMd;ye&&R(s,"DESIGN.md",ye),y&&(R(s,"lib/db.ts",['import { neon } from "@neondatabase/serverless";','import { drizzle as drizzleNeon } from "drizzle-orm/neon-http";',"","// eslint-disable-next-line @typescript-eslint/no-explicit-any","let _db: any = null;","","function getDb() {"," if (!_db) {",' if (process.env.DATABASE_URL && process.env.DATABASE_URL !== "pglite") {'," // Production / remote Postgres"," const sql = neon(process.env.DATABASE_URL);"," _db = drizzleNeon(sql);"," } else {"," // Local dev \u2014 PGlite (zero-install embedded Postgres). Lives in"," // lib/db-local.ts and is loaded through a runtime-only require"," // whose path is built from a variable, so esbuild's static"," // analysis can't follow it. Keeps pglite + its 30MB WASM out of"," // the Worker bundle.",' const localPath: string = "./" + "db-local";'," // eslint-disable-next-line @typescript-eslint/no-require-imports"," const { createLocalDb } = require(localPath);"," _db = createLocalDb();"," }"," }"," return _db;","}","","// Lazy proxy \u2014 DB isn't initialized at import/build time","// eslint-disable-next-line @typescript-eslint/no-explicit-any","export const db: any = new Proxy({} as any, {"," get(_target, prop, receiver) {"," const realDb = getDb();"," const value = Reflect.get(realDb, prop, receiver);",' if (typeof value === "function") {'," return value.bind(realDb);"," }"," return value;"," },","});",""].join(`
2403
+ `)),R(s,"lib/db-local.ts",["// Local-dev-only DB factory. Isolated from lib/db.ts so the production","// Cloudflare Worker bundle never loads drizzle-orm/pglite or its","// transitive 30MB WASM binary. Loaded via runtime-only require() from","// db.ts, where the path is built from a variable to defeat static analysis.","",'const pglitePkg: string = ["@electric-sql", "pglite"].join("/");',"// eslint-disable-next-line @typescript-eslint/no-require-imports","const { PGlite } = require(pglitePkg);","",'const drizzlePath: string = "drizzle-orm/" + "pglite";',"// eslint-disable-next-line @typescript-eslint/no-require-imports","const { drizzle } = require(drizzlePath);","","// eslint-disable-next-line @typescript-eslint/no-explicit-any","export function createLocalDb(): any {",' const client = new PGlite("./local.pg");'," return drizzle(client);","}",""].join(`
2404
+ `)),R(s,"drizzle.config.ts",['import { defineConfig } from "drizzle-kit";',"","// PGlite for local dev (no Postgres install needed), Mistflow Cloud for production",'const isPglite = !process.env.DATABASE_URL || process.env.DATABASE_URL === "pglite";',"","export default defineConfig({",' schema: "./db/schema",',' out: "./db/migrations",',' dialect: "postgresql",',' ...(isPglite ? { driver: "pglite", dbCredentials: { url: "./local.pg" } } : { dbCredentials: { url: process.env.DATABASE_URL! } }),',"});",""].join(`
2405
+ `)),R(s,"db/schema/auth.ts",['import { pgTable, text, boolean, timestamp } from "drizzle-orm/pg-core";',"",'export const user = pgTable("user", {',' id: text("id").primaryKey(),',' name: text("name").notNull(),',' email: text("email").notNull().unique(),',' emailVerified: boolean("email_verified").notNull().default(false),',' image: text("image"),',' role: text("role").default("user"),',' banned: boolean("banned").default(false),',' banReason: text("ban_reason"),',' banExpires: timestamp("ban_expires"),',' createdAt: timestamp("created_at").notNull(),',' updatedAt: timestamp("updated_at").notNull(),',"});","",'export const session = pgTable("session", {',' id: text("id").primaryKey(),',' expiresAt: timestamp("expires_at").notNull(),',' token: text("token").notNull().unique(),',' createdAt: timestamp("created_at").notNull(),',' updatedAt: timestamp("updated_at").notNull(),',' ipAddress: text("ip_address"),',' userAgent: text("user_agent"),',' userId: text("user_id").notNull().references(() => user.id, { onDelete: "cascade" }),',' impersonatedBy: text("impersonated_by"),',"});","",'export const account = pgTable("account", {',' id: text("id").primaryKey(),',' accountId: text("account_id").notNull(),',' providerId: text("provider_id").notNull(),',' userId: text("user_id").notNull().references(() => user.id, { onDelete: "cascade" }),',' accessToken: text("access_token"),',' refreshToken: text("refresh_token"),',' idToken: text("id_token"),',' accessTokenExpiresAt: timestamp("access_token_expires_at"),',' refreshTokenExpiresAt: timestamp("refresh_token_expires_at"),',' scope: text("scope"),',' password: text("password"),',' createdAt: timestamp("created_at").notNull(),',' updatedAt: timestamp("updated_at").notNull(),',"});","",'export const verification = pgTable("verification", {',' id: text("id").primaryKey(),',' identifier: text("identifier").notNull(),',' value: text("value").notNull(),',' expiresAt: timestamp("expires_at").notNull(),',' createdAt: timestamp("created_at"),',' updatedAt: timestamp("updated_at"),',"});",""].join(`
2406
+ `)),R(s,"lib/auth.ts",['import { betterAuth } from "better-auth";','import { drizzleAdapter } from "better-auth/adapters/drizzle";','import { admin } from "better-auth/plugins/admin";','import { nextCookies } from "better-auth/next-js";','import { db } from "./db";','import * as schema from "@/db";',"","async function sendEmail({ to, subject, html, fallbackUrl }: { to: string; subject: string; html: string; fallbackUrl?: string }) {"," const apiKey = process.env.RESEND_API_KEY;"," if (!apiKey) {"," if (fallbackUrl) {"," console.error(`\\n[auth] No RESEND_API_KEY set. Email to ${to} was not sent.`);"," console.error(`[auth] Dev fallback \u2014 use this link directly:\\n ${fallbackUrl}\\n`);"," } else {"," console.error(`[auth] RESEND_API_KEY not set \u2014 skipping email send to ${to}`);"," }"," return;"," }",' const from = process.env.EMAIL_FROM || "noreply@mail.mistflow.app";',' const res = await fetch("https://api.resend.com/emails", {',' method: "POST",',' headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json" },'," body: JSON.stringify({ from, to, subject, html }),"," });"," if (!res.ok) {",' const body = await res.text().catch(() => "unknown");'," console.error(`[auth] Email send failed (${res.status}): ${body}`);"," throw new Error(`Email send failed: ${res.status}`);"," }","}","","function createAuth() {",' const baseURL = process.env.BETTER_AUTH_URL || process.env.NEXT_PUBLIC_APP_URL || "http://localhost:3000";',' const isLocal = baseURL.includes("localhost") || baseURL.includes("127.0.0.1");'," const canSendEmail = Boolean(process.env.RESEND_API_KEY);"," // Refuse to boot a production app with email auth but no mail sender. Mistflow's"," // deploy pipeline injects a managed Resend key automatically; if it's missing,"," // that's a real misconfig and silent fallbacks let unverified users sign up."," if (!isLocal && !canSendEmail) {",` throw new Error("[auth] RESEND_API_KEY is required in production. The Mistflow deploy pipeline injects one automatically \u2014 if you're seeing this, check the project's env vars in the dashboard, or set your own RESEND_API_KEY.");`," }"," return betterAuth({"," baseURL,"," trustedOrigins: [baseURL],",' database: drizzleAdapter(db, { provider: "pg", schema }),'," emailAndPassword: {"," enabled: true,"," requireEmailVerification: !isLocal && canSendEmail,"," sendResetPassword: async ({ user, token }: { user: { email: string; name: string }; url: string; token: string }) => {"," // Better Auth's default reset URL points at /reset-password/:token (path),"," // which our frontend doesn't route. We build our own pointing at our"," // /reset-password?token=xxx page which reads the token from the query."," const resetUrl = `${baseURL}/reset-password?token=${token}`;"," await sendEmail({"," to: user.email,",' subject: "Reset your password",',' html: `<p>Hi ${user.name},</p><p>Click the link below to reset your password:</p><p><a href="${resetUrl}">${resetUrl}</a></p>`,'," fallbackUrl: isLocal ? resetUrl : undefined,"," });"," },"," },"," emailVerification: {"," sendOnSignUp: canSendEmail,"," autoSignInAfterVerification: true,"," sendVerificationEmail: async ({ user, url }: { user: { email: string; name: string }; url: string }) => {"," await sendEmail({"," to: user.email,",' subject: "Verify your email address",',' html: `<p>Hi ${user.name},</p><p>Click the link below to verify your email:</p><p><a href="${url}">${url}</a></p>`,'," fallbackUrl: isLocal ? url : undefined,"," });"," },"," },"," secret: process.env.AUTH_SECRET,",' plugins: [admin({ defaultRole: "user" }), nextCookies()],'," databaseHooks: {"," user: {"," create: {"," // Auto-promote the app owner to admin on first signup. ADMIN_EMAIL"," // is injected by the Mistflow deploy pipeline (the email of the"," // account that ran mist_deploy). Email verification still gates"," // login when Resend is configured, so a collision attempt can't"," // actually sign in without clicking a link delivered to the"," // owner's inbox."," before: async (user: { email?: string; [k: string]: unknown }) => {"," const adminEmail = process.env.ADMIN_EMAIL;"," if (adminEmail && user.email?.toLowerCase() === adminEmail.toLowerCase()) {",' return { data: { ...user, role: "admin" } };'," }"," return { data: user };"," },"," },"," },"," },"," socialProviders: {"," ...(process.env.GOOGLE_CLIENT_ID ? {"," google: {"," clientId: process.env.GOOGLE_CLIENT_ID,"," clientSecret: process.env.GOOGLE_CLIENT_SECRET!,"," },"," } : {}),"," ...(process.env.GITHUB_CLIENT_ID ? {"," github: {"," clientId: process.env.GITHUB_CLIENT_ID,"," clientSecret: process.env.GITHUB_CLIENT_SECRET!,"," },"," } : {}),"," },"," });","}","","// Lazy init \u2014 process.env isn't populated at module scope on Cloudflare Workers.","// The `has` trap is required: better-auth's toNextJsHandler does",'// `"handler" in auth ? auth.handler(request) : auth(request)` \u2014 without a `has`',"// trap the default forwards to the empty target object, returns false, and the","// handler tries to call the Proxy as a function, which throws TypeError and","// returns 500 on every /api/auth/* request.","let _auth: ReturnType<typeof createAuth> | null = null;","export const auth = new Proxy({} as ReturnType<typeof createAuth>, {"," get(_target, prop, receiver) {"," if (!_auth) _auth = createAuth();"," const value = Reflect.get(_auth, prop, receiver);",' if (typeof value === "function") return value.bind(_auth);'," return value;"," },"," has(_target, prop) {"," if (!_auth) _auth = createAuth();"," return prop in _auth;"," },","});",""].join(`
2407
+ `)))}else R(s,"package.json",JSON.stringify({name:o,version:"0.1.0",private:!0},null,2));let M=i?.designMd;R(s,"app/globals.css",Va(l,p,M)),R(s,"app/layout.tsx",Ya(o,l,w?.language)),R(s,"README.md",al(o,i,{hasStripe:m,hasResend:u,hasStorage:d,hasAdmin:b,hasAI:T,isNeon:y})),R(s,"contracts/README.md",oo());let J=i?.dataModel??[],H=new Set,re=0;for(let f of J){let k=f.entity??f.name;if(!k||typeof k!="string")continue;let D=io(k);H.has(D)||(H.add(D),R(s,D,so(k)),re++)}re===0&&R(s,"contracts/.gitkeep","");let q=[],v=i?.publicPages;if(Array.isArray(v))q=v;else if(typeof v=="string"){try{q=JSON.parse(v)}catch{q=[]}Array.isArray(q)||(q=[])}if(!q.includes("/")){let f=i?.steps?.some(D=>{let z=((D.name??"")+" "+(D.description??"")).toLowerCase();return z.includes("landing")||z.includes("marketing")||z.includes("homepage")}),k=i?.pages?.some(D=>D.path==="/");(f||k)&&(q=["/",...q])}let $={name:o,summary:i?.summary,authModel:i?.authModel,roles:i?.roles,defaultRole:i?.defaultRole,publicPages:q,navStyle:i?.navStyle,multiTenant:i?.multiTenant,pages:i?.pages,dataModel:i?.dataModel,design:i?.design},K=Xa($);K&&R(s,"middleware.ts",K);let ne=Za($);ne&&R(s,ne.path,ne.content);let we=el($);if(we&&R(s,"lib/roles.ts",we),R(s,"app/page.tsx",tl($)),R(s,"app/(dashboard)/layout.tsx",nl($,b)),R(s,"app/(dashboard)/dashboard/page.tsx",ol($)),$.multiTenant){let f=rl($,y);f&&R(s,"db/schema/organization.ts",f);let k=sl($);k&&R(s,"lib/org.ts",k);let D=il($);D&&R(s,"components/org-switcher.tsx",D)}R(s,"tsconfig.json",JSON.stringify({compilerOptions:{target:"ES2017",lib:["dom","dom.iterable","esnext"],allowJs:!0,skipLibCheck:!0,strict:!1,noEmit:!0,esModuleInterop:!0,module:"esnext",moduleResolution:"bundler",resolveJsonModule:!0,isolatedModules:!0,jsx:"preserve",incremental:!0,plugins:[{name:"next"}],paths:{"@/*":["./*"]}},include:["next-env.d.ts","**/*.ts","**/*.tsx",".next/types/**/*.ts"],exclude:["node_modules"]},null,2)),m&&R(s,"lib/stripe.ts",['import Stripe from "stripe";',"","let _stripe: Stripe | null = null;","","function getStripe(): Stripe {"," if (!_stripe) {"," if (!process.env.STRIPE_SECRET_KEY) {",' throw new Error("STRIPE_SECRET_KEY is not set");'," }"," _stripe = new Stripe(process.env.STRIPE_SECRET_KEY, {"," typescript: true,"," });"," }"," return _stripe;","}","","// Lazy proxy \u2014 Stripe isn't initialized at import/build time","export const stripe = new Proxy({} as Stripe, {"," get(_target, prop) {"," return (getStripe() as unknown as Record<string | symbol, unknown>)[prop];"," },","});",""].join(`
2408
+ `)),u&&(R(s,"lib/resend.ts",['import { Resend } from "resend";',"","let _resend: Resend | null = null;","","function getResend(): Resend {"," if (!_resend) {"," if (!process.env.RESEND_API_KEY) {",' throw new Error("RESEND_API_KEY is not set");'," }"," _resend = new Resend(process.env.RESEND_API_KEY);"," }"," return _resend;","}","","// Lazy proxy \u2014 Resend isn't initialized at import/build time","export const resend = new Proxy({} as Resend, {"," get(_target, prop) {"," return (getResend() as unknown as Record<string | symbol, unknown>)[prop];"," },","});",""].join(`
2409
+ `)),R(s,"lib/email.ts",['import { resend } from "./resend";',"",'const FROM = process.env.EMAIL_FROM ?? "onboarding@resend.dev";',"","export async function sendEmail({"," to,"," subject,"," react,","}: {"," to: string;"," subject: string;"," react: React.ReactElement;","}) {"," return resend.emails.send({ from: FROM, to, subject, react });","}",""].join(`
2410
+ `))),d&&(R(s,"lib/storage.ts",['const MISTFLOW_API = process.env.MISTFLOW_API_URL ?? "https://api.mistflow.ai";',"const MISTFLOW_API_KEY = process.env.MISTFLOW_API_KEY;","const PROJECT_ID = process.env.MISTFLOW_PROJECT_ID;","","interface UploadResult {"," upload_url: string;"," download_url: string;"," key: string;","}","","function authHeaders(): Record<string, string> {"," return {",' "Content-Type": "application/json",',' "Authorization": `ApiKey ${MISTFLOW_API_KEY}`,'," };","}","",'export async function getUploadUrl(filename: string, contentType: string = "application/octet-stream"): Promise<UploadResult> {'," const res = await fetch(`${MISTFLOW_API}/api/storage/upload-url`, {",' method: "POST",'," headers: authHeaders(),"," body: JSON.stringify({ project_id: PROJECT_ID, filename, content_type: contentType }),"," });"," if (!res.ok) throw new Error(`Storage error: ${res.status}`);"," return res.json();","}","","export async function getDownloadUrl(filename: string): Promise<string> {"," const res = await fetch(`${MISTFLOW_API}/api/storage/download-url`, {",' method: "POST",'," headers: authHeaders(),"," body: JSON.stringify({ project_id: PROJECT_ID, filename }),"," });"," if (!res.ok) throw new Error(`Storage error: ${res.status}`);"," const data = await res.json();"," return data.download_url;","}","","export async function deleteFile(filename: string): Promise<void> {"," await fetch(`${MISTFLOW_API}/api/storage/delete`, {",' method: "POST",'," headers: authHeaders(),"," body: JSON.stringify({ project_id: PROJECT_ID, filename }),"," });","}","","export async function uploadFile(file: File): Promise<string> {"," const { upload_url, download_url } = await getUploadUrl(file.name, file.type);",' await fetch(upload_url, { method: "PUT", body: file, headers: { "Content-Type": file.type } });'," return download_url;","}",""].join(`
2411
+ `)),R(s,"app/api/upload/route.ts",['import { NextRequest, NextResponse } from "next/server";','import { getUploadUrl } from "@/lib/storage";',"","export async function POST(req: NextRequest) {"," const { filename, contentType } = await req.json();"," if (!filename) {",' return NextResponse.json({ error: "filename is required" }, { status: 400 });'," }"," try {",' const result = await getUploadUrl(filename, contentType ?? "application/octet-stream");'," return NextResponse.json(result);"," } catch {",' return NextResponse.json({ error: "Failed to get upload URL" }, { status: 500 });'," }","}",""].join(`
2412
+ `))),T&&(R(s,"lib/ai.ts",['import { createOpenAI } from "@ai-sdk/openai";',"","export const openai = createOpenAI({"," apiKey: process.env.OPENAI_API_KEY,","});",""].join(`
2413
+ `)),R(s,"app/api/chat/route.ts",['import { openai } from "@/lib/ai";','import { streamText } from "ai";',"","export async function POST(req: Request) {"," const { messages } = await req.json();",""," const result = streamText({",' model: openai("gpt-4o"),'," messages,"," });",""," return result.toDataStreamResponse();","}",""].join(`
2414
+ `)));let C={name:o,methodologyVersion:x?.version??"1.0",createdAt:new Date().toISOString(),...a?{planId:a}:{},plan:Array.isArray(i?.steps)?{...i,steps:i.steps.map(f=>({number:f.number,name:f.name??f.title,description:f.description,entities:f.entities,pages:f.pages,features:f.features,status:"pending"}))}:i,dbProvider:"neon",env:{managed:{DATABASE_URL:{description:"Postgres connection URL",scope:"production"},AUTH_SECRET:{description:"Auth encryption secret",scope:"production"},...m?{STRIPE_SECRET_KEY:{description:"Stripe secret key",scope:"production"},STRIPE_WEBHOOK_SECRET:{description:"Stripe webhook signing secret",scope:"production"},NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY:{description:"Stripe publishable key",scope:"production"}}:{},...u?{RESEND_API_KEY:{description:"Resend API key \u2014 managed by Mistflow by default, override with your own key from resend.com",scope:"production"},EMAIL_FROM:{description:"Sender email address \u2014 managed by Mistflow by default",scope:"production"}}:{},...d?{MISTFLOW_API_KEY:{description:"Mistflow API key for file storage",scope:"production"},MISTFLOW_PROJECT_ID:{description:"Mistflow project ID",scope:"production"}}:{}},...T?{required:{OPENAI_API_KEY:{description:"OpenAI API key",setupUrl:"https://platform.openai.com/api-keys"}}}:{}},authModel:i?.authModel??"email",roles:i?.roles??null,navStyle:i?.navStyle??"sidebar",multiTenant:i?.multiTenant??!1,hasAdmin:b,hasResend:u,hasStorage:d,hasAI:T,deploy:null};R(s,"mistflow.json",JSON.stringify(C,null,2));let E=Ua(32).toString("hex"),he=m?`
2415
+ # Stripe
2416
+ STRIPE_SECRET_KEY=
2417
+ STRIPE_WEBHOOK_SECRET=
2418
+ NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=
2419
+ `:"",Se=u?`
2420
+ # Email (Resend)
2421
+ RESEND_API_KEY=
2422
+ EMAIL_FROM=onboarding@resend.dev
2423
+ `:"",j=d?`
2424
+ # File Storage (Mistflow managed)
2425
+ MISTFLOW_API_KEY=
2426
+ MISTFLOW_PROJECT_ID=
2427
+ `:"",oe=T?`
2428
+ # AI (get your key at https://platform.openai.com/api-keys)
2429
+ OPENAI_API_KEY=
2430
+ `:"",lt=`# Local dev: PGlite is used automatically (zero-install embedded Postgres)
2431
+ # Set DATABASE_URL only for production or to use a remote Postgres
2432
+ # DATABASE_URL=postgresql://postgres:postgres@localhost:5432/devdb`,Ne=`# Local dev: PGlite is used automatically (zero-install embedded Postgres)
2433
+ # Set DATABASE_URL only for production or to use a remote Postgres
2434
+ # DATABASE_URL=postgresql://postgres:postgres@localhost:5432/devdb`;R(s,".env.local",`${lt}
2435
+ AUTH_SECRET=${E}
2436
+ ${he}${Se}${j}${oe}`),R(s,".env.example",`${Ne}
2437
+ AUTH_SECRET=your-secret-here
2438
+ ${he}${Se}${j}${oe}`);let B=[],Be=(f,k)=>{B.push({phase:f,message:k})},se=(f,k)=>{let D=B.find(z=>z.phase===f&&!z.durationMs);D&&(D.durationMs=k)};if(e){let f=Yt(e.server,e.progressToken,()=>B[B.length-1]?.message??"Setting up project...");e.cleanup=()=>f.stop()}let je=w?.requestedSubdomain||void 0,ae,ct;Be("register","Registering project on Mistflow...");let ge=Date.now();try{let f=await yt(o,void 0,"neon",je);ae=f.id;let k=ue(s,"mistflow.json"),D=JSON.parse(tn(k,"utf-8"));if(D.projectId=ae,lo(k,JSON.stringify(D,null,2)),wt(s,vt(ae,o)),f.managed_env&&Object.keys(f.managed_env).length>0){let z=ue(s,".env.local"),fe=Fe(z)?tn(z,"utf-8"):"";for(let[ye,_]of Object.entries(f.managed_env)){let O=new RegExp(`^${ye}=.*$`,"m");O.test(fe)?fe=fe.replace(O,`${ye}=${_}`):fe+=`
2439
+ ${ye}=${_}`}lo(z,fe)}try{let{getBaseUrl:z,getAuthHeaders:fe}=await Promise.resolve().then(()=>(ie(),Vo)),ye=fe(),_=i?.features,O=i?.steps,h={};Array.isArray(_)&&_.length>0&&(h.features=_.map(g=>g.name)),i&&(h.plan=i),Array.isArray(O)&&O.length>0&&(h.provenance=O.map(g=>({feature:g.name??g.title??`Step ${g.number??"?"}`,user_intent:(g.description??"").slice(0,500),decisions:"Seeded from plan at init",tradeoffs:"",files_affected:[]}))),Object.keys(h).length>0&&await fetch(`${z()}/api/projects/${encodeURIComponent(ae)}/state`,{method:"PUT",headers:{...ye,"Content-Type":"application/json"},body:JSON.stringify(h)})}catch{}B[B.length-1].message=`Registered as ${ae.slice(0,8)}`}catch(f){let k=f instanceof Error?f.message:String(f);console.error("Could not register project on backend:",k),ct=`Project created locally but NOT registered on Mistflow servers (${k}). Deploy will auto-register it.`,B[B.length-1].message="Registration skipped (offline \u2014 deploy will retry)"}se("register",Date.now()-ge),Be("git","Initializing git repository...");let pn=Date.now();try{let f=La(s);await f.init(),await f.add("."),await f.commit("Initial Mistflow project setup"),B[B.length-1].message="Git repository initialized"}catch{console.error("Git initialization failed, continuing without git."),B[B.length-1].message="Git init skipped"}se("git",Date.now()-pn);let dn=B.reduce((f,k)=>f+(k.durationMs??0),0),ve={projectPath:s,projectId:ae,status:"awaiting_install"},pt=B.map(f=>{let k=f.durationMs?` (${(f.durationMs/1e3).toFixed(1)}s)`:"";return`${f.message}${k}`});ve.progress=pt,ve.totalSetupTime=`${(dn/1e3).toFixed(1)}s`;let De=[];ae||De.push("Project was not registered with Mistflow (not signed in). Run mist_setup to sign in BEFORE deploying \u2014 deploy will fail without it."),ct&&(ve.registrationWarning=ct),De.length>0&&(ve.warnings=De);let Ct=`NEXT: Call mist_install({ projectPath: "${s}" }). This is fire-and-poll \u2014 the first call returns a jobId and status: "running"; re-call with the same jobId every ~15-30s until status is "complete". Typical duration 30-90s. Do NOT ask the user for permission \u2014 install is a required follow-up to init, not a decision point. After install completes, call mist_implement({ projectPath: "${s}" }) to build the first plan step.`;return ve.nextAction=ae?Ct:`${Ct} IMPORTANT: You MUST also run mist_setup to sign in before deploying \u2014 the project could not be registered because auth is missing.`,c(JSON.stringify(ve))}var zr={name:"mist_init",description:"Initialize a new Next.js project using the Mistflow stack. Downloads templates from the Mistflow registry, installs dependencies, sets up auth, and initializes git. Use this after mist_plan to create the project.",inputSchema:qa,handler:ll};import{z as po}from"zod";import{existsSync as Tt,readFileSync as mo,writeFileSync as uo,mkdirSync as bl}from"fs";import{join as nt,resolve as wl,dirname as vl}from"path";ie();import{createConnection as xl}from"net";var Hr="\nApply these choices to every file you create. Customize the shadcn CSS variables in globals.css to match. Do NOT use default shadcn blue/zinc theme.\n\n**CRITICAL: shadcn/ui components are already installed. NEVER write components/ui/*.tsx files from scratch.** If you need a shadcn component, run `npx shadcn@latest add <component>` to pull it. The project already includes: button, card, input, label, form, dialog, table, dropdown-menu, badge, separator, skeleton, sheet, tabs, avatar, select, textarea, checkbox, switch, tooltip, popover, sonner.\n\n**shadcn MCP server is available.** You have the `shadcn` MCP server installed \u2014 use it to browse and search for components, blocks, and landing page sections from the shadcn registry. Before building a landing page section from scratch, check the registry for existing blocks (hero sections, feature grids, pricing tables, testimonials, footers) and customize them instead of reinventing.\n\n**Project routes (use these exact paths):** Login is at `/login` (NOT /sign-in). Register is at `/register` (NOT /sign-up). All landing page and nav links MUST use `/login` and `/register`.\n\n### Design quality rules (non-negotiable):\n\n**Typography**: Use the plan fonts everywhere. font-heading for headings, font-body for body text.\n- **Modular scale**: Use a 1.25-1.5 ratio between sizes. A 5-level system covers most needs: xs (0.75rem captions), sm (0.875rem metadata), base (1rem body), lg (1.25-1.5rem subheadings), xl+ (2-4rem headlines). Sizes too close together (14/15/16px) create muddy hierarchy.\n- **Vertical rhythm**: Your line-height IS your spacing unit. If body is 16px with line-height 1.5 (= 24px), vertical spacing should be multiples of 24px.\n- **Weight contrast**: Use font-medium/font-semibold contrast, not just size. Bold headings + regular body creates natural hierarchy without extra sizes.\n- **Measure**: Set `max-width: 65ch` on text containers. Long lines kill readability.\n- **OpenType polish**: Use `font-variant-numeric: tabular-nums` on data tables for aligned columns.\n- Never use more than 2 font families. One well-chosen family in multiple weights is cleaner than two competing typefaces.\n- Never fall back to system fonts. Never use decorative fonts for body text. Minimum 16px (1rem) for body.\n\n**Color**: Build a functional palette, not a rainbow.\n- **Tinted neutrals**: Pure gray is dead. Add a tiny tint toward the accent color to all your grays (borders, backgrounds, muted text). The tint should be subtle enough not to read as 'colored' but creates subconscious cohesion.\n- **60/30/10 rule**: 60% neutral backgrounds/whitespace, 30% secondary colors (text, borders, inactive states), 10% accent (CTAs, highlights, focus). Accent colors work BECAUSE they're rare. Overuse kills their power.\n- **Dangerous combos to avoid**: Light gray text on white (#1 accessibility fail). Gray text on any colored background (looks washed out and dead, use a darker shade of the background color instead). Yellow text on white. Thin light text on images.\n- **Dark mode is not inverted light mode**: Use lighter surfaces for depth (no shadows). Desaturate accents slightly. Never pure black backgrounds, use dark gray (12-18% lightness). Reduce body text weight slightly (350 vs 400) because light-on-dark reads heavier.\n- Never use pure #000 or #fff. Customize the shadcn CSS variables in globals.css to match the plan palette.\n\n**Layout & space**: Space is a design material, not leftover.\n- **4px base unit**: All spacing should be multiples of 4: 4, 8, 12, 16, 24, 32, 48, 64, 96px. No arbitrary values. Name tokens semantically (`space-sm`, `space-lg`), not by value.\n- **Use `gap` not margins**: Use `gap` for sibling spacing. It eliminates margin collapse hacks and is more predictable. Reserve margin for positioning, not spacing between siblings.\n- **Rhythm through contrast**: Tight groupings within related items (8-12px), generous separation between sections (48-96px). Vary spacing WITHIN sections too. Not every row needs the same gap. The ratio between inner and outer spacing creates hierarchy.\n- **Density matches content**: Data-dense UIs (dashboards, tables, admin panels) need tighter spacing with more information visible. Marketing pages need generous whitespace and breathing room. Match density to what the user is doing.\n- **The squint test**: Blur your eyes. Can you still identify primary, secondary, and clear groupings? If everything looks the same weight, hierarchy is broken. The most important content should be obvious within 2 seconds.\n- **Asymmetry > centering**: Left-aligned with asymmetric layouts feels more designed. Center-alignment is fine for hero sections and CTAs, not for body content.\n- **Flex vs Grid**: Use Flexbox for 1D layouts (rows of items, navbars, button groups, card contents, most component internals). Use Grid for 2D layouts (page-level structure, dashboards, data-dense interfaces where rows AND columns need coordinated control). Don't default to Grid when Flexbox with `flex-wrap` would be simpler. Use `repeat(auto-fit, minmax(280px, 1fr))` for responsive grids without breakpoints. Use named `grid-template-areas` for complex page layouts and redefine at breakpoints.\n- **Cards earn their existence**: Only use cards when content needs clear separation, grid comparison, or interaction boundaries. Not everything needs a container. Never nest cards inside cards. Vary card sizes or mix cards with non-card content to break repetition.\n- **Section variety**: Don't make every section the same structure. Alternate layouts (text-left/image-right, then image-left/text-right), vary section heights, mix full-width with contained content. Monotonous section rhythm signals no designer touched this.\n- **Z-index scale**: Use a semantic scale, not arbitrary numbers. dropdown(10) -> sticky(20) -> modal-backdrop(30) -> modal(40) -> toast(50) -> tooltip(60). Never use 999 or 9999.\n- **Touch targets**: Minimum 44x44px for all interactive elements, even if the visual element is smaller (use padding).\n\n**Cards & surfaces**: Match the plan's cardStyle. Stat cards need background fills with the accent color as a subtle icon background or tinted left border. Use bg-muted/30 for section differentiation between page areas. Build a consistent shadow scale (sm -> md -> lg) and use elevation to reinforce hierarchy, not as decoration.\n\n**Sidebar**: App name + icon at top. Nav items with hover:bg-muted rounded-md transition. Active item: bg-primary/10 text-primary font-medium. User email + sign out at bottom. The sidebar should feel like a distinct surface (bg-card or bg-muted/50).\n**Mobile navigation**: The desktop sidebar must collapse to a hamburger menu on mobile. Use the shadcn `Sheet` component: a hamburger icon button (visible at `md:hidden`) opens a Sheet from the left containing the same nav items. The desktop sidebar is `hidden md:flex`. For simple apps with 3-5 nav items, a bottom tab bar (`fixed bottom-0`) is an alternative. Always respect `env(safe-area-inset-bottom)` for notched phones.\n\n**Interaction design**: Every interactive element needs 8 states, not just default and hover.\n- **Required states**: default, hover, focus, active/pressed, disabled, loading, error, success. Design all of them intentionally.\n- **Focus rings**: Use `:focus-visible` (not `:focus`) so rings show for keyboard users but not mouse clicks. Never remove focus indicators without replacement.\n- **Button hierarchy**: Don't make every button primary. Use ghost, outline, secondary, and text-link variants. One primary CTA per view.\n- **Progressive disclosure**: Simple first, advanced behind expandable sections. Don't overwhelm with options.\n- **Undo over confirmation**: For destructive actions, remove immediately + show an undo toast, then permanently delete after timeout. Better than confirmation dialogs that users click through blindly.\n- **Form patterns**: Visible `<label>` elements always (placeholders aren't labels, they disappear). Validate on blur, not on keystroke. Show format expectations with examples.\n\n**Empty states**: Every empty list/table needs a rich empty state. Not just 'Nothing here'. Include: a simple inline SVG illustration (48x48, relevant to the feature), a specific message that teaches ('Create your first habit to start tracking your daily routine'), and a primary action button. Empty states are the first thing new users see. Make them inviting.\n\n**Loading states**: Use the shadcn `Skeleton` component. Show skeleton versions of cards, lists, and stats during data fetches. Example: `<Skeleton className=\"h-8 w-32\" />` for a stat number, `<Skeleton className=\"h-20 w-full\" />` for a card. Add a `loading.tsx` file in dashboard route groups. Never show a blank page or a lone spinner.\n\n**Images**: For Unsplash images on landing pages, use `next/image` with `placeholder=\"blur\"` and a blurDataURL (a tiny 10x6 base64 image \u2014 generate a solid color blur like `data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAGCAYAAAD68A/GAAAAP0lEQVQYV2N89+7dfwYGBgZGRkYGBgYmBjIBE7kaPHv27D8DA8N/BgYGRkZGRgYGJgYyARO5GkjWQHEoxRoAAPyTGAGBMpEAAAAASUVORK5CYII=`). This creates a smooth shimmer-in effect instead of images popping in.\n\n**Motion** (`motion` package is installed):\n\n**Hero moment strategy**: Pick ONE signature animation per page. Not scattered animations on everything. On a landing page, the hero entrance is your hero moment. On a dashboard, a satisfying success animation after the primary action. One great animation > ten mediocre ones. Everything else gets subtle transitions.\n\n**Animation opportunity audit** -- check these 5 categories:\n1. **Missing feedback**: Button has no press response? Add `active:scale-[0.97]` (CSS) or `whileTap={{ scale: 0.97 }}` (motion). Toggle has no slide? Animate it.\n2. **Jarring transitions**: Menu appears instantly? Add 200ms fade + slide. Content swaps without transition? Crossfade between states.\n3. **Unclear relationships**: Parent-child not visually connected? Stagger children 50ms after parent. Deleted item just disappears? Animate it out (opacity + height collapse).\n4. **Lack of delight**: Success state is flat? Draw a checkmark SVG with stroke-dashoffset animation. Empty state is static? Add a gentle float on the illustration.\n5. **Missed guidance**: First-time user gets no hint? Pulse the primary CTA once on page load. New feature unnoticed? Brief highlight animation.\n\n**Timing hierarchy**: 100-150ms for instant feedback (buttons, toggles). 200-300ms for state changes (menus, tooltips). 300-500ms for layout shifts (accordions, modals). 500-800ms for entrances (page loads). Exit animations run at 75% of entrance duration.\n\n**Easing**: Use exponential curves (Quart out, Expo out) for sophisticated physics. Never `ease` (generic), never bounce/elastic (dated). Real objects decelerate smoothly. CSS: `cubic-bezier(0.25, 1, 0.5, 1)` (Quart out) or `cubic-bezier(0.16, 1, 0.3, 1)` (Expo out).\n\n**Micro-interaction recipes**:\n- **Button press**: `active:scale-[0.97] transition-transform duration-100` (CSS only, no motion needed)\n- **Success checkmark**: SVG `<path>` with `stroke-dasharray` + `stroke-dashoffset` animation from full to 0. 400ms Expo out.\n- **Height expand/collapse**: `grid-template-rows: 0fr -> 1fr` with `transition: grid-template-rows 300ms`. No JS layout measurement needed.\n- **Skeleton to content**: Skeleton pulses (CSS `animate-pulse`), then crossfades to real content with `opacity` transition. Use the pre-scaffolded `<ContentLoader>` component.\n- **Staggered list entrance**: CSS `animation-delay` on children: `.animate-fade-up:nth-child(1) { animation-delay: 0ms } :nth-child(2) { animation-delay: 50ms }` etc. Or use `<StaggerList>` component.\n- **Number count-up**: Use `@number-flow/react` for smooth digit morphing on stats/metrics.\n\n**Performance**: Only animate `transform` and `opacity` (GPU-accelerated). Never animate width, height, top, left, margin, or padding. For scroll-tied effects on landing pages, use CSS `animation-timeline: scroll()` where supported (progressive enhancement).\n\n**Accessibility**: Always respect `prefers-reduced-motion`. Wrap animations in `@media (prefers-reduced-motion: no-preference) { ... }`. Provide static alternatives that convey the same information.\n\n**SSR-safe (CRITICAL)**: This app deploys to Mistflow Cloud's edge runtime where IntersectionObserver may not fire. NEVER use `initial={{ opacity: 0 }}` (content stays invisible forever). Use CSS @keyframes for entrance effects:\n```tsx\n// SAFE: CSS @keyframes in globals.css:\n// @keyframes fade-up { from { opacity: 0; transform: translateY(16px); } to { opacity: 1; transform: translateY(0); } }\n// .animate-fade-up { animation: fade-up 0.5s ease-out both; }\n// Use motion ONLY for hover/tap/gesture interactions:\nimport { motion } from 'motion/react';\n<motion.div whileHover={{ scale: 1.02 }} whileTap={{ scale: 0.97 }} transition={{ duration: 0.15 }}>\n// NEVER: <motion.div initial={{ opacity: 0 }} whileInView={{ opacity: 1 }}>\n```\nFor dashboard pages, keep animations minimal. Button press feedback + skeleton loading + success states. No entrance animations on dashboard pages.\n\n**UX writing**: Every word earns its place.\n- **Buttons**: Use outcome-focused verb + object ('Save changes', 'Create account'), never generic ('Submit', 'OK', 'Click here').\n- **Destructive actions**: State exact consequences ('Delete 5 items permanently', not 'Delete selected'). Distinguish 'Delete' (permanent) from 'Remove' (recoverable).\n- **Error messages**: Answer three questions: what happened, why, and how to fix it. 'Email address is not valid. Include an @ symbol.' not 'Invalid input'. Never blame the user.\n- **Empty states**: Explain what will appear here, why it matters, and give a clear next action.\n- **Loading/progress**: Set time expectations. 'Deploying your app (usually takes 30 seconds)' not just a spinner.\n- **Consistency**: Pick one term and stick with it. 'Delete' everywhere, not Delete/Remove/Trash interchangeably.\n- **Link text**: Make it standalone for accessibility. 'View pricing plans' not 'Click here'.\n\n**Responsive design**: Mobile-first, then enhance.\n- **Mobile-first CSS**: Write base styles for mobile, then use `min-width` media queries to enhance for larger screens. Never start with desktop and try to cram it into mobile.\n- **Input method matters more than screen size**: Use `@media (pointer: fine)` for mouse/trackpad (smaller targets OK, hover effects work), `@media (pointer: coarse)` for touch (44px targets required, no hover-dependent UI). Use `@media (hover: none)` to detect devices that can't hover. Never require hover to reveal functionality.\n- **Safe areas**: Add `<meta name='viewport' content='width=device-width, initial-scale=1, viewport-fit=cover'>` and use `env(safe-area-inset-bottom)` for bottom navigation on notched phones. Fixed bottom bars must respect safe areas.\n- **Self-adjusting grids**: Use `grid-template-columns: repeat(auto-fit, minmax(280px, 1fr))` for card grids. Columns reflow naturally without breakpoint management.\n- **Container queries over media queries**: For reusable components, use `container-type: inline-size` on the parent and `@container (min-width: 400px)` on children. Components adapt to their container, not the viewport.\n- **No horizontal scroll**: Nothing should overflow the viewport width. Test at 320px width (smallest common phone). Use `overflow-x: hidden` on the body as a safety net but fix the root cause.\n- **Touch-friendly spacing**: On mobile, increase padding on list items and nav links. Thumb zone for primary actions is bottom-center of the screen.\n- **Responsive images**: Use `next/image` with `sizes` prop for responsive sizing. For art direction changes, use `<picture>` with `<source media='...'>`. Always set `width` and `height` to prevent layout shift.\n\n**Cognitive load**: Simpler apps feel better, even with the same features.\n- **Max 4 items per group**: Humans hold 4 items in working memory. Navigation menus: max 5 top-level items. Forms: max 4 fields per section. Dashboards: max 4 metrics visible without scrolling. Pricing: max 3 tiers.\n- **Single focus per view**: Every page has ONE primary task. If there are two equal CTAs, neither is primary. One primary action, everything else is secondary or tertiary.\n- **Sequential decisions**: Don't present all options at once. Multi-step forms beat one long form. Each step should have one decision.\n- **Progressive disclosure**: Show the essential first, reveal complexity on demand. Use expandable sections, tabs, or drill-down navigation. The default view should cover 80% of use cases.\n- **Visual grouping**: Related items must be visually grouped (proximity + shared background or border). Unrelated items must have clear separation. The Gestalt principle of proximity is the easiest way to reduce cognitive load.\n- **Reduce choice anxiety**: For important decisions, provide a recommended/default option. Highlight it visually. 'Most popular' badges on pricing plans reduce decision time.\n- **Keep context visible**: In multi-step flows, show a progress indicator and allow going back. Never make users remember what they entered on a previous screen.\n\n**Production hardening**: Designs that only work with perfect data aren't production-ready.\n- **Text overflow**: Use `text-overflow: ellipsis` + `overflow: hidden` for single-line text in constrained spaces. Use `-webkit-line-clamp` for multi-line. Add `min-width: 0` on flex items to prevent overflow.\n- **Extreme inputs**: Test with 100+ character names, emoji in text fields, and empty strings. The layout should not break.\n- **Error resilience**: Every API call needs error UI. Network failures, 404s, 500s all need clear messages with recovery options. Never show raw stack traces.\n\n**Landing page component patterns** (use these for professional, non-generic landing pages):\nBuild these components inline (shadcn + motion + Tailwind is enough):\n- **Animated number counters**: Use `@number-flow/react` (installed) for stat sections. `<NumberFlow value={count} />` with smooth digit transitions.\n- **Marquee / logo ticker**: CSS-only infinite scroll (`@keyframes marquee { from { transform: translateX(0) } to { transform: translateX(-50%) } }`) with duplicated content. Pause on hover.\n- **Bento grid**: CSS grid with varied `col-span` and `row-span`. Each cell gets a unique visual. Never make all cells the same size.\n- **Animated beam / connection lines**: SVG paths with `motion` stroke-dashoffset animation for 'how it works' sections.\n- **Shimmer borders**: `background: conic-gradient(...)` wrapper with animation for hero CTAs.\n- **Comparison tables**: Sticky-header table with check/x icons and row highlighting on the recommended plan.\n- **Testimonial carousel**: CSS scroll-snap for horizontal cards with avatar, quote, name, role. Auto-scroll paused on hover.\n- **Gradient mesh backgrounds**: Multiple layered `radial-gradient` backgrounds for hero sections.\nPick 3-5 per landing page based on the app's content. The goal: every landing page should look like it was designed by a human, not generated by AI.\n\n**Landing page navbar (non-negotiable):** Fully transparent (no background, no border, no backdrop-blur) and NOT sticky/fixed. It sits inside the hero section and scrolls naturally. White text on dark hero backgrounds. Primary CTA button in the nav uses the accent color (solid fill, rounded-md). Do NOT use `sticky`, `fixed`, `top-0`, or `backdrop-blur` on landing page navbars. Dashboard sidebar/nav IS fixed (different pattern).\n\n**Design for THIS app, not any app** (the #1 way to avoid generic AI output):\n- Every layout decision should be informed by what THIS app does. A gym check-in app needs a prominent search bar. A habit tracker needs a daily view. A CRM needs a pipeline. Don't default to the same dashboard-with-stats layout for everything.\n- Write copy that is SPECIFIC to the app's domain. Not 'Manage your data efficiently' but 'Check in members in under 3 seconds'. Every headline, empty state, and CTA should reference what the user actually does in this app.\n- Choose visual emphasis based on the primary action. If the user comes to log a workout, the workout form should be the biggest thing on the dashboard.\n- Use domain-appropriate metaphors. A fitness app uses progress rings. A project tracker uses kanban columns. A recipe app uses cards with photos. Don't use the same card grid for everything.\n\n**NEVER do these (AI slop anti-patterns)**:\nThe following are telltale signs of AI-generated design. Avoid all of them.\n*Visual patterns*:\n- Cyan-on-dark, purple-to-blue gradients, neon accents on dark backgrounds (the AI color palette)\n- Gradient text on headings or metrics (decorative noise, not meaningful)\n- Glassmorphism / blur effects used decoratively (only use blur for overlays/modals that need it)\n- Rounded rectangles with thick colored border on one side (lazy accent, never looks intentional)\n- Dark mode with glowing colored box-shadows on accent elements\n- Small rounded-square icon tiles stacked above headings (the icon-tile pattern)\n- Pure black (#000) backgrounds in dark mode (use tinted dark gray instead)\n- 3-column icon + title + paragraph feature grid (use asymmetric layouts, bento grids, or varied card sizes)\n- Nested cards inside cards (visual noise, flatten the hierarchy)\n- Everything center-aligned (use left-alignment for body content, reserve center for heroes/CTAs)\n- Monotonous spacing (same gap everywhere signals no designer touched this)\n*Copy patterns*:\n- Hero text like 'Transform your X' / 'One Day at a Time' / 'Join thousands' / 'Unlock the power of' (write SPECIFIC copy)\n- Generic feature descriptions ('Powerful analytics', 'Seamless integration', 'Lightning fast')\n- The hero metric template (big number, small label, supporting stats, gradient accent)\n- Repeating information users can already see. Every word must earn its place.\n*Functional patterns*:\n- Bounce/elastic easing on animations (feels dated, real objects decelerate smoothly, use Quart/Expo out)\n- Modals for things that could be inline (modals are a last resort, not a default)\n- Nav links to pages that don't exist. EVERY link in the sidebar/topnav MUST have a corresponding page.\n- FAKE FORMS. NEVER use `setTimeout` or `await new Promise` to simulate API calls. Every form MUST have a real server action (actions.ts with 'use server') that writes to the database. If a form doesn't persist data, the feature is BROKEN.\n\n**Favicon**: A default SVG favicon exists at app/icon.svg. On the landing page step, update it to match the app. Keep it simple (a single icon/letter on a rounded square, using the accent color).\n";var Wr=`# Landing Page Rules
2440
+
2441
+ These rules apply to every landing page. They are non-negotiable.
2442
+
2443
+ ---
2444
+
2445
+ ## 1. Copy Rules
2446
+
2447
+ ### Banned Phrases (Tier 1 -- instant delete)
2448
+
2449
+ Never use these words in any landing page copy. They are the fingerprint of AI-generated text. Replace them with specifics or delete the sentence.
2450
+
2451
+ revolutionize, cutting-edge, seamless, leverage, unlock, empower, game-changing, disruptive, unprecedented, holistic approach, scalable solutions, supercharge, next-generation, state-of-the-art, harness the power, at the forefront, elevate your
2452
+
2453
+ ### Red Flag Phrases (Tier 2 -- replace with specifics)
2454
+
2455
+ These are not banned outright, but they signal lazy copy. Every time you write one, replace it with a number, a concrete outcome, or a timeframe.
2456
+
2457
+ | Red flag | Replace with |
2458
+ |----------|-------------|
2459
+ | "Innovative solutions" | What specifically does it do? "Finds duplicate invoices in 3 seconds" |
2460
+ | "Tailored to your needs" | Whose needs? "Built for 2-person startups, not enterprises" |
2461
+ | "Drive growth" | How much? "Teams using [product] close 40% more deals" |
2462
+ | "Robust platform" | What makes it robust? "99.97% uptime since launch" |
2463
+ | "Streamline your workflow" | Which workflow? "Cuts weekly reporting from 2 hours to 10 minutes" |
2464
+ | "All-in-one platform" | All of what? List the 3 things it replaces |
2465
+ | "In today's fast-paced world" | Delete the sentence. Start with what you actually offer. |
2466
+
2467
+ ### Structural AI Tells (Tier 3 -- rewrite)
2468
+
2469
+ These patterns reveal AI-generated structure even if individual words are fine:
2470
+
2471
+ - Opening with "In today's [adjective] world/landscape, businesses must..."
2472
+ - Three consecutive paragraphs of identical length
2473
+ - Transition words every 2-3 sentences ("Moreover," "Furthermore," "Additionally")
2474
+ - Vague claims without numbers or specifics
2475
+ - Headlines that work for ANY product (the logo-swap test)
2476
+
2477
+ ### The Logo-Swap Test
2478
+
2479
+ After writing every headline and subheadline, ask: "Could I swap in any competitor's logo and this copy still makes sense?"
2480
+
2481
+ - YES = rewrite. Add the product name, a specific number, or a concrete outcome.
2482
+ - NO = ship it. The copy is specific to THIS product.
2483
+
2484
+ **Examples:**
2485
+ - FAILS: "Build the future of work" (any SaaS company could say this)
2486
+ - PASSES: "Close deals 40% faster" (specific claim, specific metric)
2487
+ - FAILS: "The platform teams love" (generic)
2488
+ - PASSES: "Notion + Slack + Docs in one tab" (specific, concrete)
2489
+
2490
+ ### The Specificity Formula
2491
+
2492
+ Every claim on the landing page should follow this pattern:
2493
+
2494
+ \`\`\`
2495
+ SLOP = abstract verb + buzzword noun
2496
+ CLEAN = specific number + concrete outcome + timeframe/context
2497
+ \`\`\`
2498
+
2499
+ Examples:
2500
+ - SLOP: "Supercharge your productivity" -> CLEAN: "Ship 3x faster this quarter"
2501
+ - SLOP: "Unlock actionable insights" -> CLEAN: "See which deals will close this month"
2502
+ - SLOP: "Seamless integration" -> CLEAN: "Connects to Slack in 2 clicks. No code."
2503
+ - SLOP: "Scalable solution" -> CLEAN: "Handles 10 users or 10,000. Same price."
2504
+
2505
+ If you don't have a real number, use a plausible one based on the app's domain. A habit tracker: "Join 2,000+ people building better habits." A booking app: "Book in under 30 seconds." The number grounds the claim.
2506
+
2507
+ ---
2508
+
2509
+ ## 2. Conversion Structure
2510
+
2511
+ Every landing page follows the AIDA framework mapped to sections:
2512
+
2513
+ | Section | AIDA phase | Goal | Time budget |
2514
+ |---------|-----------|------|-------------|
2515
+ | Hero | Attention | Hook in 5 seconds. Value prop + primary CTA. | Above the fold |
2516
+ | Problem / Pain | Interest | Make visitors feel understood. State THEIR pain. | First scroll |
2517
+ | Benefits / Features | Desire | Show the outcome, not the feature | Mid-page |
2518
+ | Social Proof | Trust | Specific testimonials with numbers | Near CTA |
2519
+ | CTA | Action | One clear next step | Bottom |
2520
+
2521
+ ### Section Order by App Type
2522
+
2523
+ **SaaS / Dashboard tool:**
2524
+ Hero -> Logo bar -> Problem -> Benefits (alternating rows) -> Testimonials -> Pricing -> FAQ -> CTA -> Footer
2525
+
2526
+ **Consumer / Lifestyle app (gym, habits, recipes):**
2527
+ Hero (with photo) -> Social proof badge -> Features (bento grid) -> How it works (3 steps) -> Testimonials -> CTA -> Footer
2528
+
2529
+ **Marketplace / Platform:**
2530
+ Hero -> Logo bar -> For buyers section -> For sellers section -> Stats/numbers -> Testimonials -> CTA -> Footer
2531
+
2532
+ **Dev tool / API:**
2533
+ Hero (dark, code snippets) -> Code example -> Features (icon grid) -> Docs link -> Pricing -> Footer
2534
+
2535
+ **Game / Creative:**
2536
+ Hero (full visual, minimal text) -> Gameplay/preview -> Features -> CTA -> Footer
2537
+
2538
+ ### One Page, One Goal
2539
+
2540
+ Every element on the page drives toward the primary CTA. If a section doesn't move the visitor closer to clicking that button, cut it. Common violations:
2541
+ - Blog links in the nav (sends people away)
2542
+ - Multiple competing CTAs ("Sign up" AND "Book a demo" AND "Watch video")
2543
+ - Feature sections that read like documentation instead of benefits
2544
+
2545
+ ---
2546
+
2547
+ ## 3. Section Catalog
2548
+
2549
+ ### Hero (required)
2550
+
2551
+ The most important section. Must have:
2552
+ - A headline that's large and bold (min \`text-4xl md:text-6xl lg:text-7xl\`)
2553
+ - A subheadline (1-2 sentences, \`text-lg\` to \`text-xl\`, muted color)
2554
+ - One or two CTAs (primary filled button + optional secondary ghost/outline)
2555
+ - A visual element: product screenshot, photo, abstract shape, or animated element
2556
+
2557
+ **Hero anti-patterns (instant slop signals):**
2558
+ - Centered text + centered image below = the most overused AI layout
2559
+ - "Welcome to [Product]" headline
2560
+ - Purple-to-blue gradient background
2561
+ - Generic stock illustration of people high-fiving
2562
+ - Three identical feature cards below the hero
2563
+ - Floating dashboard mockup in empty space
2564
+
2565
+ **Good hero patterns:**
2566
+ - Split layout (text left ~55%, visual right ~45%) with slight vertical offset
2567
+ - Full-bleed background (photo or gradient mesh) with overlaid text
2568
+ - Oversized headline that breaks the grid
2569
+ - Product UI in a browser frame or glass card
2570
+
2571
+ ### Social Proof / Logo Bar
2572
+
2573
+ - Row of grayscale logos, \`opacity-50 hover:opacity-100\` transition
2574
+ - Specific metric: "Trusted by 2,847 companies" (not just "Trusted by")
2575
+ - Logos should be SVGs, same height (\`h-6\` to \`h-8\`)
2576
+ - If no real logos: use a social proof badge instead ("Rated 4.9/5 by 2,700+ users")
2577
+
2578
+ ### Problem Section
2579
+
2580
+ Make visitors feel understood. Be specific about THEIR pain.
2581
+
2582
+ - SLOP: "In today's fast-paced business environment, teams struggle with inefficiencies"
2583
+ - CLEAN: "Your team wastes 6 hours/week switching between 12 different tools"
2584
+
2585
+ Use numbers and real time measurements. State the consequence of not solving.
2586
+
2587
+ ### Benefits / Features
2588
+
2589
+ Choose ONE layout per feature section (do not use the same layout twice on one page):
2590
+ - **Bento grid**: Asymmetric grid with cards of varying sizes
2591
+ - **Alternating rows**: Feature image + text, flipping sides each row
2592
+ - **Icon grid**: 3-4 column grid with icon + title + description per card
2593
+ - **Large spotlight**: One feature takes full width with a large visual
2594
+
2595
+ Each feature:
2596
+ - Lead with the BENEFIT, not the feature name
2597
+ - SLOP: "Real-time collaboration" -> CLEAN: "See your teammate's cursor. Ship together."
2598
+ - Include a visual: real screenshot, not a generic icon
2599
+
2600
+ ### Testimonials
2601
+
2602
+ - Quote includes outcome with numbers: "We closed $847K in Q3 directly from leads [product] surfaced."
2603
+ - Real name, role, company
2604
+ - Photo if available (not stock)
2605
+ - SLOP: "Great product! Really helped our team." (delete this, it says nothing)
2606
+
2607
+ ### Pricing
2608
+
2609
+ - Use shadcn Card for each tier
2610
+ - Highlight recommended plan with \`ring-2 ring-primary\` and a badge
2611
+ - Clear feature list with checkmarks
2612
+ - CTA button per tier
2613
+ - Description says WHO this is for, not just the price
2614
+
2615
+ ### CTA Section
2616
+
2617
+ - Full-width section with contrasting background (if page is light, this section is dark, or vice versa)
2618
+ - Restate core promise in headline
2619
+ - One button. Same CTA as the hero.
2620
+ - No more than 3 elements total.
2621
+
2622
+ ### Footer
2623
+
2624
+ - Multi-column layout with link groups
2625
+ - Logo + tagline on the left
2626
+ - Link columns: Product, Company, Resources, Legal
2627
+ - Copyright at bottom
2628
+ - Keep it simple. The footer is not a second homepage.
2629
+
2630
+ ---
2631
+
2632
+ ## 4. Visual Strategy
2633
+
2634
+ The visual approach depends on the app's domain. Pick ONE strategy:
2635
+
2636
+ | App type | Visual strategy | Hero visual |
2637
+ |----------|----------------|-------------|
2638
+ | Consumer/lifestyle (gym, restaurant, travel) | **photo-hero** | Full-bleed photo with dark overlay + text on top |
2639
+ | Content/community (blog, social, marketplace) | **photo-accent** | Photos in feature sections, not hero |
2640
+ | SaaS/dashboard/dev tool | **product-ui** | Glass card or browser frame showing the app's dashboard |
2641
+ | Game/creative | **generated** | CSS art, canvas, shaders, particles |
2642
+ | Portfolio/agency | **abstract** | Geometric visuals, gradient meshes, typography-only |
2643
+
2644
+ **Photo-hero rules:**
2645
+ - Photo must be domain-specific (a gym app shows someone training, not a generic "modern workspace")
2646
+ - Dark overlay (bg-black/40 to bg-black/60) for text contrast
2647
+ - Text is white/light on the overlay
2648
+ - Photo loaded via Unsplash using the plan's \`imageSearchQueries\`
2649
+
2650
+ **Product-ui rules:**
2651
+ - Show a REAL-looking preview of what the app's dashboard will look like
2652
+ - Use a glass card (\`bg-white/10 backdrop-blur-lg border border-white/20 rounded-2xl\`) or browser chrome frame
2653
+ - Fill with domain-specific fake data (a gym app shows member stats, not lorem ipsum)
2654
+ - Never show a generic "analytics dashboard" -- it must match THIS app's data model
2655
+
2656
+ **Generated/abstract rules:**
2657
+ - CSS gradients, canvas, or WebGL only -- no assets you can't generate from code
2658
+ - Respect \`prefers-reduced-motion\` with a static fallback
2659
+ - Lazy-load heavy 3D via \`next/dynamic\` with \`{ ssr: false }\`
2660
+ - Target 60fps
2661
+
2662
+ ---
2663
+
2664
+ ## 5. Motion Menu
2665
+
2666
+ Use the \`motion\` package (Framer Motion, pre-installed) as the default. Reach for gsap only when motion cannot do the job (scroll-pinned timelines, SplitText). Never import Magic UI or Aceternity UI unless the tone file specifically says to.
2667
+
2668
+ ### Scroll Reveals (pick ONE style, apply consistently across all sections)
2669
+
2670
+ - **Fade-up**: \`initial={{ opacity: 0, y: 24 }}\` (works for all tones)
2671
+ - **Fade-scale**: \`initial={{ opacity: 0, scale: 0.96 }}\` (luxury, editorial)
2672
+ - **Clip reveal**: \`initial={{ clipPath: "inset(100% 0 0 0)" }}\` (brutalist, bold)
2673
+ - **Blur-in**: \`initial={{ opacity: 0, filter: "blur(8px)" }}\` (soft SaaS, dreamy)
2674
+
2675
+ ### Staggered Children (for card grids, feature lists)
2676
+
2677
+ \`\`\`tsx
2678
+ const container = { hidden: {}, show: { transition: { staggerChildren: 0.08 } } };
2679
+ const item = { hidden: { opacity: 0, y: 16 }, show: { opacity: 1, y: 0 } };
2680
+
2681
+ <motion.div variants={container} initial="hidden" whileInView="show" viewport={{ once: true }}>
2682
+ {features.map(f => <motion.div key={f.id} variants={item}>{/* card */}</motion.div>)}
2683
+ </motion.div>
2684
+ \`\`\`
2685
+
2686
+ ### Hero Headline Animation (pick ONE)
2687
+
2688
+ - **Per-word stagger**: words fade in individually (soft-saas, editorial)
2689
+ - **Line-by-line**: each line slides up as a block (luxury, clean)
2690
+ - **Typewriter**: characters appear one at a time (dark-terminal, dev tools)
2691
+ - **Scale entrance**: headline scales from 0.9 to 1 with opacity (bold, playful)
2692
+
2693
+ ### Card Interactions (pick ONE per app)
2694
+
2695
+ - **Hover lift**: \`hover:-translate-y-1 hover:shadow-lg\` (universal)
2696
+ - **Tilt on mouse**: perspective + rotateX/Y tracking cursor (premium)
2697
+ - **Border glow**: \`hover:border-primary/20 hover:shadow-primary/5\` (dark themes)
2698
+ - **Scale**: \`hover:scale-[1.02]\` (playful, minimal)
2699
+
2700
+ ### Background Depth (pick ONE per section, vary across the page)
2701
+
2702
+ - **Floating shapes**: \`bg-primary/5 rounded-full blur-3xl\` with CSS float animation
2703
+ - **Dot grid**: \`radial-gradient(circle, rgba(0,0,0,.06) 1px, transparent 1px)\`
2704
+ - **Grain texture**: SVG noise filter overlay at 5-8% opacity
2705
+ - **Gradient mesh**: 2-3 radial gradients at low opacity, positioned asymmetrically
2706
+
2707
+ ### Number Counter (for stats sections)
2708
+
2709
+ \`\`\`tsx
2710
+ import NumberFlow from "@number-flow/react";
2711
+ <NumberFlow value={1234} format={{ notation: "compact" }} className="text-4xl font-bold tabular-nums" />
2712
+ \`\`\`
2713
+
2714
+ ### Rules
2715
+
2716
+ - Do NOT add motion to every element. Use it for: hero entrance, section reveals, card grids, stats counters.
2717
+ - Keep all durations under 400ms for UI motion. Scroll reveals can be up to 700ms for luxury/editorial tones.
2718
+ - Always set \`viewport={{ once: true }}\` on scroll-triggered animations. Elements should not re-animate when scrolling back up.
2719
+ - Respect \`prefers-reduced-motion\`: wrap animations in a check, provide static fallback.
2720
+ - No two apps should use the same combination of motion techniques. The tone file specifies which techniques fit.
2721
+
2722
+ ---
2723
+
2724
+ ## 6. Anti-Slop Audit Checklist
2725
+
2726
+ Run this checklist before finishing the landing page. Every item must pass.
2727
+
2728
+ ### Copy Audit
2729
+
2730
+ - [ ] Zero Tier 1 banned phrases anywhere on the page
2731
+ - [ ] All Tier 2 phrases replaced with specific numbers or outcomes
2732
+ - [ ] No structural AI tells (cookie-cutter intros, uniform paragraphs, transition-word chains)
2733
+ - [ ] Every claim has a number, example, or proof point
2734
+ - [ ] Headlines pass the logo-swap test (specific to THIS product)
2735
+ - [ ] Copy sounds like a human excited about THIS product, not a press release
2736
+ - [ ] Testimonials include specific outcomes with numbers (not "Great product!")
2737
+
2738
+ ### Design Audit
2739
+
2740
+ - [ ] NOT using Inter as the only font (use the project's configured fonts from layout.tsx)
2741
+ - [ ] NOT using a purple-to-blue gradient as the primary visual
2742
+ - [ ] NOT centering everything on every section (vary alignment)
2743
+ - [ ] Hero has a real visual element (product UI, photo, or generated art -- not just text)
2744
+ - [ ] At least one section has an asymmetric or unexpected layout
2745
+ - [ ] Colors are the project's design tokens (CSS custom properties), not Tailwind defaults
2746
+ - [ ] No identical feature cards (vary card sizes, layouts, or content structure)
2747
+ - [ ] Section spacing is generous (\`py-20 md:py-32\`), not cramped
2748
+ - [ ] Typography has clear hierarchy (hero headline is dramatically larger than body)
2749
+ - [ ] Headlines use \`tracking-tight\`, body uses \`leading-relaxed\`
2750
+ - [ ] At least one dark-background section for contrast rhythm
2751
+
2752
+ ### Motion Audit
2753
+
2754
+ - [ ] At least 2-3 motion techniques used, matching the tone
2755
+ - [ ] Motion style is consistent with tone (slow for luxury, snappy for brutalist, bouncy for playful)
2756
+ - [ ] Hero headline has an entrance animation
2757
+ - [ ] Card grids use staggered entrance
2758
+ - [ ] Scroll reveals use \`viewport={{ once: true }}\`
2759
+ - [ ] No competing animations (max one animated element visible at a time)
2760
+
2761
+ ### The Ultimate Test
2762
+
2763
+ Read the page aloud. Then ask:
2764
+ - "Does this look and sound like it was made for THIS specific product?"
2765
+ - "Would someone remember this page tomorrow?"
2766
+ - "Could I swap in a competitor's logo and nothing feels wrong?"
2767
+
2768
+ If the answer to the last question is yes, the page is not done.
2769
+
2770
+ ---
2771
+
2772
+ ## 7. Color and Typography Rules
2773
+
2774
+ **This is a hard rule, not a guideline.** Every color on the page MUST come from the app's CSS custom properties. Never use Tailwind palette utilities like \`bg-emerald-500\`, \`text-blue-600\`, \`border-slate-200\`, \`from-purple-500\`, \`via-pink-300\`, \`to-amber-400\`, etc. These bypass the design system, break when the user swaps styles, and produce the "landing is emerald but the app is blue" bug.
2775
+
2776
+ **Allowed color utilities (and only these):**
2777
+
2778
+ | Role | Tailwind class |
2779
+ |-------------------|-----------------------------------------------------------|
2780
+ | Page background | \`bg-background\` |
2781
+ | Body text | \`text-foreground\` |
2782
+ | Primary accent | \`bg-primary\`, \`text-primary\`, \`text-primary-foreground\` |
2783
+ | Muted surface | \`bg-muted\`, \`text-muted-foreground\` |
2784
+ | Cards | \`bg-card\`, \`text-card-foreground\` |
2785
+ | Borders | \`border-border\`, \`ring-ring\` |
2786
+ | Destructive | \`bg-destructive\`, \`text-destructive-foreground\` |
2787
+ | Opacity variants | \`bg-primary/10\`, \`text-foreground/60\`, etc. |
2788
+
2789
+ **Forbidden patterns** \u2014 scan your output before submitting:
2790
+ - \`bg-[anything]-[number]\` (e.g. \`bg-emerald-500\`, \`bg-stone-100\`, \`bg-yellow-400\`) \u2014 banned
2791
+ - \`text-[anything]-[number]\` \u2014 banned
2792
+ - \`border-[anything]-[number]\`, \`from-*\`, \`via-*\`, \`to-*\` with numbered palettes \u2014 banned
2793
+ - Hex literals (\`#3b82f6\`), rgb/rgba/hsl literals in className or inline style \u2014 banned
2794
+
2795
+ **Gradients** use the app's primary token, not a named palette:
2796
+ \`\`\`tsx
2797
+ <span className="bg-gradient-to-r from-primary to-primary/60 bg-clip-text text-transparent">keyword</span>
2798
+ \`\`\`
2799
+
2800
+ Typography: use the fonts configured in \`layout.tsx\` (from \`plan.design.fonts\`). The heading font for headlines, the body font for everything else. Never import a different font than what the project uses.
2801
+
2802
+ ---
2803
+
2804
+ ## 8. Responsive Rules
2805
+
2806
+ - Always test at: 375px (mobile), 768px (tablet), 1280px (desktop)
2807
+ - Hero layout: stack vertically on mobile (\`flex-col lg:flex-row\`)
2808
+ - Feature grids: \`grid-cols-1 md:grid-cols-2 lg:grid-cols-3\`
2809
+ - Reduce headline sizes on mobile: \`text-3xl md:text-5xl lg:text-7xl\`
2810
+ - Touch targets: all buttons min \`h-12 px-6\` on mobile
2811
+ - Section padding: \`py-16 md:py-24 lg:py-32\` (scale up with screen size)
2812
+ - Max content width: \`max-w-6xl mx-auto\` or \`max-w-7xl mx-auto\`
2813
+ - Inner padding: \`px-6 md:px-8\`
2814
+
2815
+ ---
2816
+
2817
+ ## 9. File Structure
2818
+
2819
+ \`\`\`
2820
+ components/
2821
+ landing/
2822
+ hero.tsx
2823
+ logo-bar.tsx
2824
+ features.tsx
2825
+ testimonials.tsx
2826
+ pricing.tsx
2827
+ cta-section.tsx
2828
+ footer.tsx
2829
+ app/
2830
+ page.tsx -- composes sections, imports from components/landing/
2831
+ globals.css -- design tokens (already generated by init)
2832
+ layout.tsx -- fonts (already generated by init)
2833
+ \`\`\`
2834
+
2835
+ Each section is a separate component file. The page.tsx simply imports and stacks them. Do NOT build one monolithic page component.
2836
+ `;var Gr=`# Design Doctrine
2837
+
2838
+ This is the standard for every UI you generate. It is not aspirational. It is the floor.
2839
+
2840
+ ## The Standard
2841
+
2842
+ Every interface you produce \u2014 landing, dashboard, auth page, CRUD screen, email \u2014 is a chance to be remarkable. Remarkable is not "less generic." Remarkable means someone opening your page would describe it with a specific word: clinical, brutalist, warm, industrial, editorial, playful. They would remember it.
2843
+
2844
+ If the result could be the landing page for a hundred other SaaS products, it's a failure \u2014 no matter how cleanly the code is written.
2845
+
2846
+ ## Commit to an Extreme
2847
+
2848
+ Before writing any component:
2849
+
2850
+ 1. **Read DESIGN.md.** Note the Aesthetic Direction. It names one extreme \u2014 execute it, don't hedge toward the middle.
2851
+ 2. **Pick the memorable moment.** One signature element per page: a typographic hero, a staggered load animation, a product-specific illustration, an unexpected layout. Every page gets exactly one. Zero is generic. Three is noise.
2852
+ 3. **Decide the motion vocabulary.** Minimal-functional (only transitions that aid comprehension) or orchestrated (one designed page-load sequence). Not both, not neither.
2853
+
2854
+ Then implement code that is:
2855
+ - Production-grade and functional
2856
+ - Visually striking and memorable
2857
+ - Cohesive with the DESIGN.md direction
2858
+ - Meticulously refined in every detail
2859
+
2860
+ ## What Makes a Page Remarkable (and what doesn't)
2861
+
2862
+ | Remarkable | Generic |
2863
+ |---|---|
2864
+ | Asymmetric hero with content bleeding off-grid | Centered pill \u2192 headline \u2192 CTA \u2192 3 bullets |
2865
+ | Bold display font with character (Satoshi, Fraunces, Instrument Serif) | Inter / Geist / Roboto / system-ui |
2866
+ | One dominant color, used confidently on CTAs and accents | Evenly-distributed palette with multiple accents |
2867
+ | Product-specific copy: real feature names, real numbers, real user language | "The platform teams love" / "Boost your productivity" |
2868
+ | Real product preview (actual screens, not fake browser chrome) | A rounded box with red/yellow/green dots and "app.example.com" |
2869
+ | Typography that stops a scroll (massive hero, tight tracking, asymmetric break) | Text-xl h1 centered in a max-w-3xl container |
2870
+ | Motion that is specifically about this product | Generic fade-in everywhere |
2871
+ | Texture: grain, gradient mesh, layered transparencies, decorative borders | Solid white with emerald-500 accents |
2872
+
2873
+ ## The "Never" List
2874
+
2875
+ Never do any of these. They are instant generic-AI tells:
2876
+
2877
+ - Purple gradient on a white background. Any gradient that reads "generic AI-generated" is off-limits.
2878
+ - A badge pill that says "New:" or "Built for X" or "Made for Y" at the top of the hero.
2879
+ - A fake browser chrome mockup with three colored dots.
2880
+ - A "Trusted by these logos" row without real logos.
2881
+ - Three checkmark bullets in a row below the CTAs.
2882
+ - Tailwind palette utilities (\`bg-emerald-500\`, \`text-blue-600\`, \`border-slate-200\`). Every color goes through CSS variables \u2014 see the Color reference.
2883
+ - Inter, Roboto, Arial, system-ui as the primary font.
2884
+ - Center-stacked single-column hero. Asymmetric layouts only.
2885
+ - "Features, Pricing, Teams, About" navbar with "Sign in / Start Free" on the right. This exact navbar is on 10,000 SaaS landings.
2886
+ - Stock Unsplash photos of "diverse teams in modern offices" as hero images. If you must use a photo, it must be domain-specific (a habit-tracker hero shows actual tracked habits, not an office scene).
2887
+
2888
+ ## The Self-Audit
2889
+
2890
+ Before submitting any UI file, read it with this checklist and regenerate if any answer is wrong:
2891
+
2892
+ 1. Does the hero have a \`<Badge>\` / pill that says "Built for" or "New:" or similar? **Regenerate without it.**
2893
+ 2. Is the hero centered in a single column with headline over subheadline over two buttons? **Regenerate with an asymmetric layout.**
2894
+ 3. Are there three \`<CheckCircle2>\` / checkmark bullets in a row? **Remove or redesign.**
2895
+ 4. Does the file contain \`bg-emerald-*\`, \`bg-red-*\`, \`bg-amber-*\`, \`text-violet-*\`, or similar palette classes? **Replace with \`bg-primary\`, \`bg-success\`, \`bg-destructive\`, \`bg-warning\`, or variable-based classes.**
2896
+ 5. Is the font \`Inter\`, \`Geist\`, \`Roboto\`, \`Arial\`, or \`system-ui\`? **Pick a distinctive font from Fontshare or Google Fonts that matches DESIGN.md.**
2897
+ 6. Is there a fake browser chrome with dots and a fake URL? **Delete it. If you need a product preview, render the actual component.**
2898
+ 7. Can you describe this page in one characterful word (clinical / brutalist / warm / editorial / playful / industrial)? **If not, the aesthetic direction is too safe \u2014 pick an extreme.**
2899
+ 8. Is there ONE memorable moment on the page (signature typography / orchestrated motion / unexpected layout)? **If zero, add one. If three, keep the strongest one.**
2900
+
2901
+ If any answer fails, the UI is not ready. Redesign \u2014 not tweak \u2014 the failing piece.
2902
+ `;var Vr=`# Typography
2903
+
2904
+ Typography is the cheapest way to escape AI slop and the most visible place it shows up. Every generic landing uses Inter. Every remarkable landing picks something you'd notice.
2905
+
2906
+ ## Font Selection
2907
+
2908
+ Pick a distinctive display font. Default to the fonts declared in DESIGN.md; if DESIGN.md doesn't specify, choose from this catalog:
2909
+
2910
+ **Display / Hero (pick one):**
2911
+ - **Satoshi** \u2014 geometric with warmth, distinctive lowercase \`a\` and \`g\`. Works for dev tools and productivity. Load from Fontshare.
2912
+ - **Instrument Serif** \u2014 editorial, elegant, italic-capable. Works for luxury, editorial, content apps. Load from Google Fonts.
2913
+ - **Fraunces** \u2014 variable axis (soft\u2192wonky), magazine feel with personality. Load from Google Fonts.
2914
+ - **DM Serif Display** \u2014 high-contrast, confident, editorial. Load from Google Fonts.
2915
+ - **Space Grotesk** \u2014 geometric, technical, suits analytical dashboards. Load from Google Fonts. *(Note: has become common; use only if it genuinely fits.)*
2916
+ - **Manrope** \u2014 modern humanist, flexible, works as display OR body. Load from Google Fonts.
2917
+
2918
+ **Body / UI (pick one):**
2919
+ - **DM Sans** \u2014 clean, humanist, pairs well with almost any display. Load from Google Fonts.
2920
+ - **Manrope** \u2014 same family as above, use if also using Manrope display for harmony.
2921
+ - **Geist** \u2014 Vercel's font, works but common. Avoid if display is also Geist.
2922
+ - **Inter Tight** \u2014 denser Inter; still Inter-family so only use if you really need the readability.
2923
+
2924
+ **Code / Data (pick one):**
2925
+ - **JetBrains Mono** \u2014 distinctive lowercase \`r\`, \`g\`, \`@\`. Default for data-dense UI.
2926
+ - **IBM Plex Mono** \u2014 variable weight, more characterful than most monos.
2927
+ - **Berkeley Mono** \u2014 premium mono with character. Requires license; use only if the user has it.
2928
+
2929
+ **Banned as primary font** (use them as fallbacks only, never as the leading choice):
2930
+ - Inter, Roboto, Arial, Helvetica, system-ui, ui-sans-serif, SF Pro
2931
+
2932
+ ## Loading
2933
+
2934
+ All fonts must be loaded in \`app/layout.tsx\` via \`next/font\`. Do NOT use external \`<link>\` tags (loses preload + causes layout shift).
2935
+
2936
+ \`\`\`tsx
2937
+ // For Google Fonts
2938
+ import { Manrope, DM_Sans, JetBrains_Mono } from "next/font/google";
2939
+
2940
+ const displayFont = Manrope({ subsets: ["latin"], weight: ["700", "800"], variable: "--font-display" });
2941
+ const bodyFont = DM_Sans({ subsets: ["latin"], weight: ["400", "500", "600"], variable: "--font-body" });
2942
+ const monoFont = JetBrains_Mono({ subsets: ["latin"], variable: "--font-mono" });
2943
+ \`\`\`
2944
+
2945
+ \`\`\`tsx
2946
+ // For Fontshare fonts, use next/font/local with the TTF files
2947
+ import localFont from "next/font/local";
2948
+ const satoshi = localFont({
2949
+ src: [
2950
+ { path: "./fonts/Satoshi-Bold.woff2", weight: "700" },
2951
+ { path: "./fonts/Satoshi-Black.woff2", weight: "900" },
2952
+ ],
2953
+ variable: "--font-display",
2954
+ });
2955
+ \`\`\`
2956
+
2957
+ ## Scale
2958
+
2959
+ Use the scale declared in DESIGN.md. If not declared, default to:
2960
+
2961
+ | Role | Size | Weight | Line-height | Tracking |
2962
+ |---|---|---|---|---|
2963
+ | Hero | \`clamp(40px, 6vw, 72px)\` | 800-900 | 1.0 | -0.03em |
2964
+ | H1 | 48px | 700 | 1.1 | -0.02em |
2965
+ | H2 | 32px | 700 | 1.15 | -0.01em |
2966
+ | H3 | 24px | 600 | 1.2 | 0 |
2967
+ | H4 | 18px | 600 | 1.3 | 0 |
2968
+ | Body | 16px | 400 | 1.55 | 0 |
2969
+ | Small | 14px | 400 | 1.5 | 0 |
2970
+ | Micro | 12px | 500 | 1.4 | 0 |
2971
+ | Nano | 11px | 600 | 1.4 | 0.05em (tracking-wide, uppercase for eyebrows) |
2972
+
2973
+ ## Hierarchy Rules
2974
+
2975
+ - Hero typography carries the page. \`text-6xl md:text-7xl\` is a floor, not a ceiling. If the hero is \`text-4xl\`, it's too timid.
2976
+ - Use weight contrast aggressively. 900 display against 400 body reads as "designed". 700 against 500 reads as "default".
2977
+ - Tight tracking (\`tracking-tight\` / \`-0.02em\`) on large display. Normal tracking on body. Wide tracking (\`tracking-wider\` / 0.08em) on eyebrow labels, uppercase.
2978
+ - One typeface does most of the work. Two is a deliberate pairing. Three is a mistake.
2979
+
2980
+ ## Memorable Moment Options
2981
+
2982
+ Pick exactly one:
2983
+ - **Asymmetric headline break** \u2014 the headline splits mid-sentence to an offset second line, with an italic or color-shifted word at the break.
2984
+ - **Giant eyebrow label** \u2014 nano-sized tracked-wide uppercase label above the hero, specific to the product ("ASYNC FOR TEAMS OF 3-30").
2985
+ - **Italic pull-quote** \u2014 a sentence in the display font, italic, at the hero's bottom edge.
2986
+ - **Variable-weight transition** \u2014 a single word animates its variable-font weight on page load (for Fraunces, Satoshi Variable).
2987
+ - **Mixed case display** \u2014 Title Case for proper nouns, SmallCaps for the verb. Editorial feel.
2988
+
2989
+ Do NOT use: gradient text (\`bg-clip-text\`) on one word of the headline. That effect is the most overused generic-AI marker.
2990
+ `;var Jr="# Color\n\nColor is a commitment, not a palette swatch. The #1 failure mode in AI-generated landings is an evenly-distributed palette: a little emerald here, a little amber there, a gradient, a badge. That's visual hedging \u2014 it signals \"I couldn't pick a point of view.\"\n\nPick one color. Use it with intent.\n\n## The Token System\n\nEvery color on every screen goes through a CSS variable defined in `globals.css`. You do not hand-pick hex values in JSX. You do not use Tailwind palette utilities (`bg-emerald-500`, `text-blue-600`, `border-slate-200`). If a color isn't in the token scheme, it doesn't exist for this app.\n\n### Core tokens\n\n- `--color-background` \u2014 page background\n- `--color-foreground` \u2014 default body text\n- `--color-card` \u2014 elevated surface (cards, panels)\n- `--color-card-foreground` \u2014 text on cards\n- `--color-muted` \u2014 low-emphasis surface fill\n- `--color-muted-foreground` \u2014 secondary text (\u2265 WCAG AA contrast vs background)\n- `--color-border` \u2014 subtle dividers\n- `--color-input` \u2014 form input borders\n- `--color-popover` / `--color-popover-foreground` \u2014 dropdowns, tooltips, menus\n\n### Interactive tokens\n\n- `--color-primary` \u2014 primary CTAs, links, active tab, focus ring\n- `--color-primary-foreground` \u2014 text on primary (derived for WCAG AA contrast)\n- `--color-ring` \u2014 focus outline (same hue as primary)\n- `--color-secondary` / `--color-secondary-foreground` \u2014 quieter than primary, more than muted\n- `--color-accent` / `--color-accent-foreground` \u2014 hover states on neutrals\n\n### Semantic tokens\n\n- `--color-success` \u2014 confirmation, \"verified\" banners, positive states\n- `--color-warning` \u2014 pending states, attention, non-destructive caution\n- `--color-destructive` / `--color-destructive-foreground` \u2014 errors, irreversible actions\n- `--color-info` \u2014 neutral system messages, tips\n\n**You use these semantic tokens for all status UI.** When you need a \"green checkmark\" you reach for `text-success`, not `text-emerald-500`. When you need an amber alert you reach for `bg-warning/10 text-warning`, not `bg-amber-50 text-amber-700`.\n\n## Usage Rules\n\n**Primary color:**\n- Primary CTAs, links, the active navigation tab, focus rings.\n- NOT for headings. Black or foreground-colored headings are stronger.\n- NOT for large surface fills (it's a splash of color, not a wash).\n- NOT for borders except focus rings.\n- One bold use of primary beats twenty sprinkled uses.\n\n**Neutrals do the structural work:**\n- Background, cards, borders \u2014 all come from the neutral scale.\n- Text hierarchy via opacity on foreground (`text-foreground`, `text-foreground/80`, `text-muted-foreground`) \u2014 not different colors.\n\n**Semantic colors:**\n- Use `bg-success/10 text-success border border-success/30` for success banners (reproducibility: a light-tinted version of the success color for surface, the solid version for text and border).\n- Same shape for warning, info, destructive.\n\n## Dark Mode\n\nCommit to one theme. If DESIGN.md says the app is dark-themed, make it unambiguously dark \u2014 `#0c0c0c` / near-black base, not neutral-900. If light-themed, commit to light.\n\nDo NOT auto-swap based on `prefers-color-scheme` when the design system declared a theme direction. The appStyle chose a theme for a reason; respect it.\n\n## Forbidden\n\n- Palette utilities: `bg-emerald-500`, `text-blue-600`, `border-slate-200`, `from-purple-500`, `via-pink-300`, `to-amber-400`. Every one of these in your output is a slop indicator.\n- Hex literals inside `className` or inline `style` props.\n- Named colors from CSS (`red`, `lightgrey`, `rebeccapurple`) \u2014 never used in production UI; always a sign of rushed work.\n- Purple gradients on white. The single most overused AI-design pattern.\n- Three-color gradient pills. Rainbow badges. \"Vibrant\" anything.\n\n## Contrast\n\nEvery text/surface pair in DESIGN.md is already WCAG AA-verified by Mistflow's contrast test. You do not need to re-check \u2014 the tokens are safe by construction. You DO need to:\n\n- Not override token combinations with hardcoded classes that might fail contrast.\n- Maintain contrast for overlays (text on images, modals on scrims). When stacking text over a gradient or image, include a scrim (`bg-background/80 backdrop-blur-sm`) so body text stays \u2265 4.5:1.\n\n## Getting Color Wrong\n\nThe specific failure mode to avoid: a page where primary is used in five places, all small and incidental \u2014 a checkmark, a hover state, a tiny badge. That's AI hedging. Either use primary confidently on a hero CTA and mean it, OR don't use primary on the page at all and let neutrals carry the identity.\n";var Kr=`# Motion
2991
+
2992
+ Motion is the most-ignored dimension of AI-generated UI. Generic landings either have no motion or sprinkle tiny fades on everything. Remarkable landings pick ONE orchestrated moment and execute it precisely.
2993
+
2994
+ ## The Principle
2995
+
2996
+ One designed motion moment per page beats twenty scattered micro-interactions.
2997
+
2998
+ A user describing your page afterward should be able to name the motion: "the headline loaded line-by-line," "the product preview drifted in from below," "the nav underline sweeps across on hover." If they can't name it, there wasn't one.
2999
+
3000
+ ## Tokens
3001
+
3002
+ Use these tokens (already in \`globals.css\`), not arbitrary cubic-bezier curves:
3003
+
3004
+ **Easing:**
3005
+ - \`--ease-quart-out\` : \`cubic-bezier(0.25, 1, 0.5, 1)\` \u2014 standard enter / settle. Default.
3006
+ - \`--ease-expo-out\` : \`cubic-bezier(0.16, 1, 0.3, 1)\` \u2014 confident enter for hero elements.
3007
+ - \`--ease-back-out\` : \`cubic-bezier(0.34, 1.56, 0.64, 1)\` \u2014 overshoot for playful accents only.
3008
+
3009
+ **Duration scale:**
3010
+ - \`micro\` \u2014 80ms (button press, toggle flip)
3011
+ - \`short\` \u2014 150ms (hover, focus ring)
3012
+ - \`medium\` \u2014 250ms (modal reveal, card hover-lift, dropdown)
3013
+ - \`long\` \u2014 400ms (page section reveal, hero load)
3014
+
3015
+ ## Signature Moment Patterns
3016
+
3017
+ Pick ONE. Execute it well.
3018
+
3019
+ ### 1. Staggered hero reveal (most versatile)
3020
+ On page load, the hero's children animate in with a 50ms stagger.
3021
+
3022
+ \`\`\`tsx
3023
+ // app/page.tsx hero section
3024
+ <section className="stagger animate-fade-up">
3025
+ <p className="text-xs uppercase tracking-wider">Eyebrow label</p>
3026
+ <h1 className="text-6xl">Main headline</h1>
3027
+ <p className="text-lg">Subhead</p>
3028
+ <div className="flex gap-3"><button>Primary</button><button>Secondary</button></div>
3029
+ </section>
3030
+ \`\`\`
3031
+
3032
+ With this CSS (already in \`globals.css\`):
3033
+
3034
+ \`\`\`css
3035
+ .stagger > :nth-child(1) { animation-delay: 0ms; }
3036
+ .stagger > :nth-child(2) { animation-delay: 50ms; }
3037
+ .stagger > :nth-child(3) { animation-delay: 100ms; }
3038
+ .stagger > :nth-child(4) { animation-delay: 150ms; }
3039
+ \`\`\`
3040
+
3041
+ ### 2. Typographic reveal (for editorial / luxury)
3042
+ The headline scrubs its variable-font weight from 100 \u2192 800 over 600ms on load. Requires a variable font (Fraunces, Recursive, Satoshi Variable).
3043
+
3044
+ ### 3. Scroll-triggered product preview
3045
+ The hero's product preview slides up and into focus as the user scrolls past 20vh. Use CSS \`@supports (animation-timeline: scroll())\` with a \`view-timeline\`, or Intersection Observer for broader support.
3046
+
3047
+ ### 4. Nav underline sweep
3048
+ On hover, a 2px underline animates from 0% to 100% of the link's width with \`--ease-expo-out\` over 200ms. Subtle but deliberate.
3049
+
3050
+ ### 5. Button press depth
3051
+ Primary CTA has a 2px vertical translate on \`:active\` with 80ms \`--ease-quart-out\`. Feels physical. Already in \`globals.css\` as default button behavior.
3052
+
3053
+ ## Patterns to Avoid
3054
+
3055
+ - **Fade-in on everything.** If every section fades in on scroll, nothing is memorable.
3056
+ - **Random bounce on icons.** Decorative-only motion is noise.
3057
+ - **Autoplaying video loops as the hero background.** They look flashy for 3 seconds and become distracting on page 2.
3058
+ - **Parallax scrolling on every section.** Overused; feels dated.
3059
+ - **Spring physics on nav links.** Too much motion in a commonly-hit target is annoying.
3060
+
3061
+ ## Reduced Motion
3062
+
3063
+ \`globals.css\` already respects \`prefers-reduced-motion: reduce\` and disables animations for users who opt out. Do not override this. Do not condition content on motion playing. If the hero reveal is the memorable moment, the static state must also look intentional.
3064
+
3065
+ ## One-Line Motion Rule
3066
+
3067
+ If the motion you're about to add could be described as "subtle animation on scroll", delete it. Design motion is specific, named, and described in one sentence: "the headline lines drop in from above with a 50ms stagger at page load." If you can't describe it that specifically, you don't have a motion design \u2014 you have motion noise.
3068
+ `;var Yr=`# Spatial Composition
3069
+
3070
+ Layout is where AI-slop is most visible. A centered single-column hero with a pill, headline, subhead, and two CTAs is the most-recognizable generic-SaaS pattern on the internet. It is the layout you must not use.
3071
+
3072
+ ## The Principle
3073
+
3074
+ Commit to one layout posture per page. Asymmetry and restraint both work. The safe middle \u2014 centered, evenly-padded, predictable grid \u2014 does not.
3075
+
3076
+ ## Base Grid
3077
+
3078
+ - **Base unit:** 4px. Every spacing value is a multiple: \`2px, 4px, 8px, 12px, 16px, 20px, 24px, 32px, 48px, 64px, 96px\`.
3079
+ - **Columns:** 12 at \`lg+\` (1024px+), 6 at \`md\` (768-1023px), 1 at \`sm\` (<768px).
3080
+ - **Max content widths:**
3081
+ - Hero text block: \`max-w-2xl\` (640px) \u2014 long lines are unreadable
3082
+ - Long-form content: \`max-w-5xl\` (1080px)
3083
+ - Dashboards / data: \`max-w-6xl\` (1200px)
3084
+ - Full-bleed hero: no max width, margin by percent (\`px-[8vw]\`)
3085
+
3086
+ ## Layout Posture \u2014 Pick One
3087
+
3088
+ ### A. Asymmetric editorial
3089
+ Hero content is offset from center. Visual element (illustration, product preview, typography as image) anchors the other side. Neither side fills evenly. Example: Linear.app hero \u2014 text at left 55%, product preview at right 45% with a deliberate vertical offset.
3090
+
3091
+ ### B. Full-bleed typographic
3092
+ A giant headline fills the viewport. No sidebar visual, no product preview. The typography IS the hero. Example: Vercel.com hero, Framer's landing.
3093
+
3094
+ ### C. Split-canvas
3095
+ Two contrasting halves \u2014 one dark, one light, or one content-heavy and one illustrative. Diagonal break line often. Example: Apple product pages.
3096
+
3097
+ ### D. Spatially-dense dashboard-first
3098
+ For internal tools and admin apps \u2014 the landing SHOWS the product running with real-looking data. No marketing framing; the interface sells itself. Example: Linear when logged-out still shows a stylized issue tracker.
3099
+
3100
+ ### E. Narrative-scrolling long-form
3101
+ A landing that scrolls through a story. Each section has a distinct layout (not a repeating card grid). Requires real narrative thinking \u2014 don't attempt if the product is "a dashboard for X."
3102
+
3103
+ **Prohibited posture: centered single-column.** A pill above a headline above a subhead above two buttons in a \`max-w-3xl\` container is the default AI landing. Regenerate if you produced this.
3104
+
3105
+ ## Density
3106
+
3107
+ - **Dashboards, data tables, settings:** Comfortable-dense. \`gap-3\` between table rows, \`gap-4\` between sections. Users look at many things quickly; too-airy density reads as "underdesigned."
3108
+ - **Marketing hero:** Generous. \`py-24\` or \`py-32\` on the hero section. Oversized typography needs oversized spacing to breathe.
3109
+ - **Forms:** Structured. \`gap-4\` between fields, \`gap-2\` between label and input. No surprises.
3110
+ - **Cards in a feature grid:** Equal vertical rhythm across rows. \`gap-6\` between cards, \`p-6\` inside.
3111
+
3112
+ ## Radius Conventions
3113
+
3114
+ Use these by role, not by taste:
3115
+
3116
+ - **Buttons, inputs, badges:** \`rounded-md\` (8px). Click-target elements.
3117
+ - **Cards, panels:** \`rounded-lg\` (12px). Content containers.
3118
+ - **Modals, hero cards:** \`rounded-xl\` (16px). Elevated or focal.
3119
+ - **Pills, avatars:** \`rounded-full\`.
3120
+ - **Pricing-card "recommended" highlight:** ring-2, not a different radius.
3121
+
3122
+ If DESIGN.md says the app is brutalist, override to \`rounded-none\` everywhere. If luxury / editorial, \`rounded-sm\` (4px) \u2014 less rounded is more refined.
3123
+
3124
+ ## Breaking the Grid
3125
+
3126
+ To signal "designed, not templated":
3127
+
3128
+ - **Content bleed:** one element extends past the standard \`max-w-6xl\` container. An image bleeds to the viewport edge. A quote spans wider than surrounding text.
3129
+ - **Diagonal elements:** a rotated text block (2-4\xB0), a skewed background accent, an off-axis arrow. Use sparingly \u2014 one per page.
3130
+ - **Overlap:** a card overlaps two sections, half in one and half in another. Requires careful z-index management but reads as intentional.
3131
+ - **Asymmetric columns:** a feature grid with deliberately uneven column widths (e.g., \`grid-cols-[2fr_1fr_1fr]\`) rather than equal thirds.
3132
+
3133
+ ## The Empty-Space Rule
3134
+
3135
+ Negative space is a design element. A hero with \`py-32\` and a tight \`max-w-2xl\` text block in a wide viewport reads as confident. A hero that fills every pixel with checkmarks, logos, and counters reads as scared.
3136
+
3137
+ Pick: generous breathing (luxury / refined / editorial) OR controlled density (dashboards / industrial / brutalist). Never a mushy middle.
3138
+ `;var Qr=`# Interaction
3139
+
3140
+ Every interactive element has a designed state for every phase: rest, hover, focus, active, disabled. The difference between remarkable and adequate lives in these states.
3141
+
3142
+ ## States
3143
+
3144
+ For every button, link, input, and card:
3145
+
3146
+ - **Rest:** the default, designed state.
3147
+ - **Hover:** a specific visual change (background shift, border darken, small translate, not just \`opacity-80\`). \`transition-colors duration-150\` minimum.
3148
+ - **Focus (keyboard):** \`focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2\` \u2014 always. Keyboard users must see the target.
3149
+ - **Active (pressed):** physical feedback. Buttons: \`active:translate-y-[1px]\` or \`active:scale-[0.97]\`. Already in globals.css as default.
3150
+ - **Disabled:** \`disabled:opacity-50 disabled:cursor-not-allowed\`. Explicit, not left to default browser styling.
3151
+
3152
+ ## Forms
3153
+
3154
+ - **Label above input**, not placeholder-as-label. Placeholders disappear on focus and hide context.
3155
+ - **Inline validation** on blur, not just on submit. Red border + specific error message below ("Email must include @").
3156
+ - **Password fields** show/hide toggle, not a plain masked field.
3157
+ - **Submit buttons** have a loading state (\`disabled\` + spinner or \`"Saving\u2026"\`). Never a dead-click submit.
3158
+ - **Form field \`autocomplete\` attributes** must be correct: \`autocomplete="email"\`, \`autocomplete="current-password"\`, \`autocomplete="new-password"\`. Browsers fill forms faster when you do this.
3159
+
3160
+ ## Feedback
3161
+
3162
+ - **Success actions** show a toast or inline green banner with the specific outcome ("Invite sent to maya@team.com").
3163
+ - **Error states** show the error inline where the user can fix it, not in a dismissable toast they'll miss.
3164
+ - **Optimistic updates** for actions that usually succeed (liking, checking off a task). Rollback on failure with a visible indicator.
3165
+ - **Loading** \u2014 never a blank white screen. Skeleton loaders for content, spinners for actions under 2s, progress bars for actions over 2s.
3166
+
3167
+ ## Nav
3168
+
3169
+ - **Current page indication** via a specific marker (underline, side indicator, tinted background). Not just \`font-semibold\` \u2014 users don't read weight changes as "current".
3170
+ - **Hover on nav links** animates a signature element (underline sweep, side dot grow, color shift). See motion.md patterns.
3171
+ - **Mobile nav** has a specific close affordance \u2014 a clear \`X\` in the top-right, not just "tap outside". Users don't know outside-tap works.
3172
+
3173
+ ## Links
3174
+
3175
+ - **Primary links** are the accent color (\`text-primary\`) or underlined. Not just "blue underline" default.
3176
+ - **Hover** changes something: color shift, underline fade-in, arrow appear (\`\u2192\`), slight translate.
3177
+ - **External links** are marked (icon, new-tab indicator, or explicit \`target="_blank"\` with \`aria-label\` noting the new window).
3178
+
3179
+ ## Buttons
3180
+
3181
+ Three variants, no more:
3182
+
3183
+ - **Primary** \u2014 solid background, high-emphasis CTA. One per section max. Use the product's voice ("Start Free", "Book a Demo", "Get Started") not generic ("Submit", "Click here").
3184
+ - **Secondary / outline** \u2014 bordered or ghost, lower emphasis.
3185
+ - **Tertiary / link-like** \u2014 text-only, smallest emphasis. For "Cancel", "Skip", "Learn more".
3186
+
3187
+ Button text is specific and active-voice. "Start Free" > "Start". "Reset password" > "Submit". "Cancel" is fine when the action is ambiguous; "Keep editing" is better when it isn't.
3188
+
3189
+ ## Inputs
3190
+
3191
+ - **Base radius matches buttons** \u2014 if buttons are \`rounded-md\`, inputs are \`rounded-md\`.
3192
+ - **Focus ring** same color as primary, via \`focus-visible:ring-ring\`.
3193
+ - **Error state** is a red border (\`border-destructive\`) + error message below, not a red background fill.
3194
+ - **Required fields** marked with a subtle indicator (asterisk in muted color, or "required" label), not just browser default.
3195
+
3196
+ ## Modals
3197
+
3198
+ - **Scrim** is \`bg-background/80 backdrop-blur-sm\` \u2014 not pure black, not pure transparent.
3199
+ - **Escape key closes** the modal. Clicking outside closes unless there's unsaved input.
3200
+ - **Focus trap** \u2014 keyboard focus stays inside the modal while open, restores to the trigger on close.
3201
+ - **Animation** \u2014 fade + slight translate-up on enter (200ms medium), same in reverse on exit.
3202
+ - **Modal size** matches content \u2014 tall forms get more height, short confirmations stay compact. Never a giant modal for a "Are you sure?" confirm.
3203
+
3204
+ ## Accessibility
3205
+
3206
+ - **All interactive elements** respond to keyboard \u2014 Tab to focus, Enter/Space to activate.
3207
+ - **Alt text** on meaningful images; \`alt=""\` on decorative ones.
3208
+ - **Heading hierarchy** is correct \u2014 one \`<h1>\` per page, \`<h2>\` for main sections, \`<h3>\` for subsections. Don't skip levels.
3209
+ - **ARIA labels** on icon-only buttons (\`aria-label="Close"\`, \`aria-label="Sort"\`).
3210
+ - **Color alone is not information** \u2014 a red "error" badge needs text, not just \`bg-destructive\`.
3211
+
3212
+ ## Micro-Interactions Catalog
3213
+
3214
+ Small moments that add up to "designed":
3215
+
3216
+ - **Copy-to-clipboard** button flashes a checkmark for 1.2s, then reverts.
3217
+ - **Saving state** changes the button label: "Save" \u2192 "Saving\u2026" \u2192 "Saved" (600ms) \u2192 "Save".
3218
+ - **Delete confirm** \u2014 two-step: first click arms ("Click again to delete"), second click fires. Prevents accidental destruction.
3219
+ - **Skeleton loaders** match the shape of the loaded content, not generic gray boxes.
3220
+ - **Empty states** offer a specific next action, not "No data yet".
3221
+
3222
+ These micro-interactions are the texture of a designed product. Ship them.
3223
+ `;var Xr=`# UX Writing
3224
+
3225
+ Copy is design. A remarkable page has copy that could only be about this specific product. A generic page has copy that could describe a hundred.
3226
+
3227
+ ## The Principle
3228
+
3229
+ Specificity beats cleverness. "Cut standup meetings from 30 minutes to 3" beats "Boost team productivity". "Free for teams up to 10" beats "Free for small teams".
3230
+
3231
+ ## Voice
3232
+
3233
+ Pick a voice. Possibilities:
3234
+ - **Confident-founder** \u2014 "We replaced the 9am Zoom." Direct, declarative, uses "we" sparingly but with intent.
3235
+ - **Specific-expert** \u2014 "Engineers ship 3-4 standups per week. We made each one take 90 seconds." Numbers-forward.
3236
+ - **Plainspoken-friend** \u2014 "Post it when you post it. We'll digest." Short, rhythmic, casual.
3237
+ - **Editorial-precise** \u2014 "Async, by design. Read in two minutes." Period-heavy, pause-laden.
3238
+ - **Playful-with-teeth** \u2014 "Standups, but they don't make you want to quit." Uses humor; never saccharine.
3239
+
3240
+ Pick one. Commit. Don't mix.
3241
+
3242
+ ## Anti-Patterns
3243
+
3244
+ Every one of these is immediate-generic:
3245
+
3246
+ - **"The platform teams love"** / "The tool X companies use" / "Loved by [big logos]" without proof.
3247
+ - **"Boost your productivity"** / "Unlock insights" / "Streamline your workflow" \u2014 all empty verbs.
3248
+ - **"Designed with you in mind"** / "Built for modern teams" / "Tailored to your needs" \u2014 who isn't?
3249
+ - **"Say goodbye to X"** / "No more Y" \u2014 overused opening pattern.
3250
+ - **"Everything you need to Z"** \u2014 a filler phrase with no information.
3251
+ - **"Powered by AI"** as a hero line \u2014 unless the product IS the AI, this is filler.
3252
+ - **Exclamation points.** More than zero per page, ideally.
3253
+ - **Three-item lists with vague words** \u2014 "Fast. Simple. Reliable." \u2014 every product says this.
3254
+
3255
+ ## Rules
3256
+
3257
+ **Headlines** name a specific tension or outcome, not a category:
3258
+ - \u274C "Async team collaboration"
3259
+ - \u2705 "No more 9am standups. Your team still knows what's up."
3260
+
3261
+ **Subheads** give one concrete number, workflow, or mechanism:
3262
+ - \u274C "Streamline your team's workflow with powerful async tools."
3263
+ - \u2705 "Post in two minutes. Scan the team in thirty seconds. Spot blockers before they eat a day."
3264
+
3265
+ **Feature descriptions** use the product's actual noun:
3266
+ - \u274C "Track your progress."
3267
+ - \u2705 "See who shipped, who's blocked, and who's out this week \u2014 in one scannable feed."
3268
+
3269
+ **Button text** is the action + its result, not generic:
3270
+ - \u274C "Submit" / "Click here" / "Learn more"
3271
+ - \u2705 "Start Free" / "Book 15 minutes" / "See the pricing math"
3272
+
3273
+ **Microcopy** (error messages, empty states, tooltips) adds character, not just information:
3274
+ - \u274C "No data"
3275
+ - \u2705 "Nothing here yet. Post your first standup to get started."
3276
+ - \u274C "Error: invalid input"
3277
+ - \u2705 "That email's already on your team. Try a different one."
3278
+
3279
+ ## Numbers and Proof
3280
+
3281
+ If you have real numbers, use them. If you don't, say so or omit. Fake proof reads as fake:
3282
+
3283
+ - \u274C "Trusted by 10,000+ teams" (fabricated)
3284
+ - \u274C "Used by engineers at Google, Meta, Stripe" (unless you actually know this)
3285
+ - \u2705 "Free for teams up to 10" (concrete tier limit)
3286
+ - \u2705 "Three-step setup, under a minute" (measurable claim you can defend)
3287
+
3288
+ ## The Pricing Story
3289
+
3290
+ If there's a pricing page:
3291
+ - State the price clearly at the top of each tier, large.
3292
+ - Describe each tier in one sentence ("For teams of 1-10 who want the async feed, nothing else").
3293
+ - Free tiers don't hide: show them prominently. Hiding free tiers is the #1 complaint on pricing pages.
3294
+ - Differences between tiers are specific, not vague: "25 seats" > "small teams", "Priority support" > "Better support".
3295
+
3296
+ ## The Footer
3297
+
3298
+ A footer with the same five links ("Product / Pricing / Docs / Blog / Terms") is fine, but the company name / tagline should be specific. "StandupSync \u2014 async updates, human-sized" is better than "\xA9 2026 StandupSync Inc."
3299
+ `;function kl(t){return new Promise(e=>{let o=xl({port:t,host:"127.0.0.1"});o.on("connect",()=>{o.destroy(),e(!0)}),o.on("error",()=>{e(!1)})})}var Sl=po.object({projectPath:po.string().optional().describe("Path to the project directory (default: cwd)"),step:po.number().optional().describe("Specific step number to implement (default: next incomplete step)")});function Pl(t){let e=nt(t,"mistflow.json");if(!Tt(e))return null;try{return JSON.parse(mo(e,"utf-8"))}catch{return null}}function Zr(t,e){let o=nt(t,"mistflow.json");uo(o,JSON.stringify(e,null,2)+`
3300
+ `)}function nn(t){return t.entity??t.name??"Unknown"}function Il(t){return t.length===0?"":typeof t[0]=="string"?t.join(", "):t.map(e=>`${e.name} (${e.type})`).join(", ")}function es(t){return t.path??t.route??t.name??""}function _l(t){let e=(t||"text").toLowerCase();return e==="string"||e==="varchar"||e==="char"?"text":e==="integer"||e==="int"||e==="number"||e==="float"||e==="decimal"||e==="double"?"number":e==="boolean"||e==="bool"?"boolean":e==="date"||e==="datetime"||e==="timestamp"?"date":e==="email"?"email":e==="url"||e==="uri"?"url":e==="enum"||e==="select"?"select":e==="text"||e==="longtext"||e==="textarea"?"textarea":"text"}function ts(t,e){if(!t.entities||t.entities.length===0)return e;let o=t.entities.map(n=>n.toLowerCase());return e.filter(n=>{let r=nn(n).toLowerCase();return o.some(a=>r.includes(a)||a.includes(r))})}function Tl(t,e){if(!t.pages||t.pages.length===0)return[];let o=t.pages.map(n=>n.toLowerCase());return e.filter(n=>{let r=(n.name??"").toLowerCase(),a=es(n).toLowerCase();return o.some(s=>r.includes(s)||s.includes(r)||a.includes(s))})}var Rl=new Set(["landing","design","dashboard","crud","layout","admin","auth","schema","integration","multi-tenant","deploy","general"]);function Al(t){let e=t.stepType;if(e&&Rl.has(e))return e;if(t.integrationId)return"integration";let o=`${t.name} ${t.description}`.toLowerCase();return o.includes("crud")||o.includes("list")&&o.includes("create")?"crud":o.includes("auth")||o.includes("login")||o.includes("register")?"auth":o.includes("admin")&&(o.includes("panel")||o.includes("dashboard")||o.includes("manage")||o.includes("users"))?"admin":o.includes("dashboard")||o.includes("overview")||o.includes("analytics")?"dashboard":o.includes("schema")||o.includes("database")||o.includes("model")?"schema":o.includes("layout")||o.includes("sidebar")||o.includes("navigation")?"layout":o.includes("deploy")||o.includes("cloudflare")?"deploy":o.includes("organization")||o.includes("team")||o.includes("workspace")||o.includes("multi-tenant")||o.includes("invite member")?"multi-tenant":o.includes("landing")||o.includes("hero")||o.includes("marketing")||o.includes("homepage")?"landing":o.includes("design")||o.includes("theme")||o.includes("styling")||o.includes("ui polish")||o.includes("visual")?"design":"general"}function Cl(t){switch(t){case"landing":case"design":return{min:6,max:10};case"integration":return{min:8,max:12};case"dashboard":case"admin":return{min:5,max:7};case"crud":case"multi-tenant":return{min:4,max:6};case"schema":return{min:3,max:4};case"layout":case"auth":return{min:3,max:5};case"deploy":return{min:1,max:2};default:return{min:4,max:6}}}function El(t,e){let o=[];if(o.push("### Design choices (decided at plan time \u2014 follow these exactly):"),t.tone&&o.push(`- **App tone**: ${t.tone}`),t.fonts&&(o.push(`- **Heading font**: ${t.fonts.heading} (load from Google Fonts)`),o.push(`- **Body font**: ${t.fonts.body} (load from Google Fonts)`)),o.push("- **All color comes from CSS variables** \u2014 never use Tailwind palette utilities like `bg-emerald-500`, `text-blue-600`, `border-slate-200`. Use `bg-primary`, `text-primary-foreground`, `bg-muted`, `text-muted-foreground`, `border-border`, `bg-card`, `text-foreground`, `bg-destructive`, etc. The primary/accent is already wired in globals.css from the chosen design system."),o.push("- **Outbound URLs (invite links, email CTAs, absolute hrefs in server code):** use `process.env.BETTER_AUTH_URL` as the base. Do NOT use `process.env.NEXT_PUBLIC_APP_URL` \u2014 Next.js bakes every `NEXT_PUBLIC_*` literal into the production bundle at build time, so the scaffolded localhost default ships to prod and invite emails land on `http://localhost:3000`. Client components should use relative URLs (they resolve to the current origin automatically)."),t.borderRadius){let n={sharp:"2px",subtle:"6px",rounded:"12px",pill:"9999px"};o.push(`- **Border radius**: ${t.borderRadius} (${n[t.borderRadius]??t.borderRadius}) \u2014 set as --radius in globals.css`)}if(t.shadowStyle){let n={flat:"No shadows \u2014 use borders for separation",subtle:"shadow-sm on cards, shadow on hover",elevated:"shadow-md on cards, shadow-lg on modals, layered depth",dramatic:"shadow-lg with colored tinting, bold depth"};o.push(`- **Shadow style**: ${t.shadowStyle} \u2014 ${n[t.shadowStyle]??t.shadowStyle}`)}if(t.cardStyle){let n={filled:"bg-card with subtle background fill, no visible border",bordered:"border border-border on white/transparent background",elevated:"bg-card shadow-md, no border, lifted appearance",glass:"bg-white/60 backdrop-blur-sm border border-white/20, translucent"};o.push(`- **Card style**: ${t.cardStyle} \u2014 ${n[t.cardStyle]??t.cardStyle}`)}if(t.landingTone&&o.push(`- **Landing page tone**: ${t.landingTone}`),t.visualStrategy){let n=t.visualStrategy,r=t.heroPhoto!==!1,a=r&&!!n.heroImages?.length;if(o.push(""),o.push("### Visual strategy:"),a&&n.heroImages&&n.heroImages.length>0){o.push("**Hero image** \u2014 use this Unsplash photo as the landing page hero BACKGROUND:");let s=n.heroImages[0];o.push(`- URL: ${s.url}`),o.push(`- Alt text for img tag: "${s.alt||"Hero image"} \u2014 Photo by ${s.photographer} on Unsplash"`),o.push("- Use as full-bleed background behind the entire hero section with a dark overlay (bg-black/60 or similar for readability). The photo is atmosphere and context, NOT the main visual \u2014 the glassmorphic product card sits on top.")}else r&&!n.heroImages?.length?o.push("**Hero background** \u2014 the user requested a lifestyle photo, but no Unsplash image was fetched (likely a transient API issue). Fall back gracefully: use the design preset's gradient + glassmorphism, AND tell the user in your reply: 'I couldn't fetch a stock photo for the hero this time \u2014 using a CSS gradient instead. You can add a photo later by editing the hero section in app/page.tsx.'"):r||o.push("**Hero background** \u2014 user opted for CSS-only (no photo). Use the design preset's specified gradient, animated background, or glassmorphism. No Unsplash.");if(n.sectionImages?.length){o.push("**Section images available** \u2014 use these for feature sections, about sections, or testimonial backgrounds (NOT the hero):");for(let s of n.sectionImages)o.push(`- ${s.url} \u2014 alt: "${s.alt||"section image"} \u2014 Photo by ${s.photographer} on Unsplash"`)}(a||n.sectionImages?.length)&&o.push("For image attribution: put photographer credit in the img alt text (already provided above) and add an HTML comment <!-- Images from Unsplash --> near the images. Do NOT add visible attribution text on the page."),o.push(""),o.push("**Hero layout \u2014 split hero with product UI mockup (follow this exactly):**"),o.push("The hero uses a split layout with text on the left and a product preview on the right. This is non-negotiable for consumer apps and SaaS/tool apps alike."),o.push(""),o.push("```"),o.push("\u250C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510"),o.push("\u2502 [Logo] [Sign In] [CTA \u2192] \u2502 \u2190 transparent nav, NOT sticky"),o.push("\u251C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2524"),o.push("\u2502 \u2502"),o.push("\u2502 \u25CF Built for [audience] \u250C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u2502"),o.push("\u2502 \u2502 Glassmorphic \u2502 \u2502"),o.push("\u2502 Big bold headline, \u2502 product mockup \u2502 \u2502"),o.push("\u2502 accent color on key word \u2502 card showing \u2502 \u2502"),o.push("\u2502 \u2502 real app data \u2502 \u2502"),o.push("\u2502 Description paragraph \u2502 (stats, table, \u2502 \u2502"),o.push("\u2502 \u2502 chart, etc.) \u2502 \u2502"),o.push("\u2502 [Primary CTA \u2192] [Secondary] \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518 \u2502"),o.push("\u2502 \u2502"),o.push("\u2502 500+ 25K+ 99% \u2502"),o.push("\u2502 Label Label Label \u2502"),o.push("\u2502 \u2502"),a?o.push("\u2502 \u2190 full-bleed photo bg + dark overlay behind all \u2192 \u2502"):o.push("\u2502 \u2190 preset gradient / glass background behind all \u2192 \u2502"),o.push("\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518"),o.push("```"),o.push(""),o.push("**Left side (~55%)**: badge pill \u2192 bold headline (use accent color on ONE key word or phrase) \u2192 description \u2192 two CTA buttons (primary filled + secondary outline/ghost) \u2192 stats row with 3 proof points"),o.push("**Right side (~45%)**: A glassmorphic floating card that previews what the app looks like inside:"),o.push("- Style: `bg-white/10 backdrop-blur-lg border border-white/20 rounded-2xl shadow-2xl p-6` (over dark/photo bg) OR `bg-white/60 backdrop-blur-lg border border-black/5 rounded-2xl shadow-2xl p-6` (over light bg)"),o.push("- Content: Build realistic fake app data using styled divs \u2014 stat cards, mini table rows, schedule blocks, or charts that match what this specific app's dashboard shows"),o.push("- Must be DOMAIN-SPECIFIC: a library app shows book catalog rows, a gym app shows check-in stats, a CRM shows pipeline cards"),o.push("- Add subtle inner elements with `bg-white/5` or `bg-white/10` for depth"),o.push("**Stats row**: 3 proof-point numbers at the bottom of the left side (e.g., '500+ Gyms', '25K+ Members', '99% Uptime'). Match text color to the background contrast.")}return o.push(""),o.push(Hr),o.join(`
3301
+ `)}async function Nl(t){try{let e=await $n("nextjs",t);return{reminders:e.reminders,skill:e.skill}}catch{return{reminders:`### ${t} step
3302
+ - Follow existing patterns in the codebase
3303
+ - Server Components by default, "use client" only when interactivity is needed`,skill:""}}}async function jl(t,e,o,n,r,a){let s=[];s.push(`## Step ${t.number}: ${t.name}`),s.push(""),s.push("### What to build:"),s.push(t.description),s.push(""),e.primaryAction&&(s.push("### Primary user action (non-negotiable):"),s.push(`- **Core action**: ${e.primaryAction.action}`),s.push(`- **User flow**: ${e.primaryAction.flow}`),s.push(`- **Dashboard must show**: ${e.primaryAction.dashboardSurface}`),s.push(""),s.push("The dashboard is an ACTION surface, not a stats display. Users must be able to complete the core action directly from the dashboard without navigating to a separate page. If this step builds the dashboard, make sure the primary action is front and center with inline forms/buttons \u2014 not behind a link to another page."),s.push(""));let l=["landing","design","dashboard","crud","layout","admin","general","auth"].includes(r);if(e.design&&l?(s.push(El(e.design,{hasDesignPreset:!!e.landingDesign&&(r==="landing"||r==="design")})),s.push("")):e.design&&!l&&(s.push("### Design tokens (for reference only \u2014 this step is not UI-focused):"),e.design.fonts&&s.push(`- Fonts: ${e.design.fonts.heading} / ${e.design.fonts.body}`),s.push("- Colors come from CSS variables (`--color-primary`, `--color-background`, etc.) \u2014 not Tailwind palette names."),s.push("")),a){let w=Pr(a);if(w.length>0){s.push("### Approved wireframe (MUST READ before writing any files):"),s.push("The user approved a wireframe sketch before building. **Read these files NOW before writing any code for this step:**");for(let y of w){let x=y.replace(a,"").replace(/^\//,"");s.push(`- \`${x}\``)}s.push(""),s.push("The wireframe defines the LAYOUT and INFORMATION HIERARCHY \u2014 what goes where, what's prominent, what's secondary. It includes HTML comments explaining WHY things are placed where they are."),s.push(""),s.push("The wireframe is intentionally rough (grayscale, system fonts). Your job is to:"),s.push("1. **Keep the same layout structure** \u2014 same information hierarchy, same element placement, same sections in the same order"),s.push("2. **Apply the design tokens** \u2014 colors, fonts, shadows, radius from the plan design choices above"),s.push("3. **Elevate the visual quality** \u2014 make it feel designed for THIS specific app, not generic"),s.push("4. **Respect the HTML comments** \u2014 they explain WHY things are placed where they are"),s.push("5. **If the wireframe shows a search bar at the top of the dashboard, your dashboard MUST have a search bar at the top** \u2014 do not rearrange the layout"),s.push("")}}e.roles&&Array.isArray(e.roles)&&e.roles.length>0&&(s.push("### Role system (from plan):"),s.push(`- Roles: ${e.roles.join(", ")}`),s.push(`- Default role for new signups: ${e.defaultRole??e.roles[0]}`),s.push("- Role helpers are in `lib/roles.ts` \u2014 use `getUserRole()` and `hasRole()` for access checks"),s.push("")),e.multiTenant&&(s.push("### Multi-tenant (from plan):"),s.push("- Organization tables are in `db/schema/organization.ts`"),s.push("- Org helpers are in `lib/org.ts` \u2014 use `getCurrentOrg()` to scope queries"),s.push("- All data queries MUST be scoped to the current org (filter by orgId)"),s.push("- Org switcher component is at `components/org-switcher.tsx`"),s.push("- CRITICAL: `getCurrentOrg()` returns null for new users who haven't created an org yet. The dashboard MUST handle this \u2014 if currentOrg is null, redirect to an onboarding page or show an inline 'Create your first team/workspace' form. NEVER call .id on a null org."),s.push("- WARNING: cookies().set() in server actions does NOT work on Mistflow Cloud's edge runtime. Do NOT use setCurrentOrgId() or any cookies().set() call inside server actions. Instead, pass the orgId as a form field or query param, or store the active org in the user's database record."),s.push("")),e.language&&(s.push(`### Language: ${e.language}`),s.push(`ALL user-facing text must be written in ${e.language}:`),s.push("- Page titles, headings, labels, button text, placeholder text"),s.push("- Navigation items, menu labels, footer text"),s.push("- Error messages, success messages, empty states"),s.push("- Landing page copy, marketing text, CTAs"),s.push("- Form labels and validation messages"),s.push("Code (variable names, comments, file names) stays in English."),s.push(`Set the HTML lang attribute to the appropriate locale code for ${e.language}.`),s.push(""));let p=["landing","design","auth","general","crud","dashboard"];e.audienceType&&p.includes(r)&&(e.audienceType==="b2c"?(s.push("### Audience: this app belongs to ONE business. The landing page talks TO their customers."),s.push("- Hero: what the customer gets ('Exceptional catering for your next event'), NOT what the tool does"),s.push("- CTAs: customer action ('Order Catering', 'Book Now'), NOT business action ('Get Started Free')"),s.push("- Testimonials: from customers ('They catered our wedding'), NOT from business owners"),s.push("- Features: customer benefits ('Specify your dietary needs'), NOT business benefits ('Track preferences')"),s.push("- Stats: social proof for customers ('2,400+ events served'), NOT internal metrics ('$48k revenue')"),s.push("- The business name IS the brand. Say it like a business homepage, not a SaaS onboarding."),s.push("")):e.audienceType==="b2b"?(s.push("### Audience: this is a SaaS platform. The landing page pitches TO business owners."),s.push("- Hero: the business pain + solution ('Catering orders managed in one place')"),s.push("- CTAs: business owner action ('Start Free Trial', 'Get Started')"),s.push("- Testimonials: from business owners who use the platform"),s.push("- Features: business benefits ('Track dietary preferences across all orders')"),s.push("- Stats: platform metrics ('500+ businesses', '50K+ orders processed')"),s.push("")):e.audienceType==="internal"&&(s.push("### Audience: internal staff tool. No marketing copy needed."),s.push("- No landing page. Auth page copy is functional: 'Sign in to continue'."),s.push("- Dashboard focuses on operational efficiency, not onboarding or sales."),s.push(""))),o.length>0&&(s.push("### Already completed:"),o.forEach(w=>s.push(`- ${w}`)),s.push(""));let m=e.dataModel?ts(t,e.dataModel):[];m.length>0&&(s.push("### Data model (from plan):"),m.forEach(w=>{let y=nn(w),x=Il(w.fields);s.push(`- **${y}**: ${x}`),s.push(` Schema file: \`db/schema/${y.toLowerCase().replace(/\s+/g,"-")}.ts\``)}),s.push(""));let u=e.pages?Tl(t,e.pages):[];if(u.length>0&&(s.push("### Pages to create/update:"),u.forEach(w=>{let y=w.description?` \u2014 ${w.description}`:"";s.push(`- \`${es(w)}\`${y}`)}),s.push("")),r==="crud"&&m.length>0&&m.forEach(w=>{let y=nn(w),x=y.toLowerCase().replace(/\s+/g,"-"),M=x.endsWith("s")?x:`${x}s`;s.push(`### Files for ${y} CRUD:`),s.push(`- List page: \`app/(dashboard)/${M}/page.tsx\` (Server Component)`),s.push(`- Detail page: \`app/(dashboard)/${M}/[id]/page.tsx\``),s.push(`- Create page: \`app/(dashboard)/${M}/new/page.tsx\``),s.push(`- Server Actions: \`app/(dashboard)/${M}/actions.ts\``),s.push(`- DataTable columns: \`components/${x}-table-columns.tsx\``),s.push(`- Form: \`components/${x}-form.tsx\``),s.push("")}),l){s.push("## Design Doctrine (the standard for every UI step)"),s.push(""),s.push(Gr),s.push(""),s.push("## Design Reference Library"),s.push(""),s.push("### Typography"),s.push(Vr),s.push(""),s.push("### Color"),s.push(Jr),s.push(""),s.push("### Motion"),s.push(Kr),s.push(""),s.push("### Spatial Composition"),s.push(Yr),s.push(""),s.push("### Interaction"),s.push(Qr),s.push(""),s.push("### UX Writing"),s.push(Xr),s.push("");let w=a?nt(a,"DESIGN.md"):void 0,y=w&&Tt(w)?(()=>{try{return mo(w,"utf-8")}catch{return null}})():null;y&&(s.push("### Design system (source of truth: DESIGN.md):"),s.push(""),s.push("The project's DESIGN.md defines the visual identity. Follow it exactly. It's emitted in google-labs-code/design.md format (YAML front matter with colors/typography/rounded/spacing tokens, plus markdown rationale). All UI elements must use the project's CSS custom properties (--color-primary, --color-background, etc. \u2014 these are generated from the YAML tokens) and the fonts configured in layout.tsx. If DESIGN.md and this plan disagree, DESIGN.md wins. The user may have edited it."),s.push(""),s.push(y),s.push(""))}(r==="landing"||r==="design")&&(s.push(Wr),s.push(""));let d=t.integrationId?Qe(t.integrationId):void 0;if(d){let w=Xe(d.id);if(s.push("### Integration blueprint (follow this closely):"),s.push(""),s.push(`Using integration: **${d.name}** (${d.category})`),w?.docsUrl&&s.push(`Official docs: ${w.docsUrl}`),w?.envVars?.length){s.push(""),s.push("**Required environment variables:**");for(let y of w.envVars)s.push(`- \`${y.key}\`: ${y.description} \u2014 Get it at ${y.setupUrl}`);s.push(""),s.push("**IMPORTANT: Never ask the user to paste API keys in chat.** Direct them to set keys in the Mistflow dashboard (Project Settings > Environment Variables) or at app.mistflow.ai. Use mist_config resource='env' action='list' to check if keys are set (has_value: true/false). Only proceed with deploy once all required keys are set.")}w?.packages?.length&&(s.push(""),s.push(`**Packages to install:** \`npm install ${w.packages.join(" ")}\``)),s.push(""),s.push("---"),s.push(d.prompt),s.push("---"),s.push(""),s.push("**Adaptation rules**: Follow the file structure and code patterns above. Replace placeholder values (app names, URLs, copy) with this app's specific content. Use the app's existing data models and route structure. Never ask the user to paste API keys or secrets in the chat. Direct them to set keys in the Mistflow dashboard."),s.push("")}let{reminders:b,skill:T}=await Nl(r);return s.push(b),s.push(""),T&&!(r==="landing"&&e.landingDesign==="freeform")&&(s.push(`### ${r} reference:`),s.push(T),s.push("")),l&&(s.push("## Self-Audit \u2014 run before submitting this file"),s.push(""),s.push(`Read the file you just wrote and answer each of these. If any answer is "yes", REDESIGN the failing piece \u2014 don't tweak classes on the same template. A different composition is required.`),s.push(""),s.push('1. **Pill badge at the top?** A small rounded label saying "Built for" / "New:" / "Made for" with a colored dot? \u2192 redesign the hero without it.'),s.push("2. **Fake browser chrome?** A rounded box with red/yellow/green dots and a fake URL bar? \u2192 delete it. Show real components instead, or no preview."),s.push("3. **Centered single-column hero?** Pill \u2192 headline \u2192 subhead \u2192 two CTAs stacked vertically? \u2192 redesign asymmetrically (see spatial.md)."),s.push("4. **Three checkmark benefit bullets** below the hero CTAs? \u2192 remove the triplet pattern."),s.push("5. **Tailwind palette utilities?** Any `bg-emerald-*`, `bg-amber-*`, `text-violet-*`, `border-slate-*`, `from-purple-*`, etc.? \u2192 replace with token classes (`bg-primary`, `bg-success`, `bg-muted`, `border-border`). No exceptions."),s.push("6. **Inter, Geist, Roboto, Arial, or system-ui** as the primary font? \u2192 pick something distinctive (see typography.md)."),s.push("7. **Purple gradient on white background** anywhere? \u2192 that's the single most overused AI-design pattern. Replace."),s.push('8. **Hardcoded hex values** (e.g. `style={{ color: "#10b981" }}`) or named CSS colors (`red`, `lightgrey`) in className or inline styles? \u2192 replace with CSS variables.'),s.push('9. **Generic copy** ("Boost your productivity", "The platform teams love", "Everything you need to")? \u2192 rewrite with specific nouns, numbers, or mechanisms from this product (see writing.md).'),s.push("10. **Zero memorable moments?** Is there ONE signature element \u2014 typographic hero, orchestrated reveal, asymmetric break, product-specific illustration? If zero, the page is generic. Add one (see motion.md, typography.md)."),s.push(""),s.push(`If every answer is "no, I'm good" \u2014 then submit.`),s.push("")),s.join(`
3304
+ `)}async function Dl(t){let{projectPath:e,step:o}=t,n=wl(e??process.cwd()),r=Pl(n);if(!r)return ke(n);if(!Tt(nt(n,"node_modules")))return c(`Dependencies are not installed at ${n}. Call mist_install and projectPath='${n}' before running implement. This is a one-time setup step after init.`,!0);try{let{ensureBackendRegistered:v,ensureShadcnComponents:$}=await Promise.resolve().then(()=>(Gn(),Wn));await v(n);let K=await $(n);K.failed?console.error(`[implement] ${K.failed}`):K.installed.length>0&&console.error(`[implement] installed ${K.installed.length} shadcn components`)}catch(v){console.error("[implement] self-heal skipped:",v instanceof Error?v.message:String(v))}let a=r.plan;if(!a||!a.steps||a.steps.length===0)return c("No project plan found. Start by describing your app idea first \u2014 the AI will create a plan for you.",!0);let s,i=a.steps.find(v=>v.status==="in_progress");if(i){let v=a.steps.findIndex($=>$.number===i.number);v!==-1&&(a.steps[v].status="completed",s=`Auto-completed step ${i.number} (${i.name})`,Zr(n,r))}let l;if(o!==void 0){if(l=a.steps.find(v=>v.number===o),!l)return c(`Step ${o} not found. The plan has ${a.steps.length} steps (numbered ${a.steps[0].number} to ${a.steps[a.steps.length-1].number}).`,!0)}else if(l=a.steps.find(v=>v.status!=="completed"),!l)return c(JSON.stringify({message:"All plan steps are completed!",completedSteps:a.steps.map(v=>v.name),nextAction:"NEXT: Deploy the app now. Call mist_deploy action='deploy'. Do NOT suggest localhost or ask the user \u2014 just deploy."}));let p=a.steps.filter(v=>v.status==="completed").map(v=>`Step ${v.number}: ${v.name}`),{readLocalState:m}=await Promise.resolve().then(()=>($e(),xt)),u=m(n),d=Al(l),b=[];if((d==="crud"||d==="schema")&&a.dataModel&&l.entities&&l.entities.length>0){let v=ts(l,a.dataModel);for(let $ of v){let K=nn($);try{let ne=($.fields||[]).map(j=>typeof j=="string"?{name:j,type:"text"}:{name:j.name,type:_l(j.type),required:j.required!==!1});if(ne.length===0)continue;let we=r.dbProvider==="neon"?"nextjs-neon":"nextjs",C=await Ln(we,K,ne),E=0,he=0;for(let j of C.files){let oe=nt(n,j.path);if(Tt(oe)){he++;continue}bl(vl(oe),{recursive:!0}),uo(oe,j.content),E++}let Se=nt(n,"db","index.ts");if(Tt(Se)){let j=mo(Se,"utf-8");j.includes(C.dbExport)||uo(Se,j.trimEnd()+`
3305
+ `+C.dbExport+`
3306
+ `)}E>0?b.push(`${C.entityPascal} CRUD (${E} new files${he>0?`, ${he} existing skipped`:""})`):he>0&&b.push(`${C.entityPascal} CRUD (all ${he} files already exist \u2014 skipped)`)}catch(ne){console.error(`Module generation failed for ${K} (non-fatal):`,ne instanceof Error?ne.message:ne)}}}let T=await jl(l,a,p,null,d,n),w=a.steps.findIndex(v=>v.number===l.number);if(w!==-1&&(r.plan.steps[w].status="in_progress",Zr(n,r)),u&&r.projectId){let{syncRemoteState:v}=await Promise.resolve().then(()=>($e(),xt));v(r.projectId,u).catch(()=>{})}let y=a.steps.every(v=>v.status==="completed"||v.number===l.number),x;y?x=`THIS IS THE LAST STEP. Rules for speed:
3307
+
3308
+ 1. Write ALL files using PARALLEL tool calls \u2014 batch multiple Write/Edit calls in a single message.
3309
+ 2. Do NOT read files you already know (AGENTS.md, CLAUDE.md, mistflow.json, middleware.ts, lib/auth.ts, lib/db.ts).
3310
+ 3. After writing, run 'npm run build' and fix any errors.
3311
+ 4. Quick file checks (read only these):
3312
+ - app/page.tsx must be a real landing page, NOT a redirect to /login
3313
+ - middleware.ts must have "/" in PUBLIC_EXACT or PUBLIC_PREFIXES
3314
+ - Forms must use server actions (actions.ts with 'use server'), NOT setTimeout/simulate
3315
+ 5. Call mist_build({ projectPath }) (fire-and-poll \u2014 re-call with the returned jobId until status 'complete'), then mist_deploy({ action: 'deploy', projectPath }). Do NOT pause between these two calls to ask the user \u2014 the build is expected, the deploy is expected, just chain them.`:x=`IMPLEMENT THIS STEP NOW. Rules for speed:
3316
+
3317
+ 1. BEFORE writing any files, tell the user: "[stepTiming.announcement]" \u2014 use the exact announcement string from the stepTiming field so they know what's happening and how long it'll take. This is a one-line status update, NOT a request for permission.
3318
+ 2. Write ALL files for this step using PARALLEL tool calls \u2014 batch multiple Write/Edit calls in a single message. Do NOT write one file at a time.
3319
+ 3. Do NOT read files you already know: AGENTS.md, CLAUDE.md, mistflow.json, middleware.ts, lib/auth.ts, lib/db.ts, drizzle.config.ts \u2014 these haven't changed.
3320
+ 4. Only read a file if you need to MODIFY it (e.g. sidebar.tsx to add a nav link, db/index.ts to add an export).
3321
+ 5. After writing ALL files, call mist_implement to move to the next step. Do NOT pause to ask the user for permission between steps \u2014 proceed immediately. Do NOT offer options like "continue or stop" \u2014 the user already approved the build when they approved the plan. The previous step is auto-marked complete \u2014 do NOT call implement twice.`;let M=Cl(d),J={stepNumber:l.number,totalSteps:a.steps.length,estimatedMinutes:M,announcement:`Starting step ${l.number} of ${a.steps.length}: ${l.name}. This step usually takes ${M.min}\u2013${M.max} minutes.`},re=JSON.stringify({instruction:T,step:{number:l.number,name:l.name,description:l.description,status:"in_progress"},stepTiming:J,compactionGuidance:"If your context gets compacted mid-step (common on long builds), call mist_implement again immediately after the compaction finishes. The tool finds the in-progress step and returns fresh context \u2014 you don't need to re-read files or re-derive state.",...s?{autoCompleted:s}:{},...b.length>0?{generatedModules:b,generatedNote:"These CRUD modules were pre-generated. Review and customize them instead of writing from scratch. Focus on: business logic in actions.ts, UI polish, and wiring navigation."}:{},progress:`${p.length}/${a.steps.length} steps done`,nextAction:x});return await kl(3e3)?$o("http://localhost:3000",re):c(re)}var ns={name:"mist_implement",description:"Execute the next step (or a specific step) from the app plan. Reads mistflow.json, finds the target step, and returns rich context and detailed implementation instructions for the host AI. Auto-commits a checkpoint before changes for safe undo. Use when the user says 'mist implement' or 'mist next step'.",inputSchema:Sl,handler:Dl};import{z as me}from"zod";import{z as Ee}from"zod";import{resolve as Ol}from"path";ie();We();var uu=Ee.object({projectPath:Ee.string().optional().describe("Path to the project directory (default: cwd)"),action:Ee.enum(["set","list","delete"]).describe("Action to perform"),key:Ee.string().optional().describe("Environment variable name (required for 'set' and 'delete')"),value:Ee.string().optional().describe("Environment variable value (required for 'set')"),category:Ee.string().optional().describe("Category for the env var (default: 'custom')"),description:Ee.string().optional().describe("Description of what this env var is for"),setupUrl:Ee.string().optional().describe("URL where the user can obtain this value (e.g. Stripe dashboard)")});async function os(t){let{projectPath:e,action:o,key:n,value:r,category:a,description:s,setupUrl:i}=t,l=Ol(e??process.cwd());if(!G())return c("No Mistflow credentials found. Run mist_setup to connect your account.",!0);let m=_e(l)?.projectId;if(!m)return c("No project ID found. Deploy your project first with mist_deploy, or initialize with mist_plan + mist_init + mist_install.",!0);try{switch(o){case"set":return n?r?(await En(m,n,r,{category:a,description:s,setupUrl:i}),c(JSON.stringify({set:!0,key:n,message:`Environment variable '${n}' has been set. It will be available on your next deployment.`}))):c("Value is required. Provide the env var value.",!0):c("Key is required. Provide the env var name like 'STRIPE_SECRET_KEY'.",!0);case"list":{let u=await Cn(m);return u.length===0?c(JSON.stringify({envVars:[],message:"No environment variables configured. Use action 'set' to add one."})):c(JSON.stringify({envVars:u.map(d=>({key:d.key,category:d.category,description:d.description,hasValue:d.has_value})),message:`${u.length} environment variable(s) configured.`}))}case"delete":return n?(await Nn(m,n),c(JSON.stringify({deleted:!0,key:n,message:`Environment variable '${n}' has been removed.`}))):c("Key is required. Provide the env var name to delete.",!0);default:return c(`Unknown action: ${o}. Use set, list, or delete.`,!0)}}catch(u){if(u instanceof A)return c(u.message,!0);let d=u instanceof Error?u.message:"An unexpected error occurred";return c(d,!0)}}import{z as Rt}from"zod";import{resolve as Ml,join as Ul}from"path";import{existsSync as $l,readFileSync as Ll,writeFileSync as Fl}from"fs";ie();We();var vu=Rt.object({projectPath:Rt.string().optional().describe("Path to the project directory (default: cwd)"),action:Rt.enum(["add","list","verify","remove"]).describe("Action to perform"),domain:Rt.string().optional().describe("Domain name (required for 'add' and 'remove')"),domainId:Rt.string().optional().describe("Domain ID (required for 'verify' and 'remove')")});function ho(t,e){let o=Ul(t,"mistflow.json");if(!$l(o))return;let n;try{n=JSON.parse(Ll(o,"utf-8"))}catch{return}n.domains=e,Fl(o,JSON.stringify(n,null,2)+`
3322
+ `)}async function rs(t){let{projectPath:e,action:o,domain:n,domainId:r}=t,a=Ml(e??process.cwd());if(!G())return c("No Mistflow credentials found. Run mist_setup to connect your account.",!0);let i=_e(a)?.projectId;if(!i)return c("No project ID found. Deploy your project first with mist_deploy, or initialize with mist_plan + mist_init + mist_install.",!0);try{switch(o){case"add":{if(!n)return c("Domain name is required. Provide the domain like 'myapp.com' or 'app.mycompany.com'.",!0);let l=await Rn(i,n),p=await Te(i);return ho(a,p.map(m=>({domain:m.domain,status:m.status}))),c(JSON.stringify({added:!0,domain:l.domain,status:l.status,instructions:l.instructions,message:`Domain '${l.domain}' added. Set up DNS records as described, then use action 'verify' to check status.`}))}case"list":{let l=await Te(i);return l.length===0?c(JSON.stringify({domains:[],message:"No custom domains configured. Use action 'add' to add one."})):c(JSON.stringify({domains:l.map(p=>({id:p.id,domain:p.domain,status:p.status,ssl:p.ssl_status,error:p.error_message}))}))}case"verify":{if(!r){if(n){let u=(await Te(i)).find(d=>d.domain===n);if(u){let d=await Ft(i,u.id);return c(JSON.stringify({domain:d.domain,status:d.status,ssl:d.ssl_status,error:d.error_message,message:d.status==="active"?`Domain '${d.domain}' is active and serving traffic.`:`Domain '${d.domain}' is ${d.status}. Make sure DNS records are configured correctly.`}))}return c(`Domain '${n}' not found. Use action 'list' to see configured domains.`,!0)}return c("Provide either domainId or domain name to verify.",!0)}let l=await Ft(i,r),p=await Te(i);return ho(a,p.map(m=>({domain:m.domain,status:m.status}))),c(JSON.stringify({domain:l.domain,status:l.status,ssl:l.ssl_status,error:l.error_message,message:l.status==="active"?`Domain '${l.domain}' is active and serving traffic.`:`Domain '${l.domain}' is ${l.status}. Make sure DNS records are configured correctly.`}))}case"remove":{if(!r&&!n)return c("Provide either domainId or domain name to remove.",!0);let l=r;if(!l&&n){let u=(await Te(i)).find(d=>d.domain===n);if(!u)return c(`Domain '${n}' not found.`,!0);l=u.id}await An(i,l);let p=await Te(i);return ho(a,p.map(m=>({domain:m.domain,status:m.status}))),c(JSON.stringify({removed:!0,message:"Domain removed. It may take a few minutes for DNS changes to propagate."}))}default:return c(`Unknown action: ${o}. Use add, list, verify, or remove.`,!0)}}catch(l){if(l instanceof A)return c(l.message,!0);let p=l instanceof Error?l.message:"An unexpected error occurred";return c(p,!0)}}var ql=me.object({resource:me.enum(["env","domain"]).describe("'env' manages app secrets and configuration values. 'domain' manages custom domains."),action:me.string().describe("Action to perform. env: 'set', 'list', 'delete'. domain: 'add', 'list', 'verify', 'remove'."),projectPath:me.string().optional().describe("Path to the project directory (default: cwd)"),key:me.string().optional().describe("(env) Variable name"),value:me.string().optional().describe("(env set) Variable value"),category:me.string().optional().describe("(env set) Category"),description:me.string().optional().describe("(env set) Description"),setupUrl:me.string().optional().describe("(env set) URL to obtain the value"),domain:me.string().optional().describe("(domain) Domain name"),domainId:me.string().optional().describe("(domain) Domain ID")}),ss={name:"mist_config",description:"Manage project configuration: app secrets and custom domains. Set resource='env' to manage encrypted app secrets (set, list, delete). Set resource='domain' to manage custom domains (add, list, verify, remove). Use when the user says 'mist env' or 'mist domain'.",inputSchema:ql,handler:async t=>{let e=t;switch(e.resource){case"env":return os({projectPath:e.projectPath,action:e.action,key:e.key,value:e.value,category:e.category,description:e.description,setupUrl:e.setupUrl});case"domain":return rs({projectPath:e.projectPath,action:e.action,domain:e.domain,domainId:e.domainId});default:return c(`Unknown resource: ${e.resource}. Use env or domain.`,!0)}}};import{z as yo}from"zod";import{resolve as nc}from"path";import{existsSync as oc}from"fs";import{join as rc}from"path";import{spawn as Bl,execFileSync as zl}from"child_process";import{existsSync as on,mkdirSync as ls,openSync as go,closeSync as fo,readSync as Hl,readFileSync as cs,writeFileSync as ps,renameSync as ds,unlinkSync as Wl,readdirSync as Au,statSync as Gl}from"fs";import{homedir as us}from"os";import{join as be,dirname as ms}from"path";import{randomBytes as hs,randomUUID as Vl}from"crypto";var gs=1,Jl=`// Generated by @mistflow-ai/mcp local-jobs (v${gs}). Do not edit.
3323
+ const { spawn } = require('node:child_process');
3324
+ const { readFileSync, writeFileSync, openSync, renameSync } = require('node:fs');
3325
+ const { join, dirname } = require('node:path');
3326
+ const { randomBytes } = require('node:crypto');
3327
+
3328
+ // argv: [node, wrapper, jobDir, cwd, cmd, ...args]
3329
+ const [, , jobDir, cwd, cmd, ...args] = process.argv;
3330
+ const statusPath = join(jobDir, 'status.json');
3331
+ const stdoutPath = join(jobDir, 'stdout.log');
3332
+ const stderrPath = join(jobDir, 'stderr.log');
3333
+
3334
+ function atomicWrite(path, content) {
3335
+ const tmp = join(dirname(path), '.status.tmp.' + randomBytes(6).toString('hex'));
3336
+ writeFileSync(tmp, content);
3337
+ renameSync(tmp, path);
3338
+ }
3339
+
3340
+ function updateStatus(patch) {
3341
+ let data;
3342
+ try { data = JSON.parse(readFileSync(statusPath, 'utf-8')); } catch { data = {}; }
3343
+ Object.assign(data, patch);
3344
+ atomicWrite(statusPath, JSON.stringify(data, null, 2) + '\\n');
3345
+ }
3346
+
3347
+ const stdoutFd = openSync(stdoutPath, 'a');
3348
+ const stderrFd = openSync(stderrPath, 'a');
3349
+
3350
+ let child;
3351
+ try {
3352
+ child = spawn(cmd, args, {
3353
+ cwd: cwd || process.cwd(),
3354
+ env: process.env,
3355
+ stdio: ['ignore', stdoutFd, stderrFd],
3356
+ });
3357
+ } catch (err) {
3358
+ updateStatus({
3359
+ status: 'failed',
3360
+ error: 'spawn failed: ' + (err && err.message ? err.message : String(err)),
3361
+ endedAt: new Date().toISOString(),
3362
+ });
3363
+ process.exit(1);
3364
+ }
3365
+
3366
+ updateStatus({ pid: child.pid, status: 'running' });
3367
+
3368
+ child.on('close', (code, signal) => {
3369
+ updateStatus({
3370
+ status: code === 0 ? 'complete' : 'failed',
3371
+ exitCode: code,
3372
+ signal: signal || null,
3373
+ endedAt: new Date().toISOString(),
3374
+ });
3375
+ process.exit(code == null ? 1 : code);
3376
+ });
3377
+
3378
+ child.on('error', (err) => {
3379
+ updateStatus({
3380
+ status: 'failed',
3381
+ error: err && err.message ? err.message : String(err),
3382
+ endedAt: new Date().toISOString(),
3383
+ });
3384
+ process.exit(1);
3385
+ });
3386
+ `;function sn(){return be(us(),".mistflow","jobs")}function Kl(){return be(us(),".mistflow","job-wrapper.cjs")}function Yl(){let t=Kl(),e=ms(t);on(e)||ls(e,{recursive:!0});let o=`v${gs}`;if(on(t))try{if(cs(t,"utf-8").includes(o))return t}catch{}let n=be(e,`.job-wrapper.tmp.${hs(6).toString("hex")}`);return ps(n,Jl),ds(n,t),t}function rn(t){return be(sn(),t)}function fs(t){return be(rn(t),"status.json")}function is(t,e){let o=fs(t),n=be(ms(o),`.status.tmp.${hs(6).toString("hex")}`);try{ps(n,JSON.stringify(e,null,2)+`
3387
+ `),ds(n,o)}catch(r){try{Wl(n)}catch{}throw r}}function Ql(t){let e=fs(t);if(!on(e))return null;try{return JSON.parse(cs(e,"utf-8"))}catch{return null}}async function ot(t){let e=`job_${Vl().replace(/-/g,"").slice(0,12)}`,o=rn(e);ls(o,{recursive:!0}),fo(go(be(o,"stdout.log"),"a")),fo(go(be(o,"stderr.log"),"a"));let n=new Date().toISOString(),r={id:e,type:t.type,status:"starting",pid:0,wrapperPid:0,startedAt:n,cmd:t.cmd,args:t.args,cwd:t.cwd};is(e,r);let a=Yl(),s={...process.env,...t.env??{}},i=Bl("node",[a,o,t.cwd,t.cmd,...t.args],{detached:!0,stdio:"ignore",env:s});i.unref();let l={...r,wrapperPid:i.pid??0};return is(e,l),l}function Xl(t){let e=t.trim();if(!e)return NaN;let o=Date.parse(e);return Number.isFinite(o)?o:NaN}function Zl(t){let e=t.pid||t.wrapperPid;if(!e)return!1;try{process.kill(e,0)}catch{return!1}try{let o=zl("ps",["-o","lstart=","-p",String(e)],{encoding:"utf-8",stdio:["ignore","pipe","ignore"]}),n=Xl(o),r=Date.parse(t.startedAt);if(Number.isFinite(n)&&Number.isFinite(r)&&Math.abs(n-r)>2e3)return!1}catch{}return!0}function ec(t,e){let o=Date.parse(t),n=e?Date.parse(e):Date.now();if(!Number.isFinite(o)||!Number.isFinite(n))return"0s";let r=Math.max(0,n-o),a=Math.floor(r/1e3);if(a<60)return`${a}s`;let s=Math.floor(a/60),i=a%60;return s<60?`${s}m ${i}s`:`${Math.floor(s/60)}h ${s%60}m`}function as(t,e){if(!on(t))return"";let o=Gl(t);if(o.size===0)return"";let n=o.size>e?o.size-e:0,r=o.size-n,a=go(t,"r");try{let s=Buffer.alloc(r);return Hl(a,s,0,r,n),s.toString("utf-8")}finally{fo(a)}}function tc(t,e=20){let o=as(be(rn(t),"stdout.log"),16384),n=as(be(rn(t),"stderr.log"),16*1024),r=[];return o.trim()&&r.push(...o.split(`
3388
+ `).slice(-e).map(a=>`[out] ${a}`)),n.trim()&&r.push(...n.split(`
3389
+ `).slice(-e).map(a=>`[err] ${a}`)),r.filter(a=>a.trim().length>0).join(`
3390
+ `)}async function rt(t){let e=Ql(t);if(!e)return null;let o=e;return(e.status==="running"||e.status==="starting")&&!Zl(e)&&(o={...e,status:"unknown_exit",endedAt:e.endedAt??new Date().toISOString()}),{...o,elapsed:ec(o.startedAt,o.endedAt),logTail:tc(t)}}var sc=yo.object({projectPath:yo.string().optional().describe("Absolute path to the project. Required for the first call (to start the install)."),jobId:yo.string().optional().describe("Job ID returned from a previous call. Present means poll; absent means start.")}).refine(t=>!!t.projectPath||!!t.jobId,{message:"Pass projectPath to start an install, or jobId to poll an existing one."}),ys={name:"mist_install",description:"Install a Mistflow project's npm dependencies with the fire-and-poll pattern. First call with { projectPath } starts the install and returns a jobId. Subsequent calls with { jobId } poll for progress (status: 'running' | 'complete' | 'failed'). Re-call while status is 'running' \u2014 each response is <1s so there is no 60s timeout risk.",inputSchema:sc,handler:async t=>{let e=t;if(e.jobId){let i=await rt(e.jobId);return i?i.status==="running"||i.status==="starting"?c(JSON.stringify({status:"running",jobId:i.id,elapsed:i.elapsed,logTail:i.logTail,nextAction:"Still running. Call mist_install with the same jobId in ~15-30s to check progress."})):i.status==="complete"?c(JSON.stringify({status:"complete",jobId:i.id,elapsed:i.elapsed,exitCode:i.exitCode,nextAction:"Dependencies installed. Run mist_implement next to start executing plan steps."})):c(JSON.stringify({status:i.status,jobId:i.id,elapsed:i.elapsed,exitCode:i.exitCode,logTail:i.logTail,nextAction:"npm install failed. Read the logTail for the error and fix it \u2014 usually a missing native dep or a version conflict. After fixing, start a new install with { projectPath }."}),!0):c(JSON.stringify({status:"not_found",jobId:e.jobId,message:`No install job found for jobId '${e.jobId}'. Start a new one with { projectPath }.`}),!0)}let o=nc(e.projectPath);if(!oc(rc(o,"package.json")))return c(`No package.json at ${o}. Confirm the path and that mist_init has scaffolded the project.`,!0);let a=`npm install && (${`npx --yes shadcn@latest add -y -o ${["button","card","input","label","form","dialog","table","dropdown-menu","badge","separator","skeleton","sheet","tabs","avatar","select","textarea","checkbox","switch","tooltip","popover","sonner"].join(" ")}`} || echo 'shadcn add failed \u2014 implement will retry lazily')`,s=await ot({type:"install",cmd:"sh",args:["-c",a],cwd:o,env:{NPM_CONFIG_LEGACY_PEER_DEPS:"true"}});return c(JSON.stringify({status:"running",jobId:s.id,startedAt:s.startedAt,cwd:o,nextAction:"Install started in the background (npm install + shadcn components). Call mist_install again with { jobId: '"+s.id+"' } in ~15-30s to check progress. Typical duration: 30-90s."}))}};import{z as an}from"zod";import{resolve as ic,join as st}from"path";import{existsSync as it,readFileSync as bs}from"fs";var ac=an.object({projectPath:an.string().optional().describe("Absolute path to the project. Required for the first call."),jobId:an.string().optional().describe("Job ID from a previous call. Present means poll; absent means start."),script:an.string().optional().describe("Optional override: run `npm run <script>` instead of the Cloudflare adapter. Default behavior is `npx @opennextjs/cloudflare build` when open-next.config.ts exists, else `npm run build`. Ignored on poll calls.")}).refine(t=>!!t.projectPath||!!t.jobId,{message:"Pass projectPath to start a build, or jobId to poll an existing one."});function lc(t){let e=st(sn(),t,"stdout.log"),o=st(sn(),t,"stderr.log"),n="";try{it(e)&&(n+=bs(e,"utf-8"))}catch{}try{it(o)&&(n+=`
3391
+ `+bs(o,"utf-8"))}catch{}return n}function cc(t){let e=/Module not found:\s*(?:Error:\s*)?Can't resolve ['"]([^'"]+)['"]/g,o=new Set;for(let n of t.matchAll(e)){let r=n[1];if(r.startsWith(".")||r.startsWith("@/")||r.startsWith("~/"))continue;let a=r.startsWith("@")?r.split("/").slice(0,2).join("/"):r.split("/")[0];a&&o.add(a)}return[...o]}var ws={name:"mist_build",description:"Run a Mistflow project's production build with the fire-and-poll pattern. First call with { projectPath } starts `npm run build` and returns a jobId. Subsequent calls with { jobId } poll status. On failure, the response surfaces structured errors (from parseBuildErrors) and any missingModules[] \u2014 chain into mist_install with those modules, then mist_build again, without asking the user.",inputSchema:ac,handler:async t=>{let e=t;if(e.jobId){let l=await rt(e.jobId);if(!l)return c(JSON.stringify({status:"not_found",jobId:e.jobId,message:`No build job found for jobId '${e.jobId}'. Start a new one with { projectPath }.`}),!0);if(l.status==="running"||l.status==="starting")return c(JSON.stringify({status:"running",jobId:l.id,elapsed:l.elapsed,logTail:l.logTail,nextAction:"Build still running. Call mist_build again with the same jobId in ~20-40s."}));if(l.status==="complete")return c(JSON.stringify({status:"complete",jobId:l.id,elapsed:l.elapsed,exitCode:l.exitCode,nextAction:"Build passed. Run mist_deploy next to ship \u2014 do NOT ask the user to confirm, the build is the approval gate."}));let p=lc(l.id),m=Kt(p),u=cc(p),d=u.length>0?`Build failed with missing modules: ${u.join(", ")}. Call mist_install with these packages (or chain mist_install { projectPath } to reinstall all), then call mist_build again. Do NOT ask the user.`:m.length>0?"Build failed with structured errors (see errors[]). Fix the code, then call mist_build again.":"Build failed. Read the logTail for the error, or call mist_debug with the failed jobId to extract structured errors.";return c(JSON.stringify({status:l.status,jobId:l.id,elapsed:l.elapsed,exitCode:l.exitCode,errors:m,missingModules:u,logTail:l.logTail,nextAction:d}),!0)}let o=ic(e.projectPath);if(!it(st(o,"package.json")))return c(`No package.json at ${o}. Run mist_init first.`,!0);if(!it(st(o,"node_modules")))return c(`node_modules not installed. Run mist_install { projectPath: '${o}' } first.`,!0);let n,r,a,s=it(st(o,"open-next.config.ts"))||it(st(o,"open-next.config.js"));e.script?(n="npm",r=["run",e.script],a=e.script):s?(n="npx",r=["-y","@opennextjs/cloudflare","build"],a="@opennextjs/cloudflare build"):(n="npm",r=["run","build"],a="build");let i=await ot({type:"build",cmd:n,args:r,cwd:o});return c(JSON.stringify({status:"running",jobId:i.id,startedAt:i.startedAt,cwd:o,script:a,nextAction:`Build started. Call mist_build again with { jobId: '${i.id}' } in ~30s to check progress. Typical duration: 30-90s on a fresh scaffold (Cloudflare adapter), 10-30s on subsequent builds.`}))}};import{z as At}from"zod";import{resolve as pc,join as bo}from"path";import{existsSync as wo}from"fs";var dc=At.object({projectPath:At.string().optional().describe("Absolute path to the project. Required for the first call."),jobId:At.string().optional().describe("Job ID from a previous call. Present means poll; absent means start."),baseUrl:At.string().optional().describe("Override the URL Playwright tests hit \u2014 defaults to the project's configured base URL."),grep:At.string().optional().describe("Pass-through to `playwright test -g`. Useful for running a single scenario.")}).refine(t=>!!t.projectPath||!!t.jobId,{message:"Pass projectPath to start a QA run, or jobId to poll an existing one."}),vs={name:"mist_qa",description:"Run a Mistflow project's Playwright QA suite with the fire-and-poll pattern. First call with { projectPath, baseUrl? } starts the run and returns a jobId. Subsequent calls with { jobId } poll status. Call AFTER mist_deploy \u2014 the QA suite targets the deployed URL. Do NOT show the deploy URL to the user until mist_qa reports status: 'complete' with exitCode 0.",inputSchema:dc,handler:async t=>{let e=t;if(e.jobId){let i=await rt(e.jobId);return i?i.status==="running"||i.status==="starting"?c(JSON.stringify({status:"running",jobId:i.id,elapsed:i.elapsed,logTail:i.logTail,nextAction:"QA still running. Call mist_qa again with the same jobId in ~15-30s."})):i.status==="complete"?c(JSON.stringify({status:"complete",jobId:i.id,elapsed:i.elapsed,exitCode:i.exitCode,nextAction:"QA passed. Surface the deploy URL to the user now \u2014 the app is verified."})):c(JSON.stringify({status:i.status,jobId:i.id,elapsed:i.elapsed,exitCode:i.exitCode,logTail:i.logTail,nextAction:"QA failed. Read the logTail for which scenarios broke. Do NOT show the deploy URL to the user. Fix the code, redeploy (mist_deploy), then re-run mist_qa."}),!0):c(JSON.stringify({status:"not_found",jobId:e.jobId,message:`No QA job found for jobId '${e.jobId}'. Start a new one with { projectPath }.`}),!0)}let o=pc(e.projectPath);if(!wo(bo(o,"package.json")))return c(`No package.json at ${o}. Run mist_init first.`,!0);if(!(wo(bo(o,"playwright.config.ts"))||wo(bo(o,"playwright.config.js"))))return c(`No playwright.config.ts at ${o}. Mistflow projects scaffold with Playwright; if missing, run mist_implement to regenerate or add one manually.`,!0);let r=["playwright","test"];e.grep&&r.push("-g",e.grep);let a={};e.baseUrl&&(a.BASE_URL=e.baseUrl);let s=await ot({type:"qa",cmd:"npx",args:r,cwd:o,env:a});return c(JSON.stringify({status:"running",jobId:s.id,startedAt:s.startedAt,cwd:o,baseUrl:e.baseUrl??"(from project config)",nextAction:`QA started. Call mist_qa again with { jobId: '${s.id}' } in ~20s. Typical duration: 30-120s depending on scenario count.`}))}};import{z as at}from"zod";import{resolve as gc,join as fc}from"path";import{existsSync as yc}from"fs";ie();We();import{existsSync as ln,readdirSync as uc,statSync as xs,unlinkSync as ks}from"fs";import{join as qe}from"path";import{execFile as mc}from"child_process";function hc(t,e){let o=0;for(let n of e){let r=qe(t,n);if(ln(r))try{let a=xs(r);if(a.isFile())o++;else if(a.isDirectory()){let s=[r];for(;s.length;){let i=s.pop(),l=uc(i,{withFileTypes:!0});for(let p of l){let m=qe(i,p.name);p.isDirectory()?s.push(m):o++}}}}catch{}}return o}function Ss(t,e,o){return new Promise(n=>{mc("tar",t,{cwd:e,timeout:o,maxBuffer:10*1024*1024,env:{...process.env,COPYFILE_DISABLE:"1"}},(r,a,s)=>{n({success:!r,stderr:s?.toString()??""})})})}async function Ps(t){let e=qe(t,".open-next-build.tar.gz"),o=[".open-next"];ln(qe(t,"db"))&&o.push("db"),ln(qe(t,"drizzle.config.ts"))&&o.push("drizzle.config.ts"),ln(qe(t,"package.json"))&&o.push("package.json");let n=hc(t,o),r=await Ss(["-czf",e,"-C",t,...o],t,12e4);if(!r.success){try{ks(e)}catch{}throw new Error("Failed to create build archive. Check disk space and permissions."+(r.stderr?`
3392
+ ${r.stderr.slice(-500)}`:""))}let a=0;try{a=xs(e).size}catch{}return{path:e,sizeBytes:a,fileCount:n}}async function Is(t){let e=qe(t,".mistflow-source.tar.gz");return(await Ss(["-czf",e,"-C",t,"--exclude",".open-next","--exclude","node_modules","--exclude",".git","--exclude",".next","--exclude",".open-next-build.tar.gz","--exclude",".mistflow-source.tar.gz","."],t,12e4)).success?e:null}function vo(t){if(t)try{ks(t)}catch{}}function _s(t){return t<1024?`${t}B`:t<1024*1024?`${(t/1024).toFixed(1)}KB`:`${(t/(1024*1024)).toFixed(1)}MB`}function Ts(t){switch(t){case"pending":return"Provisioning...";case"building":return"Building your app...";case"deploying":return"Deploying to Mistflow Cloud...";case"verifying":return"Verifying deployment...";case"live":return"Live!";case"failed":return"Failed";default:return`Status: ${t}`}}var bc=at.object({action:at.enum(["deploy","promote","rollback","status"]).default("deploy").describe("'deploy' (default) tars + uploads + starts a deployment. 'promote' promotes a staging preview to production. 'rollback' rolls back to a specific deployment. 'status' polls an in-flight deployment by id."),projectPath:at.string().optional().describe("Absolute path to the project. Required for 'deploy' and 'promote'."),deploymentId:at.string().optional().describe("Deployment id \u2014 required for 'rollback' and 'status'; the preview id for 'promote'."),environment:at.enum(["production","preview"]).optional().describe("Deploy target. Defaults to 'production'; auto-redirects to 'preview' when the project uses staging mode."),adminEmail:at.string().optional().describe("Admin email injected as BETTER_AUTH_ADMIN_EMAIL on first deploy of an auth-enabled project.")});function wc(t){return yc(fc(t,".open-next"))}function vc(t){return _e(t)?.deploy?.strategy==="staging"?"preview":null}async function xc(t){let e=gc(t.projectPath);if(!G())return c("Not signed in. Call mist_setup first.",!0);let n=_e(e)?.projectId;if(!n)return c(`No projectId in mistflow.json at ${e}. Run mist_init first.`,!0);if(!wc(e))return c(`No .open-next build output at ${e}. Run mist_build first \u2014 the deploy tool ships the build artifact, not the raw source.`,!0);let r=t.environment??vc(e)??"production",a=null,s=null;try{a=await Ps(e),s=await Is(e);let i=await In(n,a.path,r,t.adminEmail,void 0,s??void 0,void 0);return c(JSON.stringify({status:"running",jobId:i.deployment_id,deploymentId:i.deployment_id,environment:i.environment??r,buildSize:_s(a.sizeBytes),buildFileCount:a.fileCount,nextAction:`Upload complete. Call mist_deploy { action: 'status', deploymentId: '${i.deployment_id}' } in ~5-10s to poll deployment progress. Do NOT surface the URL to the user until mist_qa passes.`}))}catch(i){let l=i instanceof A||i instanceof Error?i.message:String(i);return c(`Deploy upload failed: ${l}`,!0)}finally{vo(a?.path),vo(s)}}async function kc(t){try{let e=await bt(t),o=Ts(e.status);return e.status==="live"?c(JSON.stringify({status:"complete",jobId:e.id,deploymentId:e.id,url:e.url,completedAt:e.completedAt,qaRequired:!0,nextAction:`Deployment is live. Call mist_qa { projectPath, baseUrl: '${e.url??""}' } next. Do NOT show the URL to the user until mist_qa returns status: 'complete'.`})):e.status==="failed"?c(JSON.stringify({status:"failed",jobId:e.id,deploymentId:e.id,error:e.error,phase:o,nextAction:"Deployment failed. Read the error message, fix the code, then call mist_deploy { action: 'deploy', projectPath } again."}),!0):c(JSON.stringify({status:"running",jobId:e.id,deploymentId:e.id,phase:o,phaseCode:e.status,nextAction:`Still ${e.status}. Call mist_deploy { action: 'status', deploymentId: '${e.id}' } again in ~5-10s.`}))}catch(e){let o=e instanceof A||e instanceof Error?e.message:String(e);return c(`Could not fetch deploy status: ${o}`,!0)}}async function Sc(t,e){if(!G())return c("Not signed in. Call mist_setup first.",!0);let n=_e(t)?.projectId;if(!n)return c(`No projectId in mistflow.json at ${t}. Run mist_init first.`,!0);let r=e;if(!r)try{let s=(await Je(n)).find(i=>i.environment==="preview"&&i.status==="live");if(!s)return c("No live preview deployment to promote. Deploy to preview first with { action: 'deploy', environment: 'preview' }.",!0);r=s.id}catch(a){let s=a instanceof A?a.message:String(a);return c(`Could not list deployments: ${s}`,!0)}try{let a=await On(n,r);return c(JSON.stringify({status:"running",jobId:a.deployment_id,deploymentId:a.deployment_id,promotedFrom:r,nextAction:`Promote started. Call mist_deploy { action: 'status', deploymentId: '${a.deployment_id}' } to poll. Promotion re-uses the preview artifact so it typically completes in ~10s.`}))}catch(a){let s=a instanceof A||a instanceof Error?a.message:String(a);return c(`Promote failed: ${s}`,!0)}}async function Pc(t){if(!G())return c("Not signed in. Call mist_setup first.",!0);try{let e=await Mn(t);return c(JSON.stringify({status:"running",jobId:e.deployment_id,deploymentId:e.deployment_id,rollbackFrom:e.rollback_from,nextAction:`Rollback started. Call mist_deploy { action: 'status', deploymentId: '${e.deployment_id}' } to poll.`}))}catch(e){let o=e instanceof A||e instanceof Error?e.message:String(e);return c(`Rollback failed: ${o}`,!0)}}var Rs={name:"mist_deploy",description:"Deploy a Mistflow project. action='deploy' (default) tars + uploads + starts backend orchestration; action='status' polls an in-flight deployment; action='promote' ships a staging preview to prod; action='rollback' reverts to a previous deployment. Fire-and-poll: deploy/promote/rollback return a jobId immediately; poll with { action: 'status', deploymentId }. Do NOT show the deploy URL to the user until mist_qa passes \u2014 qaRequired=true is returned on completion.",inputSchema:bc,handler:async t=>{let e=t;switch(e.action??"deploy"){case"deploy":return e.projectPath?xc({projectPath:e.projectPath,environment:e.environment,adminEmail:e.adminEmail}):c("projectPath is required for action='deploy'.",!0);case"status":return e.deploymentId?kc(e.deploymentId):c("deploymentId is required for action='status'.",!0);case"promote":return e.projectPath?Sc(e.projectPath,e.deploymentId):c("projectPath is required for action='promote'.",!0);case"rollback":return e.deploymentId?Pc(e.deploymentId):c("deploymentId is required for action='rollback'.",!0)}}};var cn=new Ic({name:"mistflow",version:"1.0.0"},{capabilities:{tools:{}},instructions:Ro()}),As=[Qo,ur,mr,fr,Or,zr,Sr,ns,Ir,ss,ys,ws,vs,Rs];cn.setRequestHandler(Rc,async()=>({tools:As.map(t=>({name:t.name,description:t.description,inputSchema:Ac(t.inputSchema)}))}));cn.setRequestHandler(Tc,async t=>{let e=As.find(o=>o.name===t.params.name);if(!e)return c(`Unknown tool: ${t.params.name}`,!0);try{let o=e.inputSchema.safeParse(t.params.arguments);if(!o.success){let a=o.error.issues.map(s=>`${s.path.join(".")}: ${s.message}`).join(", ");return c(`Invalid input: ${a}`,!0)}let n=t.params._meta?.progressToken,r={server:cn,progressToken:n};try{return await e.handler(o.data,r)}finally{r.cleanup?.()}}catch(o){let n=o instanceof Error?o.message:"An unexpected error occurred";return console.error("Tool error:",o),c(n,!0)}});async function Cc(){let t=process.argv.indexOf("--api-url");t!==-1&&process.argv[t+1]&&(process.env.MISTFLOW_API_URL=process.argv[t+1]),process.argv.includes("--local")&&!process.env.MISTFLOW_API_URL&&(process.env.MISTFLOW_API_URL="http://localhost:9100");let e=new _c;await cn.connect(e),console.error(`Mistflow MCP server running on stdio (API: ${process.env.MISTFLOW_API_URL||"https://api.mistflow.ai"})`)}Cc().catch(t=>{console.error("Fatal error:",t),process.exit(1)});