@mistflow-ai/mcp 0.7.0 → 0.7.2-staging.0

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.
package/dist/index.js CHANGED
@@ -1,1452 +1,19 @@
1
- import{a as V,d as x,e as I}from"./chunk-UNFTM4B3.js";import{c as F,d as A,e as U,f as H,g as R,h as W,j as B,l as K,m as G,n as Y,o as O,p as z}from"./chunk-VQPRSDSC.js";import{Server as Ue}from"@modelcontextprotocol/sdk/server/index.js";import{StdioServerTransport as Oe}from"@modelcontextprotocol/sdk/server/stdio.js";import{CallToolRequestSchema as Le,ListToolsRequestSchema as Me}from"@modelcontextprotocol/sdk/types.js";import{zodToJsonSchema as De}from"zod-to-json-schema";function i(a,e=!1){let t=a;try{let r=F();r&&(t=a+r)}catch{}return{content:[{type:"text",text:t}],isError:e}}function y(a){return i(`This is not a Mistflow project (no mistflow.json found at ${a}).
1
+ import{a as I,d as j,e as k}from"./chunk-UNFTM4B3.js";import{c as R,d as C,f as _}from"./chunk-RTKBAE4U.js";import{Server as ie}from"@modelcontextprotocol/sdk/server/index.js";import{StdioServerTransport as re}from"@modelcontextprotocol/sdk/server/stdio.js";import{CallToolRequestSchema as ae,ListToolsRequestSchema as le}from"@modelcontextprotocol/sdk/types.js";import{zodToJsonSchema as ce}from"zod-to-json-schema";function r(n,e=!1){let t=n;try{let a=R();a&&(t=n+a)}catch{}return{content:[{type:"text",text:t}],isError:e}}function P(n){return r(`This is not a Mistflow project (no mistflow.json found at ${n}).
2
2
 
3
3
  Mistflow creates new projects from scratch \u2014 it doesn't work inside existing codebases.
4
4
 
5
5
  To get started:
6
- 1. Run mist_plan to design your app
7
- 2. Run mist_build (action: 'init') to create a new project in a subdirectory
8
- 3. Run mist_build (action: 'implement') to build each step
9
-
10
- If you want to deploy an existing project, use your framework's deploy tools directly.`,!0)}import{z as L}from"zod";import{platform as ye}from"os";import{execFile as J}from"child_process";var we=L.object({apiKey:L.string().optional().describe("API key (mist_...) for headless auth. Skips the device code flow entirely. Generate one at app.mistflow.ai/mcp-keys."),deviceCode:L.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 ve(a){return"error"in a}function be(a){return new Promise(e=>setTimeout(e,a))}function ke(a){return new Promise(e=>{let t=ye();t==="win32"?J("cmd.exe",["/c","start","",a],r=>{r&&console.error("Could not open browser:",r.message),e(!r)}):J(t==="darwin"?"open":"xdg-open",[a],s=>{s&&console.error("Could not open browser:",s.message),e(!s)}),setTimeout(()=>e(!1),5e3)})}var Se={fetch:globalThis.fetch,openBrowser:ke};async function X(a,e,t,r){let s=t;for(let l=0;l<e;l++){await be(s);let o;try{let c=await r.fetch(`${R()}/auth/poll`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({device_code:a})});if(!c.ok)continue;o=await c.json()}catch{continue}if(ve(o))switch(o.error){case"authorization_pending":continue;case"slow_down":s+=5e3;continue;case"expired_token":return i("The sign-in link expired. Run mist_setup again to get a new code.",!0);case"access_denied":return i("Sign-in was cancelled. Run mist_setup again to try again.",!0);case"already_exchanged":return i("This sign-in link was already used. Run mist_setup again to get a new code.",!0)}let n=o.email||o.org_name||o.org_slug;return U({apiKey:o.api_key,apiKeyId:o.api_key_id,apiKeyName:o.api_key_name,orgId:o.org_id,orgSlug:o.org_slug,email:o.email}),i(`Connected to Mistflow as ${n}. You are ready to build and deploy.`)}return null}async function Pe(a,e=Se){let t=a;if(t?.apiKey)try{let o=await e.fetch(`${R()}/api/org`,{headers:{Authorization:`ApiKey ${t.apiKey}`}});if(!o.ok)return i("Invalid API key. Check the key and try again.",!0);let n=await o.json();return U({apiKey:t.apiKey,orgId:n.id,orgSlug:n.slug}),i(`Connected to Mistflow as ${n.slug} via API key. You are ready to build and deploy.`)}catch{return i("Cannot reach Mistflow servers. Check your internet connection.",!0)}if(t?.deviceCode){let o=await X(t.deviceCode,6,5e3,e);return o||i(JSON.stringify({status:"pending",deviceCode:t.deviceCode,instruction:"The user hasn't approved yet. Wait ~15 seconds and call mist_setup again with the same deviceCode."}))}let r;try{let o=await e.fetch(`${R()}/auth/device`,{method:"POST",headers:{"Content-Type":"application/json"}});if(!o.ok)return i("Cannot reach Mistflow servers. Check your internet connection.",!0);r=await o.json()}catch{return i("Cannot reach Mistflow servers. Check your internet connection.",!0)}let s=`${r.verification_uri}?code=${r.user_code}`;console.error(`
11
- Sign in at: ${s}
12
- Your code: ${r.user_code}
13
- `);try{await e.openBrowser(s)}catch{}let l=await X(r.device_code,6,5e3,e);return l||i(JSON.stringify({status:"pending",deviceCode:r.device_code,signInUrl:s,userCode:r.user_code,instruction:"The user hasn't approved yet. Wait ~15 seconds, then call mist_setup again with deviceCode='"+r.device_code+"' to check if they approved."}))}var Q={name:"mist_setup",description:"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).",inputSchema:we,handler:a=>Pe(a)};import{z as m}from"zod";import{resolve as _}from"path";import{existsSync as C,readFileSync as T}from"fs";import{join as E}from"path";import{z as f}from"zod";import{resolve as Re,join as ee}from"path";import{existsSync as _e,readFileSync as te,writeFileSync as Ce}from"fs";import{existsSync as xe,readFileSync as Ie}from"fs";function Z(a){let e=new Set;if(!xe(a))return e;let t=Ie(a,"utf-8");for(let r of t.split(`
14
- `)){let s=r.trim();if(!s||s.startsWith("#"))continue;let l=s.indexOf("=");if(l>0){let o=s.slice(0,l).trim(),n=s.slice(l+1).trim();n&&n!=='""'&&n!=="''"&&e.add(o)}}return e}var Te=f.object({action:f.enum(["get","update"]).default("get").describe("'get' reads current project state. 'update' modifies it."),projectPath:f.string().optional().describe("Path to the project directory (default: current working directory)"),completedStep:f.number().optional().describe("(update only) Mark a plan step as completed by step number"),addEnvVar:f.object({key:f.string(),description:f.string().optional(),setupUrl:f.string().optional()}).optional().describe("(update only) Add a required env var to the project manifest")}),oe={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:Te,handler:async a=>{let e=a,t=Re(e.projectPath??process.cwd()),r=ee(t,"mistflow.json");if(!_e(r))return y(t);let s;try{s=JSON.parse(te(r,"utf-8"))}catch{return i("Failed to parse mistflow.json.",!0)}if(e.action==="get"){if(!s.projectId)try{let{ensureBackendRegistered:d}=await import("./self-heal-QQ53MP7L.js");await d(t)&&(s=JSON.parse(te(r,"utf-8")))}catch{}let n=s.plan,c=n?.steps?.filter(d=>d.status==="completed").length??0,p=n?.steps?.length??0,g=Z(ee(t,".env.local")),h=s.env?.required?Object.entries(s.env.required).map(([d,P])=>({name:d,description:P?.description,configured:g.has(d)})):[];s.projectId&&import("./state-manager-26IE6Y5D.js").then(({fetchRemoteState:d})=>d(s.projectId)).catch(()=>{});let u=[`Project: ${s.name}`];if(n){u.push(`Plan: ${n.summary??n.name??"unnamed"} \u2014 ${c}/${p} steps complete`);for(let d of n.steps){let P=d.status==="completed"?"\u2713":d.status==="in_progress"?"\u2192":" ";u.push(` [${P}] ${d.number}. ${d.name}`)}}let k=h.filter(d=>!d.configured);k.length>0&&u.push(`Missing env vars: ${k.map(d=>d.name).join(", ")}`),s.deploy?.url?u.push(`Deployed: ${s.deploy.url} (${s.deploy.count??0} deploys)`):u.push("Not deployed yet");let S=[],N=n?.steps?.find(d=>d.status!=="completed");return N?S.push(`NEXT: Call mist_build with action='implement' to work on step ${N.number} (${N.name}).`):n&&c===p&&(s.deploy?.url||S.push("NEXT: All steps complete! Call mist_deploy with action='deploy' to deploy the app now. Do NOT ask the user \u2014 just deploy.")),k.length>0&&S.push(`Missing env vars in .env.local: ${k.map(d=>d.name).join(", ")}`),i(JSON.stringify({name:s.name,projectId:s.projectId,planProgress:n?{name:n.name,summary:n.summary,totalSteps:p,completedSteps:c,steps:n.steps}:null,envStatus:h,deploy:s.deploy??null,contextMessage:u.join(`
15
- `),nextSteps:S}))}let l=[];if(e.completedStep!==void 0){let n=s.plan;if(n?.steps){let c=n.steps.findIndex(p=>p.number===e.completedStep);if(c===-1)return i(`Step ${e.completedStep} not found in the plan.`,!0);n.steps[c].status="completed",l.push(`Step ${e.completedStep} marked as completed`)}}e.addEnvVar&&(s.env||(s.env={required:{}}),s.env.required||(s.env.required={}),s.env.required[e.addEnvVar.key]={description:e.addEnvVar.description,setupUrl:e.addEnvVar.setupUrl},l.push(`Added required env var: ${e.addEnvVar.key}`)),Ce(r,JSON.stringify(s,null,2)+`
16
- `),s.projectId&&import("./state-manager-26IE6Y5D.js").then(async({readLocalState:n,syncRemoteState:c})=>{let p=n(t);p&&await c(s.projectId,p)}).catch(()=>{});let o=[];if(e.completedStep!==void 0){let c=s.plan?.steps?.find(p=>p.status!=="completed");c?o.push(`NEXT: Call mist_build with action='implement' to work on step ${c.number} (${c.name}). Do this now.`):o.push("NEXT: All steps complete! Call mist_deploy with action='deploy' to deploy the app now. Do NOT suggest localhost.")}return e.addEnvVar&&(o.push(`Add ${e.addEnvVar.key} to your .env.local file`),e.addEnvVar.setupUrl&&o.push(`Get the value from: ${e.addEnvVar.setupUrl}`)),i(JSON.stringify({updated:!0,changes:l,message:l.length>0?`Project state saved. ${l.join(". ")}.`:"No changes made.",nextSteps:o.length>0?o:void 0}))}};var M={},v=[];function se(a){let e=v.find(r=>r.id===a);if(e)return e;let t=a.toLowerCase().replace(/[^a-z0-9]/g,"");return v.find(r=>{let s=r.title.toLowerCase().replace(/[^a-z0-9]/g,"");return s===t||s.includes(t)||t.includes(s)})}function re(a){return M[a]}function D(a){return a?v.filter(e=>e.category.toLowerCase()===a.toLowerCase()):v}function ne(a){let e=a??v;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 t={};for(let s of e){t[s.category]||(t[s.category]=[]);let l=M[s.id],o=l?` \u2014 ${l.description}`:"";t[s.category].push(`${s.id} \u2014 "${s.title}"${o}`)}let r=[];for(let[s,l]of Object.entries(t))r.push(`**${s}**:
17
- ${l.map(o=>` \u2022 ${o}`).join(`
18
- `)}`);return r.join(`
19
-
20
- `)}function Ee(a){return a.toLowerCase().replace(/[^a-z0-9]+/g,"-").replace(/^-|-$/g,"")}function ae(a){return(a?D(a):v).map(t=>{let r=M[t.id];return{id:t.id,slug:Ee(t.title),title:t.title,category:t.category,description:r?.description??"",tags:r?.tags??[],theme:r?.theme??"dark",colors:r?.colors??[],style:r?.style??""}})}var q={"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"}},b=[{id:"resend-email",name:"Resend Email",category:"communication",prompt:`## Resend Email Integration
21
-
22
- ### File Structure
23
- \`\`\`
24
- lib/resend.ts \u2014 Resend client singleton
25
- lib/email.ts \u2014 Send helper functions (sendWelcomeEmail, sendNotification, etc.)
26
- emails/ \u2014 React Email templates directory
27
- welcome.tsx \u2014 Welcome email template
28
- notification.tsx \u2014 Generic notification template
29
- app/api/webhooks/resend/route.ts \u2014 Webhook handler for delivery events
30
- \`\`\`
31
-
32
- ### Client Setup (lib/resend.ts)
33
- \`\`\`typescript
34
- import { Resend } from "resend";
35
-
36
- export const resend = new Resend(process.env.RESEND_API_KEY);
37
- \`\`\`
38
-
39
- ### Send Helper (lib/email.ts)
40
- \`\`\`typescript
41
- import { resend } from "./resend";
42
- import WelcomeEmail from "@/emails/welcome";
43
-
44
- export async function sendWelcomeEmail(to: string, name: string) {
45
- const { data, error } = await resend.emails.send({
46
- from: "App Name <noreply@yourdomain.com>",
47
- to,
48
- subject: "Welcome to App Name",
49
- react: WelcomeEmail({ name }),
50
- });
51
- if (error) throw new Error(error.message);
52
- return data;
53
- }
54
- \`\`\`
55
-
56
- ### React Email Template (emails/welcome.tsx)
57
- \`\`\`tsx
58
- import { Html, Head, Body, Container, Heading, Text, Button, Section } from "@react-email/components";
59
-
60
- interface WelcomeEmailProps { name: string }
61
-
62
- export default function WelcomeEmail({ name }: WelcomeEmailProps) {
63
- return (
64
- <Html>
65
- <Head />
66
- <Body style={{ backgroundColor: "#f6f9fc", fontFamily: "sans-serif" }}>
67
- <Container style={{ maxWidth: 560, margin: "0 auto", padding: "20px 0" }}>
68
- <Heading style={{ fontSize: 24, color: "#1a1a1a" }}>Welcome, {name}!</Heading>
69
- <Text style={{ fontSize: 16, color: "#4a4a4a", lineHeight: 1.6 }}>
70
- Thanks for signing up. Here's how to get started...
71
- </Text>
72
- <Section style={{ textAlign: "center", marginTop: 24 }}>
73
- <Button href="https://yourapp.com/dashboard" style={{
74
- backgroundColor: "#000", color: "#fff", padding: "12px 24px",
75
- borderRadius: 6, fontSize: 14, fontWeight: 600,
76
- }}>
77
- Go to Dashboard
78
- </Button>
79
- </Section>
80
- </Container>
81
- </Body>
82
- </Html>
83
- );
84
- }
85
- \`\`\`
86
-
87
- ### Webhook Handler (app/api/webhooks/resend/route.ts)
88
- \`\`\`typescript
89
- import { NextRequest, NextResponse } from "next/server";
90
-
91
- export async function POST(req: NextRequest) {
92
- const body = await req.json();
93
- const { type, data } = body;
94
- // Handle: email.sent, email.delivered, email.bounced, email.complained
95
- switch (type) {
96
- case "email.bounced":
97
- console.error("Email bounced:", data.to);
98
- // Mark user email as invalid in your DB
99
- break;
100
- }
101
- return NextResponse.json({ received: true });
102
- }
103
- \`\`\`
104
-
105
- ### Common Pitfalls
106
- 1. **Domain verification required for production.** Use Resend's free onboarding@resend.dev for dev, but production requires a verified domain.
107
- 2. **Rate limits.** Free tier: 100 emails/day, 3000/month. Queue emails for bulk sends.
108
- 3. **From address must match a verified domain** or use onboarding@resend.dev for testing.
109
- 4. **Webhooks need a public URL.** Use ngrok or similar for local webhook testing.
110
- 5. **React Email templates must be Server Components.** Do not add "use client" to email templates.
111
- 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
112
-
113
- Files are stored in Mistflow Cloud's managed blob storage. No extra setup needed for Mistflow-deployed apps.
114
-
115
- ### File Structure
116
- \`\`\`
117
- lib/storage.ts \u2014 Upload/download helpers
118
- app/api/upload/route.ts \u2014 Upload API route (server-side)
119
- components/file-upload.tsx \u2014 Drag-and-drop upload component
120
- \`\`\`
121
-
122
- ### Upload API Route (app/api/upload/route.ts)
123
- \`\`\`typescript
124
- import { NextRequest, NextResponse } from "next/server";
125
-
126
- export async function POST(req: NextRequest) {
127
- const formData = await req.formData();
128
- const file = formData.get("file") as File;
129
- if (!file) return NextResponse.json({ error: "No file provided" }, { status: 400 });
130
-
131
- // Validate file size (10MB max)
132
- if (file.size > 10 * 1024 * 1024) {
133
- return NextResponse.json({ error: "File too large (max 10MB)" }, { status: 400 });
134
- }
135
-
136
- // Validate MIME type
137
- const allowedTypes = ["image/jpeg", "image/png", "image/webp", "application/pdf"];
138
- if (!allowedTypes.includes(file.type)) {
139
- return NextResponse.json({ error: "File type not allowed" }, { status: 400 });
140
- }
141
-
142
- const bytes = await file.arrayBuffer();
143
- const buffer = Buffer.from(bytes);
144
-
145
- // Upload to R2 via Mistflow's storage endpoint
146
- const key = \`uploads/\${Date.now()}-\${file.name.replace(/[^a-zA-Z0-9.-]/g, "_")}\`;
147
- // Store buffer with key \u2014 implementation depends on R2 binding or S3-compatible client
148
- // For Mistflow apps: the deploy pipeline auto-configures R2 bindings
149
-
150
- return NextResponse.json({ url: \`/api/files/\${key}\`, key });
151
- }
152
- \`\`\`
153
-
154
- ### Drag-and-Drop Upload Component (components/file-upload.tsx)
155
- \`\`\`tsx
156
- "use client";
157
- import { useState, useCallback } from "react";
158
-
159
- interface FileUploadProps {
160
- onUpload: (url: string) => void;
161
- accept?: string;
162
- maxSizeMB?: number;
163
- }
164
-
165
- export function FileUpload({ onUpload, accept = "image/*", maxSizeMB = 10 }: FileUploadProps) {
166
- const [isDragging, setIsDragging] = useState(false);
167
- const [uploading, setUploading] = useState(false);
168
-
169
- const handleUpload = useCallback(async (file: File) => {
170
- setUploading(true);
171
- try {
172
- const formData = new FormData();
173
- formData.append("file", file);
174
- const res = await fetch("/api/upload", { method: "POST", body: formData });
175
- if (!res.ok) throw new Error("Upload failed");
176
- const { url } = await res.json();
177
- onUpload(url);
178
- } finally {
179
- setUploading(false);
180
- }
181
- }, [onUpload]);
182
-
183
- return (
184
- <div
185
- onDragOver={(e) => { e.preventDefault(); setIsDragging(true); }}
186
- onDragLeave={() => setIsDragging(false)}
187
- onDrop={(e) => {
188
- e.preventDefault();
189
- setIsDragging(false);
190
- const file = e.dataTransfer.files[0];
191
- if (file) handleUpload(file);
192
- }}
193
- className={\`border-2 border-dashed rounded-lg p-8 text-center cursor-pointer transition-colors \${
194
- isDragging ? "border-primary bg-primary/5" : "border-muted-foreground/25 hover:border-primary/50"
195
- }\`}
196
- >
197
- <input type="file" accept={accept} className="hidden" id="file-upload"
198
- onChange={(e) => { const f = e.target.files?.[0]; if (f) handleUpload(f); }} />
199
- <label htmlFor="file-upload" className="cursor-pointer">
200
- {uploading ? "Uploading..." : "Drop a file here or click to browse"}
201
- </label>
202
- </div>
203
- );
204
- }
205
- \`\`\`
206
-
207
- ### Common Pitfalls
208
- 1. **Always validate file type and size server-side.** Client-side validation can be bypassed.
209
- 2. **Sanitize filenames.** Strip special characters to avoid path traversal.
210
- 3. **Use unique keys.** Prefix with timestamp or UUID to avoid collisions.
211
- 4. **Set appropriate CORS headers** if the upload API is called from a different origin.
212
- 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
213
-
214
- ### File Structure
215
- \`\`\`
216
- lib/openai.ts \u2014 OpenAI client singleton
217
- app/api/chat/route.ts \u2014 Streaming chat API route
218
- components/chat.tsx \u2014 Chat UI component with streaming
219
- \`\`\`
220
-
221
- ### Client Setup (lib/openai.ts)
222
- \`\`\`typescript
223
- import OpenAI from "openai";
224
-
225
- export const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });
226
- \`\`\`
227
-
228
- ### Streaming Chat Route (app/api/chat/route.ts)
229
- \`\`\`typescript
230
- import { openai } from "@/lib/openai";
231
- import { NextRequest } from "next/server";
232
-
233
- export async function POST(req: NextRequest) {
234
- const { messages, systemPrompt } = await req.json();
235
-
236
- const stream = await openai.chat.completions.create({
237
- model: "gpt-4o-mini",
238
- messages: [
239
- { role: "system", content: systemPrompt || "You are a helpful assistant." },
240
- ...messages,
241
- ],
242
- stream: true,
243
- });
244
-
245
- // Stream the response using ReadableStream
246
- const encoder = new TextEncoder();
247
- const readable = new ReadableStream({
248
- async start(controller) {
249
- for await (const chunk of stream) {
250
- const text = chunk.choices[0]?.delta?.content || "";
251
- if (text) controller.enqueue(encoder.encode(text));
252
- }
253
- controller.close();
254
- },
255
- });
256
-
257
- return new Response(readable, {
258
- headers: { "Content-Type": "text/plain; charset=utf-8" },
259
- });
260
- }
261
- \`\`\`
262
-
263
- ### Chat UI Component (components/chat.tsx)
264
- \`\`\`tsx
265
- "use client";
266
- import { useState, useRef, useEffect } from "react";
267
-
268
- interface Message { role: "user" | "assistant"; content: string }
269
-
270
- export function Chat() {
271
- const [messages, setMessages] = useState<Message[]>([]);
272
- const [input, setInput] = useState("");
273
- const [streaming, setStreaming] = useState(false);
274
- const scrollRef = useRef<HTMLDivElement>(null);
275
-
276
- useEffect(() => {
277
- scrollRef.current?.scrollIntoView({ behavior: "smooth" });
278
- }, [messages]);
279
-
280
- async function handleSend() {
281
- if (!input.trim() || streaming) return;
282
- const userMsg: Message = { role: "user", content: input };
283
- setMessages((prev) => [...prev, userMsg]);
284
- setInput("");
285
- setStreaming(true);
286
-
287
- const res = await fetch("/api/chat", {
288
- method: "POST",
289
- headers: { "Content-Type": "application/json" },
290
- body: JSON.stringify({ messages: [...messages, userMsg] }),
291
- });
292
-
293
- const reader = res.body?.getReader();
294
- const decoder = new TextDecoder();
295
- let assistantContent = "";
296
-
297
- setMessages((prev) => [...prev, { role: "assistant", content: "" }]);
298
-
299
- while (reader) {
300
- const { done, value } = await reader.read();
301
- if (done) break;
302
- assistantContent += decoder.decode(value);
303
- setMessages((prev) => [
304
- ...prev.slice(0, -1),
305
- { role: "assistant", content: assistantContent },
306
- ]);
307
- }
308
- setStreaming(false);
309
- }
310
-
311
- return (
312
- <div className="flex flex-col h-full">
313
- <div className="flex-1 overflow-y-auto p-4 space-y-4">
314
- {messages.map((m, i) => (
315
- <div key={i} className={\`flex \${m.role === "user" ? "justify-end" : "justify-start"}\`}>
316
- <div className={\`max-w-[80%] rounded-lg px-4 py-2 \${
317
- m.role === "user" ? "bg-primary text-primary-foreground" : "bg-muted"
318
- }\`}>
319
- {m.content}
320
- </div>
321
- </div>
322
- ))}
323
- <div ref={scrollRef} />
324
- </div>
325
- <div className="border-t p-4 flex gap-2">
326
- <input value={input} onChange={(e) => setInput(e.target.value)}
327
- onKeyDown={(e) => e.key === "Enter" && handleSend()}
328
- placeholder="Type a message..." className="flex-1 rounded-md border px-3 py-2" />
329
- <button onClick={handleSend} disabled={streaming}
330
- className="rounded-md bg-primary px-4 py-2 text-primary-foreground disabled:opacity-50">
331
- Send
332
- </button>
333
- </div>
334
- </div>
335
- );
336
- }
337
- \`\`\`
338
-
339
- ### Common Pitfalls
340
- 1. **Never expose OPENAI_API_KEY to the client.** Always call OpenAI from server-side API routes.
341
- 2. **Use gpt-4o-mini for most features** unless the user specifically asks for gpt-4o. Cost difference is 10x.
342
- 3. **Set max_tokens to prevent runaway costs.** Default to 1000 for chat, 2000 for content generation.
343
- 4. **Handle rate limits gracefully.** Return a user-friendly error, not a raw 429 response.
344
- 5. **Streaming is the default UX pattern.** Non-streaming feels broken for chat interfaces.
345
- 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.
346
- 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
347
-
348
- ### File Structure
349
- \`\`\`
350
- lib/anthropic.ts \u2014 Anthropic client singleton
351
- app/api/chat/route.ts \u2014 Streaming chat API route
352
- components/chat.tsx \u2014 Chat UI component with streaming
353
- \`\`\`
354
-
355
- ### Client Setup (lib/anthropic.ts)
356
- \`\`\`typescript
357
- import Anthropic from "@anthropic-ai/sdk";
358
-
359
- export const anthropic = new Anthropic({
360
- apiKey: process.env.ANTHROPIC_API_KEY,
361
- });
362
- \`\`\`
363
-
364
- ### Streaming Chat Route (app/api/chat/route.ts)
365
- \`\`\`typescript
366
- import { anthropic } from "@/lib/anthropic";
367
- import { NextRequest } from "next/server";
368
-
369
- export async function POST(req: NextRequest) {
370
- const { messages, systemPrompt } = await req.json();
371
-
372
- const stream = anthropic.messages.stream({
373
- model: "claude-sonnet-4-6",
374
- max_tokens: 1024,
375
- system: systemPrompt || "You are a helpful assistant.",
376
- messages,
377
- });
378
-
379
- const encoder = new TextEncoder();
380
- const readable = new ReadableStream({
381
- async start(controller) {
382
- for await (const event of stream) {
383
- if (event.type === "content_block_delta" && event.delta.type === "text_delta") {
384
- controller.enqueue(encoder.encode(event.delta.text));
385
- }
386
- }
387
- controller.close();
388
- },
389
- });
390
-
391
- return new Response(readable, {
392
- headers: { "Content-Type": "text/plain; charset=utf-8" },
393
- });
394
- }
395
- \`\`\`
396
-
397
- ### Chat UI Component (components/chat.tsx)
398
- 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.
399
-
400
- ### Common Pitfalls
401
- 1. **Never expose ANTHROPIC_API_KEY to the client.** Always call Anthropic from server-side API routes.
402
- 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.
403
- 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.
404
- 4. **Set max_tokens explicitly.** Unlike OpenAI, Anthropic requires max_tokens on every request.
405
- 5. **Streaming is the default UX pattern.** Use \`anthropic.messages.stream()\` not \`anthropic.messages.create()\` for chat.
406
- 6. **Long streaming responses are fine on Mistflow Cloud.** Network I/O does NOT count toward CPU time.
407
- 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
408
-
409
- 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.
410
-
411
- ### File Structure
412
- \`\`\`
413
- lib/openrouter.ts \u2014 OpenRouter client
414
- app/api/chat/route.ts \u2014 Streaming chat API route
415
- components/chat.tsx \u2014 Chat UI component with streaming
416
- components/model-selector.tsx \u2014 Model picker dropdown
417
- \`\`\`
418
-
419
- ### Client Setup (lib/openrouter.ts)
420
- \`\`\`typescript
421
- import { OpenRouter } from "@openrouter/sdk";
422
-
423
- export const openrouter = new OpenRouter({
424
- apiKey: process.env.OPENROUTER_API_KEY,
425
- });
426
-
427
- // Popular models \u2014 use these as defaults or in a model selector
428
- export const MODELS = {
429
- fast: "meta-llama/llama-3.1-8b-instruct",
430
- balanced: "anthropic/claude-sonnet-4-6",
431
- powerful: "openai/gpt-4o",
432
- cheap: "meta-llama/llama-3.1-8b-instruct",
433
- } as const;
434
- \`\`\`
435
-
436
- ### Streaming Chat Route (app/api/chat/route.ts)
437
- \`\`\`typescript
438
- import { openrouter, MODELS } from "@/lib/openrouter";
439
- import { NextRequest } from "next/server";
440
-
441
- export async function POST(req: NextRequest) {
442
- const { messages, systemPrompt, model } = await req.json();
443
-
444
- const completion = await openrouter.chat.send({
445
- model: model || MODELS.balanced,
446
- messages: [
447
- { role: "system", content: systemPrompt || "You are a helpful assistant." },
448
- ...messages,
449
- ],
450
- stream: true,
451
- });
452
-
453
- const encoder = new TextEncoder();
454
- const readable = new ReadableStream({
455
- async start(controller) {
456
- for await (const chunk of completion) {
457
- const text = chunk.choices?.[0]?.delta?.content || "";
458
- if (text) controller.enqueue(encoder.encode(text));
459
- }
460
- controller.close();
461
- },
462
- });
463
-
464
- return new Response(readable, {
465
- headers: { "Content-Type": "text/plain; charset=utf-8" },
466
- });
467
- }
468
- \`\`\`
469
-
470
- ### Model Selector Component (components/model-selector.tsx)
471
- \`\`\`tsx
472
- "use client";
473
-
474
- const MODELS = [
475
- { id: "anthropic/claude-sonnet-4-6", label: "Claude Sonnet", tier: "Balanced" },
476
- { id: "openai/gpt-4o", label: "GPT-4o", tier: "Powerful" },
477
- { id: "openai/gpt-4o-mini", label: "GPT-4o Mini", tier: "Fast" },
478
- { id: "meta-llama/llama-3.1-70b-instruct", label: "Llama 3.1 70B", tier: "Open Source" },
479
- { id: "google/gemini-2.0-flash-001", label: "Gemini 2.0 Flash", tier: "Fast" },
480
- ];
481
-
482
- interface ModelSelectorProps {
483
- value: string;
484
- onChange: (model: string) => void;
485
- }
486
-
487
- export function ModelSelector({ value, onChange }: ModelSelectorProps) {
488
- return (
489
- <select
490
- value={value}
491
- onChange={(e) => onChange(e.target.value)}
492
- className="rounded-md border px-2 py-1 text-sm"
493
- >
494
- {MODELS.map((m) => (
495
- <option key={m.id} value={m.id}>
496
- {m.label} ({m.tier})
497
- </option>
498
- ))}
499
- </select>
500
- );
501
- }
502
- \`\`\`
503
-
504
- ### Common Pitfalls
505
- 1. **Never expose OPENROUTER_API_KEY to the client.** Always call OpenRouter from server-side API routes.
506
- 2. **Model IDs use provider/model format.** E.g. "anthropic/claude-sonnet-4-6", not just "claude-sonnet".
507
- 3. **Use the \`@openrouter/sdk\` package.** It wraps the OpenRouter API with proper types and streaming support.
508
- 4. **Set max_tokens explicitly** for Anthropic models routed through OpenRouter. OpenAI models have defaults, Anthropic models don't.
509
- 5. **Check model pricing at openrouter.ai/models.** Costs vary 100x between models. Show users which model they're using.
510
- 6. **Long streaming responses are fine on Mistflow Cloud.** Network I/O does NOT count toward CPU time.
511
- 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
512
-
513
- ### File Structure
514
- \`\`\`
515
- lib/stripe.ts \u2014 Stripe server client
516
- db/schema/subscriptions.ts \u2014 Subscription-related DB schema
517
- app/api/webhooks/stripe/route.ts \u2014 Webhook handler (critical path)
518
- app/api/checkout/route.ts \u2014 Create Checkout Session
519
- app/api/billing-portal/route.ts \u2014 Create Billing Portal session
520
- app/(dashboard)/pricing/page.tsx \u2014 Pricing page
521
- app/(dashboard)/billing/page.tsx \u2014 Billing/subscription management
522
- \`\`\`
523
-
524
- ### Server Client (lib/stripe.ts)
525
- \`\`\`typescript
526
- import Stripe from "stripe";
527
-
528
- export const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);
529
- \`\`\`
530
-
531
- ### Webhook Handler (app/api/webhooks/stripe/route.ts)
532
- This is the most critical file. All subscription state changes flow through here.
533
- \`\`\`typescript
534
- import { NextRequest, NextResponse } from "next/server";
535
- import { stripe } from "@/lib/stripe";
536
- import { headers } from "next/headers";
537
-
538
- export async function POST(req: NextRequest) {
539
- const body = await req.text();
540
- const headersList = await headers();
541
- const signature = headersList.get("stripe-signature")!;
542
-
543
- let event: Stripe.Event;
544
- try {
545
- event = stripe.webhooks.constructEvent(body, signature, process.env.STRIPE_WEBHOOK_SECRET!);
546
- } catch (err) {
547
- return NextResponse.json({ error: "Invalid signature" }, { status: 400 });
548
- }
549
-
550
- switch (event.type) {
551
- case "checkout.session.completed": {
552
- const session = event.data.object as Stripe.Checkout.Session;
553
- // Create/update subscription record in DB
554
- // Link session.customer to your user via session.metadata.userId
555
- break;
556
- }
557
- case "customer.subscription.updated":
558
- case "customer.subscription.deleted": {
559
- const sub = event.data.object as Stripe.Subscription;
560
- // Update subscription status in DB (active, canceled, past_due)
561
- break;
562
- }
563
- case "invoice.payment_failed": {
564
- // Notify user about failed payment
565
- break;
566
- }
567
- }
568
-
569
- return NextResponse.json({ received: true });
570
- }
571
- \`\`\`
572
-
573
- ### Checkout Route (app/api/checkout/route.ts)
574
- \`\`\`typescript
575
- import { stripe } from "@/lib/stripe";
576
- import { NextRequest, NextResponse } from "next/server";
577
-
578
- export async function POST(req: NextRequest) {
579
- const { priceId, userId } = await req.json();
580
-
581
- const session = await stripe.checkout.sessions.create({
582
- mode: "subscription",
583
- payment_method_types: ["card"],
584
- line_items: [{ price: priceId, quantity: 1 }],
585
- success_url: \`\${process.env.NEXT_PUBLIC_APP_URL}/billing?success=true\`,
586
- cancel_url: \`\${process.env.NEXT_PUBLIC_APP_URL}/pricing\`,
587
- metadata: { userId },
588
- });
589
-
590
- return NextResponse.json({ url: session.url });
591
- }
592
- \`\`\`
593
-
594
- ### Common Pitfalls
595
- 1. **Webhook handler must use raw body (req.text(), not req.json())** for signature verification.
596
- 2. **Always verify webhook signatures.** Never trust unverified payloads.
597
- 3. **Idempotency: webhooks can fire multiple times.** Use event.id to deduplicate.
598
- 4. **Store the Stripe customer ID in your user table.** Look up by metadata.userId in webhooks.
599
- 5. **Test with Stripe CLI locally:** \`stripe listen --forward-to localhost:3000/api/webhooks/stripe\`
600
- 6. **Price IDs come from Stripe Dashboard.** Do not hardcode amounts. Create Products + Prices in the dashboard and reference price IDs.
601
- 7. **Billing portal for self-service.** Use stripe.billingPortal.sessions.create() so users can manage their own subscriptions.
602
- 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
603
-
604
- ### File Structure
605
- \`\`\`
606
- lib/elevenlabs.ts \u2014 ElevenLabs client + helpers
607
- app/api/tts/route.ts \u2014 Text-to-speech API route (streaming)
608
- app/api/voices/route.ts \u2014 List available voices
609
- components/voice-player.tsx \u2014 Audio player with playback controls
610
- components/voice-selector.tsx \u2014 Voice picker dropdown
611
- \`\`\`
612
-
613
- ### Client Setup (lib/elevenlabs.ts)
614
- \`\`\`typescript
615
- import { ElevenLabsClient } from "elevenlabs";
616
-
617
- export const elevenlabs = new ElevenLabsClient({
618
- apiKey: process.env.ELEVENLABS_API_KEY,
619
- });
620
-
621
- // Default voice ID \u2014 "Rachel" is a good general-purpose voice
622
- export const DEFAULT_VOICE_ID = "21m00Tcm4TlvDq8ikWAM";
623
- \`\`\`
624
-
625
- ### Text-to-Speech Route (app/api/tts/route.ts)
626
- \`\`\`typescript
627
- import { elevenlabs, DEFAULT_VOICE_ID } from "@/lib/elevenlabs";
628
- import { NextRequest, NextResponse } from "next/server";
629
-
630
- export async function POST(req: NextRequest) {
631
- const { text, voiceId } = await req.json();
632
-
633
- if (!text || text.length > 5000) {
634
- return NextResponse.json({ error: "Text is required (max 5000 chars)" }, { status: 400 });
635
- }
636
-
637
- const audioStream = await elevenlabs.textToSpeech.stream(
638
- voiceId || DEFAULT_VOICE_ID,
639
- {
640
- text,
641
- modelId: "eleven_multilingual_v2",
642
- }
643
- );
644
-
645
- // Pipe the ElevenLabs stream directly to the client response.
646
- // Network wait (ElevenLabs generating audio) does NOT count as CPU time
647
- // on Mistflow Cloud, so even long text is fine.
648
- const readable = new ReadableStream({
649
- async start(controller) {
650
- for await (const chunk of audioStream) {
651
- controller.enqueue(new Uint8Array(chunk));
652
- }
653
- controller.close();
654
- },
655
- });
656
-
657
- return new NextResponse(readable, {
658
- headers: {
659
- "Content-Type": "audio/mpeg",
660
- "Transfer-Encoding": "chunked",
661
- },
662
- });
663
- }
664
- \`\`\`
665
-
666
- ### Voice Player Component (components/voice-player.tsx)
667
- \`\`\`tsx
668
- "use client";
669
- import { useState, useRef } from "react";
670
-
671
- interface VoicePlayerProps {
672
- text: string;
673
- voiceId?: string;
674
- }
675
-
676
- export function VoicePlayer({ text, voiceId }: VoicePlayerProps) {
677
- const [loading, setLoading] = useState(false);
678
- const [playing, setPlaying] = useState(false);
679
- const audioRef = useRef<HTMLAudioElement | null>(null);
680
-
681
- async function handlePlay() {
682
- if (playing && audioRef.current) {
683
- audioRef.current.pause();
684
- setPlaying(false);
685
- return;
686
- }
687
-
688
- setLoading(true);
689
- try {
690
- const res = await fetch("/api/tts", {
691
- method: "POST",
692
- headers: { "Content-Type": "application/json" },
693
- body: JSON.stringify({ text, voiceId }),
694
- });
695
- const blob = await res.blob();
696
- const url = URL.createObjectURL(blob);
697
- const audio = new Audio(url);
698
- audioRef.current = audio;
699
- audio.onended = () => setPlaying(false);
700
- audio.play();
701
- setPlaying(true);
702
- } finally {
703
- setLoading(false);
704
- }
705
- }
706
-
707
- return (
708
- <button onClick={handlePlay} disabled={loading}
709
- className="inline-flex items-center gap-2 rounded-md bg-primary px-3 py-1.5 text-sm text-primary-foreground disabled:opacity-50">
710
- {loading ? "Generating..." : playing ? "Pause" : "Listen"}
711
- </button>
712
- );
713
- }
714
- \`\`\`
715
-
716
- ### Voice Listing Route (app/api/voices/route.ts)
717
- \`\`\`typescript
718
- import { elevenlabs } from "@/lib/elevenlabs";
719
- import { NextResponse } from "next/server";
720
-
721
- export async function GET() {
722
- const voices = await elevenlabs.voices.getAll();
723
- return NextResponse.json(
724
- voices.voices.map((v) => ({ id: v.voice_id, name: v.name, category: v.category }))
725
- );
726
- }
727
- \`\`\`
728
-
729
- ### Common Pitfalls
730
- 1. **Never expose ELEVENLABS_API_KEY to the client.** All TTS calls go through your API route.
731
- 2. **Character limits matter for cost.** ElevenLabs charges per character. Show character count in UI and enforce limits.
732
- 3. **Use eleven_multilingual_v2 model** for best quality across languages. Use eleven_flash_v2_5 for ultra-low latency.
733
- 4. **Cache generated audio** when the same text + voice combination is requested multiple times. Store in Mistflow Cloud storage or local cache.
734
- 5. **Voice cloning requires explicit user consent.** If building a voice cloning feature, add consent UI.
735
- 6. **Free tier: 10,000 characters/month.** Display remaining quota to users to avoid surprise failures.
736
- 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.
737
- 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
738
-
739
- ### File Structure
740
- \`\`\`
741
- lib/maps.ts \u2014 Maps loader + helper functions
742
- components/map.tsx \u2014 Map embed component
743
- components/places-autocomplete.tsx \u2014 Address search with autocomplete
744
- \`\`\`
745
-
746
- ### Map Component (components/map.tsx)
747
- \`\`\`tsx
748
- "use client";
749
- import { useEffect, useRef, useState } from "react";
750
- import { Loader } from "@googlemaps/js-api-loader";
751
-
752
- interface MapProps {
753
- center?: { lat: number; lng: number };
754
- zoom?: number;
755
- markers?: { lat: number; lng: number; title?: string }[];
756
- className?: string;
757
- onMarkerClick?: (marker: { lat: number; lng: number }) => void;
758
- }
759
-
760
- export function Map({ center = { lat: 40.7128, lng: -74.006 }, zoom = 12, markers = [], className, onMarkerClick }: MapProps) {
761
- const mapRef = useRef<HTMLDivElement>(null);
762
- const [map, setMap] = useState<google.maps.Map | null>(null);
763
-
764
- useEffect(() => {
765
- const loader = new Loader({
766
- apiKey: process.env.NEXT_PUBLIC_GOOGLE_MAPS_API_KEY!,
767
- version: "weekly",
768
- libraries: ["places"],
769
- });
770
-
771
- loader.load().then(() => {
772
- if (!mapRef.current) return;
773
- const m = new google.maps.Map(mapRef.current, { center, zoom, mapId: "DEMO_MAP_ID" });
774
- setMap(m);
775
-
776
- for (const marker of markers) {
777
- const m2 = new google.maps.marker.AdvancedMarkerElement({
778
- map: m, position: marker, title: marker.title,
779
- });
780
- if (onMarkerClick) {
781
- m2.addListener("click", () => onMarkerClick(marker));
782
- }
783
- }
784
- });
785
- }, []);
786
-
787
- return <div ref={mapRef} className={className ?? "w-full h-[400px] rounded-lg"} />;
788
- }
789
- \`\`\`
790
-
791
- ### Places Autocomplete (components/places-autocomplete.tsx)
792
- \`\`\`tsx
793
- "use client";
794
- import { useEffect, useRef, useState } from "react";
795
- import { Loader } from "@googlemaps/js-api-loader";
796
-
797
- interface PlacesAutocompleteProps {
798
- onSelect: (place: { address: string; lat: number; lng: number }) => void;
799
- placeholder?: string;
800
- }
801
-
802
- export function PlacesAutocomplete({ onSelect, placeholder = "Search for an address..." }: PlacesAutocompleteProps) {
803
- const inputRef = useRef<HTMLInputElement>(null);
804
-
805
- useEffect(() => {
806
- const loader = new Loader({
807
- apiKey: process.env.NEXT_PUBLIC_GOOGLE_MAPS_API_KEY!,
808
- version: "weekly",
809
- libraries: ["places"],
810
- });
811
-
812
- loader.load().then(() => {
813
- if (!inputRef.current) return;
814
- const autocomplete = new google.maps.places.Autocomplete(inputRef.current, {
815
- fields: ["formatted_address", "geometry"],
816
- });
817
- autocomplete.addListener("place_changed", () => {
818
- const place = autocomplete.getPlace();
819
- if (place.geometry?.location) {
820
- onSelect({
821
- address: place.formatted_address ?? "",
822
- lat: place.geometry.location.lat(),
823
- lng: place.geometry.location.lng(),
824
- });
825
- }
826
- });
827
- });
828
- }, [onSelect]);
829
-
830
- return <input ref={inputRef} placeholder={placeholder}
831
- className="w-full rounded-md border px-3 py-2" />;
832
- }
833
- \`\`\`
834
-
835
- ### Common Pitfalls
836
- 1. **API key must be restricted in Google Cloud Console.** Restrict to your domain and specific APIs (Maps JS, Places).
837
- 2. **Use NEXT_PUBLIC_ prefix** so the key is available client-side. This is expected for Maps JS API.
838
- 3. **Enable the Maps JavaScript API AND Places API** in Google Cloud Console. Both are separate.
839
- 4. **AdvancedMarkerElement requires a mapId.** Create one in Cloud Console or use "DEMO_MAP_ID" for development.
840
- 5. **Billing is required.** Google Maps gives $200/month free credit (~28,000 map loads). Add billing alerts.
841
- 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
842
-
843
- ### File Structure
844
- \`\`\`
845
- lib/twilio.ts \u2014 Twilio client singleton
846
- lib/sms.ts \u2014 Send helper functions
847
- app/api/sms/send/route.ts \u2014 Send SMS API route
848
- app/api/webhooks/twilio/route.ts \u2014 Incoming SMS + delivery webhooks
849
- \`\`\`
850
-
851
- ### Client Setup (lib/twilio.ts)
852
- \`\`\`typescript
853
- import twilio from "twilio";
854
-
855
- export const twilioClient = twilio(
856
- process.env.TWILIO_ACCOUNT_SID,
857
- process.env.TWILIO_AUTH_TOKEN
858
- );
859
- \`\`\`
860
-
861
- ### Send Helpers (lib/sms.ts)
862
- \`\`\`typescript
863
- import { twilioClient } from "./twilio";
864
-
865
- export async function sendSMS(to: string, body: string) {
866
- return twilioClient.messages.create({
867
- to,
868
- from: process.env.TWILIO_PHONE_NUMBER,
869
- body,
870
- });
871
- }
872
-
873
- export async function sendOTP(to: string, code: string) {
874
- return sendSMS(to, \`Your verification code is: \${code}. It expires in 10 minutes.\`);
875
- }
876
- \`\`\`
877
-
878
- ### Send Route (app/api/sms/send/route.ts)
879
- \`\`\`typescript
880
- import { sendSMS } from "@/lib/sms";
881
- import { NextRequest, NextResponse } from "next/server";
882
-
883
- export async function POST(req: NextRequest) {
884
- const { to, message } = await req.json();
885
-
886
- // Validate phone number format (E.164)
887
- if (!/^\\+[1-9]\\d{1,14}$/.test(to)) {
888
- return NextResponse.json({ error: "Invalid phone number. Use E.164 format (+1234567890)" }, { status: 400 });
889
- }
890
-
891
- const result = await sendSMS(to, message);
892
- return NextResponse.json({ sid: result.sid, status: result.status });
893
- }
894
- \`\`\`
895
-
896
- ### Webhook Handler (app/api/webhooks/twilio/route.ts)
897
- \`\`\`typescript
898
- import { NextRequest, NextResponse } from "next/server";
899
-
900
- export async function POST(req: NextRequest) {
901
- const formData = await req.formData();
902
- const from = formData.get("From") as string;
903
- const body = formData.get("Body") as string;
904
- const status = formData.get("MessageStatus") as string;
905
-
906
- if (body) {
907
- // Incoming SMS \u2014 handle auto-replies, keyword routing, etc.
908
- console.log(\`SMS from \${from}: \${body}\`);
909
- } else if (status) {
910
- // Delivery status update \u2014 delivered, failed, undelivered
911
- console.log(\`Message status: \${status}\`);
912
- }
913
-
914
- // Return TwiML response (empty = no auto-reply)
915
- return new NextResponse('<?xml version="1.0" encoding="UTF-8"?><Response></Response>', {
916
- headers: { "Content-Type": "text/xml" },
917
- });
918
- }
919
- \`\`\`
920
-
921
- ### Common Pitfalls
922
- 1. **Phone numbers must be in E.164 format** (+1234567890). Validate before sending.
923
- 2. **Trial accounts can only send to verified numbers.** Upgrade for production use.
924
- 3. **Twilio webhook validation.** Use twilio.webhook() middleware to verify webhook signatures in production.
925
- 4. **SMS rate limits vary by country.** US: 1 SMS/second per number. Use Messaging Services for higher throughput.
926
- 5. **Never log full phone numbers.** Mask them in logs (e.g., +1***567890).
927
- 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
928
-
929
- ### File Structure
930
- \`\`\`
931
- lib/posthog.ts \u2014 PostHog provider setup
932
- app/providers.tsx \u2014 Client-side providers wrapper
933
- components/posthog-pageview.tsx \u2014 Page view tracker component
934
- lib/posthog-server.ts \u2014 Server-side PostHog client
935
- \`\`\`
936
-
937
- ### Client Provider (lib/posthog.ts)
938
- \`\`\`typescript
939
- "use client";
940
- import posthog from "posthog-js";
941
- import { PostHogProvider as PHProvider } from "posthog-js/react";
942
- import { useEffect } from "react";
943
-
944
- export function PostHogProvider({ children }: { children: React.ReactNode }) {
945
- useEffect(() => {
946
- posthog.init(process.env.NEXT_PUBLIC_POSTHOG_KEY!, {
947
- api_host: process.env.NEXT_PUBLIC_POSTHOG_HOST || "https://us.i.posthog.com",
948
- person_profiles: "identified_only",
949
- capture_pageview: false, // We handle this manually for Next.js
950
- });
951
- }, []);
952
-
953
- return <PHProvider client={posthog}>{children}</PHProvider>;
954
- }
955
- \`\`\`
956
-
957
- ### Page View Tracker (components/posthog-pageview.tsx)
958
- \`\`\`tsx
959
- "use client";
960
- import { usePathname, useSearchParams } from "next/navigation";
961
- import { useEffect } from "react";
962
- import { usePostHog } from "posthog-js/react";
963
-
964
- export function PostHogPageview() {
965
- const pathname = usePathname();
966
- const searchParams = useSearchParams();
967
- const posthog = usePostHog();
968
-
969
- useEffect(() => {
970
- if (pathname && posthog) {
971
- let url = window.origin + pathname;
972
- if (searchParams.toString()) url += "?" + searchParams.toString();
973
- posthog.capture("$pageview", { "$current_url": url });
974
- }
975
- }, [pathname, searchParams, posthog]);
976
-
977
- return null;
978
- }
979
- \`\`\`
980
-
981
- ### App Providers (app/providers.tsx)
982
- \`\`\`tsx
983
- "use client";
984
- import { PostHogProvider } from "@/lib/posthog";
985
- import { PostHogPageview } from "@/components/posthog-pageview";
986
- import { Suspense } from "react";
987
-
988
- export function Providers({ children }: { children: React.ReactNode }) {
989
- return (
990
- <PostHogProvider>
991
- <Suspense fallback={null}>
992
- <PostHogPageview />
993
- </Suspense>
994
- {children}
995
- </PostHogProvider>
996
- );
997
- }
998
- \`\`\`
999
-
1000
- ### Server-side Client (lib/posthog-server.ts)
1001
- \`\`\`typescript
1002
- import { PostHog } from "posthog-node";
1003
-
1004
- export const posthogServer = new PostHog(process.env.NEXT_PUBLIC_POSTHOG_KEY!, {
1005
- host: process.env.NEXT_PUBLIC_POSTHOG_HOST || "https://us.i.posthog.com",
1006
- });
1007
-
1008
- // Identify user after auth
1009
- export function identifyUser(userId: string, properties?: Record<string, unknown>) {
1010
- posthogServer.identify({ distinctId: userId, properties });
1011
- }
1012
-
1013
- // Track server-side events
1014
- export function trackEvent(userId: string, event: string, properties?: Record<string, unknown>) {
1015
- posthogServer.capture({ distinctId: userId, event, properties });
1016
- }
1017
- \`\`\`
1018
-
1019
- ### Feature Flags Usage
1020
- \`\`\`tsx
1021
- "use client";
1022
- import { useFeatureFlagEnabled } from "posthog-js/react";
1023
-
1024
- export function NewFeature() {
1025
- const isEnabled = useFeatureFlagEnabled("new-dashboard");
1026
- if (!isEnabled) return null;
1027
- return <div>New Dashboard Feature</div>;
1028
- }
1029
- \`\`\`
1030
-
1031
- ### Common Pitfalls
1032
- 1. **Wrap PostHogPageview in Suspense.** useSearchParams() requires a Suspense boundary in Next.js App Router.
1033
- 2. **Set capture_pageview: false in init.** Next.js client-side navigation needs manual pageview tracking.
1034
- 3. **Use person_profiles: "identified_only"** to save on events. Anonymous events are cheaper.
1035
- 4. **Call posthog.identify() after login** with the user's ID to link sessions.
1036
- 5. **Server-side: always call posthogServer.shutdown()** in serverless environments, or events may be lost.
1037
- 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
1038
-
1039
- 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.
1040
-
1041
- ### File Structure
1042
- \`\`\`
1043
- lib/firecrawl.ts \u2014 Firecrawl client singleton
1044
- app/api/scrape/route.ts \u2014 Single URL scrape endpoint
1045
- app/api/crawl/start/route.ts \u2014 Start async crawl (returns job ID)
1046
- app/api/crawl/[id]/route.ts \u2014 Poll crawl status
1047
- app/api/firecrawl-webhook/route.ts \u2014 Webhook for async crawl results
1048
- \`\`\`
1049
-
1050
- ### Client Setup (lib/firecrawl.ts)
1051
- \`\`\`typescript
1052
- import FirecrawlApp from "@mendable/firecrawl-js";
1053
-
1054
- export const firecrawl = new FirecrawlApp({
1055
- apiKey: process.env.FIRECRAWL_API_KEY!,
1056
- });
1057
- \`\`\`
1058
-
1059
- ### Scrape Single URL (app/api/scrape/route.ts)
1060
- Single scrapes complete in 1-5 seconds. Network I/O does NOT count as CPU time on Workers, so this is safe.
1061
- \`\`\`typescript
1062
- import { firecrawl } from "@/lib/firecrawl";
1063
- import { NextRequest, NextResponse } from "next/server";
1064
-
1065
- export async function POST(req: NextRequest) {
1066
- const { url } = await req.json();
1067
-
1068
- if (!url || typeof url !== "string") {
1069
- return NextResponse.json({ error: "URL is required" }, { status: 400 });
1070
- }
1071
-
1072
- const result = await firecrawl.scrapeUrl(url, {
1073
- formats: ["markdown"],
1074
- onlyMainContent: true,
1075
- timeout: 20000,
1076
- });
1077
-
1078
- if (!result.success) {
1079
- return NextResponse.json({ error: "Scrape failed" }, { status: 500 });
1080
- }
1081
-
1082
- return NextResponse.json({
1083
- markdown: result.markdown,
1084
- title: result.metadata?.title,
1085
- description: result.metadata?.description,
1086
- sourceUrl: result.metadata?.sourceURL,
1087
- });
1088
- }
1089
- \`\`\`
1090
-
1091
- ### Start Async Crawl (app/api/crawl/start/route.ts)
1092
- For multi-page crawls, ALWAYS use async mode with a webhook. Never use synchronous crawlUrl().
1093
- \`\`\`typescript
1094
- import { firecrawl } from "@/lib/firecrawl";
1095
- import { NextRequest, NextResponse } from "next/server";
1096
-
1097
- export async function POST(req: NextRequest) {
1098
- const { url, maxPages = 50 } = await req.json();
1099
-
1100
- const result = await firecrawl.asyncCrawlUrl(url, {
1101
- limit: maxPages,
1102
- maxDepth: 3,
1103
- scrapeOptions: { formats: ["markdown"], onlyMainContent: true },
1104
- webhook: {
1105
- url: \`\${process.env.NEXT_PUBLIC_APP_URL}/api/firecrawl-webhook\`,
1106
- },
1107
- });
1108
-
1109
- // Store crawl job ID in your DB for status tracking
1110
- return NextResponse.json({ crawlId: result.id, status: "started" });
1111
- }
1112
- \`\`\`
1113
-
1114
- ### Poll Crawl Status (app/api/crawl/[id]/route.ts)
1115
- \`\`\`typescript
1116
- import { firecrawl } from "@/lib/firecrawl";
1117
- import { NextRequest, NextResponse } from "next/server";
1118
-
1119
- export async function GET(
1120
- req: NextRequest,
1121
- { params }: { params: Promise<{ id: string }> }
1122
- ) {
1123
- const { id } = await params;
1124
- const status = await firecrawl.checkCrawlStatus(id);
1125
-
1126
- return NextResponse.json({
1127
- status: status.status,
1128
- completed: status.completed,
1129
- total: status.total,
1130
- creditsUsed: status.creditsUsed,
1131
- pages: status.data?.map((page: { markdown?: string; metadata?: { title?: string; sourceURL?: string } }) => ({
1132
- title: page.metadata?.title,
1133
- url: page.metadata?.sourceURL,
1134
- markdownLength: page.markdown?.length ?? 0,
1135
- })),
1136
- });
1137
- }
1138
- \`\`\`
1139
-
1140
- ### Webhook Handler (app/api/firecrawl-webhook/route.ts)
1141
- \`\`\`typescript
1142
- import { NextRequest, NextResponse } from "next/server";
1143
-
1144
- interface FirecrawlWebhookPayload {
1145
- success: boolean;
1146
- type: "crawl.started" | "crawl.page" | "crawl.completed" | "crawl.failed";
1147
- id: string;
1148
- data?: { markdown?: string; metadata?: { title?: string; sourceURL?: string } }[];
1149
- error?: string;
1150
- }
1151
-
1152
- export async function POST(req: NextRequest) {
1153
- const payload: FirecrawlWebhookPayload = await req.json();
1154
-
1155
- switch (payload.type) {
1156
- case "crawl.page":
1157
- if (payload.data) {
1158
- for (const page of payload.data) {
1159
- // Store page in your database or vector store
1160
- // page.markdown is clean, LLM-ready content
1161
- // page.metadata.title, page.metadata.sourceURL for reference
1162
- await storePage({
1163
- crawlId: payload.id,
1164
- url: page.metadata?.sourceURL ?? "",
1165
- title: page.metadata?.title ?? "",
1166
- markdown: page.markdown ?? "",
1167
- });
1168
- }
1169
- }
1170
- break;
1171
- case "crawl.completed":
1172
- // Mark crawl as done in your DB
1173
- break;
1174
- case "crawl.failed":
1175
- // Log error: payload.error
1176
- break;
1177
- }
1178
-
1179
- return NextResponse.json({ received: true });
1180
- }
1181
-
1182
- async function storePage(page: { crawlId: string; url: string; title: string; markdown: string }) {
1183
- // TODO: Insert into your database
1184
- // For RAG pipelines: chunk the markdown and generate embeddings
1185
- }
1186
- \`\`\`
1187
-
1188
- ### Extract Structured Data (with Zod schema)
1189
- \`\`\`typescript
1190
- import { z } from "zod";
1191
- import { firecrawl } from "@/lib/firecrawl";
1192
-
1193
- const ProductSchema = z.object({
1194
- name: z.string(),
1195
- price: z.number(),
1196
- description: z.string(),
1197
- inStock: z.boolean(),
1198
- });
1199
-
1200
- const result = await firecrawl.extract(
1201
- ["https://shop.example.com/products/*"],
1202
- {
1203
- schema: ProductSchema,
1204
- prompt: "Extract product details from each product page",
1205
- }
1206
- );
1207
- // result.data is typed as { name: string, price: number, ... }
1208
- \`\`\`
1209
-
1210
- ### Map a Website (discover URLs before crawling)
1211
- \`\`\`typescript
1212
- import { firecrawl } from "@/lib/firecrawl";
1213
-
1214
- // Discover all URLs on a site (1 credit flat, regardless of URL count)
1215
- const map = await firecrawl.mapUrl("https://docs.example.com", {
1216
- search: "API reference",
1217
- limit: 200,
1218
- });
1219
- // map.links: Array<{ url: string, title?: string }>
1220
-
1221
- // Then selectively scrape only relevant URLs
1222
- const relevantUrls = map.links.slice(0, 20).map((l: { url: string }) => l.url);
1223
- \`\`\`
1224
-
1225
- ### RAG Pipeline Pattern (Scrape -> Chunk -> Embed)
1226
- Firecrawl's markdown output is already chunking-friendly. Headings are natural semantic boundaries.
1227
- \`\`\`typescript
1228
- function chunkMarkdown(markdown: string, maxSize = 1500): string[] {
1229
- const sections = markdown.split(/\\n(?=#{1,3} )/);
1230
- const chunks: string[] = [];
1231
-
1232
- for (const section of sections) {
1233
- if (section.length <= maxSize) {
1234
- chunks.push(section.trim());
1235
- } else {
1236
- const paragraphs = section.split(/\\n\\n+/);
1237
- let current = "";
1238
- for (const para of paragraphs) {
1239
- if (current.length + para.length > maxSize) {
1240
- if (current) chunks.push(current.trim());
1241
- current = para;
1242
- } else {
1243
- current += "\\n\\n" + para;
1244
- }
1245
- }
1246
- if (current) chunks.push(current.trim());
1247
- }
1248
- }
1249
-
1250
- return chunks.filter((c) => c.length > 50);
1251
- }
1252
-
1253
- // Usage: scrape a URL and prepare chunks for embedding
1254
- const result = await firecrawl.scrapeUrl("https://docs.example.com/guide", {
1255
- formats: ["markdown"],
1256
- onlyMainContent: true,
1257
- });
1258
- const chunks = chunkMarkdown(result.markdown ?? "");
1259
- // Now embed each chunk with OpenAI embeddings and store in your vector DB
1260
- \`\`\`
1261
-
1262
- ### Common Pitfalls
1263
- 1. **Always use asyncCrawlUrl() for multi-page crawls, never crawlUrl().** The synchronous version polls until completion and will hold your Worker request open unnecessarily.
1264
- 2. **Map first, crawl selectively.** mapUrl() costs 1 credit flat. Do not waste credits crawling irrelevant pages.
1265
- 3. **onlyMainContent: true is the default.** It strips nav, footer, and ads. Set to false only if you need the full page structure.
1266
- 4. **Crawl results expire in 24 hours.** Store them in your database before the expiry.
1267
- 5. **Extract mode costs 5 credits per page** (1 scrape + 4 for JSON extraction). Use sparingly.
1268
- 6. **Markdown is the right format for AI apps.** It strips boilerplate, preserves structure, and is what LLMs expect.
1269
- 7. **Free tier: 500 credits (one-time).** Hobby plan is $16/month for 3,000 credits.
1270
- 8. **Network I/O does NOT count as CPU time.** Single scrapes (1-5s of network wait) are safe on Workers.
1271
- 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)
1272
-
1273
- 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.
1274
-
1275
- ### File Structure
1276
- \`\`\`
1277
- lib/replicate.ts \u2014 Replicate client singleton + model helpers
1278
- app/api/generate-image/route.ts \u2014 Image generation endpoint
1279
- app/api/generate-video/route.ts \u2014 Video generation endpoint (async with polling)
1280
- components/image-generator.tsx \u2014 Image generation UI
1281
- \`\`\`
1282
-
1283
- ### Client Setup (lib/replicate.ts)
1284
- \`\`\`typescript
1285
- import Replicate from "replicate";
1286
-
1287
- export const replicate = new Replicate({
1288
- auth: process.env.REPLICATE_API_TOKEN,
1289
- });
1290
-
1291
- // Recommended models \u2014 change these to use different models
1292
- export const MODELS = {
1293
- imageFast: "black-forest-labs/flux-schnell" as const,
1294
- imageQuality: "black-forest-labs/flux-1.1-pro" as const,
1295
- videoFast: "wan-video/wan-2.2-i2v-fast" as const,
1296
- } as const;
1297
- \`\`\`
1298
-
1299
- ### Image Generation Route (app/api/generate-image/route.ts)
1300
- Image generation typically completes in 2-10 seconds. Safe for Workers.
1301
- \`\`\`typescript
1302
- import { replicate, MODELS } from "@/lib/replicate";
1303
- import { NextRequest, NextResponse } from "next/server";
1304
-
1305
- export async function POST(req: NextRequest) {
1306
- const { prompt, model } = await req.json();
1307
-
1308
- if (!prompt || typeof prompt !== "string") {
1309
- return NextResponse.json({ error: "Prompt is required" }, { status: 400 });
1310
- }
1311
-
1312
- const output = await replicate.run(model || MODELS.imageQuality, {
1313
- input: {
1314
- prompt,
1315
- aspect_ratio: "1:1",
1316
- output_format: "webp",
1317
- },
1318
- });
1319
-
1320
- // Output shape depends on the model. Most image models return a URL string or array of URLs.
1321
- const imageUrl = Array.isArray(output) ? output[0] : output;
1322
-
1323
- return NextResponse.json({ imageUrl });
1324
- }
1325
- \`\`\`
1326
-
1327
- ### Video Generation Route (app/api/generate-video/route.ts)
1328
- Video generation takes 30s-5min. Use async predictions with polling.
1329
- \`\`\`typescript
1330
- import { replicate, MODELS } from "@/lib/replicate";
1331
- import { NextRequest, NextResponse } from "next/server";
1332
-
1333
- // Start video generation (returns immediately with prediction ID)
1334
- export async function POST(req: NextRequest) {
1335
- const { prompt, imageUrl } = await req.json();
1336
-
1337
- const prediction = await replicate.predictions.create({
1338
- model: MODELS.videoFast,
1339
- input: {
1340
- prompt,
1341
- ...(imageUrl ? { image: imageUrl } : {}),
1342
- },
1343
- });
1344
-
1345
- return NextResponse.json({
1346
- predictionId: prediction.id,
1347
- status: prediction.status,
1348
- });
1349
- }
1350
-
1351
- // Poll for completion (call from client on interval)
1352
- export async function GET(req: NextRequest) {
1353
- const id = req.nextUrl.searchParams.get("id");
1354
- if (!id) return NextResponse.json({ error: "Missing prediction ID" }, { status: 400 });
1355
-
1356
- const prediction = await replicate.predictions.get(id);
1357
-
1358
- return NextResponse.json({
1359
- status: prediction.status,
1360
- output: prediction.output,
1361
- error: prediction.error,
1362
- });
1363
- }
1364
- \`\`\`
1365
-
1366
- ### Image Generator Component (components/image-generator.tsx)
1367
- \`\`\`tsx
1368
- "use client";
1369
- import { useState } from "react";
1370
-
1371
- export function ImageGenerator() {
1372
- const [prompt, setPrompt] = useState("");
1373
- const [imageUrl, setImageUrl] = useState<string | null>(null);
1374
- const [loading, setLoading] = useState(false);
1375
-
1376
- async function handleGenerate() {
1377
- if (!prompt.trim() || loading) return;
1378
- setLoading(true);
1379
- setImageUrl(null);
1380
-
1381
- try {
1382
- const res = await fetch("/api/generate-image", {
1383
- method: "POST",
1384
- headers: { "Content-Type": "application/json" },
1385
- body: JSON.stringify({ prompt }),
1386
- });
1387
- const { imageUrl: url } = await res.json();
1388
- setImageUrl(url);
1389
- } finally {
1390
- setLoading(false);
1391
- }
1392
- }
1393
-
1394
- return (
1395
- <div className="space-y-4">
1396
- <div className="flex gap-2">
1397
- <input
1398
- value={prompt}
1399
- onChange={(e) => setPrompt(e.target.value)}
1400
- onKeyDown={(e) => e.key === "Enter" && handleGenerate()}
1401
- placeholder="Describe the image you want..."
1402
- className="flex-1 rounded-md border px-3 py-2"
1403
- />
1404
- <button
1405
- onClick={handleGenerate}
1406
- disabled={loading || !prompt.trim()}
1407
- className="rounded-md bg-primary px-4 py-2 text-primary-foreground disabled:opacity-50"
1408
- >
1409
- {loading ? "Generating..." : "Generate"}
1410
- </button>
1411
- </div>
1412
- {imageUrl && (
1413
- <img src={imageUrl} alt={prompt} className="max-w-md rounded-lg shadow-md" />
1414
- )}
1415
- </div>
1416
- );
1417
- }
1418
- \`\`\`
1419
-
1420
- ### Popular Models Reference
1421
- **Image generation:**
1422
- - \`black-forest-labs/flux-schnell\` \u2014 fast, good quality, cheapest
1423
- - \`black-forest-labs/flux-1.1-pro\` \u2014 best quality, slower
1424
- - \`black-forest-labs/flux-2-pro\` \u2014 highest fidelity, product photography, character consistency
1425
- - \`google/nano-banana-pro\` \u2014 Google's model, strong prompt following, multilingual text rendering
1426
-
1427
- **Video generation:**
1428
- - \`wan-video/wan-2.2-i2v-fast\` \u2014 image-to-video, fast and affordable (10M+ runs)
1429
- - \`google/veo-3.1-fast\` \u2014 high fidelity with audio
1430
- - \`kwaivgi/kling-v3-video\` \u2014 multi-shot storytelling with native audio
1431
-
1432
- ### Common Pitfalls
1433
- 1. **Never expose REPLICATE_API_TOKEN to the client.** All generation calls go through your API routes.
1434
- 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.
1435
- 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.
1436
- 4. **Model IDs use owner/name format.** E.g. \`black-forest-labs/flux-schnell\`, not just \`flux-schnell\`.
1437
- 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.
1438
- 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.
1439
- 7. **Network I/O does NOT count as CPU time on Workers.** Image generation wait time is all network I/O.
1440
- 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 je(a){return a.toLowerCase().replace(/[^a-z0-9]+/g,"-").replace(/^-|-$/g,"")}function ie(a){let e=b.find(r=>r.id===a);if(e)return e;let t=a.toLowerCase().replace(/[^a-z0-9]/g,"");return b.find(r=>{let s=r.name.toLowerCase().replace(/[^a-z0-9]/g,"");return s===t||s.includes(t)||t.includes(s)})}function le(a){return q[a]}function $(a){return a?b.filter(e=>e.category.toLowerCase()===a.toLowerCase()):b}function ce(a){let e=a??b,t={};for(let s of e){t[s.category]||(t[s.category]=[]);let l=q[s.id],o=l?` \u2014 ${l.description}`:"",n=l?.packages.length?` (${l.packages.join(", ")})`:"";t[s.category].push(`${s.id} \u2014 "${s.name}"${o}${n}`)}let r=[];for(let[s,l]of Object.entries(t))r.push(`**${s}**:
1441
- ${l.map(o=>` - ${o}`).join(`
1442
- `)}`);return r.join(`
1443
-
1444
- `)}function pe(a){return(a?$(a):b).map(t=>{let r=q[t.id];return{id:t.id,slug:je(t.name),name:t.name,category:t.category,description:r?.description??"",tags:r?.tags??[],envVars:r?.envVars??[],docsUrl:r?.docsUrl??"",packages:r?.packages??[],difficulty:r?.difficulty??"medium"}})}var Ne=m.object({action:m.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:m.string().optional().describe("Path to the project directory (default: cwd)"),completedStep:m.number().optional().describe("(update) Mark a plan step as completed by step number"),addEnvVar:m.object({key:m.string(),description:m.string().optional(),setupUrl:m.string().optional()}).optional().describe("(update) Add a required env var to the project manifest"),templateDescription:m.string().optional().describe("(share) Short description of what this template builds"),category:m.string().optional().describe("(landing-designs) Filter by category"),presetId:m.string().optional().describe("(landing-designs) Get full details for a specific landing design by ID"),integrationId:m.string().optional().describe("(integrations) Get full details for a specific integration preset by ID (e.g. 'stripe-payments', 'resend-email', 'elevenlabs-voice')"),period:m.string().optional().describe("(errors) Time period for errors: '1h', '24h', '7d' (default: '7d')"),deploymentId:m.string().optional().describe("(logs) Deployment ID to fetch logs for. If omitted, fetches logs for the latest deployment.")}),de={name:"mist_project",description:"Read or update Mistflow project state. 'get' loads plan progress, env vars, and deploy info. 'update' marks plan steps complete or adds env vars (note: mist_build implement auto-marks the previous step, so manual updates are rarely needed). 'share' makes the project a forkable template with a shareable URL. 'landing-designs' lists curated landing page hero designs \u2014 pass an ID to mist_plan's landingDesign field to apply it. 'integrations' lists third-party service integration blueprints (Stripe, Resend, ElevenLabs, OpenAI, Twilio, etc.) \u2014 these are auto-injected during implementation when the plan includes matching integration steps. 'errors' fetches runtime errors from the live deployed app (same data shown on the dashboard). 'logs' fetches deploy logs for a deployment (phase-by-phase progress with error details). 'deployments' lists deployment history with status and error messages.",inputSchema:Ne,handler:async a=>{let e=a;if(["share","errors","logs","deployments"].includes(e.action)&&!H())return i("You need to sign in first. Run mist_setup to connect your account.",!0);switch(e.action){case"get":case"update":return oe.handler({action:e.action,projectPath:e.projectPath,completedStep:e.completedStep,addEnvVar:e.addEnvVar});case"share":{let r=_(e.projectPath??process.cwd()),s=E(r,"mistflow.json");if(!C(s))return y(r);let l;try{l=JSON.parse(T(s,"utf-8"))}catch{return i("Could not read mistflow.json.",!0)}let o=l.projectId;if(!o)return i("No project ID found. Deploy the project first to register it.",!0);try{let n=await z(o,{isTemplate:!0,description:e.templateDescription});return i(JSON.stringify({shareUrl:n.share_url,shareToken:n.share_token,message:`Your project is now a shareable template!
1445
-
1446
- Anyone can fork it: ${n.share_url}
1447
-
1448
- Others can use it in their AI editor:
1449
- "build me something like ${n.share_url}"`}))}catch(n){let c=n instanceof Error?n.message:"Failed to share project";return i(c,!0)}}case"landing-designs":{if(e.presetId){let o=se(e.presetId);if(!o)return i(`Preset '${e.presetId}' not found. Use mist_project action='presets' without presetId to list all available presets.`,!0);let n=re(e.presetId);return i(JSON.stringify({preset:{id:o.id,title:o.title,category:o.category,description:n?.description??"",style:n?.style??"",theme:n?.theme??"dark",colors:n?.colors??[],tags:n?.tags??[],promptLength:o.prompt.length},message:`Landing design "${o.title}" (${o.category}) \u2014 ${n?.description??""}. To use it, pass landingDesign="${o.id}" when calling mist_plan.`}))}let r=ae(e.category??void 0),s=D(e.category),l=ne(s);return i(JSON.stringify({count:r.length,presets:r.map(o=>({id:o.id,title:o.title,category:o.category,description:o.description,style:o.style,theme:o.theme,colors:o.colors})),formatted:l,message:`${r.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 ${W()}/designs?tab=landing-designs.`}))}case"integrations":{if(e.integrationId){let o=ie(e.integrationId);if(!o)return i(`Integration '${e.integrationId}' not found. Use mist_project action='integrations' without integrationId to list all available integrations.`,!0);let n=le(o.id);return i(JSON.stringify({integration:{id:o.id,name:o.name,category:o.category,description:n?.description??"",packages:n?.packages??[],envVars:n?.envVars??[],docsUrl:n?.docsUrl??"",difficulty:n?.difficulty??"medium"},message:`Integration "${o.name}" (${o.category}) \u2014 ${n?.description??""}. This blueprint is auto-injected during implementation when your plan has a matching integration step. Required env vars: ${n?.envVars?.map(c=>c.key).join(", ")||"none"}. Docs: ${n?.docsUrl??"n/a"}.`}))}let r=pe(e.category??void 0),s=$(e.category??void 0),l=ce(s);return i(JSON.stringify({count:r.length,integrations:r.map(o=>({id:o.id,name:o.name,category:o.category,description:o.description,packages:o.packages,difficulty:o.difficulty,envVars:o.envVars.map(n=>n.key)})),formatted:l,message:`${r.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 r=_(e.projectPath??process.cwd()),s=E(r,"mistflow.json");if(!C(s))return y(r);let l;try{l=JSON.parse(T(s,"utf-8"))}catch{return i("Could not read mistflow.json.",!0)}let o=l.projectId;if(!o)return i("No project ID found. Deploy the project first.",!0);try{let n=await Y(o,e.period??"7d");return n.total===0?i(JSON.stringify({total:0,period:n.period,message:`No runtime errors in the last ${n.period}. The app is running clean.`})):i(JSON.stringify({total:n.total,period:n.period,errors:n.errors,message:`${n.total} runtime error(s) in the last ${n.period}. Review the errors above and use mist_build debug to investigate.`}))}catch(n){let c=n instanceof Error?n.message:"Failed to fetch errors";return i(c,!0)}}case"logs":{let r=_(e.projectPath??process.cwd()),s=E(r,"mistflow.json");if(!C(s))return y(r);let l;try{l=JSON.parse(T(s,"utf-8"))}catch{return i("Could not read mistflow.json.",!0)}let o=l.projectId;if(!o)return i("No project ID found. Deploy the project first.",!0);let n=e.deploymentId;if(!n)try{let c=await O(o);if(c.length===0)return i("No deployments found for this project.",!0);n=c[0].id}catch(c){let p=c instanceof Error?c.message:"Failed to fetch deployments";return i(p,!0)}try{let[c,p]=await Promise.all([G(n),K(n)]),g=c.filter(u=>u.level==="error"),h=c.filter(u=>u.level==="warn");return i(JSON.stringify({deploymentId:n,status:p.status,errorMessage:p.error??null,totalLogs:c.length,errorCount:g.length,warnCount:h.length,logs:c.map(u=>({time:u.timestamp,level:u.level,phase:u.phase,message:u.message})),message:p.status==="failed"?`Deployment failed. ${g.length} error(s) found in logs. Review the logs above to diagnose the issue.`:`Deployment status: ${p.status}. ${c.length} log entries (${g.length} errors, ${h.length} warnings).`}))}catch(c){let p=c instanceof Error?c.message:"Failed to fetch deploy logs";return i(p,!0)}}case"deployments":{let r=_(e.projectPath??process.cwd()),s=E(r,"mistflow.json");if(!C(s))return y(r);let l;try{l=JSON.parse(T(s,"utf-8"))}catch{return i("Could not read mistflow.json.",!0)}let o=l.projectId;if(!o)return i("No project ID found. Deploy the project first.",!0);try{let n=await O(o);return i(JSON.stringify({total:n.length,deployments:n.map(c=>({id:c.id,status:c.status,errorMessage:c.error_message,durationSeconds:c.duration_seconds,isRollback:!!c.rollback_from_id,createdAt:c.created_at})),message:`${n.length} deployment(s) found. Use mist_project action='logs' deploymentId='<id>' to see detailed logs for any deployment.`}))}catch(n){let c=n instanceof Error?n.message:"Failed to fetch deployments";return i(c,!0)}}case"version":{A().backendSignalReceived||await B();let r=A(),s=r.severity==="none",l={none:"up to date",patch:"patch update available",minor:"minor update available",major:"major update available",unsupported:"UNSUPPORTED \u2014 upgrade required"};return i(JSON.stringify({current:r.current,latest:r.latest||"unknown",minSupported:r.minSupported||"unknown",severity:r.severity,upToDate:s,upgradeCmd:r.upgradeCmd,changelogUrl:r.changelogUrl,backendSignalReceived:r.backendSignalReceived,message:r.backendSignalReceived?`Mistflow MCP ${r.current} (${l[r.severity]??r.severity}). Latest: ${r.latest}.${s?"":` Run \`${r.upgradeCmd}\` and restart your editor to upgrade.`}`:`Mistflow MCP ${r.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 i(`Unknown action: ${e.action}. Use get, update, share, landing-designs, integrations, errors, logs, deployments, or version.`,!0)}}};import{z as w}from"zod";var Ae=w.object({action:w.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:w.string().optional().describe("URL to navigate to. Required for 'navigate'; optional for 'screenshot' (navigates before capturing)."),selector:w.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:w.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:w.boolean().default(!1).describe("For 'screenshot': capture the full scrollable page instead of just the viewport."),includeScreenshot:w.boolean().default(!1).describe("For navigate/interact actions: also return a screenshot alongside the accessibility snapshot.")}),ue={name:"mist_browser",description:"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.",inputSchema:Ae,handler:async a=>{let e=a,t=await V();if(e.action==="navigate"){if(!e.url)return i("URL is required for 'navigate'.",!0);let l=[],o=p=>{p.type()==="error"&&l.push(p.text())};t.on("console",o),await t.goto(e.url,{waitUntil:"domcontentloaded",timeout:3e4}),await t.waitForLoadState("networkidle").catch(()=>{});let n=[],c=p=>n.push(p.message);if(t.on("pageerror",c),await t.waitForTimeout(500),t.removeListener("console",o),t.removeListener("pageerror",c),l.length>0||n.length>0){let p=await x(t),g=[{type:"text",text:JSON.stringify({url:t.url(),title:await t.title(),snapshot:p,consoleErrors:l,pageErrors:n,hasErrors:!0})}];if(e.includeScreenshot){let h=await I(t);g.push({type:"image",data:h.toString("base64"),mimeType:"image/png"})}return{content:g}}}else if(e.action==="go_back")await t.goBack({waitUntil:"domcontentloaded",timeout:1e4});else if(e.action==="go_forward")await t.goForward({waitUntil:"domcontentloaded",timeout:1e4});else if(e.action==="click"){if(!e.selector)return i("Selector is required for 'click'.",!0);await t.click(e.selector,{timeout:1e4}),await t.waitForLoadState("domcontentloaded").catch(()=>{}),await t.waitForTimeout(500)}else if(e.action==="type"){if(!e.selector)return i("Selector is required for 'type'.",!0);if(!e.value)return i("Value is required for 'type'.",!0);await t.type(e.selector,e.value,{delay:50})}else if(e.action==="fill"){if(!e.selector)return i("Selector is required for 'fill'.",!0);if(!e.value)return i("Value is required for 'fill'.",!0);await t.fill(e.selector,e.value)}else if(e.action==="select_option"){if(!e.selector)return i("Selector is required for 'select_option'.",!0);if(!e.value)return i("Value is required for 'select_option'.",!0);await t.selectOption(e.selector,e.value)}else if(e.action==="hover"){if(!e.selector)return i("Selector is required for 'hover'.",!0);await t.hover(e.selector,{timeout:1e4})}else if(e.action==="press_key"){if(!e.value)return i("Value is required for 'press_key' (e.g. 'Enter').",!0);await t.keyboard.press(e.value),await t.waitForLoadState("domcontentloaded").catch(()=>{}),await t.waitForTimeout(500)}else if(e.action==="screenshot"){e.url&&(await t.goto(e.url,{waitUntil:"domcontentloaded",timeout:3e4}),await t.waitForLoadState("networkidle").catch(()=>{}));let l;if(e.selector){let o=await t.$(e.selector);if(!o)return i(`Element not found: ${e.selector}`,!0);l=await o.screenshot({type:"png"})}else l=await I(t,e.fullPage);return{content:[{type:"text",text:JSON.stringify({url:t.url(),title:await t.title(),message:`Screenshot captured (${e.fullPage?"full page":"viewport"})`})},{type:"image",data:l.toString("base64"),mimeType:"image/png"}]}}else if(e.action==="snapshot"){let l=await x(t);return{content:[{type:"text",text:JSON.stringify({url:t.url(),title:await t.title(),snapshot:l})}]}}let r=await x(t),s=[{type:"text",text:JSON.stringify({url:t.url(),title:await t.title(),snapshot:r})}];if(e.includeScreenshot){let l=await I(t);s.push({type:"image",data:l.toString("base64"),mimeType:"image/png"})}return{content:s}}};import{z as me}from"zod";var ge=`# Mistflow CLI reference
6
+ 1. Run mist plan --describe "<your app idea>"
7
+ 2. Run mist init --plan-id <planId> --path <absolute-path>
8
+ 3. Run mist install <absolute-path>, then mist implement <absolute-path>
9
+
10
+ If you want to deploy an existing project, use your framework's deploy tools directly.`,!0)}import{z as x}from"zod";import{platform as W}from"os";import{execFile as M}from"child_process";var K=x.object({apiKey:x.string().optional().describe("API key (mist_...) for headless auth. Skips the device code flow entirely. Generate one at app.mistflow.ai/mcp-keys."),deviceCode:x.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 B(n){return"error"in n}function L(n){return new Promise(e=>setTimeout(e,n))}function H(n){return new Promise(e=>{let t=W();t==="win32"?M("cmd.exe",["/c","start","",n],a=>{a&&console.error("Could not open browser:",a.message),e(!a)}):M(t==="darwin"?"open":"xdg-open",[n],o=>{o&&console.error("Could not open browser:",o.message),e(!o)}),setTimeout(()=>e(!1),5e3)})}var G={fetch:globalThis.fetch,openBrowser:H,sleep:L};async function q(n,e,t,a){let o=t,l=a.sleep??L;for(let i=0;i<e;i++){await l(o);let s;try{let p=await a.fetch(`${_()}/auth/poll`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({device_code:n})});if(!p.ok)continue;s=await p.json()}catch{continue}if(B(s))switch(s.error){case"authorization_pending":continue;case"slow_down":o+=5e3;continue;case"expired_token":return r("The sign-in link expired. Run mist_setup again to get a new code.",!0);case"access_denied":return r("Sign-in was cancelled. Run mist_setup again to try again.",!0);case"already_exchanged":return r("This sign-in link was already used. Run mist_setup again to get a new code.",!0)}let d=s.email||s.org_name||s.org_slug;return C({apiKey:s.api_key,apiKeyId:s.api_key_id,apiKeyName:s.api_key_name,orgId:s.org_id,orgSlug:s.org_slug,email:s.email}),r(`Connected to Mistflow as ${d}. You are ready to build and deploy.`)}return null}async function Y(n,e=G){let t=n;if(t?.apiKey)try{let i=await e.fetch(`${_()}/api/org`,{headers:{Authorization:`ApiKey ${t.apiKey}`}});if(!i.ok)return r("Invalid API key. Check the key and try again.",!0);let s=await i.json();return C({apiKey:t.apiKey,orgId:s.id,orgSlug:s.slug}),r(`Connected to Mistflow as ${s.slug} via API key. You are ready to build and deploy.`)}catch{return r("Cannot reach Mistflow servers. Check your internet connection.",!0)}if(t?.deviceCode){let i=await q(t.deviceCode,6,5e3,e);return i||r(JSON.stringify({status:"pending",deviceCode:t.deviceCode,instruction:"The user hasn't approved yet. Wait ~15 seconds and call mist_setup again with the same deviceCode."}))}let a;try{let i=await e.fetch(`${_()}/auth/device`,{method:"POST",headers:{"Content-Type":"application/json"}});if(!i.ok)return r("Cannot reach Mistflow servers. Check your internet connection.",!0);a=await i.json()}catch{return r("Cannot reach Mistflow servers. Check your internet connection.",!0)}let o=`${a.verification_uri}?code=${a.user_code}`;console.error(`
11
+ Sign in at: ${o}
12
+ Your code: ${a.user_code}
13
+ `);try{await e.openBrowser(o)}catch{}let l=await q(a.device_code,6,5e3,e);return l||r(JSON.stringify({status:"pending",deviceCode:a.device_code,signInUrl:o,userCode:a.user_code,instruction:"The user hasn't approved yet. Wait ~15 seconds, then call mist_setup again with deviceCode='"+a.device_code+"' to check if they approved."}))}var E={name:"mist_setup",description:"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).",inputSchema:K,handler:n=>Y(n)};import{z as m}from"zod";import{z as u}from"zod";import{resolve as Z,join as U}from"path";import{existsSync as ee,readFileSync as A,writeFileSync as te}from"fs";import{existsSync as Q,readFileSync as X}from"fs";function O(n){let e=new Set;if(!Q(n))return e;let t=X(n,"utf-8");for(let a of t.split(`
14
+ `)){let o=a.trim();if(!o||o.startsWith("#"))continue;let l=o.indexOf("=");if(l>0){let i=o.slice(0,l).trim(),s=o.slice(l+1).trim();s&&s!=='""'&&s!=="''"&&e.add(i)}}return e}var oe=u.object({action:u.enum(["get","update"]).default("get").describe("'get' reads current project state. 'update' modifies it."),projectPath:u.string().optional().describe("Path to the project directory (default: current working directory)"),completedStep:u.number().optional().describe("(update only) Mark a plan step as completed by step number"),addEnvVar:u.object({key:u.string(),description:u.string().optional(),setupUrl:u.string().optional()}).optional().describe("(update only) Add a required env var to the project manifest")}),N={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:oe,handler:async n=>{let e=n,t=Z(e.projectPath??process.cwd()),a=U(t,"mistflow.json");if(!ee(a))return P(t);let o;try{o=JSON.parse(A(a,"utf-8"))}catch{return r("Failed to parse mistflow.json.",!0)}if(e.action==="get"){if(!o.projectId)try{let{ensureBackendRegistered:c}=await import("./self-heal-KDCW562K.js");await c(t)&&(o=JSON.parse(A(a,"utf-8")))}catch{}let s=o.plan,d=s?.steps?.filter(c=>c.status==="completed").length??0,p=s?.steps?.length??0,g=O(U(t,".env.local")),y=o.env?.required?Object.entries(o.env.required).map(([c,b])=>({name:c,description:b?.description,configured:g.has(c)})):[];o.projectId&&import("./state-manager-NJPMKZCE.js").then(({fetchRemoteState:c})=>c(o.projectId)).catch(()=>{});let f=[`Project: ${o.name}`];if(s){f.push(`Plan: ${s.summary??s.name??"unnamed"} \u2014 ${d}/${p} steps complete`);for(let c of s.steps){let b=c.status==="completed"?"\u2713":c.status==="in_progress"?"\u2192":" ";f.push(` [${b}] ${c.number}. ${c.name}`)}}let w=y.filter(c=>!c.configured);w.length>0&&f.push(`Missing env vars: ${w.map(c=>c.name).join(", ")}`),o.deploy?.url?f.push(`Deployed: ${o.deploy.url} (${o.deploy.count??0} deploys)`):f.push("Not deployed yet");let v=[],T=s?.steps?.find(c=>c.status!=="completed");return T?v.push(`NEXT: Run mist implement <absolute-path> to work on step ${T.number} (${T.name}).`):s&&d===p&&(o.deploy?.url||v.push("NEXT: All steps complete! Run mist deploy <absolute-path> to deploy the app now. Do NOT ask the user \u2014 just deploy.")),w.length>0&&v.push(`Missing env vars in .env.local: ${w.map(c=>c.name).join(", ")}`),r(JSON.stringify({name:o.name,projectId:o.projectId,planProgress:s?{name:s.name,summary:s.summary,totalSteps:p,completedSteps:d,steps:s.steps}:null,envStatus:y,deploy:o.deploy??null,contextMessage:f.join(`
15
+ `),nextSteps:v}))}let l=[];if(e.completedStep!==void 0){let s=o.plan;if(s?.steps){let d=s.steps.findIndex(p=>p.number===e.completedStep);if(d===-1)return r(`Step ${e.completedStep} not found in the plan.`,!0);s.steps[d].status="completed",l.push(`Step ${e.completedStep} marked as completed`)}}e.addEnvVar&&(o.env||(o.env={required:{}}),o.env.required||(o.env.required={}),o.env.required[e.addEnvVar.key]={description:e.addEnvVar.description,setupUrl:e.addEnvVar.setupUrl},l.push(`Added required env var: ${e.addEnvVar.key}`)),te(a,JSON.stringify(o,null,2)+`
16
+ `),o.projectId&&import("./state-manager-NJPMKZCE.js").then(async({readLocalState:s,syncRemoteState:d})=>{let p=s(t);p&&await d(o.projectId,p)}).catch(()=>{});let i=[];if(e.completedStep!==void 0){let d=o.plan?.steps?.find(p=>p.status!=="completed");d?i.push(`NEXT: Run mist implement <absolute-path> to work on step ${d.number} (${d.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 e.addEnvVar&&(i.push(`Add ${e.addEnvVar.key} to your .env.local file`),e.addEnvVar.setupUrl&&i.push(`Get the value from: ${e.addEnvVar.setupUrl}`)),r(JSON.stringify({updated:!0,changes:l,message:l.length>0?`Project state saved. ${l.join(". ")}.`:"No changes made.",nextSteps:i.length>0?i:void 0}))}};var se=m.object({action:m.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:m.string().optional().describe("Path to the project directory (default: cwd)"),completedStep:m.number().optional().describe("(update) Mark a plan step as completed by step number"),addEnvVar:m.object({key:m.string(),description:m.string().optional(),setupUrl:m.string().optional()}).optional().describe("(update) Add a required env var to the project manifest")}),$={name:"mist_project",description:"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).",inputSchema:se,handler:async n=>{let e=n;return N.handler({action:e.action,projectPath:e.projectPath,completedStep:e.completedStep,addEnvVar:e.addEnvVar})}};import{z as h}from"zod";var ne=h.object({action:h.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:h.string().optional().describe("URL to navigate to. Required for 'navigate'; optional for 'screenshot' (navigates before capturing)."),selector:h.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:h.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:h.boolean().default(!1).describe("For 'screenshot': capture the full scrollable page instead of just the viewport."),includeScreenshot:h.boolean().default(!1).describe("For navigate/interact actions: also return a screenshot alongside the accessibility snapshot.")}),D={name:"mist_browser",description:"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.",inputSchema:ne,handler:async n=>{let e=n,t=await I();if(e.action==="navigate"){if(!e.url)return r("URL is required for 'navigate'.",!0);let l=[],i=p=>{p.type()==="error"&&l.push(p.text())};t.on("console",i),await t.goto(e.url,{waitUntil:"domcontentloaded",timeout:3e4}),await t.waitForLoadState("networkidle").catch(()=>{});let s=[],d=p=>s.push(p.message);if(t.on("pageerror",d),await t.waitForTimeout(500),t.removeListener("console",i),t.removeListener("pageerror",d),l.length>0||s.length>0){let p=await j(t),g=[{type:"text",text:JSON.stringify({url:t.url(),title:await t.title(),snapshot:p,consoleErrors:l,pageErrors:s,hasErrors:!0})}];if(e.includeScreenshot){let y=await k(t);g.push({type:"image",data:y.toString("base64"),mimeType:"image/png"})}return{content:g}}}else if(e.action==="go_back")await t.goBack({waitUntil:"domcontentloaded",timeout:1e4});else if(e.action==="go_forward")await t.goForward({waitUntil:"domcontentloaded",timeout:1e4});else if(e.action==="click"){if(!e.selector)return r("Selector is required for 'click'.",!0);await t.click(e.selector,{timeout:1e4}),await t.waitForLoadState("domcontentloaded").catch(()=>{}),await t.waitForTimeout(500)}else if(e.action==="type"){if(!e.selector)return r("Selector is required for 'type'.",!0);if(!e.value)return r("Value is required for 'type'.",!0);await t.type(e.selector,e.value,{delay:50})}else if(e.action==="fill"){if(!e.selector)return r("Selector is required for 'fill'.",!0);if(!e.value)return r("Value is required for 'fill'.",!0);await t.fill(e.selector,e.value)}else if(e.action==="select_option"){if(!e.selector)return r("Selector is required for 'select_option'.",!0);if(!e.value)return r("Value is required for 'select_option'.",!0);await t.selectOption(e.selector,e.value)}else if(e.action==="hover"){if(!e.selector)return r("Selector is required for 'hover'.",!0);await t.hover(e.selector,{timeout:1e4})}else if(e.action==="press_key"){if(!e.value)return r("Value is required for 'press_key' (e.g. 'Enter').",!0);await t.keyboard.press(e.value),await t.waitForLoadState("domcontentloaded").catch(()=>{}),await t.waitForTimeout(500)}else if(e.action==="screenshot"){e.url&&(await t.goto(e.url,{waitUntil:"domcontentloaded",timeout:3e4}),await t.waitForLoadState("networkidle").catch(()=>{}));let l;if(e.selector){let i=await t.$(e.selector);if(!i)return r(`Element not found: ${e.selector}`,!0);l=await i.screenshot({type:"png"})}else l=await k(t,e.fullPage);return{content:[{type:"text",text:JSON.stringify({url:t.url(),title:await t.title(),message:`Screenshot captured (${e.fullPage?"full page":"viewport"})`})},{type:"image",data:l.toString("base64"),mimeType:"image/png"}]}}else if(e.action==="snapshot"){let l=await j(t);return{content:[{type:"text",text:JSON.stringify({url:t.url(),title:await t.title(),snapshot:l})}]}}let a=await j(t),o=[{type:"text",text:JSON.stringify({url:t.url(),title:await t.title(),snapshot:a})}];if(e.includeScreenshot){let l=await k(t);o.push({type:"image",data:l.toString("base64"),mimeType:"image/png"})}return{content:o}}};import{z as F}from"zod";var V=`# Mistflow CLI reference
1450
17
 
1451
18
  The Mistflow CLI handles local execution and long-running operations that
1452
19
  would hit the MCP 60s tool-call ceiling. Every command below is invokable
@@ -1469,9 +36,8 @@ Start a new plan. Returns \`status: "clarify"\` with questions to relay to
1469
36
  the user, OR \`status: "ready"\` / \`status: "design_clarify_pending"\` if
1470
37
  the description was specific enough for the backend to skip discovery.
1471
38
 
1472
- **Prefer this over the mist_plan MCP tool for ALL planning calls.** The
1473
- mist_plan MCP tool routinely hits -32001 timeout when the backend Sonnet
1474
- call takes >60s; this CLI has no ceiling.
39
+ The CLI caches ready plans locally and returns a \`planId\` for init /
40
+ mockup / future modifications.
1475
41
 
1476
42
  ### \`mist plan --token <conversationId> --answers-stdin [--json]\`
1477
43
  Submit the user's answers to a previous \`clarify\` round. Pipe the
@@ -1482,6 +48,12 @@ answers object on stdin:
1482
48
 
1483
49
  Returns \`status: "ready"\` or \`status: "design_clarify_pending"\`.
1484
50
 
51
+ ### \`mist plan --describe "<change request>" --existing-plan-id <planId> [--json]\`
52
+ Modify an existing saved plan for a feature addition or integration
53
+ addition. Reads the current plan from \`~/.mistflow/plans/<planId>.json\`
54
+ or, when run inside the matching project, from \`mistflow.json\`. Returns
55
+ \`status: "ready"\` with the modified plan and a diff summary.
56
+
1485
57
  ### \`mist plan --cid <designConversationId> --pick-stdin [--json]\`
1486
58
  Finalize the plan by submitting the user's picked design direction.
1487
59
 
@@ -1493,15 +65,40 @@ Poll for asynchronously generated design directions. Without \`--wait\`,
1493
65
  returns the current status once. With \`--wait\`, polls every 2s (up to 2
1494
66
  min) until ready or failed.
1495
67
 
68
+ ### \`mist init --plan-id <planId> --path <absolute-path> [--json]\`
69
+ Scaffold a fresh Mistflow app from a cached plan. Fast and fully CLI-driven.
70
+
71
+ ### \`mist mockup --plan-id <planId> [--json]\`
72
+ Generate or iterate on a grayscale HTML wireframe before scaffolding. Use
73
+ \`--feedback\` to revise and \`--approved\` to lock it in.
74
+
1496
75
  ### \`mist install <projectPath> [--json]\`
1497
76
  Run npm install in a Mistflow project. Streams output live, no timeout.
1498
- **Prefer this over the mist_build install MCP action.**
77
+
78
+ ### \`mist implement <absolute-path> [--json]\`
79
+ Return the next plan step for the host AI to implement. Call repeatedly
80
+ until all steps are complete.
81
+
82
+ ### \`mist build <absolute-path> [--json]\`
83
+ Run the local production build.
84
+
85
+ ### \`mist seed <absolute-path> [--reset] [--allow-remote] [--json]\`
86
+ Seed realistic sample rows from \`plan.dataModel[].sampleRows\` into the
87
+ project database. Defaults to local-only seeding against PGlite; pass
88
+ \`--allow-remote\` only when you intentionally want to seed a remote
89
+ environment. Use before QA when acceptance criteria depend on populated
90
+ tables or lists. This is for app/domain data, not admin/test account
91
+ bootstrap.
92
+
93
+ ### \`mist qa [live-url] [--json]\`
94
+ Run browser QA against the live deployed app. If no URL is passed, Mistflow
95
+ reads \`deploy.url\` from the current project's \`mistflow.json\`.
1499
96
 
1500
97
  ### \`mist login\`, \`mist deploy\`, \`mist status <id>\`, \`mist logs <id>\`,
1501
98
  ### \`mist projects [id]\`, \`mist env <subcommand>\`, \`mist domains <subcommand>\`,
1502
99
  ### \`mist rollback <id>\`
1503
- Shell-native equivalents of the corresponding MCP actions. Primarily for
1504
- CI/scripted use; MCP tools work too for interactive AI-driven flows.
100
+ Shell-native cloud/project coordination. Use \`mist env\` to set required
101
+ integration keys before deploying a feature that depends on them.
1505
102
 
1506
103
  ## MCP-vs-CLI rule
1507
104
 
@@ -1509,7 +106,7 @@ Stays MCP: \`mist_setup\`, \`mist_browser\`, \`mist_project\`, \`mist_help\`
1509
106
  (this tool). These return structured data the AI routes on and aren't
1510
107
  long-running.
1511
108
 
1512
- Prefers CLI: planning, install, build, qa, mockup \u2014 anything that can
109
+ Prefers CLI: planning, install, build, seed, qa, mockup \u2014 anything that can
1513
110
  exceed 30s or streams useful progress.
1514
111
 
1515
112
  ## Chaining pattern
@@ -1527,7 +124,13 @@ and decides the next command. Example end-to-end chain:
1527
124
  # AI shows picker, user picks, then:
1528
125
  echo '{...}' | mist plan --cid ... --pick-stdin --json
1529
126
  # \u2192 {"status":"ready","plan":{...}}
1530
- # Then: mist_build init (MCP), mist install (CLI), mist_build implement (MCP), etc.
1531
- `,fe={name:"mist_help",description:"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.",inputSchema:me.object({command:me.string().optional().describe("Optional: name of a specific command to get focused reference for. Omit to get the full catalog.")}),handler:async a=>{let{command:e}=a;if(!e)return i(ge);let t=ge.split(`
1532
- `),r=new RegExp(`^### \`mist ${e}`),s=-1,l=t.length;for(let o=0;o<t.length;o++)if(r.test(t[o]))s=o;else if(s>=0&&t[o].startsWith("### ")){l=o;break}else if(s>=0&&t[o].startsWith("## ")&&o>s){l=o;break}return s<0?i(`No command named '${e}' found. Call mist_help with no args to see the full catalog.`,!0):i(t.slice(s,l).join(`
1533
- `).trim())}};var j=new Ue({name:"mistflow",version:"0.3.0"},{capabilities:{tools:{}},instructions:"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.\n\nCall `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.\n\nNew app workflow (entirely CLI-driven):\n1. 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`. Present the directions picker, then 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.\n2. 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.\n3. Scaffold: run `mist init --plan-id <id> --path <absolute-path>` via your shell/bash tool. Writes the Next.js scaffold, contracts/, AGENTS.md, and registers the project with Mistflow. Returns fast; does NOT run npm install.\n4. Install dependencies: run `mist install <projectPath>` via your shell/bash tool (streams output).\n5. Implement: run `mist implement --project-path <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.\n6. 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`.\n7. QA: run `mist qa --project-path <path>` via your shell/bash tool. Call AFTER deploy. Do NOT show the URL to the user until QA passes.\n\nCompanion 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:\n- `mist plan` / `mist plan-directions` \u2014 plan an app.\n- `mist init` \u2014 scaffold a new project from a plan (fast, ~10s).\n- `mist install` / `mist build` / `mist mockup` / `mist implement` / `mist debug` / `mist qa` \u2014 local project lifecycle.\n- `mist deploy` (+ promote/preview/rollback/verify/redeploy subcommands) \u2014 deploy orchestration.\n- `mist status` / `mist fix` \u2014 feature manifest viewer + iteration loop.\n- `mist contracts` \u2014 integration-contract layer management.\n- `mist doctor` \u2014 project health diagnostics.\n- `mist login` / `mist projects` / `mist logs` / `mist env` / `mist domains` / `mist rollback` \u2014 cloud-coordination commands.\n- Call mist_help for the full reference.\n\nIMPORTANT \u2014 chaining discipline: once the user approves the plan, the init \u2192 install \u2192 implement (repeat) \u2192 build \u2192 deploy \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.\n\nDesign 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`.\n\nUpdating an existing Mistflow app:\n- Cosmetic or single-file changes: edit files directly, then `mist deploy` to publish.\n- New page or feature (no new data model): mist_project action=get for context, build it, then `mist deploy`.\n- Feature needing new data model or integration: run `mist plan --existing-plan-id <id>`, then `mist implement`, then `mist deploy`.\n- Bug fix: run `mist debug --project-path <path>` to analyze, fix the code, then `mist deploy`.\n\nTemplate 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.\n\nThe 4 MCP tools:\n- 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.\n- 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.\n- 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.\n- mist_help: returns the full CLI command reference. Call once per session to learn the available `mist` commands."}),he=[Q,de,ue,fe];j.setRequestHandler(Me,async()=>({tools:he.map(a=>({name:a.name,description:a.description,inputSchema:De(a.inputSchema)}))}));j.setRequestHandler(Le,async a=>{let e=he.find(t=>t.name===a.params.name);if(!e)return i(`Unknown tool: ${a.params.name}`,!0);try{let t=e.inputSchema.safeParse(a.params.arguments);if(!t.success){let l=t.error.issues.map(o=>`${o.path.join(".")}: ${o.message}`).join(", ");return i(`Invalid input: ${l}`,!0)}let r=a.params._meta?.progressToken,s={server:j,progressToken:r};try{return await e.handler(t.data,s)}finally{s.cleanup?.()}}catch(t){let r=t instanceof Error?t.message:"An unexpected error occurred";return console.error("Tool error:",t),i(r,!0)}});async function qe(){let a=process.argv.indexOf("--api-url");a!==-1&&process.argv[a+1]&&(process.env.MISTFLOW_API_URL=process.argv[a+1]),process.argv.includes("--local")&&!process.env.MISTFLOW_API_URL&&(process.env.MISTFLOW_API_URL="http://localhost:9100");let e=new Oe;await j.connect(e),console.error(`Mistflow MCP server running on stdio (API: ${process.env.MISTFLOW_API_URL||"https://api.mistflow.ai"})`)}qe().catch(a=>{console.error("Fatal error:",a),process.exit(1)});
127
+ mist init --plan-id ... --path /Users/you/projects/habit-tracker --json
128
+ mist install /Users/you/projects/habit-tracker --json
129
+ mist implement /Users/you/projects/habit-tracker --json
130
+ mist build /Users/you/projects/habit-tracker --json
131
+ mist deploy /Users/you/projects/habit-tracker --json
132
+ mist seed /Users/you/projects/habit-tracker --json
133
+ mist qa https://your-app.mistflow.app --json
134
+ `,J={name:"mist_help",description:"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.",inputSchema:F.object({command:F.string().optional().describe("Optional: name of a specific command to get focused reference for. Omit to get the full catalog.")}),handler:async n=>{let{command:e}=n;if(!e)return r(V);let t=V.split(`
135
+ `),a=new RegExp(`^### \`mist ${e}`),o=-1,l=t.length;for(let i=0;i<t.length;i++)if(a.test(t[i]))o=i;else if(o>=0&&t[i].startsWith("### ")){l=i;break}else if(o>=0&&t[i].startsWith("## ")&&i>o){l=i;break}return o<0?r(`No command named '${e}' found. Call mist_help with no args to see the full catalog.`,!0):r(t.slice(o,l).join(`
136
+ `).trim())}};var S=new ie({name:"mistflow",version:"0.3.0"},{capabilities:{tools:{}},instructions:"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.\n\nCall `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.\n\nNew app workflow (entirely CLI-driven):\n1. 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`. Present the directions picker, then 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.\n2. 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.\n3. Scaffold: run `mist init --plan-id <id> --path <absolute-path>` via your shell/bash tool. Writes the Next.js scaffold, contracts/, AGENTS.md, and registers the project with Mistflow. Returns fast; does NOT run npm install.\n4. Install dependencies: run `mist install <projectPath>` via your shell/bash tool (streams output).\n5. 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.\n6. 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`.\n7. 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.\n8. 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.\n\nCompanion 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:\n- `mist plan` / `mist plan-directions` \u2014 plan an app.\n- `mist init` \u2014 scaffold a new project from a plan (fast, ~10s).\n- `mist install` / `mist build` / `mist mockup` / `mist implement` / `mist debug` / `mist seed` / `mist qa` \u2014 local project lifecycle.\n- `mist deploy` (+ promote/preview/rollback/verify/redeploy subcommands) \u2014 deploy orchestration.\n- `mist status` / `mist fix` \u2014 feature manifest viewer + iteration loop.\n- `mist contracts` \u2014 integration-contract layer management.\n- `mist doctor` \u2014 project health diagnostics.\n- `mist login` / `mist projects` / `mist logs` / `mist env` / `mist domains` / `mist rollback` \u2014 cloud-coordination commands.\n- Call mist_help for the full reference.\n\nIMPORTANT \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.\n\nDesign 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`.\n\nUpdating an existing Mistflow app:\n- Cosmetic or single-file changes: edit files directly, then `mist deploy` to publish.\n- New page or feature (no new data model): mist_project action=get for context, build it, then `mist deploy`.\n- 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`.\n- 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`.\n- Bug fix: run `mist debug <absolute-path>` to analyze, fix the code, then `mist deploy`.\n\nTemplate 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.\n\nThe 4 MCP tools:\n- 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.\n- 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.\n- 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.\n- mist_help: returns the full CLI command reference. Call once per session to learn the available `mist` commands."}),z=[E,$,D,J];S.setRequestHandler(le,async()=>({tools:z.map(n=>({name:n.name,description:n.description,inputSchema:ce(n.inputSchema)}))}));S.setRequestHandler(ae,async n=>{let e=z.find(t=>t.name===n.params.name);if(!e)return r(`Unknown tool: ${n.params.name}`,!0);try{let t=e.inputSchema.safeParse(n.params.arguments);if(!t.success){let l=t.error.issues.map(i=>`${i.path.join(".")}: ${i.message}`).join(", ");return r(`Invalid input: ${l}`,!0)}let a=n.params._meta?.progressToken,o={server:S,progressToken:a};try{return await e.handler(t.data,o)}finally{o.cleanup?.()}}catch(t){let a=t instanceof Error?t.message:"An unexpected error occurred";return console.error("Tool error:",t),r(a,!0)}});async function pe(){let n=process.argv.indexOf("--api-url");n!==-1&&process.argv[n+1]&&(process.env.MISTFLOW_API_URL=process.argv[n+1]),process.argv.includes("--local")&&!process.env.MISTFLOW_API_URL&&(process.env.MISTFLOW_API_URL="http://localhost:9100");let e=new re;await S.connect(e),console.error(`Mistflow MCP server running on stdio (API: ${process.env.MISTFLOW_API_URL||"https://api.mistflow.ai"})`)}pe().catch(n=>{console.error("Fatal error:",n),process.exit(1)});