@mistflow-ai/mcp 0.6.0 → 0.7.1
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/{chunk-H3J2YAUK.js → chunk-EED3GL6G.js} +1 -1
- package/dist/chunk-RTKBAE4U.js +23 -0
- package/dist/cli.js +9 -1
- package/dist/index.js +11 -1689
- package/dist/self-heal-KDCW562K.js +1 -0
- package/dist/state-manager-NJPMKZCE.js +1 -0
- package/package.json +1 -1
- package/dist/api-client-ORACYR2I.js +0 -1
- package/dist/chunk-QZYSCQQ4.js +0 -23
- package/dist/self-heal-X2KBLZZU.js +0 -1
- package/dist/state-manager-W7N7IHSQ.js +0 -1
package/dist/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import{a as
|
|
1
|
+
import{a as I,d as _,e as k}from"./chunk-UNFTM4B3.js";import{c as x,d as T,f as j}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=x();a&&(t=n+a)}catch{}return{content:[{type:"text",text:t}],isError:e}}function R(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
|
|
|
@@ -7,1691 +7,13 @@ To get started:
|
|
|
7
7
|
2. Run mist_build (action: 'init') to create a new project in a subdirectory
|
|
8
8
|
3. Run mist_build (action: 'implement') to build each step
|
|
9
9
|
|
|
10
|
-
If you want to deploy an existing project, use your framework's deploy tools directly.`,!0)}import{z as
|
|
11
|
-
Sign in at: ${
|
|
12
|
-
Your code: ${
|
|
13
|
-
`);try{await
|
|
14
|
-
`)
|
|
15
|
-
`)}
|
|
16
|
-
`)}var
|
|
17
|
-
|
|
18
|
-
`+t}function Ue(i,t){let s=ut(i),e=t??ts(i);return['import { createSelectSchema, createInsertSchema } from "drizzle-zod";','import { z } from "zod";',`import { ${e} } from "@/db/schema";`,"","// Select schema \u2014 shape of a row read from the DB. Use this on both","// sides of the wire: backend validates responses, frontend gets types.",`export const ${s}Schema = createSelectSchema(${e});`,`export type ${s} = z.infer<typeof ${s}Schema>;`,"","// Insert schema \u2014 shape accepted when creating a new row. id +","// createdAt are generated server-side, so we strip them from the","// input contract. Adjust if your schema uses different column names.",`export const Create${s}Input = createInsertSchema(${e}).omit({`," id: true,"," createdAt: true,","});",`export type Create${s}Input = z.infer<typeof Create${s}Input>;`,""].join(`
|
|
19
|
-
`)}function De(i){return`contracts/${es(i)}.ts`}function ns(i){let t=re(ze(i)),s=10,e=0;for(;e<s&&t!==re(t);){if(F(x(t,"pnpm-workspace.yaml"))||F(x(t,"lerna.json")))return t;let r=x(t,"package.json");if(F(r))try{if(JSON.parse(fe(r,"utf-8")).workspaces)return t}catch{}t=re(t),e++}return null}function $e(i,t,s,e,r,a){return new Promise(n=>{let o=rs(i,t,{cwd:s,stdio:["pipe","pipe","pipe"],timeout:e,env:a?{...process.env,...a}:process.env}),l="",c="";o.stdout?.on("data",h=>{let w=h.toString();if(c+=w,r)for(let d of w.split(`
|
|
20
|
-
`).filter(Boolean))r(d)}),o.stderr?.on("data",h=>{let w=h.toString();if(l+=w,r)for(let d of w.split(`
|
|
21
|
-
`).filter(Boolean))r(d)}),o.on("close",h=>{if(h===0)n({success:!0});else{let w=l.split(`
|
|
22
|
-
`).find(d=>d.startsWith("npm error"))??l.slice(0,300);n({success:!1,error:w})}}),o.on("error",h=>{n({success:!1,error:h.message})})})}var vr=me.object({name:me.string().min(1),plan:me.any(),path:me.string().optional()});function y(i,t,s){let e=x(i,t);ge(re(e),{recursive:!0}),z(e,s)}function wt(i,t){if(!t||typeof t!="object")return;let s=t.acceptanceCriteria;if(!Array.isArray(s)||s.length===0)return;let e=s.map(r=>{let a=r;return{id:String(a.id??""),feature:String(a.feature??""),description:String(a.description??""),priority:a.priority??"p1",status:a.status??"planned",...a.implementedAt?{implementedAt:a.implementedAt}:{},...a.verifiedAt?{verifiedAt:a.verifiedAt}:{},...a.failedAt?{failedAt:a.failedAt}:{},...a.lastError?{lastError:a.lastError}:{}}});pt(i,{version:1,criteria:e})}function vt(i){if(!F(i))return!0;let t;try{t=ft(i)}catch{return!1}return t.filter(e=>e!==".mistflow").length===0}var is={sharp:"0.125rem",subtle:"0.375rem",rounded:"0.75rem",pill:"9999px"},as=[["--color-background","#ffffff"],["--color-foreground","#0a0a0a"],["--color-card","#ffffff"],["--color-card-foreground","#0a0a0a"],["--color-popover","#ffffff"],["--color-popover-foreground","#0a0a0a"],["--color-muted","#f5f5f5"],["--color-muted-foreground","#525252"],["--color-border","#e5e5e5"],["--color-input","#e5e5e5"],["--color-primary","#0a0a0a"],["--color-primary-foreground","#ffffff"],["--color-ring","#0a0a0a"],["--color-secondary","#f5f5f5"],["--color-secondary-foreground","#0a0a0a"],["--color-accent","#f5f5f5"],["--color-accent-foreground","#0a0a0a"],["--color-destructive","#b91c1c"],["--color-destructive-foreground","#ffffff"],["--color-success","#15803d"],["--color-success-foreground","#ffffff"],["--color-warning","#a16207"],["--color-warning-foreground","#ffffff"],["--color-info","#0369a1"],["--color-info-foreground","#ffffff"]];function ls(i){let t=i.match(/^---\n([\s\S]*?)\n---/);if(!t)return null;let s=t[1],e={theme:"light",colors:{},typography:{},rounded:{},spacing:{}},r=s.split(`
|
|
23
|
-
`),a=null,n=null,o=l=>l.replace(/^["']|["']$/g,"").trim();for(let l of r){let c=l.replace(/\r$/,"");if(!c.trim()||c.trim().startsWith("#"))continue;if(c.startsWith("theme:")){let d=o(c.slice(6).trim());(d==="dark"||d==="light")&&(e.theme=d);continue}let h=c.match(/^(colors|typography|rounded|spacing):\s*$/);if(h){a=h[1],n=null;continue}if(a==="typography"){let d=c.match(/^ {2}([a-zA-Z0-9_-]+):\s*$/);if(d){n=d[1].replace(/-/g,"_"),e.typography[n]={};continue}let S=c.match(/^ {4}([a-zA-Z0-9_-]+):\s*(.+)$/);if(S&&n){e.typography[n][S[1]]=o(S[2]);continue}}let w=c.match(/^ {2}([a-zA-Z0-9_-]+):\s*(.+)$/);if(w&&a){let d=w[1].replace(/-/g,"_"),S=o(w[2]);a==="colors"?e.colors[d]=S:a==="rounded"?e.rounded[d]=S:a==="spacing"&&(e.spacing[d]=S)}}return e}function cs(i,t){let s=i.colors,r=[["--color-background",s.background],["--color-foreground",s.on_background??s.on_surface],["--color-card",s.surface??s.background],["--color-card-foreground",s.on_surface??s.on_background],["--color-popover",s.surface??s.background],["--color-popover-foreground",s.on_surface??s.on_background],["--color-muted",s.surface_variant??s.outline_variant??s.outline],["--color-muted-foreground",s.on_surface_variant??s.outline],["--color-border",s.outline_variant??s.outline],["--color-input",s.outline_variant??s.outline],["--color-primary",s.primary],["--color-primary-foreground",s.on_primary],["--color-ring",s.primary],["--color-secondary",s.secondary],["--color-secondary-foreground",s.on_secondary],["--color-accent",s.secondary],["--color-accent-foreground",s.on_secondary],["--color-destructive",s.error],["--color-destructive-foreground",s.on_error],["--color-success",s.success],["--color-success-foreground",s.on_success],["--color-warning",s.warning],["--color-warning-foreground",s.on_warning],["--color-info",s.info??s.primary],["--color-info-foreground",s.on_info??s.on_primary]].filter(o=>typeof o[1]=="string").map(([o,l])=>` ${o}: ${l};`).join(`
|
|
24
|
-
`),a=[` --radius-sm: ${i.rounded.sm??"0.25rem"};`,` --radius-md: ${i.rounded.md??t};`,` --radius-lg: ${i.rounded.lg??"0.5rem"};`,` --radius-xl: ${i.rounded.xl??"0.75rem"};`].join(`
|
|
25
|
-
`);return`@import "tailwindcss";
|
|
26
|
-
@import "tw-animate-css";
|
|
27
|
-
|
|
28
|
-
@theme {
|
|
29
|
-
${r}
|
|
30
|
-
${a}
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
@layer base {
|
|
35
|
-
* { border-color: var(--color-border); }
|
|
36
|
-
body { background-color: var(--color-background); color: var(--color-foreground); }
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
:root {
|
|
40
|
-
--ease-quart-out: cubic-bezier(0.25, 1, 0.5, 1);
|
|
41
|
-
--ease-expo-out: cubic-bezier(0.16, 1, 0.3, 1);
|
|
42
|
-
--ease-back-out: cubic-bezier(0.34, 1.56, 0.64, 1);
|
|
43
|
-
--duration-micro: 80ms;
|
|
44
|
-
--duration-short: 150ms;
|
|
45
|
-
--duration-medium: 250ms;
|
|
46
|
-
--duration-long: 400ms;
|
|
47
|
-
--z-dropdown: 10;
|
|
48
|
-
--z-sticky: 20;
|
|
49
|
-
--z-modal-backdrop: 30;
|
|
50
|
-
--z-modal: 40;
|
|
51
|
-
--z-toast: 50;
|
|
52
|
-
--z-tooltip: 60;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
@keyframes fade-up { from { opacity: 0; transform: translateY(12px); } to { opacity: 1; transform: translateY(0); } }
|
|
56
|
-
@keyframes fade-in { from { opacity: 0; } to { opacity: 1; } }
|
|
57
|
-
.animate-fade-up { animation: fade-up 0.5s var(--ease-expo-out) both; }
|
|
58
|
-
.animate-fade-in { animation: fade-in 0.3s var(--ease-quart-out) both; }
|
|
59
|
-
|
|
60
|
-
.stagger > :nth-child(1) { animation-delay: 0ms; }
|
|
61
|
-
.stagger > :nth-child(2) { animation-delay: 50ms; }
|
|
62
|
-
.stagger > :nth-child(3) { animation-delay: 100ms; }
|
|
63
|
-
.stagger > :nth-child(4) { animation-delay: 150ms; }
|
|
64
|
-
.stagger > :nth-child(5) { animation-delay: 200ms; }
|
|
65
|
-
.stagger > :nth-child(6) { animation-delay: 250ms; }
|
|
66
|
-
|
|
67
|
-
@media (prefers-reduced-motion: reduce) {
|
|
68
|
-
*, *::before, *::after {
|
|
69
|
-
animation-duration: 0.01ms !important;
|
|
70
|
-
animation-iteration-count: 1 !important;
|
|
71
|
-
transition-duration: 0.01ms !important;
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
:focus-visible { outline: 2px solid var(--color-primary); outline-offset: 2px; }
|
|
76
|
-
:focus:not(:focus-visible) { outline: none; }
|
|
77
|
-
|
|
78
|
-
button, [role="button"] { transition: transform 100ms var(--ease-quart-out); }
|
|
79
|
-
button:active:not(:disabled), [role="button"]:active:not(:disabled) { transform: scale(0.97); }
|
|
80
|
-
`}function ps(i,t,s){let e=i?.borderRadius??"subtle",r=is[e]??"0.375rem";if(s){let n=ls(s);if(n)return cs(n,r)}return`@import "tailwindcss";
|
|
81
|
-
@import "tw-animate-css";
|
|
82
|
-
|
|
83
|
-
@theme {
|
|
84
|
-
${[...as.map(([n,o])=>` ${n}: ${o};`)," --radius-sm: 0.25rem;",` --radius-md: ${r};`," --radius-lg: 0.5rem;"," --radius-xl: 0.75rem;"].join(`
|
|
85
|
-
`)}
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
@layer base {
|
|
89
|
-
* { border-color: var(--color-border); }
|
|
90
|
-
body { background-color: var(--color-background); color: var(--color-foreground); }
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
:root {
|
|
94
|
-
--ease-quart-out: cubic-bezier(0.25, 1, 0.5, 1);
|
|
95
|
-
--ease-expo-out: cubic-bezier(0.16, 1, 0.3, 1);
|
|
96
|
-
--ease-back-out: cubic-bezier(0.34, 1.56, 0.64, 1);
|
|
97
|
-
--duration-micro: 80ms;
|
|
98
|
-
--duration-short: 150ms;
|
|
99
|
-
--duration-medium: 250ms;
|
|
100
|
-
--duration-long: 400ms;
|
|
101
|
-
--z-dropdown: 10;
|
|
102
|
-
--z-sticky: 20;
|
|
103
|
-
--z-modal-backdrop: 30;
|
|
104
|
-
--z-modal: 40;
|
|
105
|
-
--z-toast: 50;
|
|
106
|
-
--z-tooltip: 60;
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
@keyframes fade-up { from { opacity: 0; transform: translateY(12px); } to { opacity: 1; transform: translateY(0); } }
|
|
110
|
-
@keyframes fade-in { from { opacity: 0; } to { opacity: 1; } }
|
|
111
|
-
.animate-fade-up { animation: fade-up 0.5s var(--ease-expo-out) both; }
|
|
112
|
-
.animate-fade-in { animation: fade-in 0.3s var(--ease-quart-out) both; }
|
|
113
|
-
|
|
114
|
-
.stagger > :nth-child(1) { animation-delay: 0ms; }
|
|
115
|
-
.stagger > :nth-child(2) { animation-delay: 50ms; }
|
|
116
|
-
.stagger > :nth-child(3) { animation-delay: 100ms; }
|
|
117
|
-
.stagger > :nth-child(4) { animation-delay: 150ms; }
|
|
118
|
-
.stagger > :nth-child(5) { animation-delay: 200ms; }
|
|
119
|
-
.stagger > :nth-child(6) { animation-delay: 250ms; }
|
|
120
|
-
|
|
121
|
-
@media (prefers-reduced-motion: reduce) {
|
|
122
|
-
*, *::before, *::after {
|
|
123
|
-
animation-duration: 0.01ms !important;
|
|
124
|
-
animation-iteration-count: 1 !important;
|
|
125
|
-
transition-duration: 0.01ms !important;
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
:focus-visible { outline: 2px solid var(--color-primary); outline-offset: 2px; }
|
|
130
|
-
:focus:not(:focus-visible) { outline: none; }
|
|
131
|
-
|
|
132
|
-
button, [role="button"] { transition: transform 100ms var(--ease-quart-out); }
|
|
133
|
-
button:active:not(:disabled), [role="button"]:active:not(:disabled) { transform: scale(0.97); }
|
|
134
|
-
`}var ht={"Fredoka One":"Fredoka","Source Sans Pro":"Source_Sans_3","Source Serif Pro":"Source_Serif_4","Open Sans Condensed":"Open_Sans","Baloo 2":"Baloo_2","DM Serif Display":"DM_Serif_Display","DM Serif Text":"DM_Serif_Text","IBM Plex Mono":"IBM_Plex_Mono","IBM Plex Sans":"IBM_Plex_Sans","IBM Plex Serif":"IBM_Plex_Serif","Fira Code":"Fira_Code","Fira Sans":"Fira_Sans","Noto Sans JP":"Noto_Sans_JP","PT Sans":"PT_Sans","PT Serif":"PT_Serif","Work Sans":"Work_Sans","Space Mono":"Space_Mono","Space Grotesk":"Space_Grotesk","Plus Jakarta Sans":"Plus_Jakarta_Sans"};function gt(i){let t=i.replace(/[^A-Za-z0-9_ -]/g,"");return ht[t]?ht[t]:t.replace(/\s+/g,"_")}function us(i){return i?{english:"en",spanish:"es",french:"fr",german:"de",italian:"it",portuguese:"pt",dutch:"nl",russian:"ru",japanese:"ja",chinese:"zh",korean:"ko",arabic:"ar",hebrew:"he",hindi:"hi",turkish:"tr",polish:"pl",swedish:"sv",norwegian:"no",danish:"da",finnish:"fi",thai:"th",vietnamese:"vi",indonesian:"id",malay:"ms",farsi:"fa",persian:"fa",czech:"cs",greek:"el",romanian:"ro",hungarian:"hu",ukrainian:"uk",bengali:"bn",tamil:"ta",telugu:"te",urdu:"ur"}[i.toLowerCase()]??"en":"en"}var ds=new Set(["ar","he","fa","ur"]);function ms(i,t,s){let e=i.replace(/[\\"`$]/g,""),r=us(s),n=ds.has(r)?`lang="${r}" dir="rtl"`:`lang="${r}"`,o=t?.fonts?.heading,l=t?.fonts?.body;if(!o&&!l)return`import type { Metadata } from "next";
|
|
135
|
-
import { DM_Sans } from "next/font/google";
|
|
136
|
-
import { Toaster } from "sonner";
|
|
137
|
-
import "./globals.css";
|
|
138
|
-
|
|
139
|
-
const body = DM_Sans({ subsets: ["latin"], variable: "--font-body" });
|
|
140
|
-
|
|
141
|
-
export const metadata: Metadata = { title: "${e}", description: "Built with Mistflow" };
|
|
142
|
-
|
|
143
|
-
export default function RootLayout({ children }: { children: React.ReactNode }) {
|
|
144
|
-
return (
|
|
145
|
-
<html ${n}>
|
|
146
|
-
<body className={body.variable}>{children}<Toaster richColors /></body>
|
|
147
|
-
</html>
|
|
148
|
-
);
|
|
149
|
-
}
|
|
150
|
-
`;let c=gt(o??l),h=gt(l??o);return c===h?`import type { Metadata } from "next";
|
|
151
|
-
import { ${c} } from "next/font/google";
|
|
152
|
-
import { Toaster } from "sonner";
|
|
153
|
-
import "./globals.css";
|
|
154
|
-
|
|
155
|
-
const font = ${c}({ subsets: ["latin"], variable: "--font-body" });
|
|
156
|
-
|
|
157
|
-
export const metadata: Metadata = { title: "${e}", description: "Built with Mistflow" };
|
|
158
|
-
|
|
159
|
-
export default function RootLayout({ children }: { children: React.ReactNode }) {
|
|
160
|
-
return (
|
|
161
|
-
<html ${n}>
|
|
162
|
-
<body className={font.variable}>{children}<Toaster richColors /></body>
|
|
163
|
-
</html>
|
|
164
|
-
);
|
|
165
|
-
}
|
|
166
|
-
`:`import type { Metadata } from "next";
|
|
167
|
-
import { ${c}, ${h} } from "next/font/google";
|
|
168
|
-
import { Toaster } from "sonner";
|
|
169
|
-
import "./globals.css";
|
|
170
|
-
|
|
171
|
-
const heading = ${c}({ subsets: ["latin"], variable: "--font-heading" });
|
|
172
|
-
const body = ${h}({ subsets: ["latin"], variable: "--font-body" });
|
|
173
|
-
|
|
174
|
-
export const metadata: Metadata = { title: "${e}", description: "Built with Mistflow" };
|
|
175
|
-
|
|
176
|
-
export default function RootLayout({ children }: { children: React.ReactNode }) {
|
|
177
|
-
return (
|
|
178
|
-
<html ${n}>
|
|
179
|
-
<body className={\`\${heading.variable} \${body.variable}\`}>{children}<Toaster richColors /></body>
|
|
180
|
-
</html>
|
|
181
|
-
);
|
|
182
|
-
}
|
|
183
|
-
`}function he(i,...t){let s=JSON.stringify(i).toLowerCase();return t.some(e=>s.includes(e.toLowerCase()))}var hs={dashboard:"Home",home:"Home",overview:"Home",patient:"Users",member:"Users",user:"Users",people:"Users",team:"Users",client:"Users",contact:"Users",customer:"Users",appointment:"Calendar",schedule:"Calendar",booking:"Calendar",event:"Calendar",billing:"CreditCard",invoice:"CreditCard",payment:"CreditCard",pricing:"CreditCard",treatment:"ClipboardList",plan:"ClipboardList",task:"CheckSquare",exercise:"Dumbbell",workout:"Dumbbell",report:"BarChart3",analytics:"BarChart3",stats:"BarChart3",setting:"Settings",config:"Settings",profile:"User",account:"User",message:"MessageSquare",chat:"MessageSquare",inbox:"MessageSquare",product:"Package",item:"Package",catalog:"Package",order:"ShoppingCart",cart:"ShoppingCart",file:"FileText",document:"FileText",upload:"Upload",notification:"Bell",alert:"Bell",project:"FolderKanban",board:"FolderKanban",post:"PenSquare",blog:"PenSquare",article:"PenSquare",course:"GraduationCap",lesson:"GraduationCap",class:"GraduationCap",habit:"Target",goal:"Target",streak:"Flame",progress:"TrendingUp",feature:"Sparkles",subscription:"CreditCard",price:"CreditCard",recipe:"ChefHat",food:"UtensilsCrossed",meal:"UtensilsCrossed",pet:"PawPrint",animal:"PawPrint",music:"Music",playlist:"ListMusic",song:"Music",photo:"Image",image:"Image",gallery:"Images",video:"Video",movie:"Film",map:"MapPin",location:"MapPin",place:"MapPin",search:"Search",explore:"Compass",inventory:"Boxes",stock:"Boxes",warehouse:"Warehouse",review:"Star",rating:"Star",feedback:"Star",log:"ScrollText",history:"Clock",activity:"Activity"};function qe(i){let t=i.toLowerCase().replace(/[^a-z]/g,"");for(let[s,e]of Object.entries(hs))if(t.includes(s))return e;return"Circle"}function oe(i){return i.split("-").map(t=>t.charAt(0).toUpperCase()+t.slice(1)).join(" ")}function gs(i){if(i.authModel==="none")return null;let t=["/login","/register","/forgot-password","/reset-password","/api/auth","/api/health","/api/webhooks","/api/admin/seed"];if(i.publicPages&&Array.isArray(i.publicPages))for(let a of i.publicPages){if(typeof a!="string"||a.length<1)continue;let n=a.replace(/[\u201C\u201D\u201E\u201F\u2018\u2019\u2033\u2036]/g,"").trim();if(!n)continue;let o=n.startsWith("/")?n:"/"+n;t.includes(o)||t.push(o)}let s=t.filter(a=>a==="/"),e=t.filter(a=>a!=="/"),r=[];r.push('import { NextRequest, NextResponse } from "next/server";'),r.push(""),r.push("const PUBLIC_PREFIXES = [");for(let a of e)r.push(' "'+a+'",');return r.push("];"),r.push(""),s.length>0&&(r.push('const PUBLIC_EXACT = ["'+s.join('", "')+'"];'),r.push("")),r.push("export function middleware(req: NextRequest) {"),r.push(" const { pathname, search } = req.nextUrl;"),r.push(""),s.length>0&&r.push(" if (PUBLIC_EXACT.includes(pathname)) return NextResponse.next();"),r.push(" if (PUBLIC_PREFIXES.some((p) => pathname.startsWith(p))) return NextResponse.next();"),r.push(""),r.push(' const token = req.cookies.get("better-auth.session_token")?.value || req.cookies.get("__Secure-better-auth.session_token")?.value;'),r.push(" if (!token) {"),r.push(' const loginUrl = new URL("/login", req.url);'),r.push(" const params = new URLSearchParams(search);"),r.push(' for (const key of ["verified", "error"]) {'),r.push(" const v = params.get(key);"),r.push(" if (v) loginUrl.searchParams.set(key, v);"),r.push(" }"),r.push(" return NextResponse.redirect(loginUrl);"),r.push(" }"),r.push(""),r.push(" return NextResponse.next();"),r.push("}"),r.push(""),r.push("export const config = {"),r.push(' matcher: ["/((?!_next|static|favicon\\\\.ico).*)"],'),r.push("};"),r.push(""),r.join(`
|
|
184
|
-
`)}function fs(i){if(i.navStyle==="none")return null;let t=["/api","/login","/register","/sign-in","/sign-up","/admin","/pricing","/about","/contact","/terms","/privacy","/onboarding","/join","/forgot-password","/reset-password"],e=(i.pages??[]).filter(l=>{let c=l.path??l.route??"";return c==="/"||c===""||c.includes("[")||c.replace(/^\//,"").split("/").length>1?!1:!t.some(w=>c.startsWith(w))}).map(l=>{let c=l.path??l.route??"",h=c.startsWith("/")?c:"/"+c,w=l.name??oe(c.replace(/^\//,"")),d=qe(w);return{label:w,href:h,icon:d}});e.some(l=>l.href==="/dashboard")||e.unshift({label:"Dashboard",href:"/dashboard",icon:"Home"});let r=i.authModel==="none",a=[...new Set(e.map(l=>l.icon))];r||a.push("LogOut");let n=oe(i.name);if(i.navStyle==="topbar"){let l=[];l.push('"use client";'),l.push(""),l.push('import Link from "next/link";'),l.push('import { usePathname } from "next/navigation";'),r||l.push('import { authClient } from "@/lib/auth-client";'),l.push('import { Button } from "@/components/ui/button";'),l.push('import { cn } from "@/lib/utils";'),l.push("import { "+a.join(", ")+' } from "lucide-react";'),l.push(""),l.push("interface TopNavProps {"),l.push(" user: { name: string | null; email: string; role?: string | undefined };"),l.push("}"),l.push(""),l.push("const NAV_ITEMS = [");for(let c of e)l.push(' { label: "'+c.label+'", href: "'+c.href+'", icon: '+c.icon+" },");return l.push("];"),l.push(""),l.push("export default function TopNav({ user }: TopNavProps) {"),l.push(" const pathname = usePathname();"),l.push(""),l.push(" return ("),l.push(' <nav className="border-b bg-card">'),l.push(' <div className="mx-auto flex h-14 max-w-7xl items-center justify-between px-4">'),l.push(' <div className="flex items-center gap-6">'),l.push(' <span className="text-lg font-semibold">'+n+"</span>"),l.push(' <div className="flex items-center gap-1">'),l.push(" {NAV_ITEMS.map((item) => ("),l.push(" <Link"),l.push(" key={item.href}"),l.push(" href={item.href}"),l.push(' aria-current={pathname === item.href ? "page" : undefined}'),l.push(" className={cn("),l.push(' "flex items-center gap-1.5 rounded-md px-3 py-1.5 text-sm font-medium transition-colors",'),l.push(' pathname === item.href ? "bg-primary/10 text-primary" : "text-muted-foreground hover:text-foreground"'),l.push(" )}"),l.push(" >"),l.push(' <item.icon className="h-4 w-4" />'),l.push(" {item.label}"),l.push(" </Link>"),l.push(" ))}"),l.push(" </div>"),l.push(" </div>"),r?l.push(' <span className="text-sm text-muted-foreground">{user.name}</span>'):(l.push(' <div className="flex items-center gap-2">'),l.push(' <span className="text-sm text-muted-foreground">{user.email}</span>'),l.push(" <Button"),l.push(' variant="ghost"'),l.push(' size="sm"'),l.push(' onClick={() => authClient.signOut({ fetchOptions: { onSuccess: () => { window.location.href = "/login"; } } })}'),l.push(" >"),l.push(' <LogOut className="h-4 w-4" />'),l.push(" </Button>"),l.push(" </div>")),l.push(" </div>"),l.push(" </nav>"),l.push(" );"),l.push("}"),l.push(""),{path:"components/topnav.tsx",content:l.join(`
|
|
185
|
-
`)}}let o=[];o.push('"use client";'),o.push(""),o.push('import { useState } from "react";'),o.push('import Link from "next/link";'),o.push('import { usePathname } from "next/navigation";'),r||o.push('import { authClient } from "@/lib/auth-client";'),o.push('import { Button } from "@/components/ui/button";'),o.push('import { Sheet, SheetContent, SheetTrigger, SheetTitle } from "@/components/ui/sheet";'),o.push('import { cn } from "@/lib/utils";'),o.push("import { Menu, "+a.join(", ")+' } from "lucide-react";'),o.push(""),o.push("interface SidebarProps {"),o.push(" user: { name: string | null; email: string; role?: string | undefined };"),o.push("}"),o.push(""),o.push("const NAV_ITEMS = [");for(let l of e)o.push(' { label: "'+l.label+'", href: "'+l.href+'", icon: '+l.icon+" },");return o.push("];"),o.push(""),o.push('function NavContent({ pathname, user, onNavigate }: { pathname: string; user: SidebarProps["user"]; onNavigate?: () => void }) {'),o.push(" return ("),o.push(" <>"),o.push(' <div className="flex h-14 items-center border-b px-4">'),o.push(' <span className="text-lg font-semibold">'+n+"</span>"),o.push(" </div>"),o.push(' <nav className="flex-1 space-y-1 p-2">'),o.push(" {NAV_ITEMS.map((item) => ("),o.push(" <Link"),o.push(" key={item.href}"),o.push(" href={item.href}"),o.push(" onClick={onNavigate}"),o.push(' aria-current={pathname === item.href ? "page" : undefined}'),o.push(" className={cn("),o.push(' "flex items-center gap-3 rounded-md px-3 py-2.5 text-sm font-medium transition-colors",'),o.push(' pathname === item.href ? "bg-primary/10 text-primary" : "text-muted-foreground hover:bg-muted hover:text-foreground"'),o.push(" )}"),o.push(" >"),o.push(' <item.icon className="h-4 w-4" />'),o.push(" {item.label}"),o.push(" </Link>"),o.push(" ))}"),o.push(" </nav>"),o.push(' <div className="border-t p-4">'),o.push(' <div className="flex items-center gap-3">'),o.push(' <div className="flex-1 truncate">'),o.push(' <p className="truncate text-sm font-medium">{user.name ?? "User"}</p>'),o.push(' <p className="truncate text-xs text-muted-foreground">{user.email}</p>'),o.push(" </div>"),r||(o.push(" <Button"),o.push(' variant="ghost"'),o.push(' size="icon"'),o.push(' onClick={() => authClient.signOut({ fetchOptions: { onSuccess: () => { window.location.href = "/login"; } } })}'),o.push(" >"),o.push(' <LogOut className="h-4 w-4" />'),o.push(" </Button>")),o.push(" </div>"),o.push(" </div>"),o.push(" </>"),o.push(" );"),o.push("}"),o.push(""),o.push("export default function Sidebar({ user }: SidebarProps) {"),o.push(" const pathname = usePathname();"),o.push(" const [open, setOpen] = useState(false);"),o.push(""),o.push(" return ("),o.push(" <>"),o.push(' <aside className="hidden md:flex h-screen w-64 flex-col border-r bg-card">'),o.push(" <NavContent pathname={pathname} user={user} />"),o.push(" </aside>"),o.push(' <div className="sticky top-0 z-[var(--z-sticky)] flex h-14 items-center gap-3 border-b bg-card px-4 md:hidden">'),o.push(" <Sheet open={open} onOpenChange={setOpen}>"),o.push(" <SheetTrigger asChild>"),o.push(' <Button variant="ghost" size="icon" className="-ml-2">'),o.push(' <Menu className="h-5 w-5" />'),o.push(" </Button>"),o.push(" </SheetTrigger>"),o.push(' <SheetContent side="left" className="w-64 p-0">'),o.push(' <SheetTitle className="sr-only">Navigation</SheetTitle>'),o.push(' <div className="flex h-full flex-col">'),o.push(" <NavContent pathname={pathname} user={user} onNavigate={() => setOpen(false)} />"),o.push(" </div>"),o.push(" </SheetContent>"),o.push(" </Sheet>"),o.push(' <span className="text-lg font-semibold">'+n+"</span>"),o.push(" </div>"),o.push(" </>"),o.push(" );"),o.push("}"),o.push(""),{path:"components/sidebar.tsx",content:o.join(`
|
|
186
|
-
`)}}function ys(i){if(!i.roles||i.roles.length===0)return null;let t=i.roles,s=i.defaultRole??t[0],e=[];e.push("export type Role = "+t.map(r=>'"'+r+'"').join(" | ")+";"),e.push(""),e.push("export const ROLES = ["+t.map(r=>'"'+r+'"').join(", ")+"] as const;"),e.push(""),e.push('export const DEFAULT_ROLE: Role = "'+s+'";'),e.push(""),e.push("export const ROLE_LABELS: Record<Role, string> = {");for(let r of t){let a=r.charAt(0).toUpperCase()+r.slice(1);e.push(' "'+r+'": "'+a+'",')}return e.push("};"),e.push(""),e.push("export function getUserRole(user: Record<string, unknown>): Role {"),e.push(" const role = (user.role as string) ?? DEFAULT_ROLE;"),e.push(" if (ROLES.includes(role as Role)) return role as Role;"),e.push(" return DEFAULT_ROLE;"),e.push("}"),e.push(""),e.push("export function hasRole(userRole: string | undefined, required: Role | Role[]): boolean {"),e.push(" if (!userRole) return false;"),e.push(" const allowed = Array.isArray(required) ? required : [required];"),e.push(" return allowed.includes(userRole as Role);"),e.push("}"),e.push(""),e.join(`
|
|
187
|
-
`)}function bs(i){let t=oe(i.name);if(i.authModel==="none"){let a=[];return a.push("export default function HomePage() {"),a.push(" return ("),a.push(' <main className="flex min-h-screen flex-col items-center justify-center p-8">'),a.push(' <h1 className="text-4xl font-bold">'+t+"</h1>"),i.summary&&a.push(' <p className="mt-4 text-lg text-muted-foreground">'+i.summary+"</p>"),a.push(" </main>"),a.push(" );"),a.push("}"),a.push(""),a.join(`
|
|
188
|
-
`)}let s=i.publicPages?.includes("/"),e=i.design?.landingTone;if(s&&e){let a=[];return a.push('import Link from "next/link";'),a.push(""),a.push("export default function HomePage() {"),a.push(" return ("),a.push(' <main className="flex min-h-screen flex-col">'),a.push(' <section className="flex flex-1 flex-col items-center justify-center gap-6 px-4 py-24 text-center">'),a.push(' <h1 className="text-5xl font-bold tracking-tight">'+t+"</h1>"),i.summary&&a.push(' <p className="max-w-2xl text-xl text-muted-foreground">'+i.summary+"</p>"),a.push(' <div className="flex gap-4">'),a.push(' <Link href="/register" className="inline-flex h-11 items-center rounded-md bg-primary px-8 text-sm font-medium text-primary-foreground hover:bg-primary/90">'),a.push(" Get Started"),a.push(" </Link>"),a.push(' <Link href="/login" className="inline-flex h-11 items-center rounded-md border px-8 text-sm font-medium hover:bg-muted">'),a.push(" Sign In"),a.push(" </Link>"),a.push(" </div>"),a.push(" </section>"),a.push(" </main>"),a.push(" );"),a.push("}"),a.push(""),a.join(`
|
|
189
|
-
`)}if(s){let a=[];return a.push('import Link from "next/link";'),a.push(""),a.push("export default function HomePage() {"),a.push(" return ("),a.push(' <main className="flex min-h-screen flex-col items-center justify-center gap-6 p-8 text-center">'),a.push(' <h1 className="text-4xl font-bold">'+t+"</h1>"),i.summary&&a.push(' <p className="text-lg text-muted-foreground">'+i.summary+"</p>"),a.push(' <Link href="/login" className="inline-flex h-10 items-center rounded-md bg-primary px-6 text-sm font-medium text-primary-foreground hover:bg-primary/90">'),a.push(" Sign In"),a.push(" </Link>"),a.push(" </main>"),a.push(" );"),a.push("}"),a.push(""),a.join(`
|
|
190
|
-
`)}let r=[];return r.push('import { headers } from "next/headers";'),r.push('import { redirect } from "next/navigation";'),r.push('import { auth } from "@/lib/auth";'),r.push(""),r.push("export default async function HomePage() {"),r.push(" const session = await auth.api.getSession({ headers: await headers() });"),r.push(' if (session) redirect("/dashboard");'),r.push(' redirect("/login");'),r.push("}"),r.push(""),r.join(`
|
|
191
|
-
`)}function ws(i,t){let s=i.authModel==="none",e=[];s||(e.push('import { Suspense } from "react";'),e.push('import { headers } from "next/headers";'),e.push('import { redirect } from "next/navigation";'),e.push('import { auth } from "@/lib/auth";'),e.push('import { VerifiedBanner } from "@/components/auth/verified-banner";')),i.navStyle==="topbar"?e.push('import TopNav from "@/components/topnav";'):i.navStyle!=="none"&&e.push('import Sidebar from "@/components/sidebar";'),!s&&i.roles&&i.roles.length>0&&e.push('import { getUserRole } from "@/lib/roles";'),e.push(""),e.push("export default async function DashboardLayout({ children }: { children: React.ReactNode }) {"),s?e.push(' const user = { name: "Guest", email: "" };'):(e.push(" const session = await auth.api.getSession({ headers: await headers() });"),e.push(' if (!session) redirect("/login");'),e.push(""),i.roles&&i.roles.length>0?(e.push(" const role = getUserRole(session.user as Record<string, unknown>);"),e.push(" const user = { name: session.user.name, email: session.user.email, role };")):(e.push(" const user = {"),e.push(" name: session.user.name,"),e.push(" email: session.user.email,"),e.push(" role: (session.user as Record<string, unknown>).role as string | undefined,"),e.push(" };"))),e.push("");let r=s?"":"<Suspense fallback={null}><VerifiedBanner /></Suspense>";return i.navStyle==="topbar"?(e.push(" return ("),e.push(' <div className="min-h-screen">'),e.push(" <TopNav user={user} />"),e.push(' <main className="mx-auto max-w-7xl p-6">'+r+"{children}</main>"),e.push(" </div>"),e.push(" );")):i.navStyle==="none"?(e.push(" return ("),e.push(' <div className="min-h-screen">'),e.push(' <main className="mx-auto max-w-5xl p-6">'+r+"{children}</main>"),e.push(" </div>"),e.push(" );")):(e.push(" return ("),e.push(' <div className="flex flex-col md:flex-row min-h-screen">'),e.push(" <Sidebar user={user} />"),e.push(' <main className="flex-1 overflow-x-hidden p-4 md:p-6">'+r+"{children}</main>"),e.push(" </div>"),e.push(" );")),e.push("}"),e.push(""),e.join(`
|
|
192
|
-
`)}function vs(i){let t=oe(i.name),s=i.dataModel??[],e=[];if(s.length>0){let r=s.map(n=>qe(n.entity??n.name??"item")),a=[...new Set(r)];e.push('import { Card, CardContent } from "@/components/ui/card";'),e.push("import { "+a.join(", ")+' } from "lucide-react";'),e.push("")}if(e.push("export default function DashboardPage() {"),e.push(" return ("),e.push(' <div className="space-y-6">'),e.push(" <div>"),e.push(' <h1 className="text-3xl font-bold">'+t+"</h1>"),i.summary&&e.push(' <p className="mt-1 text-muted-foreground">'+i.summary+"</p>"),e.push(" </div>"),s.length>0){e.push(' <div className="rounded-lg border p-8 text-center">'),e.push(' <h2 className="text-lg font-semibold">Get started</h2>'),e.push(` <p className="mt-1 text-sm text-muted-foreground">Here's what you can do</p>`),e.push(' <div className="mt-6 grid gap-3 sm:grid-cols-2 text-left">');for(let r of s){let a=r.entity??r.name??"Item",n=qe(a),o=oe(a.replace(/_/g,"-"));e.push(' <div className="flex items-center gap-3 rounded-md border p-3">'),e.push(" <"+n+' className="h-5 w-5 text-muted-foreground" />'),e.push(' <span className="text-sm font-medium">Add your first '+o+"</span>"),e.push(" </div>")}e.push(" </div>"),e.push(" </div>")}return e.push(" </div>"),e.push(" );"),e.push("}"),e.push(""),e.join(`
|
|
193
|
-
`)}function Ss(i,t=!1){if(!i.multiTenant)return null;let s=[];return t?(s.push('import { pgTable, text, timestamp, index } from "drizzle-orm/pg-core";'),s.push('import { user } from "./auth";'),s.push(""),s.push('export const organization = pgTable("organization", {'),s.push(' id: text("id").primaryKey(),'),s.push(' name: text("name").notNull(),'),s.push(' slug: text("slug").unique().notNull(),'),s.push(' createdAt: timestamp("created_at").defaultNow().notNull(),'),s.push(' updatedAt: timestamp("updated_at").defaultNow().notNull(),'),s.push("});"),s.push(""),s.push("export const orgMember = pgTable(")):(s.push('import { sqliteTable, text, index } from "drizzle-orm/sqlite-core";'),s.push('import { sql } from "drizzle-orm";'),s.push('import { user } from "./auth";'),s.push(""),s.push('export const organization = sqliteTable("organization", {'),s.push(' id: text("id").primaryKey(),'),s.push(' name: text("name").notNull(),'),s.push(' slug: text("slug").unique().notNull(),'),s.push(' createdAt: text("created_at").default(sql`CURRENT_TIMESTAMP`).notNull(),'),s.push(' updatedAt: text("updated_at").default(sql`CURRENT_TIMESTAMP`).notNull(),'),s.push("});"),s.push(""),s.push("export const orgMember = sqliteTable(")),s.push(' "org_member",'),s.push(" {"),s.push(' id: text("id").primaryKey(),'),s.push(' orgId: text("org_id").notNull().references(() => organization.id),'),s.push(' userId: text("user_id").notNull().references(() => user.id),'),s.push(' role: text("role").notNull(),'),t?s.push(' joinedAt: timestamp("joined_at").defaultNow().notNull(),'):s.push(' joinedAt: text("joined_at").default(sql`CURRENT_TIMESTAMP`).notNull(),'),s.push(" },"),s.push(" (table) => ({"),s.push(' orgIdx: index("org_member_org_idx").on(table.orgId),'),s.push(' userIdx: index("org_member_user_idx").on(table.userId),'),s.push(" }),"),s.push(");"),s.push(""),s.join(`
|
|
194
|
-
`)}function xs(i){if(!i.multiTenant)return null;let t=[];return t.push('import { db } from "./db";'),t.push('import { organization, orgMember } from "@/db/schema/organization";'),t.push('import { eq } from "drizzle-orm";'),t.push(""),t.push("export async function getCurrentOrg(userId: string) {"),t.push(" const membership = await db"),t.push(" .select()"),t.push(" .from(orgMember)"),t.push(" .where(eq(orgMember.userId, userId))"),t.push(" .limit(1);"),t.push(" if (membership.length === 0) return null;"),t.push(" const org = await db"),t.push(" .select()"),t.push(" .from(organization)"),t.push(" .where(eq(organization.id, membership[0].orgId))"),t.push(" .limit(1);"),t.push(" return org[0] ?? null;"),t.push("}"),t.push(""),t.push("export async function getOrgMembers(orgId: string) {"),t.push(" return db"),t.push(" .select()"),t.push(" .from(orgMember)"),t.push(" .where(eq(orgMember.orgId, orgId));"),t.push("}"),t.push(""),t.push("export async function inviteToOrg(orgId: string, email: string, role: string) {"),t.push(" const id = crypto.randomUUID();"),t.push(" await db.insert(orgMember).values({"),t.push(" id,"),t.push(" orgId,"),t.push(" userId: email,"),t.push(" role,"),t.push(" });"),t.push(" return { id, orgId, email, role };"),t.push("}"),t.push(""),t.join(`
|
|
195
|
-
`)}function ks(i){if(!i.multiTenant)return null;let t=[];return t.push('"use client";'),t.push(""),t.push("import {"),t.push(" DropdownMenu,"),t.push(" DropdownMenuContent,"),t.push(" DropdownMenuItem,"),t.push(" DropdownMenuTrigger,"),t.push('} from "@/components/ui/dropdown-menu";'),t.push('import { Button } from "@/components/ui/button";'),t.push('import { ChevronsUpDown } from "lucide-react";'),t.push(""),t.push("interface OrgSwitcherProps {"),t.push(" orgs: Array<{ id: string; name: string }>;"),t.push(" currentOrgId: string;"),t.push("}"),t.push(""),t.push("export default function OrgSwitcher({ orgs, currentOrgId }: OrgSwitcherProps) {"),t.push(" const currentOrg = orgs.find((o) => o.id === currentOrgId);"),t.push(""),t.push(" return ("),t.push(" <DropdownMenu>"),t.push(" <DropdownMenuTrigger asChild>"),t.push(' <Button variant="outline" className="w-full justify-between">'),t.push(' <span className="truncate">{currentOrg?.name ?? "Select org"}</span>'),t.push(' <ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />'),t.push(" </Button>"),t.push(" </DropdownMenuTrigger>"),t.push(' <DropdownMenuContent className="w-56">'),t.push(" {orgs.map((org) => ("),t.push(" <DropdownMenuItem key={org.id}>"),t.push(" {org.name}"),t.push(" </DropdownMenuItem>"),t.push(" ))}"),t.push(" </DropdownMenuContent>"),t.push(" </DropdownMenu>"),t.push(" );"),t.push("}"),t.push(""),t.join(`
|
|
196
|
-
`)}function Ps(i,t,s){let e=[],r=i.split("-").map(l=>l.charAt(0).toUpperCase()+l.slice(1)).join(" ");e.push(`# ${r}`),e.push(""),t?.summary&&(e.push(t.summary),e.push(""));let a=t?.features??[];if(a.length>0){e.push("## Features"),e.push("");for(let l of a){let c=l.description?` \u2014 ${l.description}`:"";e.push(`- **${l.name}**${c}`)}e.push("")}e.push("## Tech Stack"),e.push(""),e.push("| Layer | Technology |"),e.push("|-------|------------|"),e.push("| Framework | Next.js 15 (App Router) |"),e.push("| Database | Mistflow Cloud (Postgres) + Drizzle ORM |"),e.push("| Auth | Better Auth (email/password, social login) |"),e.push("| Styling | Tailwind CSS + shadcn/ui |"),e.push("| Deployment | Mistflow Cloud |"),s.hasStripe&&e.push("| Payments | Stripe |"),s.hasResend&&e.push("| Email | Resend + React Email |"),s.hasStorage&&e.push("| File Storage | Mistflow Cloud (managed blob storage) |"),s.hasAdmin&&e.push("| Admin | Better Auth admin plugin |"),s.hasAI&&e.push("| AI | Vercel AI SDK + OpenAI |"),e.push("");let n=t?.pages??[];if(n.length>0){e.push("## Pages"),e.push(""),e.push("| Route | Description |"),e.push("|-------|-------------|");for(let l of n){let c=l.path??l.route??l.name??"",h=l.description??"";e.push(`| \`${c.startsWith("/")?c:"/"+c}\` | ${h} |`)}e.push("")}let o=t?.dataModel??[];if(o.length>0){e.push("## Data Model"),e.push("");for(let l of o){let c=l.entity??l.name??"Unknown";if(e.push(`### ${c}`),e.push(""),l.fields.length>0){if(typeof l.fields[0]=="string")e.push(`Fields: ${l.fields.join(", ")}`);else{e.push("| Field | Type |"),e.push("|-------|------|");for(let h of l.fields)e.push(`| ${h.name} | ${h.type} |`)}e.push("")}}}return e.push("## Getting Started"),e.push(""),e.push("### Prerequisites"),e.push(""),e.push("- Node.js 20+"),e.push("- npm"),e.push(""),e.push("### Install"),e.push(""),e.push("Use the Mistflow CLI (recommended \u2014 streams output live and has no 60s tool-call timeout):"),e.push(""),e.push("```bash"),e.push("npx -y @mistflow-ai/cli install"),e.push("```"),e.push(""),e.push("Or plain npm if you prefer:"),e.push(""),e.push("```bash"),e.push("npm install"),e.push("```"),e.push(""),e.push("### Set up environment"),e.push(""),e.push("Copy `.env.example` to `.env.local` and fill in the values:"),e.push(""),e.push("```bash"),e.push("cp .env.example .env.local"),e.push("```"),e.push(""),e.push("| Variable | Description | Required |"),e.push("|----------|-------------|----------|"),s.isNeon?e.push("| `DATABASE_URL` | Postgres connection URL | Yes |"):(e.push("| `TURSO_URL` | Database connection URL | Yes |"),e.push("| `TURSO_AUTH_TOKEN` | Database auth token | Yes |")),e.push("| `AUTH_SECRET` | Auth encryption secret (auto-generated) | Yes |"),s.hasStripe&&(e.push("| `STRIPE_SECRET_KEY` | Stripe secret key | Yes |"),e.push("| `STRIPE_WEBHOOK_SECRET` | Stripe webhook signing secret | Yes |"),e.push("| `NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY` | Stripe publishable key | Yes |")),s.hasResend&&(e.push("| `RESEND_API_KEY` | Resend API key | Yes |"),e.push("| `EMAIL_FROM` | Sender email address | Yes (production) |")),s.hasStorage&&(e.push("| `MISTFLOW_API_KEY` | Mistflow API key for file storage | Yes |"),e.push("| `MISTFLOW_PROJECT_ID` | Mistflow project ID | Yes |")),s.hasAI&&e.push("| `OPENAI_API_KEY` | OpenAI API key | Yes |"),e.push(""),e.push("### Local database"),e.push(""),s.isNeon?(e.push("For local development, start a local Postgres server:"),e.push(""),e.push("```bash"),e.push("# Using Docker:"),e.push("docker run -d --name postgres -e POSTGRES_PASSWORD=postgres -p 5432:5432 postgres:17"),e.push("# Or install via Homebrew: brew install postgresql@17 && brew services start postgresql@17"),e.push("```")):(e.push("For local development, start a local Turso server:"),e.push(""),e.push("```bash"),e.push("npx turso dev"),e.push("```")),e.push(""),e.push("Then set up the database:"),e.push(""),e.push("```bash"),e.push("npm run db:push"),e.push("```"),e.push(""),e.push("### Run"),e.push(""),e.push("```bash"),e.push("npm run dev"),e.push("```"),e.push(""),e.push("Open [http://localhost:3000](http://localhost:3000)."),e.push(""),e.push("## Project Structure"),e.push(""),e.push("```"),e.push("app/"),e.push(" (auth)/ Login and registration pages"),e.push(" (dashboard)/ Authenticated app pages"),s.hasAdmin&&e.push(" (admin)/ Admin panel pages"),e.push(" api/ API routes (auth, health, webhooks)"),e.push(" layout.tsx Root layout with fonts and providers"),e.push(" globals.css Design tokens and Tailwind config"),e.push("components/ Reusable UI components"),e.push("db/"),e.push(" schema/ Database table definitions"),e.push(" index.ts Schema exports"),e.push("lib/"),e.push(" auth.ts Better Auth server config"),e.push(" auth-client.ts Better Auth client config"),e.push(` db.ts ${s.isNeon?"Postgres":"SQLite"} database connection`),s.hasStripe&&e.push(" stripe.ts Stripe client"),s.hasResend&&(e.push(" resend.ts Resend client"),e.push(" email.ts Email send helpers")),s.hasStorage&&e.push(" storage.ts File upload/download helpers"),s.hasAI&&e.push(" ai.ts AI client (Vercel AI SDK + OpenAI)"),s.hasResend&&e.push("emails/ React Email templates"),e.push("```"),e.push(""),e.push("## Deploy"),e.push(""),e.push("Deploy to production with Mistflow:"),e.push(""),e.push("```"),e.push("# In your AI editor (Claude Code, Cursor, etc.):"),e.push("mist_deploy action='deploy'"),e.push("```"),e.push(""),e.push("Your app will be live at `https://<app-name>.mistflow.app`."),e.push(""),t?.design&&(e.push("## Design"),e.push(""),t.design.tone&&e.push(`- **Tone**: ${t.design.tone}`),t.design.fonts&&(e.push(`- **Heading font**: ${t.design.fonts.heading}`),e.push(`- **Body font**: ${t.design.fonts.body}`)),t.design.accentColor&&e.push(`- **Accent color**: ${t.design.accentColor}`),t.design.borderRadius&&e.push(`- **Border radius**: ${t.design.borderRadius}`),e.push("")),e.push("---"),e.push(""),e.push("Built with [Mistflow](https://mistflow.ai)"),e.push(""),e.join(`
|
|
197
|
-
`)}async function St(i,t){let{name:s,plan:e,path:r,planId:a}=i;if(!r)return p("mist_build init requires an explicit 'path' \u2014 the absolute directory where the project should be scaffolded. Pass the user's project directory (e.g. /Users/alice/projects/my-app). Do not rely on a default.",!0);if(!yt(r))return p(`mist_build init 'path' must be an absolute path \u2014 received '${r}'. Pass the full absolute path to the target directory.`,!0);let n=ze(r),o=e?.design,l=e?.appStyle,c=e?he(e,"stripe","payment","billing","subscription","checkout","pricing"):!1,h=!0,w=e?he(e,"upload","file storage","image upload","profile picture","attachment","gallery","media","blob"):!1,d=e?he(e,"admin panel","admin dashboard","admin management"):!1,S=e?he(e,"ai integration","openai","llm","ai chat","chatbot","gpt"):!1,M=e,P=!0;if(!vt(n))return p(`A project already exists at this location (${n}). Choose a different name, or delete the existing folder first. (A .mistflow/ folder left over from planning is fine \u2014 init will preserve it.)`,!0);ge(n,{recursive:!0});try{let u=x(re(n),".mistflow","mockups");if(F(u)){let g=ft(u).filter(v=>v.endsWith(".html"));if(g.length>0){let v=x(n,".mistflow","mockups");ge(v,{recursive:!0});for(let E of g)ss(x(u,E),x(v,E));console.error(`Copied ${g.length} mockup file(s) into project`)}}}catch(u){console.error("Could not copy mockup files:",u instanceof Error?u.message:u)}let m=null;try{m=await st("nextjs")}catch(u){console.error("Could not fetch scaffold from API, using minimal scaffold:",u instanceof Error?u.message:u)}if(m){let u=s.toLowerCase().replace(/[^a-z0-9-]/g,"-");for(let b of m.files){if(b.path==="package.json"||b.path==="middleware.ts"||b.path==="components/sidebar.tsx"||b.path==="components/topnav.tsx"||b.path==="app/(dashboard)/layout.tsx"||b.path==="app/(dashboard)/page.tsx"||b.path==="app/(dashboard)/dashboard/page.tsx"||!c&&(b.path.includes("stripe")||b.path.includes("webhook/stripe"))||!h&&(b.path.includes("resend")||b.path.includes("emails/"))||!d&&(b.path.includes("(admin)")||b.path.includes("admin-sidebar"))||P&&(b.path==="lib/db.ts"||b.path==="lib/auth.ts"||b.path==="drizzle.config.ts"||b.path==="db/schema/auth.ts"))continue;let k=b.content.replace(/\{\{APP_NAME\}\}/g,s).replace(/\{\{WORKER_NAME\}\}/g,u);if(P&&b.path==="next.config.ts"&&(k=k.replace(/serverExternalPackages:\s*\[[^\]]*\],?/g,'serverExternalPackages: ["@electric-sql/pglite"],')),b.path==="next.config.ts"){let q=ns(n);q&&(console.error(`[init] Project is inside monorepo at ${q} \u2014 adding outputFileTracingRoot`),k.includes("outputFileTracingRoot")||(k=k.replace('import type { NextConfig } from "next";',`import type { NextConfig } from "next";
|
|
198
|
-
import { dirname } from "path";
|
|
199
|
-
import { fileURLToPath } from "url";
|
|
200
|
-
|
|
201
|
-
const __dirname = dirname(fileURLToPath(import.meta.url));`),k=k.replace("images: {",`outputFileTracingRoot: __dirname,
|
|
202
|
-
images: {`)))}!d&&b.path.includes("sidebar")&&(k=k.replace(/\{user\.role === "admin"[\s\S]*?<\/Link>\s*\)\}/m,""),k=k.replace(/, Shield/g,"")),y(n,b.path,k)}let g={...m.dependencies},v={...m.devDependencies};if(g["drizzle-zod"]||(g["drizzle-zod"]="^0.5.1"),P&&(delete g["@libsql/client"],g["@neondatabase/serverless"]="^0.10.0",v["@electric-sql/pglite"]="^0.2.0"),c&&(g.stripe="^17.0.0"),h&&(g.resend="^4.0.0",g["@react-email/components"]="^0.0.31"),S&&(g.ai="^4.0.0",g["@ai-sdk/openai"]="^1.0.0",g.openai="^4.0.0"),y(n,"package.json",JSON.stringify({name:s,version:"0.1.0",private:!0,scripts:{dev:"next dev",build:"next build",start:"next start",lint:"next lint","db:push":"drizzle-kit push","db:studio":"drizzle-kit studio"},dependencies:g,devDependencies:v,optionalDependencies:{"@noble/ciphers":"^1.3.0"},overrides:{react:"19.1.0","react-dom":"19.1.0",punycode:"^2.3.1"}},null,2)),m.methodology){let b=m.methodology;P&&(b=b.replace(/sqliteTable/g,"pgTable").replace(/drizzle-orm\/sqlite-core/g,"drizzle-orm/pg-core").replace(/Use `text` for dates \(SQLite stores dates as text\)/g,"Use `timestamp` for dates and `boolean` for booleans (native Postgres types)").replace(/text\("created_at"\)\.notNull\(\)\.default\(sql`\(CURRENT_TIMESTAMP\)`\)/g,'timestamp("created_at").notNull().defaultNow()').replace(/text\("updated_at"\)\.notNull\(\)\.default\(sql`\(CURRENT_TIMESTAMP\)`\)/g,'timestamp("updated_at").notNull().defaultNow()').replace(/import { sqliteTable, text, integer } from "drizzle-orm\/sqlite-core";\nimport { sql } from "drizzle-orm";/g,'import { pgTable, text, timestamp, boolean } from "drizzle-orm/pg-core";').replace(/`drizzle-kit push` for SQLite\/Turso/g,"`drizzle-kit push` for Postgres")),b=Le(b),y(n,"AGENTS.md",b),y(n,"CLAUDE.md",b)}let $=e?.designMd;$&&y(n,"DESIGN.md",$),P&&(y(n,"lib/db.ts",['import { neon } from "@neondatabase/serverless";','import { drizzle as drizzleNeon } from "drizzle-orm/neon-http";',"","// eslint-disable-next-line @typescript-eslint/no-explicit-any","let _db: any = null;","","function getDb() {"," if (!_db) {",' if (process.env.DATABASE_URL && process.env.DATABASE_URL !== "pglite") {'," // Production / remote Postgres"," const sql = neon(process.env.DATABASE_URL);"," _db = drizzleNeon(sql);"," } else {"," // Local dev \u2014 PGlite (zero-install embedded Postgres). Lives in"," // lib/db-local.ts and is loaded through a runtime-only require"," // whose path is built from a variable, so esbuild's static"," // analysis can't follow it. Keeps pglite + its 30MB WASM out of"," // the Worker bundle.",' const localPath: string = "./" + "db-local";'," // eslint-disable-next-line @typescript-eslint/no-require-imports"," const { createLocalDb } = require(localPath);"," _db = createLocalDb();"," }"," }"," return _db;","}","","// Lazy proxy \u2014 DB isn't initialized at import/build time","// eslint-disable-next-line @typescript-eslint/no-explicit-any","export const db: any = new Proxy({} as any, {"," get(_target, prop, receiver) {"," const realDb = getDb();"," const value = Reflect.get(realDb, prop, receiver);",' if (typeof value === "function") {'," return value.bind(realDb);"," }"," return value;"," },","});",""].join(`
|
|
203
|
-
`)),y(n,"lib/db-local.ts",["// Local-dev-only DB factory. Isolated from lib/db.ts so the production","// Cloudflare Worker bundle never loads drizzle-orm/pglite or its","// transitive 30MB WASM binary. Loaded via runtime-only require() from","// db.ts, where the path is built from a variable to defeat static analysis.","",'const pglitePkg: string = ["@electric-sql", "pglite"].join("/");',"// eslint-disable-next-line @typescript-eslint/no-require-imports","const { PGlite } = require(pglitePkg);","",'const drizzlePath: string = "drizzle-orm/" + "pglite";',"// eslint-disable-next-line @typescript-eslint/no-require-imports","const { drizzle } = require(drizzlePath);","","// eslint-disable-next-line @typescript-eslint/no-explicit-any","export function createLocalDb(): any {",' const client = new PGlite("./local.pg");'," return drizzle(client);","}",""].join(`
|
|
204
|
-
`)),y(n,"drizzle.config.ts",['import { defineConfig } from "drizzle-kit";',"","// PGlite for local dev (no Postgres install needed), Mistflow Cloud for production",'const isPglite = !process.env.DATABASE_URL || process.env.DATABASE_URL === "pglite";',"","export default defineConfig({",' schema: "./db/schema",',' out: "./db/migrations",',' dialect: "postgresql",',' ...(isPglite ? { driver: "pglite", dbCredentials: { url: "./local.pg" } } : { dbCredentials: { url: process.env.DATABASE_URL! } }),',"});",""].join(`
|
|
205
|
-
`)),y(n,"db/schema/auth.ts",['import { pgTable, text, boolean, timestamp } from "drizzle-orm/pg-core";',"",'export const user = pgTable("user", {',' id: text("id").primaryKey(),',' name: text("name").notNull(),',' email: text("email").notNull().unique(),',' emailVerified: boolean("email_verified").notNull().default(false),',' image: text("image"),',' role: text("role").default("user"),',' banned: boolean("banned").default(false),',' banReason: text("ban_reason"),',' banExpires: timestamp("ban_expires"),',' createdAt: timestamp("created_at").notNull(),',' updatedAt: timestamp("updated_at").notNull(),',"});","",'export const session = pgTable("session", {',' id: text("id").primaryKey(),',' expiresAt: timestamp("expires_at").notNull(),',' token: text("token").notNull().unique(),',' createdAt: timestamp("created_at").notNull(),',' updatedAt: timestamp("updated_at").notNull(),',' ipAddress: text("ip_address"),',' userAgent: text("user_agent"),',' userId: text("user_id").notNull().references(() => user.id, { onDelete: "cascade" }),',' impersonatedBy: text("impersonated_by"),',"});","",'export const account = pgTable("account", {',' id: text("id").primaryKey(),',' accountId: text("account_id").notNull(),',' providerId: text("provider_id").notNull(),',' userId: text("user_id").notNull().references(() => user.id, { onDelete: "cascade" }),',' accessToken: text("access_token"),',' refreshToken: text("refresh_token"),',' idToken: text("id_token"),',' accessTokenExpiresAt: timestamp("access_token_expires_at"),',' refreshTokenExpiresAt: timestamp("refresh_token_expires_at"),',' scope: text("scope"),',' password: text("password"),',' createdAt: timestamp("created_at").notNull(),',' updatedAt: timestamp("updated_at").notNull(),',"});","",'export const verification = pgTable("verification", {',' id: text("id").primaryKey(),',' identifier: text("identifier").notNull(),',' value: text("value").notNull(),',' expiresAt: timestamp("expires_at").notNull(),',' createdAt: timestamp("created_at"),',' updatedAt: timestamp("updated_at"),',"});",""].join(`
|
|
206
|
-
`)),y(n,"lib/auth.ts",['import { betterAuth } from "better-auth";','import { drizzleAdapter } from "better-auth/adapters/drizzle";','import { admin } from "better-auth/plugins/admin";','import { nextCookies } from "better-auth/next-js";','import { db } from "./db";','import * as schema from "@/db";',"","async function sendEmail({ to, subject, html, fallbackUrl }: { to: string; subject: string; html: string; fallbackUrl?: string }) {"," const apiKey = process.env.RESEND_API_KEY;"," if (!apiKey) {"," if (fallbackUrl) {"," console.error(`\\n[auth] No RESEND_API_KEY set. Email to ${to} was not sent.`);"," console.error(`[auth] Dev fallback \u2014 use this link directly:\\n ${fallbackUrl}\\n`);"," } else {"," console.error(`[auth] RESEND_API_KEY not set \u2014 skipping email send to ${to}`);"," }"," return;"," }",' const from = process.env.EMAIL_FROM || "noreply@mail.mistflow.app";',' const res = await fetch("https://api.resend.com/emails", {',' method: "POST",',' headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json" },'," body: JSON.stringify({ from, to, subject, html }),"," });"," if (!res.ok) {",' const body = await res.text().catch(() => "unknown");'," console.error(`[auth] Email send failed (${res.status}): ${body}`);"," throw new Error(`Email send failed: ${res.status}`);"," }","}","","function createAuth() {",' const baseURL = process.env.BETTER_AUTH_URL || process.env.NEXT_PUBLIC_APP_URL || "http://localhost:3000";',' const isLocal = baseURL.includes("localhost") || baseURL.includes("127.0.0.1");'," const canSendEmail = Boolean(process.env.RESEND_API_KEY);"," // Refuse to boot a production app with email auth but no mail sender. Mistflow's"," // deploy pipeline injects a managed Resend key automatically; if it's missing,"," // that's a real misconfig and silent fallbacks let unverified users sign up."," if (!isLocal && !canSendEmail) {",` throw new Error("[auth] RESEND_API_KEY is required in production. The Mistflow deploy pipeline injects one automatically \u2014 if you're seeing this, check the project's env vars in the dashboard, or set your own RESEND_API_KEY.");`," }"," return betterAuth({"," baseURL,"," trustedOrigins: [baseURL],",' database: drizzleAdapter(db, { provider: "pg", schema }),'," emailAndPassword: {"," enabled: true,"," requireEmailVerification: !isLocal && canSendEmail,"," sendResetPassword: async ({ user, token }: { user: { email: string; name: string }; url: string; token: string }) => {"," // Better Auth's default reset URL points at /reset-password/:token (path),"," // which our frontend doesn't route. We build our own pointing at our"," // /reset-password?token=xxx page which reads the token from the query."," const resetUrl = `${baseURL}/reset-password?token=${token}`;"," await sendEmail({"," to: user.email,",' subject: "Reset your password",',' html: `<p>Hi ${user.name},</p><p>Click the link below to reset your password:</p><p><a href="${resetUrl}">${resetUrl}</a></p>`,'," fallbackUrl: isLocal ? resetUrl : undefined,"," });"," },"," },"," emailVerification: {"," sendOnSignUp: canSendEmail,"," autoSignInAfterVerification: true,"," sendVerificationEmail: async ({ user, url }: { user: { email: string; name: string }; url: string }) => {"," await sendEmail({"," to: user.email,",' subject: "Verify your email address",',' html: `<p>Hi ${user.name},</p><p>Click the link below to verify your email:</p><p><a href="${url}">${url}</a></p>`,'," fallbackUrl: isLocal ? url : undefined,"," });"," },"," },"," secret: process.env.AUTH_SECRET,",' plugins: [admin({ defaultRole: "user" }), nextCookies()],'," databaseHooks: {"," user: {"," create: {"," // Auto-promote the app owner to admin on first signup. ADMIN_EMAIL"," // is injected by the Mistflow deploy pipeline (the email of the"," // account that ran mist_deploy). Email verification still gates"," // login when Resend is configured, so a collision attempt can't"," // actually sign in without clicking a link delivered to the"," // owner's inbox."," before: async (user: { email?: string; [k: string]: unknown }) => {"," const adminEmail = process.env.ADMIN_EMAIL;"," if (adminEmail && user.email?.toLowerCase() === adminEmail.toLowerCase()) {",' return { data: { ...user, role: "admin" } };'," }"," return { data: user };"," },"," },"," },"," },"," socialProviders: {"," ...(process.env.GOOGLE_CLIENT_ID ? {"," google: {"," clientId: process.env.GOOGLE_CLIENT_ID,"," clientSecret: process.env.GOOGLE_CLIENT_SECRET!,"," },"," } : {}),"," ...(process.env.GITHUB_CLIENT_ID ? {"," github: {"," clientId: process.env.GITHUB_CLIENT_ID,"," clientSecret: process.env.GITHUB_CLIENT_SECRET!,"," },"," } : {}),"," },"," });","}","","// Lazy init \u2014 process.env isn't populated at module scope on Cloudflare Workers.","// The `has` trap is required: better-auth's toNextJsHandler does",'// `"handler" in auth ? auth.handler(request) : auth(request)` \u2014 without a `has`',"// trap the default forwards to the empty target object, returns false, and the","// handler tries to call the Proxy as a function, which throws TypeError and","// returns 500 on every /api/auth/* request.","let _auth: ReturnType<typeof createAuth> | null = null;","export const auth = new Proxy({} as ReturnType<typeof createAuth>, {"," get(_target, prop, receiver) {"," if (!_auth) _auth = createAuth();"," const value = Reflect.get(_auth, prop, receiver);",' if (typeof value === "function") return value.bind(_auth);'," return value;"," },"," has(_target, prop) {"," if (!_auth) _auth = createAuth();"," return prop in _auth;"," },","});",""].join(`
|
|
207
|
-
`)))}else y(n,"package.json",JSON.stringify({name:s,version:"0.1.0",private:!0},null,2));let _=e?.designMd;y(n,"app/globals.css",ps(o,l,_)),y(n,"app/layout.tsx",ms(s,o,M?.language)),y(n,"README.md",Ps(s,e,{hasStripe:c,hasResend:h,hasStorage:w,hasAdmin:d,hasAI:S,isNeon:P})),y(n,"contracts/README.md",Oe());let H=e?.dataModel??[],L=new Set,U=0;for(let u of H){let g=u.entity??u.name;if(!g||typeof g!="string")continue;let v=De(g);L.has(v)||(L.add(v),y(n,v,Ue(g)),U++)}U===0&&y(n,"contracts/.gitkeep","");let R=[],O=e?.publicPages;if(Array.isArray(O))R=O;else if(typeof O=="string"){try{R=JSON.parse(O)}catch{R=[]}Array.isArray(R)||(R=[])}if(!R.includes("/")){let u=e?.steps?.some(v=>{let E=((v.name??"")+" "+(v.description??"")).toLowerCase();return E.includes("landing")||E.includes("marketing")||E.includes("homepage")}),g=e?.pages?.some(v=>v.path==="/");(u||g)&&(R=["/",...R])}let T={name:s,summary:e?.summary,authModel:e?.authModel,roles:e?.roles,defaultRole:e?.defaultRole,publicPages:R,navStyle:e?.navStyle,multiTenant:e?.multiTenant,pages:e?.pages,dataModel:e?.dataModel,design:e?.design},ne=gs(T);ne&&y(n,"middleware.ts",ne);let te=fs(T);te&&y(n,te.path,te.content);let ie=ys(T);if(ie&&y(n,"lib/roles.ts",ie),y(n,"app/page.tsx",bs(T)),y(n,"app/(dashboard)/layout.tsx",ws(T,d)),y(n,"app/(dashboard)/dashboard/page.tsx",vs(T)),T.multiTenant){let u=Ss(T,P);u&&y(n,"db/schema/organization.ts",u);let g=xs(T);g&&y(n,"lib/org.ts",g);let v=ks(T);v&&y(n,"components/org-switcher.tsx",v)}y(n,"tsconfig.json",JSON.stringify({compilerOptions:{target:"ES2017",lib:["dom","dom.iterable","esnext"],allowJs:!0,skipLibCheck:!0,strict:!1,noEmit:!0,esModuleInterop:!0,module:"esnext",moduleResolution:"bundler",resolveJsonModule:!0,isolatedModules:!0,jsx:"preserve",incremental:!0,plugins:[{name:"next"}],paths:{"@/*":["./*"]}},include:["next-env.d.ts","**/*.ts","**/*.tsx",".next/types/**/*.ts"],exclude:["node_modules"]},null,2)),c&&y(n,"lib/stripe.ts",['import Stripe from "stripe";',"","let _stripe: Stripe | null = null;","","function getStripe(): Stripe {"," if (!_stripe) {"," if (!process.env.STRIPE_SECRET_KEY) {",' throw new Error("STRIPE_SECRET_KEY is not set");'," }"," _stripe = new Stripe(process.env.STRIPE_SECRET_KEY, {"," typescript: true,"," });"," }"," return _stripe;","}","","// Lazy proxy \u2014 Stripe isn't initialized at import/build time","export const stripe = new Proxy({} as Stripe, {"," get(_target, prop) {"," return (getStripe() as unknown as Record<string | symbol, unknown>)[prop];"," },","});",""].join(`
|
|
208
|
-
`)),h&&(y(n,"lib/resend.ts",['import { Resend } from "resend";',"","let _resend: Resend | null = null;","","function getResend(): Resend {"," if (!_resend) {"," if (!process.env.RESEND_API_KEY) {",' throw new Error("RESEND_API_KEY is not set");'," }"," _resend = new Resend(process.env.RESEND_API_KEY);"," }"," return _resend;","}","","// Lazy proxy \u2014 Resend isn't initialized at import/build time","export const resend = new Proxy({} as Resend, {"," get(_target, prop) {"," return (getResend() as unknown as Record<string | symbol, unknown>)[prop];"," },","});",""].join(`
|
|
209
|
-
`)),y(n,"lib/email.ts",['import { resend } from "./resend";',"",'const FROM = process.env.EMAIL_FROM ?? "onboarding@resend.dev";',"","export async function sendEmail({"," to,"," subject,"," react,","}: {"," to: string;"," subject: string;"," react: React.ReactElement;","}) {"," return resend.emails.send({ from: FROM, to, subject, react });","}",""].join(`
|
|
210
|
-
`))),w&&(y(n,"lib/storage.ts",['const MISTFLOW_API = process.env.MISTFLOW_API_URL ?? "https://api.mistflow.ai";',"const MISTFLOW_API_KEY = process.env.MISTFLOW_API_KEY;","const PROJECT_ID = process.env.MISTFLOW_PROJECT_ID;","","interface UploadResult {"," upload_url: string;"," download_url: string;"," key: string;","}","","function authHeaders(): Record<string, string> {"," return {",' "Content-Type": "application/json",',' "Authorization": `ApiKey ${MISTFLOW_API_KEY}`,'," };","}","",'export async function getUploadUrl(filename: string, contentType: string = "application/octet-stream"): Promise<UploadResult> {'," const res = await fetch(`${MISTFLOW_API}/api/storage/upload-url`, {",' method: "POST",'," headers: authHeaders(),"," body: JSON.stringify({ project_id: PROJECT_ID, filename, content_type: contentType }),"," });"," if (!res.ok) throw new Error(`Storage error: ${res.status}`);"," return res.json();","}","","export async function getDownloadUrl(filename: string): Promise<string> {"," const res = await fetch(`${MISTFLOW_API}/api/storage/download-url`, {",' method: "POST",'," headers: authHeaders(),"," body: JSON.stringify({ project_id: PROJECT_ID, filename }),"," });"," if (!res.ok) throw new Error(`Storage error: ${res.status}`);"," const data = await res.json();"," return data.download_url;","}","","export async function deleteFile(filename: string): Promise<void> {"," await fetch(`${MISTFLOW_API}/api/storage/delete`, {",' method: "POST",'," headers: authHeaders(),"," body: JSON.stringify({ project_id: PROJECT_ID, filename }),"," });","}","","export async function uploadFile(file: File): Promise<string> {"," const { upload_url, download_url } = await getUploadUrl(file.name, file.type);",' await fetch(upload_url, { method: "PUT", body: file, headers: { "Content-Type": file.type } });'," return download_url;","}",""].join(`
|
|
211
|
-
`)),y(n,"app/api/upload/route.ts",['import { NextRequest, NextResponse } from "next/server";','import { getUploadUrl } from "@/lib/storage";',"","export async function POST(req: NextRequest) {"," const { filename, contentType } = await req.json();"," if (!filename) {",' return NextResponse.json({ error: "filename is required" }, { status: 400 });'," }"," try {",' const result = await getUploadUrl(filename, contentType ?? "application/octet-stream");'," return NextResponse.json(result);"," } catch {",' return NextResponse.json({ error: "Failed to get upload URL" }, { status: 500 });'," }","}",""].join(`
|
|
212
|
-
`))),S&&(y(n,"lib/ai.ts",['import { createOpenAI } from "@ai-sdk/openai";',"","export const openai = createOpenAI({"," apiKey: process.env.OPENAI_API_KEY,","});",""].join(`
|
|
213
|
-
`)),y(n,"app/api/chat/route.ts",['import { openai } from "@/lib/ai";','import { streamText } from "ai";',"","export async function POST(req: Request) {"," const { messages } = await req.json();",""," const result = streamText({",' model: openai("gpt-4o"),'," messages,"," });",""," return result.toDataStreamResponse();","}",""].join(`
|
|
214
|
-
`)));let ke={name:s,methodologyVersion:m?.version??"1.0",createdAt:new Date().toISOString(),...a?{planId:a}:{},plan:Array.isArray(e?.steps)?{...e,steps:e.steps.map(u=>({number:u.number,name:u.name??u.title,description:u.description,entities:u.entities,pages:u.pages,features:u.features,status:"pending"}))}:e,dbProvider:"neon",env:{managed:{DATABASE_URL:{description:"Postgres connection URL",scope:"production"},AUTH_SECRET:{description:"Auth encryption secret",scope:"production"},...c?{STRIPE_SECRET_KEY:{description:"Stripe secret key",scope:"production"},STRIPE_WEBHOOK_SECRET:{description:"Stripe webhook signing secret",scope:"production"},NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY:{description:"Stripe publishable key",scope:"production"}}:{},...h?{RESEND_API_KEY:{description:"Resend API key \u2014 managed by Mistflow by default, override with your own key from resend.com",scope:"production"},EMAIL_FROM:{description:"Sender email address \u2014 managed by Mistflow by default",scope:"production"}}:{},...w?{MISTFLOW_API_KEY:{description:"Mistflow API key for file storage",scope:"production"},MISTFLOW_PROJECT_ID:{description:"Mistflow project ID",scope:"production"}}:{}},...S?{required:{OPENAI_API_KEY:{description:"OpenAI API key",setupUrl:"https://platform.openai.com/api-keys"}}}:{}},authModel:e?.authModel??"email",roles:e?.roles??null,navStyle:e?.navStyle??"sidebar",multiTenant:e?.multiTenant??!1,hasAdmin:d,hasResend:h,hasStorage:w,hasAI:S,deploy:null};y(n,"mistflow.json",JSON.stringify(ke,null,2)),wt(n,e);let Pe=os(32).toString("hex"),ae=c?`
|
|
215
|
-
# Stripe
|
|
216
|
-
STRIPE_SECRET_KEY=
|
|
217
|
-
STRIPE_WEBHOOK_SECRET=
|
|
218
|
-
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=
|
|
219
|
-
`:"",Y=h?`
|
|
220
|
-
# Email (Resend)
|
|
221
|
-
RESEND_API_KEY=
|
|
222
|
-
EMAIL_FROM=onboarding@resend.dev
|
|
223
|
-
`:"",K=w?`
|
|
224
|
-
# File Storage (Mistflow managed)
|
|
225
|
-
MISTFLOW_API_KEY=
|
|
226
|
-
MISTFLOW_PROJECT_ID=
|
|
227
|
-
`:"",le=S?`
|
|
228
|
-
# AI (get your key at https://platform.openai.com/api-keys)
|
|
229
|
-
OPENAI_API_KEY=
|
|
230
|
-
`:"",ce=`# Local dev: PGlite is used automatically (zero-install embedded Postgres)
|
|
231
|
-
# Set DATABASE_URL only for production or to use a remote Postgres
|
|
232
|
-
# DATABASE_URL=postgresql://postgres:postgres@localhost:5432/devdb`,se=`# Local dev: PGlite is used automatically (zero-install embedded Postgres)
|
|
233
|
-
# Set DATABASE_URL only for production or to use a remote Postgres
|
|
234
|
-
# DATABASE_URL=postgresql://postgres:postgres@localhost:5432/devdb`;y(n,".env.local",`${ce}
|
|
235
|
-
AUTH_SECRET=${Pe}
|
|
236
|
-
${ae}${Y}${K}${le}`),y(n,".env.example",`${se}
|
|
237
|
-
AUTH_SECRET=your-secret-here
|
|
238
|
-
${ae}${Y}${K}${le}`);let I=[],G=(u,g)=>{I.push({phase:u,message:g})},J=(u,g)=>{let v=I.find(E=>E.phase===u&&!E.durationMs);v&&(v.durationMs=g)};if(t){let u=lt(t.server,t.progressToken,()=>I[I.length-1]?.message??"Setting up project...");t.cleanup=()=>u.stop()}let _e=M?.requestedSubdomain||void 0,N,f;G("register","Registering project on Mistflow...");let C=Date.now();try{let u=await Ze(s,void 0,"neon",_e);N=u.id;let g=x(n,"mistflow.json"),v=JSON.parse(fe(g,"utf-8"));if(v.projectId=N,z(g,JSON.stringify(v,null,2)),Ae(n,Ne(N,s)),u.managed_env&&Object.keys(u.managed_env).length>0){let E=x(n,".env.local"),D=F(E)?fe(E,"utf-8"):"";for(let[$,b]of Object.entries(u.managed_env)){let k=new RegExp(`^${$}=.*$`,"m");k.test(D)?D=D.replace(k,`${$}=${b}`):D+=`
|
|
239
|
-
${$}=${b}`}z(E,D)}try{let{getBaseUrl:E,getAuthHeaders:D}=await import("./api-client-ORACYR2I.js"),$=D(),b=e?.features,k=e?.steps,q={};Array.isArray(b)&&b.length>0&&(q.features=b.map(Z=>Z.name)),e&&(q.plan=e),Array.isArray(k)&&k.length>0&&(q.provenance=k.map(Z=>({feature:Z.name??Z.title??`Step ${Z.number??"?"}`,user_intent:(Z.description??"").slice(0,500),decisions:"Seeded from plan at init",tradeoffs:"",files_affected:[]}))),Object.keys(q).length>0&&await fetch(`${E()}/api/projects/${encodeURIComponent(N)}/state`,{method:"PUT",headers:{...$,"Content-Type":"application/json"},body:JSON.stringify(q)})}catch{}I[I.length-1].message=`Registered as ${N.slice(0,8)}`}catch(u){let g=u instanceof Error?u.message:String(u);console.error("Could not register project on backend:",g),f=`Project created locally but NOT registered on Mistflow servers (${g}). Deploy will auto-register it.`,I[I.length-1].message="Registration skipped (offline \u2014 deploy will retry)"}J("register",Date.now()-C),G("git","Initializing git repository...");let Ie=Date.now();try{let u=bt(n);await u.init(),await u.add("."),await u.commit("Initial Mistflow project setup"),I[I.length-1].message="Git repository initialized"}catch{console.error("Git initialization failed, continuing without git."),I[I.length-1].message="Git init skipped"}J("git",Date.now()-Ie);let zt=I.reduce((u,g)=>u+(g.durationMs??0),0),X={projectPath:n,projectId:N,status:"awaiting_install"},Ft=I.map(u=>{let g=u.durationMs?` (${(u.durationMs/1e3).toFixed(1)}s)`:"";return`${u.message}${g}`});X.progress=Ft,X.totalSetupTime=`${(zt/1e3).toFixed(1)}s`;let Re=[];N||Re.push("Project was not registered with Mistflow (not signed in). Run mist_setup to sign in BEFORE deploying \u2014 deploy will fail without it."),f&&(X.registrationWarning=f),Re.length>0&&(X.warnings=Re);let Ve=`NEXT: Run \`npx -y @mistflow-ai/cli install ${n}\` via your shell/bash tool \u2014 NOT via the mist_build MCP tool. The CLI streams npm output live and has no MCP 60s timeout; the MCP install action can hit -32001 timeout on slow installs. Do NOT ask the user for permission \u2014 install is a required follow-up to init, not a decision point. If your environment can't run the CLI, fall back to mist_build action='install' projectPath='${n}'. After install finishes, call mist_build action='implement' to build the first plan step \u2014 also without asking.`;return X.nextAction=N?Ve:`${Ve} IMPORTANT: You MUST also run mist_setup to sign in before deploying \u2014 the project could not be registered because auth is missing.`,p(JSON.stringify(X))}async function xt(i){let{name:t,plan:s,projectId:e,sourceDeploymentId:r,forkToken:a,requiredEnvVars:n,dbProvider:o,planId:l}=i;if(!i.path)return p("mist_build init requires an explicit 'path' \u2014 the absolute directory where the forked project should be scaffolded. Pass the user's project directory (e.g. /Users/alice/projects/my-app). Do not rely on a default.",!0);if(!yt(i.path))return p(`mist_build init 'path' must be an absolute path \u2014 received '${i.path}'. Pass the full absolute path to the target directory.`,!0);let c=ze(i.path);if(!vt(c))return p(`A project already exists at this location (${c}). Choose a different name, or delete the existing folder first. (A .mistflow/ folder left over from planning is fine \u2014 init will preserve it.)`,!0);let{mkdtempSync:h,renameSync:w,rmSync:d,cpSync:S}=await import("fs"),{tmpdir:M}=await import("os"),P=h(x(M(),"mistflow-fork-")),m=x(P,"project");ge(m,{recursive:!0});let _=[],H=(U,R)=>_.push({phase:U,message:R}),L=(U,R)=>{let O=_.find(T=>T.phase===U);O&&(O.durationMs=R)};try{H("download","Downloading source code from template...");let U=Date.now(),R=x(P,"source.tar.gz");try{await rt(r,a,R)}catch(f){d(P,{recursive:!0,force:!0});let C=f instanceof Error?f.message:"Source download failed";return p(`Source code download failed: ${C}. You can still build from the plan \u2014 run mist_build init without the source (omit planId and pass the plan directly).`,!0)}L("download",Date.now()-U),_[_.length-1].message="Source code downloaded",H("extract","Extracting source code...");let O=Date.now(),T=await $e("tar",["-xzf",R,"-C",m,"--exclude","node_modules","--exclude",".git"],m,6e4);if(!T.success)return d(P,{recursive:!0,force:!0}),p(`Failed to extract source archive: ${T.error}`,!0);if(L("extract",Date.now()-O),!F(x(m,"package.json")))return d(P,{recursive:!0,force:!0}),p("Source archive does not contain a package.json. The template may be corrupted.",!0);let ne=[".mistflow",".env.local",".env.example","local.db","local.pg"];for(let f of ne){let C=x(m,f);F(C)&&d(C,{recursive:!0,force:!0})}let te={name:t,projectId:e,template:"nextjs",createdAt:new Date().toISOString(),planId:l??void 0,plan:s,dbProvider:o};z(x(m,"mistflow.json"),JSON.stringify(te,null,2)),wt(m,s);let ie=n.map(f=>{let C=f.description?`# ${f.description}`:`# ${f.key}`,Ie=f.setup_url?` (${f.setup_url})`:"";return`${C}${Ie}
|
|
240
|
-
${f.key}=`});z(x(m,".env.local"),ie.join(`
|
|
241
|
-
|
|
242
|
-
`)+`
|
|
243
|
-
`);let ke=n.map(f=>`${f.key}=`);z(x(m,".env.example"),ke.join(`
|
|
244
|
-
`)+`
|
|
245
|
-
`),z(x(m,"README.md"),`# ${t}
|
|
246
|
-
|
|
247
|
-
Forked from a Mistflow template. Built with Next.js, Drizzle ORM, and Better Auth.
|
|
248
|
-
|
|
249
|
-
## Getting Started
|
|
250
|
-
|
|
251
|
-
1. Copy \`.env.example\` to \`.env.local\` and fill in your values
|
|
252
|
-
2. Run \`npm run dev\`
|
|
253
|
-
3. Deploy with \`mist_deploy\`
|
|
254
|
-
`),H("setup","Setting up project directory...");let Pe=Date.now();try{w(m,c)}catch(f){if(f.code==="EXDEV")S(m,c,{recursive:!0}),d(m,{recursive:!0,force:!0});else throw f}L("setup",Date.now()-Pe),H("install","Installing packages...");let ae=Date.now(),Y=0,K=await $e("npm",["install"],c,12e4,f=>{let C=f.match(/added (\d+) packages/);C&&(Y=parseInt(C[1],10))});if(K.success||(console.error("[initFromSource] npm install failed, retrying..."),K=await $e("npm",["install"],c,12e4,f=>{let C=f.match(/added (\d+) packages/);C&&(Y=parseInt(C[1],10))})),!K.success)return p(`Source code was restored at ${c} but package installation failed: ${K.error}. Run "npm install" manually.`,!0);L("install",Date.now()-ae),_[_.length-1].message=`Installed ${Y||"all"} packages`,H("git","Initializing git repository...");let le=Date.now();try{let f=bt(c);await f.init(),await f.add("."),await f.commit("Forked from Mistflow template"),_[_.length-1].message="Git repository initialized"}catch{console.error("Git initialization failed, continuing without git."),_[_.length-1].message="Git init skipped"}L("git",Date.now()-le);try{let{markLocalSetupDone:f}=await import("./api-client-ORACYR2I.js");await f(e)}catch{console.error("[initFromSource] Could not mark local_setup_done on backend, continuing.")}let ce=JSON.parse(fe(x(c,"mistflow.json"),"utf-8"));ce.projectId=e,z(x(c,"mistflow.json"),JSON.stringify(ce,null,2)),Ae(c,Ne(e,t));let se=s.steps,I=se?.filter(f=>f.status==="completed").length??0,G=se?.filter(f=>f.status==="pending").length??0,J=se?.length??0,_e=n.length>0?`
|
|
255
|
-
|
|
256
|
-
Environment variables needed:
|
|
257
|
-
`+n.map(f=>` \u2022 ${f.key}${f.description?` \u2014 ${f.description}`:""}`).join(`
|
|
258
|
-
`):"",N=G>0?`Source code restored with ${I}/${J} steps complete. ${G} steps need implementation \u2014 call mist_build with action='implement' to apply your changes.`:`Source code fully restored (${J} steps complete). Configure your .env.local, then deploy with mist_deploy.`;return p(JSON.stringify({status:"success",projectPath:c,projectId:e,planStepsCompleted:I,planStepsTotal:J,pendingSteps:G,progress:_,nextAction:N+_e}))}finally{try{d(P,{recursive:!0,force:!0})}catch{}}}var Fe={},Q=[];function ye(i){let t=Q.find(e=>e.id===i);if(t)return t;let s=i.toLowerCase().replace(/[^a-z0-9]/g,"");return Q.find(e=>{let r=e.title.toLowerCase().replace(/[^a-z0-9]/g,"");return r===s||r.includes(s)||s.includes(r)})}function kt(i){return Fe[i]}function Be(i){return i?Q.filter(t=>t.category.toLowerCase()===i.toLowerCase()):Q}function Pt(i){let t=i??Q;if(t.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 s={};for(let r of t){s[r.category]||(s[r.category]=[]);let a=Fe[r.id],n=a?` \u2014 ${a.description}`:"";s[r.category].push(`${r.id} \u2014 "${r.title}"${n}`)}let e=[];for(let[r,a]of Object.entries(s))e.push(`**${r}**:
|
|
259
|
-
${a.map(n=>` \u2022 ${n}`).join(`
|
|
260
|
-
`)}`);return e.join(`
|
|
261
|
-
|
|
262
|
-
`)}function _s(i){return i.toLowerCase().replace(/[^a-z0-9]+/g,"-").replace(/^-|-$/g,"")}function _t(i){return(i?Be(i):Q).map(s=>{let e=Fe[s.id];return{id:s.id,slug:_s(s.title),title:s.title,category:s.category,description:e?.description??"",tags:e?.tags??[],theme:e?.theme??"dark",colors:e?.colors??[],style:e?.style??""}})}function Cs(i){let t=Es(Ts(),".mistflow","plans",`${i}.json`);if(!Is(t))return null;try{let s=JSON.parse(Rs(t,"utf-8"));return s.plan?s:null}catch{return null}}var As=j.object({action:j.enum(["init"]).describe("'init' creates and sets up a new project from your plan \u2014 writes files, registers the project, initializes git. Returns fast; does NOT run npm install."),name:j.string().optional().describe("(init) Project name"),planId:j.string().optional().describe("(init) Plan ID from mist_plan"),plan:j.any().optional().describe("(init) Full plan object \u2014 use planId instead when available"),path:j.string().optional().describe("(init) REQUIRED for action='init'. Absolute path to the target directory where the project should be scaffolded (e.g. /Users/alice/projects/my-app). Do not omit \u2014 there is no default. If unsure, ask the user for their working directory."),landingDesign:j.string().optional().describe("(init) Landing design ID to apply to the landing page. Can be set here if not set during mist_plan. Use mist_project action='landing-designs' to browse."),appStyle:j.string().optional().describe("(init) Optional aesthetic-direction hint passed to the backend LLM. DESIGN.md is generated per-product; this string is a hint, not a catalog ID."),confirmDarkTheme:j.boolean().optional().describe("(init) Set to true only after confirming with the user that a dark-themed app style is intentional for a consumer (b2c) app. Skips the dark-on-consumer-app warning."),heroPhoto:j.boolean().optional().describe("(init) Whether the landing page hero uses a lifestyle photo background. true = Unsplash photo + overlaid glass card (HabitFlow, Airbnb). false = pure CSS gradients + glassmorphism (Stripe, Linear). Pass the user's answer to the heroPhotoQuestion from mist_plan. If omitted, defaults based on the plan's audienceType (b2c \u2192 photo, else \u2192 CSS).")}),It={name:"mist_build",description:"STEP 2 of the Mistflow workflow \u2014 scaffolds a new Mistflow project from a plan. 'init' writes files, registers the project, and initializes git. Returns fast (~10s); does NOT run npm install. Pass the planId returned by mist_plan \u2014 do NOT pass the full plan object. All other build steps (install, implement, build, debug, qa, mockup) moved to the `mist` CLI \u2014 invoke via your shell/bash tool: `npx -y @mistflow-ai/cli <command>`. Call `mist_help` for the full CLI reference.",inputSchema:As,handler:async(i,t)=>{let s=i;switch(s.action){case"init":{if(!s.name)return p("Project name is required for init.",!0);let e=s.plan,r=null;if(s.planId){if(r=Cs(s.planId),!r)return p(`Plan not found for planId '${s.planId}'. The plan may have expired. Call mist_plan again to generate a new plan.`,!0);e=r.plan}if(!e)return p("No plan provided. Pass the planId returned by mist_plan, or call mist_plan first to generate a plan.",!0);if(!Array.isArray(e?.steps)||e.steps.length===0)return p("The plan is missing a 'steps' array. This usually means the plan generation was incomplete. Call mist_plan again with the same description to get a complete plan with implementation steps.",!0);if(r?.sourceDeploymentId&&r?.forkToken&&r?.projectId)return xt({name:s.name,plan:e,path:s.path,projectId:r.projectId,sourceDeploymentId:r.sourceDeploymentId,forkToken:r.forkToken,requiredEnvVars:r.requiredEnvVars??[],dbProvider:r.dbProvider??"neon",planId:s.planId});if(s.landingDesign){let c=ye(s.landingDesign);c?e.landingDesign=c.id:console.error(`Landing design '${s.landingDesign}' not found \u2014 ignoring.`)}s.appStyle&&(e.appStyle=s.appStyle);let a=e.design,o=e.audienceType==="b2c",l=s.heroPhoto??o;return e.design={...a??{},heroPhoto:l},St({name:s.name,plan:e,path:s.path,planId:s.planId},t)}default:return p(`Unknown action: ${s.action}. Only 'init' is supported on mist_build. All other build steps (install, implement, build, debug, qa, mockup) moved to the \`mist\` CLI \u2014 invoke via your shell/bash tool: \`npx -y @mistflow-ai/cli <command>\`. Call \`mist_help\` for the full CLI reference.`,!0)}}};import{z as A}from"zod";import{resolve as be}from"path";import{existsSync as we,readFileSync as ve}from"fs";import{join as Se}from"path";import{z as B}from"zod";import{resolve as Ms,join as Et}from"path";import{existsSync as Os,readFileSync as Tt,writeFileSync as Ls}from"fs";import{existsSync as Ns,readFileSync as js}from"fs";function Rt(i){let t=new Set;if(!Ns(i))return t;let s=js(i,"utf-8");for(let e of s.split(`
|
|
263
|
-
`)){let r=e.trim();if(!r||r.startsWith("#"))continue;let a=r.indexOf("=");if(a>0){let n=r.slice(0,a).trim(),o=r.slice(a+1).trim();o&&o!=='""'&&o!=="''"&&t.add(n)}}return t}var Us=B.object({action:B.enum(["get","update"]).default("get").describe("'get' reads current project state. 'update' modifies it."),projectPath:B.string().optional().describe("Path to the project directory (default: current working directory)"),completedStep:B.number().optional().describe("(update only) Mark a plan step as completed by step number"),addEnvVar:B.object({key:B.string(),description:B.string().optional(),setupUrl:B.string().optional()}).optional().describe("(update only) Add a required env var to the project manifest")}),Ct={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:Us,handler:async i=>{let t=i,s=Ms(t.projectPath??process.cwd()),e=Et(s,"mistflow.json");if(!Os(e))return V(s);let r;try{r=JSON.parse(Tt(e,"utf-8"))}catch{return p("Failed to parse mistflow.json.",!0)}if(t.action==="get"){if(!r.projectId)try{let{ensureBackendRegistered:m}=await import("./self-heal-X2KBLZZU.js");await m(s)&&(r=JSON.parse(Tt(e,"utf-8")))}catch{}let o=r.plan,l=o?.steps?.filter(m=>m.status==="completed").length??0,c=o?.steps?.length??0,h=Rt(Et(s,".env.local")),w=r.env?.required?Object.entries(r.env.required).map(([m,_])=>({name:m,description:_?.description,configured:h.has(m)})):[];r.projectId&&import("./state-manager-W7N7IHSQ.js").then(({fetchRemoteState:m})=>m(r.projectId)).catch(()=>{});let d=[`Project: ${r.name}`];if(o){d.push(`Plan: ${o.summary??o.name??"unnamed"} \u2014 ${l}/${c} steps complete`);for(let m of o.steps){let _=m.status==="completed"?"\u2713":m.status==="in_progress"?"\u2192":" ";d.push(` [${_}] ${m.number}. ${m.name}`)}}let S=w.filter(m=>!m.configured);S.length>0&&d.push(`Missing env vars: ${S.map(m=>m.name).join(", ")}`),r.deploy?.url?d.push(`Deployed: ${r.deploy.url} (${r.deploy.count??0} deploys)`):d.push("Not deployed yet");let M=[],P=o?.steps?.find(m=>m.status!=="completed");return P?M.push(`NEXT: Call mist_build with action='implement' to work on step ${P.number} (${P.name}).`):o&&l===c&&(r.deploy?.url||M.push("NEXT: All steps complete! Call mist_deploy with action='deploy' to deploy the app now. Do NOT ask the user \u2014 just deploy.")),S.length>0&&M.push(`Missing env vars in .env.local: ${S.map(m=>m.name).join(", ")}`),p(JSON.stringify({name:r.name,projectId:r.projectId,planProgress:o?{name:o.name,summary:o.summary,totalSteps:c,completedSteps:l,steps:o.steps}:null,envStatus:w,deploy:r.deploy??null,contextMessage:d.join(`
|
|
264
|
-
`),nextSteps:M}))}let a=[];if(t.completedStep!==void 0){let o=r.plan;if(o?.steps){let l=o.steps.findIndex(c=>c.number===t.completedStep);if(l===-1)return p(`Step ${t.completedStep} not found in the plan.`,!0);o.steps[l].status="completed",a.push(`Step ${t.completedStep} marked as completed`)}}t.addEnvVar&&(r.env||(r.env={required:{}}),r.env.required||(r.env.required={}),r.env.required[t.addEnvVar.key]={description:t.addEnvVar.description,setupUrl:t.addEnvVar.setupUrl},a.push(`Added required env var: ${t.addEnvVar.key}`)),Ls(e,JSON.stringify(r,null,2)+`
|
|
265
|
-
`),r.projectId&&import("./state-manager-W7N7IHSQ.js").then(async({readLocalState:o,syncRemoteState:l})=>{let c=o(s);c&&await l(r.projectId,c)}).catch(()=>{});let n=[];if(t.completedStep!==void 0){let l=r.plan?.steps?.find(c=>c.status!=="completed");l?n.push(`NEXT: Call mist_build with action='implement' to work on step ${l.number} (${l.name}). Do this now.`):n.push("NEXT: All steps complete! Call mist_deploy with action='deploy' to deploy the app now. Do NOT suggest localhost.")}return t.addEnvVar&&(n.push(`Add ${t.addEnvVar.key} to your .env.local file`),t.addEnvVar.setupUrl&&n.push(`Get the value from: ${t.addEnvVar.setupUrl}`)),p(JSON.stringify({updated:!0,changes:a,message:a.length>0?`Project state saved. ${a.join(". ")}.`:"No changes made.",nextSteps:n.length>0?n:void 0}))}};var He={"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"}},ee=[{id:"resend-email",name:"Resend Email",category:"communication",prompt:`## Resend Email Integration
|
|
266
|
-
|
|
267
|
-
### File Structure
|
|
268
|
-
\`\`\`
|
|
269
|
-
lib/resend.ts \u2014 Resend client singleton
|
|
270
|
-
lib/email.ts \u2014 Send helper functions (sendWelcomeEmail, sendNotification, etc.)
|
|
271
|
-
emails/ \u2014 React Email templates directory
|
|
272
|
-
welcome.tsx \u2014 Welcome email template
|
|
273
|
-
notification.tsx \u2014 Generic notification template
|
|
274
|
-
app/api/webhooks/resend/route.ts \u2014 Webhook handler for delivery events
|
|
275
|
-
\`\`\`
|
|
276
|
-
|
|
277
|
-
### Client Setup (lib/resend.ts)
|
|
278
|
-
\`\`\`typescript
|
|
279
|
-
import { Resend } from "resend";
|
|
280
|
-
|
|
281
|
-
export const resend = new Resend(process.env.RESEND_API_KEY);
|
|
282
|
-
\`\`\`
|
|
283
|
-
|
|
284
|
-
### Send Helper (lib/email.ts)
|
|
285
|
-
\`\`\`typescript
|
|
286
|
-
import { resend } from "./resend";
|
|
287
|
-
import WelcomeEmail from "@/emails/welcome";
|
|
288
|
-
|
|
289
|
-
export async function sendWelcomeEmail(to: string, name: string) {
|
|
290
|
-
const { data, error } = await resend.emails.send({
|
|
291
|
-
from: "App Name <noreply@yourdomain.com>",
|
|
292
|
-
to,
|
|
293
|
-
subject: "Welcome to App Name",
|
|
294
|
-
react: WelcomeEmail({ name }),
|
|
295
|
-
});
|
|
296
|
-
if (error) throw new Error(error.message);
|
|
297
|
-
return data;
|
|
298
|
-
}
|
|
299
|
-
\`\`\`
|
|
300
|
-
|
|
301
|
-
### React Email Template (emails/welcome.tsx)
|
|
302
|
-
\`\`\`tsx
|
|
303
|
-
import { Html, Head, Body, Container, Heading, Text, Button, Section } from "@react-email/components";
|
|
304
|
-
|
|
305
|
-
interface WelcomeEmailProps { name: string }
|
|
306
|
-
|
|
307
|
-
export default function WelcomeEmail({ name }: WelcomeEmailProps) {
|
|
308
|
-
return (
|
|
309
|
-
<Html>
|
|
310
|
-
<Head />
|
|
311
|
-
<Body style={{ backgroundColor: "#f6f9fc", fontFamily: "sans-serif" }}>
|
|
312
|
-
<Container style={{ maxWidth: 560, margin: "0 auto", padding: "20px 0" }}>
|
|
313
|
-
<Heading style={{ fontSize: 24, color: "#1a1a1a" }}>Welcome, {name}!</Heading>
|
|
314
|
-
<Text style={{ fontSize: 16, color: "#4a4a4a", lineHeight: 1.6 }}>
|
|
315
|
-
Thanks for signing up. Here's how to get started...
|
|
316
|
-
</Text>
|
|
317
|
-
<Section style={{ textAlign: "center", marginTop: 24 }}>
|
|
318
|
-
<Button href="https://yourapp.com/dashboard" style={{
|
|
319
|
-
backgroundColor: "#000", color: "#fff", padding: "12px 24px",
|
|
320
|
-
borderRadius: 6, fontSize: 14, fontWeight: 600,
|
|
321
|
-
}}>
|
|
322
|
-
Go to Dashboard
|
|
323
|
-
</Button>
|
|
324
|
-
</Section>
|
|
325
|
-
</Container>
|
|
326
|
-
</Body>
|
|
327
|
-
</Html>
|
|
328
|
-
);
|
|
329
|
-
}
|
|
330
|
-
\`\`\`
|
|
331
|
-
|
|
332
|
-
### Webhook Handler (app/api/webhooks/resend/route.ts)
|
|
333
|
-
\`\`\`typescript
|
|
334
|
-
import { NextRequest, NextResponse } from "next/server";
|
|
335
|
-
|
|
336
|
-
export async function POST(req: NextRequest) {
|
|
337
|
-
const body = await req.json();
|
|
338
|
-
const { type, data } = body;
|
|
339
|
-
// Handle: email.sent, email.delivered, email.bounced, email.complained
|
|
340
|
-
switch (type) {
|
|
341
|
-
case "email.bounced":
|
|
342
|
-
console.error("Email bounced:", data.to);
|
|
343
|
-
// Mark user email as invalid in your DB
|
|
344
|
-
break;
|
|
345
|
-
}
|
|
346
|
-
return NextResponse.json({ received: true });
|
|
347
|
-
}
|
|
348
|
-
\`\`\`
|
|
349
|
-
|
|
350
|
-
### Common Pitfalls
|
|
351
|
-
1. **Domain verification required for production.** Use Resend's free onboarding@resend.dev for dev, but production requires a verified domain.
|
|
352
|
-
2. **Rate limits.** Free tier: 100 emails/day, 3000/month. Queue emails for bulk sends.
|
|
353
|
-
3. **From address must match a verified domain** or use onboarding@resend.dev for testing.
|
|
354
|
-
4. **Webhooks need a public URL.** Use ngrok or similar for local webhook testing.
|
|
355
|
-
5. **React Email templates must be Server Components.** Do not add "use client" to email templates.
|
|
356
|
-
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
|
|
357
|
-
|
|
358
|
-
Files are stored in Mistflow Cloud's managed blob storage. No extra setup needed for Mistflow-deployed apps.
|
|
359
|
-
|
|
360
|
-
### File Structure
|
|
361
|
-
\`\`\`
|
|
362
|
-
lib/storage.ts \u2014 Upload/download helpers
|
|
363
|
-
app/api/upload/route.ts \u2014 Upload API route (server-side)
|
|
364
|
-
components/file-upload.tsx \u2014 Drag-and-drop upload component
|
|
365
|
-
\`\`\`
|
|
366
|
-
|
|
367
|
-
### Upload API Route (app/api/upload/route.ts)
|
|
368
|
-
\`\`\`typescript
|
|
369
|
-
import { NextRequest, NextResponse } from "next/server";
|
|
370
|
-
|
|
371
|
-
export async function POST(req: NextRequest) {
|
|
372
|
-
const formData = await req.formData();
|
|
373
|
-
const file = formData.get("file") as File;
|
|
374
|
-
if (!file) return NextResponse.json({ error: "No file provided" }, { status: 400 });
|
|
375
|
-
|
|
376
|
-
// Validate file size (10MB max)
|
|
377
|
-
if (file.size > 10 * 1024 * 1024) {
|
|
378
|
-
return NextResponse.json({ error: "File too large (max 10MB)" }, { status: 400 });
|
|
379
|
-
}
|
|
380
|
-
|
|
381
|
-
// Validate MIME type
|
|
382
|
-
const allowedTypes = ["image/jpeg", "image/png", "image/webp", "application/pdf"];
|
|
383
|
-
if (!allowedTypes.includes(file.type)) {
|
|
384
|
-
return NextResponse.json({ error: "File type not allowed" }, { status: 400 });
|
|
385
|
-
}
|
|
386
|
-
|
|
387
|
-
const bytes = await file.arrayBuffer();
|
|
388
|
-
const buffer = Buffer.from(bytes);
|
|
389
|
-
|
|
390
|
-
// Upload to R2 via Mistflow's storage endpoint
|
|
391
|
-
const key = \`uploads/\${Date.now()}-\${file.name.replace(/[^a-zA-Z0-9.-]/g, "_")}\`;
|
|
392
|
-
// Store buffer with key \u2014 implementation depends on R2 binding or S3-compatible client
|
|
393
|
-
// For Mistflow apps: the deploy pipeline auto-configures R2 bindings
|
|
394
|
-
|
|
395
|
-
return NextResponse.json({ url: \`/api/files/\${key}\`, key });
|
|
396
|
-
}
|
|
397
|
-
\`\`\`
|
|
398
|
-
|
|
399
|
-
### Drag-and-Drop Upload Component (components/file-upload.tsx)
|
|
400
|
-
\`\`\`tsx
|
|
401
|
-
"use client";
|
|
402
|
-
import { useState, useCallback } from "react";
|
|
403
|
-
|
|
404
|
-
interface FileUploadProps {
|
|
405
|
-
onUpload: (url: string) => void;
|
|
406
|
-
accept?: string;
|
|
407
|
-
maxSizeMB?: number;
|
|
408
|
-
}
|
|
409
|
-
|
|
410
|
-
export function FileUpload({ onUpload, accept = "image/*", maxSizeMB = 10 }: FileUploadProps) {
|
|
411
|
-
const [isDragging, setIsDragging] = useState(false);
|
|
412
|
-
const [uploading, setUploading] = useState(false);
|
|
413
|
-
|
|
414
|
-
const handleUpload = useCallback(async (file: File) => {
|
|
415
|
-
setUploading(true);
|
|
416
|
-
try {
|
|
417
|
-
const formData = new FormData();
|
|
418
|
-
formData.append("file", file);
|
|
419
|
-
const res = await fetch("/api/upload", { method: "POST", body: formData });
|
|
420
|
-
if (!res.ok) throw new Error("Upload failed");
|
|
421
|
-
const { url } = await res.json();
|
|
422
|
-
onUpload(url);
|
|
423
|
-
} finally {
|
|
424
|
-
setUploading(false);
|
|
425
|
-
}
|
|
426
|
-
}, [onUpload]);
|
|
427
|
-
|
|
428
|
-
return (
|
|
429
|
-
<div
|
|
430
|
-
onDragOver={(e) => { e.preventDefault(); setIsDragging(true); }}
|
|
431
|
-
onDragLeave={() => setIsDragging(false)}
|
|
432
|
-
onDrop={(e) => {
|
|
433
|
-
e.preventDefault();
|
|
434
|
-
setIsDragging(false);
|
|
435
|
-
const file = e.dataTransfer.files[0];
|
|
436
|
-
if (file) handleUpload(file);
|
|
437
|
-
}}
|
|
438
|
-
className={\`border-2 border-dashed rounded-lg p-8 text-center cursor-pointer transition-colors \${
|
|
439
|
-
isDragging ? "border-primary bg-primary/5" : "border-muted-foreground/25 hover:border-primary/50"
|
|
440
|
-
}\`}
|
|
441
|
-
>
|
|
442
|
-
<input type="file" accept={accept} className="hidden" id="file-upload"
|
|
443
|
-
onChange={(e) => { const f = e.target.files?.[0]; if (f) handleUpload(f); }} />
|
|
444
|
-
<label htmlFor="file-upload" className="cursor-pointer">
|
|
445
|
-
{uploading ? "Uploading..." : "Drop a file here or click to browse"}
|
|
446
|
-
</label>
|
|
447
|
-
</div>
|
|
448
|
-
);
|
|
449
|
-
}
|
|
450
|
-
\`\`\`
|
|
451
|
-
|
|
452
|
-
### Common Pitfalls
|
|
453
|
-
1. **Always validate file type and size server-side.** Client-side validation can be bypassed.
|
|
454
|
-
2. **Sanitize filenames.** Strip special characters to avoid path traversal.
|
|
455
|
-
3. **Use unique keys.** Prefix with timestamp or UUID to avoid collisions.
|
|
456
|
-
4. **Set appropriate CORS headers** if the upload API is called from a different origin.
|
|
457
|
-
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
|
|
458
|
-
|
|
459
|
-
### File Structure
|
|
460
|
-
\`\`\`
|
|
461
|
-
lib/openai.ts \u2014 OpenAI client singleton
|
|
462
|
-
app/api/chat/route.ts \u2014 Streaming chat API route
|
|
463
|
-
components/chat.tsx \u2014 Chat UI component with streaming
|
|
464
|
-
\`\`\`
|
|
465
|
-
|
|
466
|
-
### Client Setup (lib/openai.ts)
|
|
467
|
-
\`\`\`typescript
|
|
468
|
-
import OpenAI from "openai";
|
|
469
|
-
|
|
470
|
-
export const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });
|
|
471
|
-
\`\`\`
|
|
472
|
-
|
|
473
|
-
### Streaming Chat Route (app/api/chat/route.ts)
|
|
474
|
-
\`\`\`typescript
|
|
475
|
-
import { openai } from "@/lib/openai";
|
|
476
|
-
import { NextRequest } from "next/server";
|
|
477
|
-
|
|
478
|
-
export async function POST(req: NextRequest) {
|
|
479
|
-
const { messages, systemPrompt } = await req.json();
|
|
480
|
-
|
|
481
|
-
const stream = await openai.chat.completions.create({
|
|
482
|
-
model: "gpt-4o-mini",
|
|
483
|
-
messages: [
|
|
484
|
-
{ role: "system", content: systemPrompt || "You are a helpful assistant." },
|
|
485
|
-
...messages,
|
|
486
|
-
],
|
|
487
|
-
stream: true,
|
|
488
|
-
});
|
|
489
|
-
|
|
490
|
-
// Stream the response using ReadableStream
|
|
491
|
-
const encoder = new TextEncoder();
|
|
492
|
-
const readable = new ReadableStream({
|
|
493
|
-
async start(controller) {
|
|
494
|
-
for await (const chunk of stream) {
|
|
495
|
-
const text = chunk.choices[0]?.delta?.content || "";
|
|
496
|
-
if (text) controller.enqueue(encoder.encode(text));
|
|
497
|
-
}
|
|
498
|
-
controller.close();
|
|
499
|
-
},
|
|
500
|
-
});
|
|
501
|
-
|
|
502
|
-
return new Response(readable, {
|
|
503
|
-
headers: { "Content-Type": "text/plain; charset=utf-8" },
|
|
504
|
-
});
|
|
505
|
-
}
|
|
506
|
-
\`\`\`
|
|
507
|
-
|
|
508
|
-
### Chat UI Component (components/chat.tsx)
|
|
509
|
-
\`\`\`tsx
|
|
510
|
-
"use client";
|
|
511
|
-
import { useState, useRef, useEffect } from "react";
|
|
512
|
-
|
|
513
|
-
interface Message { role: "user" | "assistant"; content: string }
|
|
514
|
-
|
|
515
|
-
export function Chat() {
|
|
516
|
-
const [messages, setMessages] = useState<Message[]>([]);
|
|
517
|
-
const [input, setInput] = useState("");
|
|
518
|
-
const [streaming, setStreaming] = useState(false);
|
|
519
|
-
const scrollRef = useRef<HTMLDivElement>(null);
|
|
520
|
-
|
|
521
|
-
useEffect(() => {
|
|
522
|
-
scrollRef.current?.scrollIntoView({ behavior: "smooth" });
|
|
523
|
-
}, [messages]);
|
|
524
|
-
|
|
525
|
-
async function handleSend() {
|
|
526
|
-
if (!input.trim() || streaming) return;
|
|
527
|
-
const userMsg: Message = { role: "user", content: input };
|
|
528
|
-
setMessages((prev) => [...prev, userMsg]);
|
|
529
|
-
setInput("");
|
|
530
|
-
setStreaming(true);
|
|
531
|
-
|
|
532
|
-
const res = await fetch("/api/chat", {
|
|
533
|
-
method: "POST",
|
|
534
|
-
headers: { "Content-Type": "application/json" },
|
|
535
|
-
body: JSON.stringify({ messages: [...messages, userMsg] }),
|
|
536
|
-
});
|
|
537
|
-
|
|
538
|
-
const reader = res.body?.getReader();
|
|
539
|
-
const decoder = new TextDecoder();
|
|
540
|
-
let assistantContent = "";
|
|
541
|
-
|
|
542
|
-
setMessages((prev) => [...prev, { role: "assistant", content: "" }]);
|
|
543
|
-
|
|
544
|
-
while (reader) {
|
|
545
|
-
const { done, value } = await reader.read();
|
|
546
|
-
if (done) break;
|
|
547
|
-
assistantContent += decoder.decode(value);
|
|
548
|
-
setMessages((prev) => [
|
|
549
|
-
...prev.slice(0, -1),
|
|
550
|
-
{ role: "assistant", content: assistantContent },
|
|
551
|
-
]);
|
|
552
|
-
}
|
|
553
|
-
setStreaming(false);
|
|
554
|
-
}
|
|
555
|
-
|
|
556
|
-
return (
|
|
557
|
-
<div className="flex flex-col h-full">
|
|
558
|
-
<div className="flex-1 overflow-y-auto p-4 space-y-4">
|
|
559
|
-
{messages.map((m, i) => (
|
|
560
|
-
<div key={i} className={\`flex \${m.role === "user" ? "justify-end" : "justify-start"}\`}>
|
|
561
|
-
<div className={\`max-w-[80%] rounded-lg px-4 py-2 \${
|
|
562
|
-
m.role === "user" ? "bg-primary text-primary-foreground" : "bg-muted"
|
|
563
|
-
}\`}>
|
|
564
|
-
{m.content}
|
|
565
|
-
</div>
|
|
566
|
-
</div>
|
|
567
|
-
))}
|
|
568
|
-
<div ref={scrollRef} />
|
|
569
|
-
</div>
|
|
570
|
-
<div className="border-t p-4 flex gap-2">
|
|
571
|
-
<input value={input} onChange={(e) => setInput(e.target.value)}
|
|
572
|
-
onKeyDown={(e) => e.key === "Enter" && handleSend()}
|
|
573
|
-
placeholder="Type a message..." className="flex-1 rounded-md border px-3 py-2" />
|
|
574
|
-
<button onClick={handleSend} disabled={streaming}
|
|
575
|
-
className="rounded-md bg-primary px-4 py-2 text-primary-foreground disabled:opacity-50">
|
|
576
|
-
Send
|
|
577
|
-
</button>
|
|
578
|
-
</div>
|
|
579
|
-
</div>
|
|
580
|
-
);
|
|
581
|
-
}
|
|
582
|
-
\`\`\`
|
|
583
|
-
|
|
584
|
-
### Common Pitfalls
|
|
585
|
-
1. **Never expose OPENAI_API_KEY to the client.** Always call OpenAI from server-side API routes.
|
|
586
|
-
2. **Use gpt-4o-mini for most features** unless the user specifically asks for gpt-4o. Cost difference is 10x.
|
|
587
|
-
3. **Set max_tokens to prevent runaway costs.** Default to 1000 for chat, 2000 for content generation.
|
|
588
|
-
4. **Handle rate limits gracefully.** Return a user-friendly error, not a raw 429 response.
|
|
589
|
-
5. **Streaming is the default UX pattern.** Non-streaming feels broken for chat interfaces.
|
|
590
|
-
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.
|
|
591
|
-
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
|
|
592
|
-
|
|
593
|
-
### File Structure
|
|
594
|
-
\`\`\`
|
|
595
|
-
lib/anthropic.ts \u2014 Anthropic client singleton
|
|
596
|
-
app/api/chat/route.ts \u2014 Streaming chat API route
|
|
597
|
-
components/chat.tsx \u2014 Chat UI component with streaming
|
|
598
|
-
\`\`\`
|
|
599
|
-
|
|
600
|
-
### Client Setup (lib/anthropic.ts)
|
|
601
|
-
\`\`\`typescript
|
|
602
|
-
import Anthropic from "@anthropic-ai/sdk";
|
|
603
|
-
|
|
604
|
-
export const anthropic = new Anthropic({
|
|
605
|
-
apiKey: process.env.ANTHROPIC_API_KEY,
|
|
606
|
-
});
|
|
607
|
-
\`\`\`
|
|
608
|
-
|
|
609
|
-
### Streaming Chat Route (app/api/chat/route.ts)
|
|
610
|
-
\`\`\`typescript
|
|
611
|
-
import { anthropic } from "@/lib/anthropic";
|
|
612
|
-
import { NextRequest } from "next/server";
|
|
613
|
-
|
|
614
|
-
export async function POST(req: NextRequest) {
|
|
615
|
-
const { messages, systemPrompt } = await req.json();
|
|
616
|
-
|
|
617
|
-
const stream = anthropic.messages.stream({
|
|
618
|
-
model: "claude-sonnet-4-6",
|
|
619
|
-
max_tokens: 1024,
|
|
620
|
-
system: systemPrompt || "You are a helpful assistant.",
|
|
621
|
-
messages,
|
|
622
|
-
});
|
|
623
|
-
|
|
624
|
-
const encoder = new TextEncoder();
|
|
625
|
-
const readable = new ReadableStream({
|
|
626
|
-
async start(controller) {
|
|
627
|
-
for await (const event of stream) {
|
|
628
|
-
if (event.type === "content_block_delta" && event.delta.type === "text_delta") {
|
|
629
|
-
controller.enqueue(encoder.encode(event.delta.text));
|
|
630
|
-
}
|
|
631
|
-
}
|
|
632
|
-
controller.close();
|
|
633
|
-
},
|
|
634
|
-
});
|
|
635
|
-
|
|
636
|
-
return new Response(readable, {
|
|
637
|
-
headers: { "Content-Type": "text/plain; charset=utf-8" },
|
|
638
|
-
});
|
|
639
|
-
}
|
|
640
|
-
\`\`\`
|
|
641
|
-
|
|
642
|
-
### Chat UI Component (components/chat.tsx)
|
|
643
|
-
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.
|
|
644
|
-
|
|
645
|
-
### Common Pitfalls
|
|
646
|
-
1. **Never expose ANTHROPIC_API_KEY to the client.** Always call Anthropic from server-side API routes.
|
|
647
|
-
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.
|
|
648
|
-
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.
|
|
649
|
-
4. **Set max_tokens explicitly.** Unlike OpenAI, Anthropic requires max_tokens on every request.
|
|
650
|
-
5. **Streaming is the default UX pattern.** Use \`anthropic.messages.stream()\` not \`anthropic.messages.create()\` for chat.
|
|
651
|
-
6. **Long streaming responses are fine on Mistflow Cloud.** Network I/O does NOT count toward CPU time.
|
|
652
|
-
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
|
|
653
|
-
|
|
654
|
-
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.
|
|
655
|
-
|
|
656
|
-
### File Structure
|
|
657
|
-
\`\`\`
|
|
658
|
-
lib/openrouter.ts \u2014 OpenRouter client
|
|
659
|
-
app/api/chat/route.ts \u2014 Streaming chat API route
|
|
660
|
-
components/chat.tsx \u2014 Chat UI component with streaming
|
|
661
|
-
components/model-selector.tsx \u2014 Model picker dropdown
|
|
662
|
-
\`\`\`
|
|
663
|
-
|
|
664
|
-
### Client Setup (lib/openrouter.ts)
|
|
665
|
-
\`\`\`typescript
|
|
666
|
-
import { OpenRouter } from "@openrouter/sdk";
|
|
667
|
-
|
|
668
|
-
export const openrouter = new OpenRouter({
|
|
669
|
-
apiKey: process.env.OPENROUTER_API_KEY,
|
|
670
|
-
});
|
|
671
|
-
|
|
672
|
-
// Popular models \u2014 use these as defaults or in a model selector
|
|
673
|
-
export const MODELS = {
|
|
674
|
-
fast: "meta-llama/llama-3.1-8b-instruct",
|
|
675
|
-
balanced: "anthropic/claude-sonnet-4-6",
|
|
676
|
-
powerful: "openai/gpt-4o",
|
|
677
|
-
cheap: "meta-llama/llama-3.1-8b-instruct",
|
|
678
|
-
} as const;
|
|
679
|
-
\`\`\`
|
|
680
|
-
|
|
681
|
-
### Streaming Chat Route (app/api/chat/route.ts)
|
|
682
|
-
\`\`\`typescript
|
|
683
|
-
import { openrouter, MODELS } from "@/lib/openrouter";
|
|
684
|
-
import { NextRequest } from "next/server";
|
|
685
|
-
|
|
686
|
-
export async function POST(req: NextRequest) {
|
|
687
|
-
const { messages, systemPrompt, model } = await req.json();
|
|
688
|
-
|
|
689
|
-
const completion = await openrouter.chat.send({
|
|
690
|
-
model: model || MODELS.balanced,
|
|
691
|
-
messages: [
|
|
692
|
-
{ role: "system", content: systemPrompt || "You are a helpful assistant." },
|
|
693
|
-
...messages,
|
|
694
|
-
],
|
|
695
|
-
stream: true,
|
|
696
|
-
});
|
|
697
|
-
|
|
698
|
-
const encoder = new TextEncoder();
|
|
699
|
-
const readable = new ReadableStream({
|
|
700
|
-
async start(controller) {
|
|
701
|
-
for await (const chunk of completion) {
|
|
702
|
-
const text = chunk.choices?.[0]?.delta?.content || "";
|
|
703
|
-
if (text) controller.enqueue(encoder.encode(text));
|
|
704
|
-
}
|
|
705
|
-
controller.close();
|
|
706
|
-
},
|
|
707
|
-
});
|
|
708
|
-
|
|
709
|
-
return new Response(readable, {
|
|
710
|
-
headers: { "Content-Type": "text/plain; charset=utf-8" },
|
|
711
|
-
});
|
|
712
|
-
}
|
|
713
|
-
\`\`\`
|
|
714
|
-
|
|
715
|
-
### Model Selector Component (components/model-selector.tsx)
|
|
716
|
-
\`\`\`tsx
|
|
717
|
-
"use client";
|
|
718
|
-
|
|
719
|
-
const MODELS = [
|
|
720
|
-
{ id: "anthropic/claude-sonnet-4-6", label: "Claude Sonnet", tier: "Balanced" },
|
|
721
|
-
{ id: "openai/gpt-4o", label: "GPT-4o", tier: "Powerful" },
|
|
722
|
-
{ id: "openai/gpt-4o-mini", label: "GPT-4o Mini", tier: "Fast" },
|
|
723
|
-
{ id: "meta-llama/llama-3.1-70b-instruct", label: "Llama 3.1 70B", tier: "Open Source" },
|
|
724
|
-
{ id: "google/gemini-2.0-flash-001", label: "Gemini 2.0 Flash", tier: "Fast" },
|
|
725
|
-
];
|
|
726
|
-
|
|
727
|
-
interface ModelSelectorProps {
|
|
728
|
-
value: string;
|
|
729
|
-
onChange: (model: string) => void;
|
|
730
|
-
}
|
|
731
|
-
|
|
732
|
-
export function ModelSelector({ value, onChange }: ModelSelectorProps) {
|
|
733
|
-
return (
|
|
734
|
-
<select
|
|
735
|
-
value={value}
|
|
736
|
-
onChange={(e) => onChange(e.target.value)}
|
|
737
|
-
className="rounded-md border px-2 py-1 text-sm"
|
|
738
|
-
>
|
|
739
|
-
{MODELS.map((m) => (
|
|
740
|
-
<option key={m.id} value={m.id}>
|
|
741
|
-
{m.label} ({m.tier})
|
|
742
|
-
</option>
|
|
743
|
-
))}
|
|
744
|
-
</select>
|
|
745
|
-
);
|
|
746
|
-
}
|
|
747
|
-
\`\`\`
|
|
748
|
-
|
|
749
|
-
### Common Pitfalls
|
|
750
|
-
1. **Never expose OPENROUTER_API_KEY to the client.** Always call OpenRouter from server-side API routes.
|
|
751
|
-
2. **Model IDs use provider/model format.** E.g. "anthropic/claude-sonnet-4-6", not just "claude-sonnet".
|
|
752
|
-
3. **Use the \`@openrouter/sdk\` package.** It wraps the OpenRouter API with proper types and streaming support.
|
|
753
|
-
4. **Set max_tokens explicitly** for Anthropic models routed through OpenRouter. OpenAI models have defaults, Anthropic models don't.
|
|
754
|
-
5. **Check model pricing at openrouter.ai/models.** Costs vary 100x between models. Show users which model they're using.
|
|
755
|
-
6. **Long streaming responses are fine on Mistflow Cloud.** Network I/O does NOT count toward CPU time.
|
|
756
|
-
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
|
|
757
|
-
|
|
758
|
-
### File Structure
|
|
759
|
-
\`\`\`
|
|
760
|
-
lib/stripe.ts \u2014 Stripe server client
|
|
761
|
-
db/schema/subscriptions.ts \u2014 Subscription-related DB schema
|
|
762
|
-
app/api/webhooks/stripe/route.ts \u2014 Webhook handler (critical path)
|
|
763
|
-
app/api/checkout/route.ts \u2014 Create Checkout Session
|
|
764
|
-
app/api/billing-portal/route.ts \u2014 Create Billing Portal session
|
|
765
|
-
app/(dashboard)/pricing/page.tsx \u2014 Pricing page
|
|
766
|
-
app/(dashboard)/billing/page.tsx \u2014 Billing/subscription management
|
|
767
|
-
\`\`\`
|
|
768
|
-
|
|
769
|
-
### Server Client (lib/stripe.ts)
|
|
770
|
-
\`\`\`typescript
|
|
771
|
-
import Stripe from "stripe";
|
|
772
|
-
|
|
773
|
-
export const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);
|
|
774
|
-
\`\`\`
|
|
775
|
-
|
|
776
|
-
### Webhook Handler (app/api/webhooks/stripe/route.ts)
|
|
777
|
-
This is the most critical file. All subscription state changes flow through here.
|
|
778
|
-
\`\`\`typescript
|
|
779
|
-
import { NextRequest, NextResponse } from "next/server";
|
|
780
|
-
import { stripe } from "@/lib/stripe";
|
|
781
|
-
import { headers } from "next/headers";
|
|
782
|
-
|
|
783
|
-
export async function POST(req: NextRequest) {
|
|
784
|
-
const body = await req.text();
|
|
785
|
-
const headersList = await headers();
|
|
786
|
-
const signature = headersList.get("stripe-signature")!;
|
|
787
|
-
|
|
788
|
-
let event: Stripe.Event;
|
|
789
|
-
try {
|
|
790
|
-
event = stripe.webhooks.constructEvent(body, signature, process.env.STRIPE_WEBHOOK_SECRET!);
|
|
791
|
-
} catch (err) {
|
|
792
|
-
return NextResponse.json({ error: "Invalid signature" }, { status: 400 });
|
|
793
|
-
}
|
|
794
|
-
|
|
795
|
-
switch (event.type) {
|
|
796
|
-
case "checkout.session.completed": {
|
|
797
|
-
const session = event.data.object as Stripe.Checkout.Session;
|
|
798
|
-
// Create/update subscription record in DB
|
|
799
|
-
// Link session.customer to your user via session.metadata.userId
|
|
800
|
-
break;
|
|
801
|
-
}
|
|
802
|
-
case "customer.subscription.updated":
|
|
803
|
-
case "customer.subscription.deleted": {
|
|
804
|
-
const sub = event.data.object as Stripe.Subscription;
|
|
805
|
-
// Update subscription status in DB (active, canceled, past_due)
|
|
806
|
-
break;
|
|
807
|
-
}
|
|
808
|
-
case "invoice.payment_failed": {
|
|
809
|
-
// Notify user about failed payment
|
|
810
|
-
break;
|
|
811
|
-
}
|
|
812
|
-
}
|
|
813
|
-
|
|
814
|
-
return NextResponse.json({ received: true });
|
|
815
|
-
}
|
|
816
|
-
\`\`\`
|
|
817
|
-
|
|
818
|
-
### Checkout Route (app/api/checkout/route.ts)
|
|
819
|
-
\`\`\`typescript
|
|
820
|
-
import { stripe } from "@/lib/stripe";
|
|
821
|
-
import { NextRequest, NextResponse } from "next/server";
|
|
822
|
-
|
|
823
|
-
export async function POST(req: NextRequest) {
|
|
824
|
-
const { priceId, userId } = await req.json();
|
|
825
|
-
|
|
826
|
-
const session = await stripe.checkout.sessions.create({
|
|
827
|
-
mode: "subscription",
|
|
828
|
-
payment_method_types: ["card"],
|
|
829
|
-
line_items: [{ price: priceId, quantity: 1 }],
|
|
830
|
-
success_url: \`\${process.env.NEXT_PUBLIC_APP_URL}/billing?success=true\`,
|
|
831
|
-
cancel_url: \`\${process.env.NEXT_PUBLIC_APP_URL}/pricing\`,
|
|
832
|
-
metadata: { userId },
|
|
833
|
-
});
|
|
834
|
-
|
|
835
|
-
return NextResponse.json({ url: session.url });
|
|
836
|
-
}
|
|
837
|
-
\`\`\`
|
|
838
|
-
|
|
839
|
-
### Common Pitfalls
|
|
840
|
-
1. **Webhook handler must use raw body (req.text(), not req.json())** for signature verification.
|
|
841
|
-
2. **Always verify webhook signatures.** Never trust unverified payloads.
|
|
842
|
-
3. **Idempotency: webhooks can fire multiple times.** Use event.id to deduplicate.
|
|
843
|
-
4. **Store the Stripe customer ID in your user table.** Look up by metadata.userId in webhooks.
|
|
844
|
-
5. **Test with Stripe CLI locally:** \`stripe listen --forward-to localhost:3000/api/webhooks/stripe\`
|
|
845
|
-
6. **Price IDs come from Stripe Dashboard.** Do not hardcode amounts. Create Products + Prices in the dashboard and reference price IDs.
|
|
846
|
-
7. **Billing portal for self-service.** Use stripe.billingPortal.sessions.create() so users can manage their own subscriptions.
|
|
847
|
-
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
|
|
848
|
-
|
|
849
|
-
### File Structure
|
|
850
|
-
\`\`\`
|
|
851
|
-
lib/elevenlabs.ts \u2014 ElevenLabs client + helpers
|
|
852
|
-
app/api/tts/route.ts \u2014 Text-to-speech API route (streaming)
|
|
853
|
-
app/api/voices/route.ts \u2014 List available voices
|
|
854
|
-
components/voice-player.tsx \u2014 Audio player with playback controls
|
|
855
|
-
components/voice-selector.tsx \u2014 Voice picker dropdown
|
|
856
|
-
\`\`\`
|
|
857
|
-
|
|
858
|
-
### Client Setup (lib/elevenlabs.ts)
|
|
859
|
-
\`\`\`typescript
|
|
860
|
-
import { ElevenLabsClient } from "elevenlabs";
|
|
861
|
-
|
|
862
|
-
export const elevenlabs = new ElevenLabsClient({
|
|
863
|
-
apiKey: process.env.ELEVENLABS_API_KEY,
|
|
864
|
-
});
|
|
865
|
-
|
|
866
|
-
// Default voice ID \u2014 "Rachel" is a good general-purpose voice
|
|
867
|
-
export const DEFAULT_VOICE_ID = "21m00Tcm4TlvDq8ikWAM";
|
|
868
|
-
\`\`\`
|
|
869
|
-
|
|
870
|
-
### Text-to-Speech Route (app/api/tts/route.ts)
|
|
871
|
-
\`\`\`typescript
|
|
872
|
-
import { elevenlabs, DEFAULT_VOICE_ID } from "@/lib/elevenlabs";
|
|
873
|
-
import { NextRequest, NextResponse } from "next/server";
|
|
874
|
-
|
|
875
|
-
export async function POST(req: NextRequest) {
|
|
876
|
-
const { text, voiceId } = await req.json();
|
|
877
|
-
|
|
878
|
-
if (!text || text.length > 5000) {
|
|
879
|
-
return NextResponse.json({ error: "Text is required (max 5000 chars)" }, { status: 400 });
|
|
880
|
-
}
|
|
881
|
-
|
|
882
|
-
const audioStream = await elevenlabs.textToSpeech.stream(
|
|
883
|
-
voiceId || DEFAULT_VOICE_ID,
|
|
884
|
-
{
|
|
885
|
-
text,
|
|
886
|
-
modelId: "eleven_multilingual_v2",
|
|
887
|
-
}
|
|
888
|
-
);
|
|
889
|
-
|
|
890
|
-
// Pipe the ElevenLabs stream directly to the client response.
|
|
891
|
-
// Network wait (ElevenLabs generating audio) does NOT count as CPU time
|
|
892
|
-
// on Mistflow Cloud, so even long text is fine.
|
|
893
|
-
const readable = new ReadableStream({
|
|
894
|
-
async start(controller) {
|
|
895
|
-
for await (const chunk of audioStream) {
|
|
896
|
-
controller.enqueue(new Uint8Array(chunk));
|
|
897
|
-
}
|
|
898
|
-
controller.close();
|
|
899
|
-
},
|
|
900
|
-
});
|
|
901
|
-
|
|
902
|
-
return new NextResponse(readable, {
|
|
903
|
-
headers: {
|
|
904
|
-
"Content-Type": "audio/mpeg",
|
|
905
|
-
"Transfer-Encoding": "chunked",
|
|
906
|
-
},
|
|
907
|
-
});
|
|
908
|
-
}
|
|
909
|
-
\`\`\`
|
|
910
|
-
|
|
911
|
-
### Voice Player Component (components/voice-player.tsx)
|
|
912
|
-
\`\`\`tsx
|
|
913
|
-
"use client";
|
|
914
|
-
import { useState, useRef } from "react";
|
|
915
|
-
|
|
916
|
-
interface VoicePlayerProps {
|
|
917
|
-
text: string;
|
|
918
|
-
voiceId?: string;
|
|
919
|
-
}
|
|
920
|
-
|
|
921
|
-
export function VoicePlayer({ text, voiceId }: VoicePlayerProps) {
|
|
922
|
-
const [loading, setLoading] = useState(false);
|
|
923
|
-
const [playing, setPlaying] = useState(false);
|
|
924
|
-
const audioRef = useRef<HTMLAudioElement | null>(null);
|
|
925
|
-
|
|
926
|
-
async function handlePlay() {
|
|
927
|
-
if (playing && audioRef.current) {
|
|
928
|
-
audioRef.current.pause();
|
|
929
|
-
setPlaying(false);
|
|
930
|
-
return;
|
|
931
|
-
}
|
|
932
|
-
|
|
933
|
-
setLoading(true);
|
|
934
|
-
try {
|
|
935
|
-
const res = await fetch("/api/tts", {
|
|
936
|
-
method: "POST",
|
|
937
|
-
headers: { "Content-Type": "application/json" },
|
|
938
|
-
body: JSON.stringify({ text, voiceId }),
|
|
939
|
-
});
|
|
940
|
-
const blob = await res.blob();
|
|
941
|
-
const url = URL.createObjectURL(blob);
|
|
942
|
-
const audio = new Audio(url);
|
|
943
|
-
audioRef.current = audio;
|
|
944
|
-
audio.onended = () => setPlaying(false);
|
|
945
|
-
audio.play();
|
|
946
|
-
setPlaying(true);
|
|
947
|
-
} finally {
|
|
948
|
-
setLoading(false);
|
|
949
|
-
}
|
|
950
|
-
}
|
|
951
|
-
|
|
952
|
-
return (
|
|
953
|
-
<button onClick={handlePlay} disabled={loading}
|
|
954
|
-
className="inline-flex items-center gap-2 rounded-md bg-primary px-3 py-1.5 text-sm text-primary-foreground disabled:opacity-50">
|
|
955
|
-
{loading ? "Generating..." : playing ? "Pause" : "Listen"}
|
|
956
|
-
</button>
|
|
957
|
-
);
|
|
958
|
-
}
|
|
959
|
-
\`\`\`
|
|
960
|
-
|
|
961
|
-
### Voice Listing Route (app/api/voices/route.ts)
|
|
962
|
-
\`\`\`typescript
|
|
963
|
-
import { elevenlabs } from "@/lib/elevenlabs";
|
|
964
|
-
import { NextResponse } from "next/server";
|
|
965
|
-
|
|
966
|
-
export async function GET() {
|
|
967
|
-
const voices = await elevenlabs.voices.getAll();
|
|
968
|
-
return NextResponse.json(
|
|
969
|
-
voices.voices.map((v) => ({ id: v.voice_id, name: v.name, category: v.category }))
|
|
970
|
-
);
|
|
971
|
-
}
|
|
972
|
-
\`\`\`
|
|
973
|
-
|
|
974
|
-
### Common Pitfalls
|
|
975
|
-
1. **Never expose ELEVENLABS_API_KEY to the client.** All TTS calls go through your API route.
|
|
976
|
-
2. **Character limits matter for cost.** ElevenLabs charges per character. Show character count in UI and enforce limits.
|
|
977
|
-
3. **Use eleven_multilingual_v2 model** for best quality across languages. Use eleven_flash_v2_5 for ultra-low latency.
|
|
978
|
-
4. **Cache generated audio** when the same text + voice combination is requested multiple times. Store in Mistflow Cloud storage or local cache.
|
|
979
|
-
5. **Voice cloning requires explicit user consent.** If building a voice cloning feature, add consent UI.
|
|
980
|
-
6. **Free tier: 10,000 characters/month.** Display remaining quota to users to avoid surprise failures.
|
|
981
|
-
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.
|
|
982
|
-
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
|
|
983
|
-
|
|
984
|
-
### File Structure
|
|
985
|
-
\`\`\`
|
|
986
|
-
lib/maps.ts \u2014 Maps loader + helper functions
|
|
987
|
-
components/map.tsx \u2014 Map embed component
|
|
988
|
-
components/places-autocomplete.tsx \u2014 Address search with autocomplete
|
|
989
|
-
\`\`\`
|
|
990
|
-
|
|
991
|
-
### Map Component (components/map.tsx)
|
|
992
|
-
\`\`\`tsx
|
|
993
|
-
"use client";
|
|
994
|
-
import { useEffect, useRef, useState } from "react";
|
|
995
|
-
import { Loader } from "@googlemaps/js-api-loader";
|
|
996
|
-
|
|
997
|
-
interface MapProps {
|
|
998
|
-
center?: { lat: number; lng: number };
|
|
999
|
-
zoom?: number;
|
|
1000
|
-
markers?: { lat: number; lng: number; title?: string }[];
|
|
1001
|
-
className?: string;
|
|
1002
|
-
onMarkerClick?: (marker: { lat: number; lng: number }) => void;
|
|
1003
|
-
}
|
|
1004
|
-
|
|
1005
|
-
export function Map({ center = { lat: 40.7128, lng: -74.006 }, zoom = 12, markers = [], className, onMarkerClick }: MapProps) {
|
|
1006
|
-
const mapRef = useRef<HTMLDivElement>(null);
|
|
1007
|
-
const [map, setMap] = useState<google.maps.Map | null>(null);
|
|
1008
|
-
|
|
1009
|
-
useEffect(() => {
|
|
1010
|
-
const loader = new Loader({
|
|
1011
|
-
apiKey: process.env.NEXT_PUBLIC_GOOGLE_MAPS_API_KEY!,
|
|
1012
|
-
version: "weekly",
|
|
1013
|
-
libraries: ["places"],
|
|
1014
|
-
});
|
|
1015
|
-
|
|
1016
|
-
loader.load().then(() => {
|
|
1017
|
-
if (!mapRef.current) return;
|
|
1018
|
-
const m = new google.maps.Map(mapRef.current, { center, zoom, mapId: "DEMO_MAP_ID" });
|
|
1019
|
-
setMap(m);
|
|
1020
|
-
|
|
1021
|
-
for (const marker of markers) {
|
|
1022
|
-
const m2 = new google.maps.marker.AdvancedMarkerElement({
|
|
1023
|
-
map: m, position: marker, title: marker.title,
|
|
1024
|
-
});
|
|
1025
|
-
if (onMarkerClick) {
|
|
1026
|
-
m2.addListener("click", () => onMarkerClick(marker));
|
|
1027
|
-
}
|
|
1028
|
-
}
|
|
1029
|
-
});
|
|
1030
|
-
}, []);
|
|
1031
|
-
|
|
1032
|
-
return <div ref={mapRef} className={className ?? "w-full h-[400px] rounded-lg"} />;
|
|
1033
|
-
}
|
|
1034
|
-
\`\`\`
|
|
1035
|
-
|
|
1036
|
-
### Places Autocomplete (components/places-autocomplete.tsx)
|
|
1037
|
-
\`\`\`tsx
|
|
1038
|
-
"use client";
|
|
1039
|
-
import { useEffect, useRef, useState } from "react";
|
|
1040
|
-
import { Loader } from "@googlemaps/js-api-loader";
|
|
1041
|
-
|
|
1042
|
-
interface PlacesAutocompleteProps {
|
|
1043
|
-
onSelect: (place: { address: string; lat: number; lng: number }) => void;
|
|
1044
|
-
placeholder?: string;
|
|
1045
|
-
}
|
|
1046
|
-
|
|
1047
|
-
export function PlacesAutocomplete({ onSelect, placeholder = "Search for an address..." }: PlacesAutocompleteProps) {
|
|
1048
|
-
const inputRef = useRef<HTMLInputElement>(null);
|
|
1049
|
-
|
|
1050
|
-
useEffect(() => {
|
|
1051
|
-
const loader = new Loader({
|
|
1052
|
-
apiKey: process.env.NEXT_PUBLIC_GOOGLE_MAPS_API_KEY!,
|
|
1053
|
-
version: "weekly",
|
|
1054
|
-
libraries: ["places"],
|
|
1055
|
-
});
|
|
1056
|
-
|
|
1057
|
-
loader.load().then(() => {
|
|
1058
|
-
if (!inputRef.current) return;
|
|
1059
|
-
const autocomplete = new google.maps.places.Autocomplete(inputRef.current, {
|
|
1060
|
-
fields: ["formatted_address", "geometry"],
|
|
1061
|
-
});
|
|
1062
|
-
autocomplete.addListener("place_changed", () => {
|
|
1063
|
-
const place = autocomplete.getPlace();
|
|
1064
|
-
if (place.geometry?.location) {
|
|
1065
|
-
onSelect({
|
|
1066
|
-
address: place.formatted_address ?? "",
|
|
1067
|
-
lat: place.geometry.location.lat(),
|
|
1068
|
-
lng: place.geometry.location.lng(),
|
|
1069
|
-
});
|
|
1070
|
-
}
|
|
1071
|
-
});
|
|
1072
|
-
});
|
|
1073
|
-
}, [onSelect]);
|
|
1074
|
-
|
|
1075
|
-
return <input ref={inputRef} placeholder={placeholder}
|
|
1076
|
-
className="w-full rounded-md border px-3 py-2" />;
|
|
1077
|
-
}
|
|
1078
|
-
\`\`\`
|
|
1079
|
-
|
|
1080
|
-
### Common Pitfalls
|
|
1081
|
-
1. **API key must be restricted in Google Cloud Console.** Restrict to your domain and specific APIs (Maps JS, Places).
|
|
1082
|
-
2. **Use NEXT_PUBLIC_ prefix** so the key is available client-side. This is expected for Maps JS API.
|
|
1083
|
-
3. **Enable the Maps JavaScript API AND Places API** in Google Cloud Console. Both are separate.
|
|
1084
|
-
4. **AdvancedMarkerElement requires a mapId.** Create one in Cloud Console or use "DEMO_MAP_ID" for development.
|
|
1085
|
-
5. **Billing is required.** Google Maps gives $200/month free credit (~28,000 map loads). Add billing alerts.
|
|
1086
|
-
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
|
|
1087
|
-
|
|
1088
|
-
### File Structure
|
|
1089
|
-
\`\`\`
|
|
1090
|
-
lib/twilio.ts \u2014 Twilio client singleton
|
|
1091
|
-
lib/sms.ts \u2014 Send helper functions
|
|
1092
|
-
app/api/sms/send/route.ts \u2014 Send SMS API route
|
|
1093
|
-
app/api/webhooks/twilio/route.ts \u2014 Incoming SMS + delivery webhooks
|
|
1094
|
-
\`\`\`
|
|
1095
|
-
|
|
1096
|
-
### Client Setup (lib/twilio.ts)
|
|
1097
|
-
\`\`\`typescript
|
|
1098
|
-
import twilio from "twilio";
|
|
1099
|
-
|
|
1100
|
-
export const twilioClient = twilio(
|
|
1101
|
-
process.env.TWILIO_ACCOUNT_SID,
|
|
1102
|
-
process.env.TWILIO_AUTH_TOKEN
|
|
1103
|
-
);
|
|
1104
|
-
\`\`\`
|
|
1105
|
-
|
|
1106
|
-
### Send Helpers (lib/sms.ts)
|
|
1107
|
-
\`\`\`typescript
|
|
1108
|
-
import { twilioClient } from "./twilio";
|
|
1109
|
-
|
|
1110
|
-
export async function sendSMS(to: string, body: string) {
|
|
1111
|
-
return twilioClient.messages.create({
|
|
1112
|
-
to,
|
|
1113
|
-
from: process.env.TWILIO_PHONE_NUMBER,
|
|
1114
|
-
body,
|
|
1115
|
-
});
|
|
1116
|
-
}
|
|
1117
|
-
|
|
1118
|
-
export async function sendOTP(to: string, code: string) {
|
|
1119
|
-
return sendSMS(to, \`Your verification code is: \${code}. It expires in 10 minutes.\`);
|
|
1120
|
-
}
|
|
1121
|
-
\`\`\`
|
|
1122
|
-
|
|
1123
|
-
### Send Route (app/api/sms/send/route.ts)
|
|
1124
|
-
\`\`\`typescript
|
|
1125
|
-
import { sendSMS } from "@/lib/sms";
|
|
1126
|
-
import { NextRequest, NextResponse } from "next/server";
|
|
1127
|
-
|
|
1128
|
-
export async function POST(req: NextRequest) {
|
|
1129
|
-
const { to, message } = await req.json();
|
|
1130
|
-
|
|
1131
|
-
// Validate phone number format (E.164)
|
|
1132
|
-
if (!/^\\+[1-9]\\d{1,14}$/.test(to)) {
|
|
1133
|
-
return NextResponse.json({ error: "Invalid phone number. Use E.164 format (+1234567890)" }, { status: 400 });
|
|
1134
|
-
}
|
|
1135
|
-
|
|
1136
|
-
const result = await sendSMS(to, message);
|
|
1137
|
-
return NextResponse.json({ sid: result.sid, status: result.status });
|
|
1138
|
-
}
|
|
1139
|
-
\`\`\`
|
|
1140
|
-
|
|
1141
|
-
### Webhook Handler (app/api/webhooks/twilio/route.ts)
|
|
1142
|
-
\`\`\`typescript
|
|
1143
|
-
import { NextRequest, NextResponse } from "next/server";
|
|
1144
|
-
|
|
1145
|
-
export async function POST(req: NextRequest) {
|
|
1146
|
-
const formData = await req.formData();
|
|
1147
|
-
const from = formData.get("From") as string;
|
|
1148
|
-
const body = formData.get("Body") as string;
|
|
1149
|
-
const status = formData.get("MessageStatus") as string;
|
|
1150
|
-
|
|
1151
|
-
if (body) {
|
|
1152
|
-
// Incoming SMS \u2014 handle auto-replies, keyword routing, etc.
|
|
1153
|
-
console.log(\`SMS from \${from}: \${body}\`);
|
|
1154
|
-
} else if (status) {
|
|
1155
|
-
// Delivery status update \u2014 delivered, failed, undelivered
|
|
1156
|
-
console.log(\`Message status: \${status}\`);
|
|
1157
|
-
}
|
|
1158
|
-
|
|
1159
|
-
// Return TwiML response (empty = no auto-reply)
|
|
1160
|
-
return new NextResponse('<?xml version="1.0" encoding="UTF-8"?><Response></Response>', {
|
|
1161
|
-
headers: { "Content-Type": "text/xml" },
|
|
1162
|
-
});
|
|
1163
|
-
}
|
|
1164
|
-
\`\`\`
|
|
1165
|
-
|
|
1166
|
-
### Common Pitfalls
|
|
1167
|
-
1. **Phone numbers must be in E.164 format** (+1234567890). Validate before sending.
|
|
1168
|
-
2. **Trial accounts can only send to verified numbers.** Upgrade for production use.
|
|
1169
|
-
3. **Twilio webhook validation.** Use twilio.webhook() middleware to verify webhook signatures in production.
|
|
1170
|
-
4. **SMS rate limits vary by country.** US: 1 SMS/second per number. Use Messaging Services for higher throughput.
|
|
1171
|
-
5. **Never log full phone numbers.** Mask them in logs (e.g., +1***567890).
|
|
1172
|
-
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
|
|
1173
|
-
|
|
1174
|
-
### File Structure
|
|
1175
|
-
\`\`\`
|
|
1176
|
-
lib/posthog.ts \u2014 PostHog provider setup
|
|
1177
|
-
app/providers.tsx \u2014 Client-side providers wrapper
|
|
1178
|
-
components/posthog-pageview.tsx \u2014 Page view tracker component
|
|
1179
|
-
lib/posthog-server.ts \u2014 Server-side PostHog client
|
|
1180
|
-
\`\`\`
|
|
1181
|
-
|
|
1182
|
-
### Client Provider (lib/posthog.ts)
|
|
1183
|
-
\`\`\`typescript
|
|
1184
|
-
"use client";
|
|
1185
|
-
import posthog from "posthog-js";
|
|
1186
|
-
import { PostHogProvider as PHProvider } from "posthog-js/react";
|
|
1187
|
-
import { useEffect } from "react";
|
|
1188
|
-
|
|
1189
|
-
export function PostHogProvider({ children }: { children: React.ReactNode }) {
|
|
1190
|
-
useEffect(() => {
|
|
1191
|
-
posthog.init(process.env.NEXT_PUBLIC_POSTHOG_KEY!, {
|
|
1192
|
-
api_host: process.env.NEXT_PUBLIC_POSTHOG_HOST || "https://us.i.posthog.com",
|
|
1193
|
-
person_profiles: "identified_only",
|
|
1194
|
-
capture_pageview: false, // We handle this manually for Next.js
|
|
1195
|
-
});
|
|
1196
|
-
}, []);
|
|
1197
|
-
|
|
1198
|
-
return <PHProvider client={posthog}>{children}</PHProvider>;
|
|
1199
|
-
}
|
|
1200
|
-
\`\`\`
|
|
1201
|
-
|
|
1202
|
-
### Page View Tracker (components/posthog-pageview.tsx)
|
|
1203
|
-
\`\`\`tsx
|
|
1204
|
-
"use client";
|
|
1205
|
-
import { usePathname, useSearchParams } from "next/navigation";
|
|
1206
|
-
import { useEffect } from "react";
|
|
1207
|
-
import { usePostHog } from "posthog-js/react";
|
|
1208
|
-
|
|
1209
|
-
export function PostHogPageview() {
|
|
1210
|
-
const pathname = usePathname();
|
|
1211
|
-
const searchParams = useSearchParams();
|
|
1212
|
-
const posthog = usePostHog();
|
|
1213
|
-
|
|
1214
|
-
useEffect(() => {
|
|
1215
|
-
if (pathname && posthog) {
|
|
1216
|
-
let url = window.origin + pathname;
|
|
1217
|
-
if (searchParams.toString()) url += "?" + searchParams.toString();
|
|
1218
|
-
posthog.capture("$pageview", { "$current_url": url });
|
|
1219
|
-
}
|
|
1220
|
-
}, [pathname, searchParams, posthog]);
|
|
1221
|
-
|
|
1222
|
-
return null;
|
|
1223
|
-
}
|
|
1224
|
-
\`\`\`
|
|
1225
|
-
|
|
1226
|
-
### App Providers (app/providers.tsx)
|
|
1227
|
-
\`\`\`tsx
|
|
1228
|
-
"use client";
|
|
1229
|
-
import { PostHogProvider } from "@/lib/posthog";
|
|
1230
|
-
import { PostHogPageview } from "@/components/posthog-pageview";
|
|
1231
|
-
import { Suspense } from "react";
|
|
1232
|
-
|
|
1233
|
-
export function Providers({ children }: { children: React.ReactNode }) {
|
|
1234
|
-
return (
|
|
1235
|
-
<PostHogProvider>
|
|
1236
|
-
<Suspense fallback={null}>
|
|
1237
|
-
<PostHogPageview />
|
|
1238
|
-
</Suspense>
|
|
1239
|
-
{children}
|
|
1240
|
-
</PostHogProvider>
|
|
1241
|
-
);
|
|
1242
|
-
}
|
|
1243
|
-
\`\`\`
|
|
1244
|
-
|
|
1245
|
-
### Server-side Client (lib/posthog-server.ts)
|
|
1246
|
-
\`\`\`typescript
|
|
1247
|
-
import { PostHog } from "posthog-node";
|
|
1248
|
-
|
|
1249
|
-
export const posthogServer = new PostHog(process.env.NEXT_PUBLIC_POSTHOG_KEY!, {
|
|
1250
|
-
host: process.env.NEXT_PUBLIC_POSTHOG_HOST || "https://us.i.posthog.com",
|
|
1251
|
-
});
|
|
1252
|
-
|
|
1253
|
-
// Identify user after auth
|
|
1254
|
-
export function identifyUser(userId: string, properties?: Record<string, unknown>) {
|
|
1255
|
-
posthogServer.identify({ distinctId: userId, properties });
|
|
1256
|
-
}
|
|
1257
|
-
|
|
1258
|
-
// Track server-side events
|
|
1259
|
-
export function trackEvent(userId: string, event: string, properties?: Record<string, unknown>) {
|
|
1260
|
-
posthogServer.capture({ distinctId: userId, event, properties });
|
|
1261
|
-
}
|
|
1262
|
-
\`\`\`
|
|
1263
|
-
|
|
1264
|
-
### Feature Flags Usage
|
|
1265
|
-
\`\`\`tsx
|
|
1266
|
-
"use client";
|
|
1267
|
-
import { useFeatureFlagEnabled } from "posthog-js/react";
|
|
1268
|
-
|
|
1269
|
-
export function NewFeature() {
|
|
1270
|
-
const isEnabled = useFeatureFlagEnabled("new-dashboard");
|
|
1271
|
-
if (!isEnabled) return null;
|
|
1272
|
-
return <div>New Dashboard Feature</div>;
|
|
1273
|
-
}
|
|
1274
|
-
\`\`\`
|
|
1275
|
-
|
|
1276
|
-
### Common Pitfalls
|
|
1277
|
-
1. **Wrap PostHogPageview in Suspense.** useSearchParams() requires a Suspense boundary in Next.js App Router.
|
|
1278
|
-
2. **Set capture_pageview: false in init.** Next.js client-side navigation needs manual pageview tracking.
|
|
1279
|
-
3. **Use person_profiles: "identified_only"** to save on events. Anonymous events are cheaper.
|
|
1280
|
-
4. **Call posthog.identify() after login** with the user's ID to link sessions.
|
|
1281
|
-
5. **Server-side: always call posthogServer.shutdown()** in serverless environments, or events may be lost.
|
|
1282
|
-
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
|
|
1283
|
-
|
|
1284
|
-
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.
|
|
1285
|
-
|
|
1286
|
-
### File Structure
|
|
1287
|
-
\`\`\`
|
|
1288
|
-
lib/firecrawl.ts \u2014 Firecrawl client singleton
|
|
1289
|
-
app/api/scrape/route.ts \u2014 Single URL scrape endpoint
|
|
1290
|
-
app/api/crawl/start/route.ts \u2014 Start async crawl (returns job ID)
|
|
1291
|
-
app/api/crawl/[id]/route.ts \u2014 Poll crawl status
|
|
1292
|
-
app/api/firecrawl-webhook/route.ts \u2014 Webhook for async crawl results
|
|
1293
|
-
\`\`\`
|
|
1294
|
-
|
|
1295
|
-
### Client Setup (lib/firecrawl.ts)
|
|
1296
|
-
\`\`\`typescript
|
|
1297
|
-
import FirecrawlApp from "@mendable/firecrawl-js";
|
|
1298
|
-
|
|
1299
|
-
export const firecrawl = new FirecrawlApp({
|
|
1300
|
-
apiKey: process.env.FIRECRAWL_API_KEY!,
|
|
1301
|
-
});
|
|
1302
|
-
\`\`\`
|
|
1303
|
-
|
|
1304
|
-
### Scrape Single URL (app/api/scrape/route.ts)
|
|
1305
|
-
Single scrapes complete in 1-5 seconds. Network I/O does NOT count as CPU time on Workers, so this is safe.
|
|
1306
|
-
\`\`\`typescript
|
|
1307
|
-
import { firecrawl } from "@/lib/firecrawl";
|
|
1308
|
-
import { NextRequest, NextResponse } from "next/server";
|
|
1309
|
-
|
|
1310
|
-
export async function POST(req: NextRequest) {
|
|
1311
|
-
const { url } = await req.json();
|
|
1312
|
-
|
|
1313
|
-
if (!url || typeof url !== "string") {
|
|
1314
|
-
return NextResponse.json({ error: "URL is required" }, { status: 400 });
|
|
1315
|
-
}
|
|
1316
|
-
|
|
1317
|
-
const result = await firecrawl.scrapeUrl(url, {
|
|
1318
|
-
formats: ["markdown"],
|
|
1319
|
-
onlyMainContent: true,
|
|
1320
|
-
timeout: 20000,
|
|
1321
|
-
});
|
|
1322
|
-
|
|
1323
|
-
if (!result.success) {
|
|
1324
|
-
return NextResponse.json({ error: "Scrape failed" }, { status: 500 });
|
|
1325
|
-
}
|
|
1326
|
-
|
|
1327
|
-
return NextResponse.json({
|
|
1328
|
-
markdown: result.markdown,
|
|
1329
|
-
title: result.metadata?.title,
|
|
1330
|
-
description: result.metadata?.description,
|
|
1331
|
-
sourceUrl: result.metadata?.sourceURL,
|
|
1332
|
-
});
|
|
1333
|
-
}
|
|
1334
|
-
\`\`\`
|
|
1335
|
-
|
|
1336
|
-
### Start Async Crawl (app/api/crawl/start/route.ts)
|
|
1337
|
-
For multi-page crawls, ALWAYS use async mode with a webhook. Never use synchronous crawlUrl().
|
|
1338
|
-
\`\`\`typescript
|
|
1339
|
-
import { firecrawl } from "@/lib/firecrawl";
|
|
1340
|
-
import { NextRequest, NextResponse } from "next/server";
|
|
1341
|
-
|
|
1342
|
-
export async function POST(req: NextRequest) {
|
|
1343
|
-
const { url, maxPages = 50 } = await req.json();
|
|
1344
|
-
|
|
1345
|
-
const result = await firecrawl.asyncCrawlUrl(url, {
|
|
1346
|
-
limit: maxPages,
|
|
1347
|
-
maxDepth: 3,
|
|
1348
|
-
scrapeOptions: { formats: ["markdown"], onlyMainContent: true },
|
|
1349
|
-
webhook: {
|
|
1350
|
-
url: \`\${process.env.NEXT_PUBLIC_APP_URL}/api/firecrawl-webhook\`,
|
|
1351
|
-
},
|
|
1352
|
-
});
|
|
1353
|
-
|
|
1354
|
-
// Store crawl job ID in your DB for status tracking
|
|
1355
|
-
return NextResponse.json({ crawlId: result.id, status: "started" });
|
|
1356
|
-
}
|
|
1357
|
-
\`\`\`
|
|
1358
|
-
|
|
1359
|
-
### Poll Crawl Status (app/api/crawl/[id]/route.ts)
|
|
1360
|
-
\`\`\`typescript
|
|
1361
|
-
import { firecrawl } from "@/lib/firecrawl";
|
|
1362
|
-
import { NextRequest, NextResponse } from "next/server";
|
|
1363
|
-
|
|
1364
|
-
export async function GET(
|
|
1365
|
-
req: NextRequest,
|
|
1366
|
-
{ params }: { params: Promise<{ id: string }> }
|
|
1367
|
-
) {
|
|
1368
|
-
const { id } = await params;
|
|
1369
|
-
const status = await firecrawl.checkCrawlStatus(id);
|
|
1370
|
-
|
|
1371
|
-
return NextResponse.json({
|
|
1372
|
-
status: status.status,
|
|
1373
|
-
completed: status.completed,
|
|
1374
|
-
total: status.total,
|
|
1375
|
-
creditsUsed: status.creditsUsed,
|
|
1376
|
-
pages: status.data?.map((page: { markdown?: string; metadata?: { title?: string; sourceURL?: string } }) => ({
|
|
1377
|
-
title: page.metadata?.title,
|
|
1378
|
-
url: page.metadata?.sourceURL,
|
|
1379
|
-
markdownLength: page.markdown?.length ?? 0,
|
|
1380
|
-
})),
|
|
1381
|
-
});
|
|
1382
|
-
}
|
|
1383
|
-
\`\`\`
|
|
1384
|
-
|
|
1385
|
-
### Webhook Handler (app/api/firecrawl-webhook/route.ts)
|
|
1386
|
-
\`\`\`typescript
|
|
1387
|
-
import { NextRequest, NextResponse } from "next/server";
|
|
1388
|
-
|
|
1389
|
-
interface FirecrawlWebhookPayload {
|
|
1390
|
-
success: boolean;
|
|
1391
|
-
type: "crawl.started" | "crawl.page" | "crawl.completed" | "crawl.failed";
|
|
1392
|
-
id: string;
|
|
1393
|
-
data?: { markdown?: string; metadata?: { title?: string; sourceURL?: string } }[];
|
|
1394
|
-
error?: string;
|
|
1395
|
-
}
|
|
1396
|
-
|
|
1397
|
-
export async function POST(req: NextRequest) {
|
|
1398
|
-
const payload: FirecrawlWebhookPayload = await req.json();
|
|
1399
|
-
|
|
1400
|
-
switch (payload.type) {
|
|
1401
|
-
case "crawl.page":
|
|
1402
|
-
if (payload.data) {
|
|
1403
|
-
for (const page of payload.data) {
|
|
1404
|
-
// Store page in your database or vector store
|
|
1405
|
-
// page.markdown is clean, LLM-ready content
|
|
1406
|
-
// page.metadata.title, page.metadata.sourceURL for reference
|
|
1407
|
-
await storePage({
|
|
1408
|
-
crawlId: payload.id,
|
|
1409
|
-
url: page.metadata?.sourceURL ?? "",
|
|
1410
|
-
title: page.metadata?.title ?? "",
|
|
1411
|
-
markdown: page.markdown ?? "",
|
|
1412
|
-
});
|
|
1413
|
-
}
|
|
1414
|
-
}
|
|
1415
|
-
break;
|
|
1416
|
-
case "crawl.completed":
|
|
1417
|
-
// Mark crawl as done in your DB
|
|
1418
|
-
break;
|
|
1419
|
-
case "crawl.failed":
|
|
1420
|
-
// Log error: payload.error
|
|
1421
|
-
break;
|
|
1422
|
-
}
|
|
1423
|
-
|
|
1424
|
-
return NextResponse.json({ received: true });
|
|
1425
|
-
}
|
|
1426
|
-
|
|
1427
|
-
async function storePage(page: { crawlId: string; url: string; title: string; markdown: string }) {
|
|
1428
|
-
// TODO: Insert into your database
|
|
1429
|
-
// For RAG pipelines: chunk the markdown and generate embeddings
|
|
1430
|
-
}
|
|
1431
|
-
\`\`\`
|
|
1432
|
-
|
|
1433
|
-
### Extract Structured Data (with Zod schema)
|
|
1434
|
-
\`\`\`typescript
|
|
1435
|
-
import { z } from "zod";
|
|
1436
|
-
import { firecrawl } from "@/lib/firecrawl";
|
|
1437
|
-
|
|
1438
|
-
const ProductSchema = z.object({
|
|
1439
|
-
name: z.string(),
|
|
1440
|
-
price: z.number(),
|
|
1441
|
-
description: z.string(),
|
|
1442
|
-
inStock: z.boolean(),
|
|
1443
|
-
});
|
|
1444
|
-
|
|
1445
|
-
const result = await firecrawl.extract(
|
|
1446
|
-
["https://shop.example.com/products/*"],
|
|
1447
|
-
{
|
|
1448
|
-
schema: ProductSchema,
|
|
1449
|
-
prompt: "Extract product details from each product page",
|
|
1450
|
-
}
|
|
1451
|
-
);
|
|
1452
|
-
// result.data is typed as { name: string, price: number, ... }
|
|
1453
|
-
\`\`\`
|
|
1454
|
-
|
|
1455
|
-
### Map a Website (discover URLs before crawling)
|
|
1456
|
-
\`\`\`typescript
|
|
1457
|
-
import { firecrawl } from "@/lib/firecrawl";
|
|
1458
|
-
|
|
1459
|
-
// Discover all URLs on a site (1 credit flat, regardless of URL count)
|
|
1460
|
-
const map = await firecrawl.mapUrl("https://docs.example.com", {
|
|
1461
|
-
search: "API reference",
|
|
1462
|
-
limit: 200,
|
|
1463
|
-
});
|
|
1464
|
-
// map.links: Array<{ url: string, title?: string }>
|
|
1465
|
-
|
|
1466
|
-
// Then selectively scrape only relevant URLs
|
|
1467
|
-
const relevantUrls = map.links.slice(0, 20).map((l: { url: string }) => l.url);
|
|
1468
|
-
\`\`\`
|
|
1469
|
-
|
|
1470
|
-
### RAG Pipeline Pattern (Scrape -> Chunk -> Embed)
|
|
1471
|
-
Firecrawl's markdown output is already chunking-friendly. Headings are natural semantic boundaries.
|
|
1472
|
-
\`\`\`typescript
|
|
1473
|
-
function chunkMarkdown(markdown: string, maxSize = 1500): string[] {
|
|
1474
|
-
const sections = markdown.split(/\\n(?=#{1,3} )/);
|
|
1475
|
-
const chunks: string[] = [];
|
|
1476
|
-
|
|
1477
|
-
for (const section of sections) {
|
|
1478
|
-
if (section.length <= maxSize) {
|
|
1479
|
-
chunks.push(section.trim());
|
|
1480
|
-
} else {
|
|
1481
|
-
const paragraphs = section.split(/\\n\\n+/);
|
|
1482
|
-
let current = "";
|
|
1483
|
-
for (const para of paragraphs) {
|
|
1484
|
-
if (current.length + para.length > maxSize) {
|
|
1485
|
-
if (current) chunks.push(current.trim());
|
|
1486
|
-
current = para;
|
|
1487
|
-
} else {
|
|
1488
|
-
current += "\\n\\n" + para;
|
|
1489
|
-
}
|
|
1490
|
-
}
|
|
1491
|
-
if (current) chunks.push(current.trim());
|
|
1492
|
-
}
|
|
1493
|
-
}
|
|
1494
|
-
|
|
1495
|
-
return chunks.filter((c) => c.length > 50);
|
|
1496
|
-
}
|
|
1497
|
-
|
|
1498
|
-
// Usage: scrape a URL and prepare chunks for embedding
|
|
1499
|
-
const result = await firecrawl.scrapeUrl("https://docs.example.com/guide", {
|
|
1500
|
-
formats: ["markdown"],
|
|
1501
|
-
onlyMainContent: true,
|
|
1502
|
-
});
|
|
1503
|
-
const chunks = chunkMarkdown(result.markdown ?? "");
|
|
1504
|
-
// Now embed each chunk with OpenAI embeddings and store in your vector DB
|
|
1505
|
-
\`\`\`
|
|
1506
|
-
|
|
1507
|
-
### Common Pitfalls
|
|
1508
|
-
1. **Always use asyncCrawlUrl() for multi-page crawls, never crawlUrl().** The synchronous version polls until completion and will hold your Worker request open unnecessarily.
|
|
1509
|
-
2. **Map first, crawl selectively.** mapUrl() costs 1 credit flat. Do not waste credits crawling irrelevant pages.
|
|
1510
|
-
3. **onlyMainContent: true is the default.** It strips nav, footer, and ads. Set to false only if you need the full page structure.
|
|
1511
|
-
4. **Crawl results expire in 24 hours.** Store them in your database before the expiry.
|
|
1512
|
-
5. **Extract mode costs 5 credits per page** (1 scrape + 4 for JSON extraction). Use sparingly.
|
|
1513
|
-
6. **Markdown is the right format for AI apps.** It strips boilerplate, preserves structure, and is what LLMs expect.
|
|
1514
|
-
7. **Free tier: 500 credits (one-time).** Hobby plan is $16/month for 3,000 credits.
|
|
1515
|
-
8. **Network I/O does NOT count as CPU time.** Single scrapes (1-5s of network wait) are safe on Workers.
|
|
1516
|
-
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)
|
|
1517
|
-
|
|
1518
|
-
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.
|
|
1519
|
-
|
|
1520
|
-
### File Structure
|
|
1521
|
-
\`\`\`
|
|
1522
|
-
lib/replicate.ts \u2014 Replicate client singleton + model helpers
|
|
1523
|
-
app/api/generate-image/route.ts \u2014 Image generation endpoint
|
|
1524
|
-
app/api/generate-video/route.ts \u2014 Video generation endpoint (async with polling)
|
|
1525
|
-
components/image-generator.tsx \u2014 Image generation UI
|
|
1526
|
-
\`\`\`
|
|
1527
|
-
|
|
1528
|
-
### Client Setup (lib/replicate.ts)
|
|
1529
|
-
\`\`\`typescript
|
|
1530
|
-
import Replicate from "replicate";
|
|
1531
|
-
|
|
1532
|
-
export const replicate = new Replicate({
|
|
1533
|
-
auth: process.env.REPLICATE_API_TOKEN,
|
|
1534
|
-
});
|
|
1535
|
-
|
|
1536
|
-
// Recommended models \u2014 change these to use different models
|
|
1537
|
-
export const MODELS = {
|
|
1538
|
-
imageFast: "black-forest-labs/flux-schnell" as const,
|
|
1539
|
-
imageQuality: "black-forest-labs/flux-1.1-pro" as const,
|
|
1540
|
-
videoFast: "wan-video/wan-2.2-i2v-fast" as const,
|
|
1541
|
-
} as const;
|
|
1542
|
-
\`\`\`
|
|
1543
|
-
|
|
1544
|
-
### Image Generation Route (app/api/generate-image/route.ts)
|
|
1545
|
-
Image generation typically completes in 2-10 seconds. Safe for Workers.
|
|
1546
|
-
\`\`\`typescript
|
|
1547
|
-
import { replicate, MODELS } from "@/lib/replicate";
|
|
1548
|
-
import { NextRequest, NextResponse } from "next/server";
|
|
1549
|
-
|
|
1550
|
-
export async function POST(req: NextRequest) {
|
|
1551
|
-
const { prompt, model } = await req.json();
|
|
1552
|
-
|
|
1553
|
-
if (!prompt || typeof prompt !== "string") {
|
|
1554
|
-
return NextResponse.json({ error: "Prompt is required" }, { status: 400 });
|
|
1555
|
-
}
|
|
1556
|
-
|
|
1557
|
-
const output = await replicate.run(model || MODELS.imageQuality, {
|
|
1558
|
-
input: {
|
|
1559
|
-
prompt,
|
|
1560
|
-
aspect_ratio: "1:1",
|
|
1561
|
-
output_format: "webp",
|
|
1562
|
-
},
|
|
1563
|
-
});
|
|
1564
|
-
|
|
1565
|
-
// Output shape depends on the model. Most image models return a URL string or array of URLs.
|
|
1566
|
-
const imageUrl = Array.isArray(output) ? output[0] : output;
|
|
1567
|
-
|
|
1568
|
-
return NextResponse.json({ imageUrl });
|
|
1569
|
-
}
|
|
1570
|
-
\`\`\`
|
|
1571
|
-
|
|
1572
|
-
### Video Generation Route (app/api/generate-video/route.ts)
|
|
1573
|
-
Video generation takes 30s-5min. Use async predictions with polling.
|
|
1574
|
-
\`\`\`typescript
|
|
1575
|
-
import { replicate, MODELS } from "@/lib/replicate";
|
|
1576
|
-
import { NextRequest, NextResponse } from "next/server";
|
|
1577
|
-
|
|
1578
|
-
// Start video generation (returns immediately with prediction ID)
|
|
1579
|
-
export async function POST(req: NextRequest) {
|
|
1580
|
-
const { prompt, imageUrl } = await req.json();
|
|
1581
|
-
|
|
1582
|
-
const prediction = await replicate.predictions.create({
|
|
1583
|
-
model: MODELS.videoFast,
|
|
1584
|
-
input: {
|
|
1585
|
-
prompt,
|
|
1586
|
-
...(imageUrl ? { image: imageUrl } : {}),
|
|
1587
|
-
},
|
|
1588
|
-
});
|
|
1589
|
-
|
|
1590
|
-
return NextResponse.json({
|
|
1591
|
-
predictionId: prediction.id,
|
|
1592
|
-
status: prediction.status,
|
|
1593
|
-
});
|
|
1594
|
-
}
|
|
1595
|
-
|
|
1596
|
-
// Poll for completion (call from client on interval)
|
|
1597
|
-
export async function GET(req: NextRequest) {
|
|
1598
|
-
const id = req.nextUrl.searchParams.get("id");
|
|
1599
|
-
if (!id) return NextResponse.json({ error: "Missing prediction ID" }, { status: 400 });
|
|
1600
|
-
|
|
1601
|
-
const prediction = await replicate.predictions.get(id);
|
|
1602
|
-
|
|
1603
|
-
return NextResponse.json({
|
|
1604
|
-
status: prediction.status,
|
|
1605
|
-
output: prediction.output,
|
|
1606
|
-
error: prediction.error,
|
|
1607
|
-
});
|
|
1608
|
-
}
|
|
1609
|
-
\`\`\`
|
|
1610
|
-
|
|
1611
|
-
### Image Generator Component (components/image-generator.tsx)
|
|
1612
|
-
\`\`\`tsx
|
|
1613
|
-
"use client";
|
|
1614
|
-
import { useState } from "react";
|
|
1615
|
-
|
|
1616
|
-
export function ImageGenerator() {
|
|
1617
|
-
const [prompt, setPrompt] = useState("");
|
|
1618
|
-
const [imageUrl, setImageUrl] = useState<string | null>(null);
|
|
1619
|
-
const [loading, setLoading] = useState(false);
|
|
1620
|
-
|
|
1621
|
-
async function handleGenerate() {
|
|
1622
|
-
if (!prompt.trim() || loading) return;
|
|
1623
|
-
setLoading(true);
|
|
1624
|
-
setImageUrl(null);
|
|
1625
|
-
|
|
1626
|
-
try {
|
|
1627
|
-
const res = await fetch("/api/generate-image", {
|
|
1628
|
-
method: "POST",
|
|
1629
|
-
headers: { "Content-Type": "application/json" },
|
|
1630
|
-
body: JSON.stringify({ prompt }),
|
|
1631
|
-
});
|
|
1632
|
-
const { imageUrl: url } = await res.json();
|
|
1633
|
-
setImageUrl(url);
|
|
1634
|
-
} finally {
|
|
1635
|
-
setLoading(false);
|
|
1636
|
-
}
|
|
1637
|
-
}
|
|
1638
|
-
|
|
1639
|
-
return (
|
|
1640
|
-
<div className="space-y-4">
|
|
1641
|
-
<div className="flex gap-2">
|
|
1642
|
-
<input
|
|
1643
|
-
value={prompt}
|
|
1644
|
-
onChange={(e) => setPrompt(e.target.value)}
|
|
1645
|
-
onKeyDown={(e) => e.key === "Enter" && handleGenerate()}
|
|
1646
|
-
placeholder="Describe the image you want..."
|
|
1647
|
-
className="flex-1 rounded-md border px-3 py-2"
|
|
1648
|
-
/>
|
|
1649
|
-
<button
|
|
1650
|
-
onClick={handleGenerate}
|
|
1651
|
-
disabled={loading || !prompt.trim()}
|
|
1652
|
-
className="rounded-md bg-primary px-4 py-2 text-primary-foreground disabled:opacity-50"
|
|
1653
|
-
>
|
|
1654
|
-
{loading ? "Generating..." : "Generate"}
|
|
1655
|
-
</button>
|
|
1656
|
-
</div>
|
|
1657
|
-
{imageUrl && (
|
|
1658
|
-
<img src={imageUrl} alt={prompt} className="max-w-md rounded-lg shadow-md" />
|
|
1659
|
-
)}
|
|
1660
|
-
</div>
|
|
1661
|
-
);
|
|
1662
|
-
}
|
|
1663
|
-
\`\`\`
|
|
1664
|
-
|
|
1665
|
-
### Popular Models Reference
|
|
1666
|
-
**Image generation:**
|
|
1667
|
-
- \`black-forest-labs/flux-schnell\` \u2014 fast, good quality, cheapest
|
|
1668
|
-
- \`black-forest-labs/flux-1.1-pro\` \u2014 best quality, slower
|
|
1669
|
-
- \`black-forest-labs/flux-2-pro\` \u2014 highest fidelity, product photography, character consistency
|
|
1670
|
-
- \`google/nano-banana-pro\` \u2014 Google's model, strong prompt following, multilingual text rendering
|
|
1671
|
-
|
|
1672
|
-
**Video generation:**
|
|
1673
|
-
- \`wan-video/wan-2.2-i2v-fast\` \u2014 image-to-video, fast and affordable (10M+ runs)
|
|
1674
|
-
- \`google/veo-3.1-fast\` \u2014 high fidelity with audio
|
|
1675
|
-
- \`kwaivgi/kling-v3-video\` \u2014 multi-shot storytelling with native audio
|
|
1676
|
-
|
|
1677
|
-
### Common Pitfalls
|
|
1678
|
-
1. **Never expose REPLICATE_API_TOKEN to the client.** All generation calls go through your API routes.
|
|
1679
|
-
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.
|
|
1680
|
-
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.
|
|
1681
|
-
4. **Model IDs use owner/name format.** E.g. \`black-forest-labs/flux-schnell\`, not just \`flux-schnell\`.
|
|
1682
|
-
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.
|
|
1683
|
-
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.
|
|
1684
|
-
7. **Network I/O does NOT count as CPU time on Workers.** Image generation wait time is all network I/O.
|
|
1685
|
-
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 Ds(i){return i.toLowerCase().replace(/[^a-z0-9]+/g,"-").replace(/^-|-$/g,"")}function At(i){let t=ee.find(e=>e.id===i);if(t)return t;let s=i.toLowerCase().replace(/[^a-z0-9]/g,"");return ee.find(e=>{let r=e.name.toLowerCase().replace(/[^a-z0-9]/g,"");return r===s||r.includes(s)||s.includes(r)})}function Nt(i){return He[i]}function Ke(i){return i?ee.filter(t=>t.category.toLowerCase()===i.toLowerCase()):ee}function jt(i){let t=i??ee,s={};for(let r of t){s[r.category]||(s[r.category]=[]);let a=He[r.id],n=a?` \u2014 ${a.description}`:"",o=a?.packages.length?` (${a.packages.join(", ")})`:"";s[r.category].push(`${r.id} \u2014 "${r.name}"${n}${o}`)}let e=[];for(let[r,a]of Object.entries(s))e.push(`**${r}**:
|
|
1686
|
-
${a.map(n=>` - ${n}`).join(`
|
|
1687
|
-
`)}`);return e.join(`
|
|
1688
|
-
|
|
1689
|
-
`)}function Mt(i){return(i?Ke(i):ee).map(s=>{let e=He[s.id];return{id:s.id,slug:Ds(s.name),name:s.name,category:s.category,description:e?.description??"",tags:e?.tags??[],envVars:e?.envVars??[],docsUrl:e?.docsUrl??"",packages:e?.packages??[],difficulty:e?.difficulty??"medium"}})}var $s=A.object({action:A.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:A.string().optional().describe("Path to the project directory (default: cwd)"),completedStep:A.number().optional().describe("(update) Mark a plan step as completed by step number"),addEnvVar:A.object({key:A.string(),description:A.string().optional(),setupUrl:A.string().optional()}).optional().describe("(update) Add a required env var to the project manifest"),templateDescription:A.string().optional().describe("(share) Short description of what this template builds"),category:A.string().optional().describe("(landing-designs) Filter by category"),presetId:A.string().optional().describe("(landing-designs) Get full details for a specific landing design by ID"),integrationId:A.string().optional().describe("(integrations) Get full details for a specific integration preset by ID (e.g. 'stripe-payments', 'resend-email', 'elevenlabs-voice')"),period:A.string().optional().describe("(errors) Time period for errors: '1h', '24h', '7d' (default: '7d')"),deploymentId:A.string().optional().describe("(logs) Deployment ID to fetch logs for. If omitted, fetches logs for the latest deployment.")}),Ot={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:$s,handler:async i=>{let t=i;if(["share","errors","logs","deployments"].includes(t.action)&&!Ge())return p("You need to sign in first. Run mist_setup to connect your account.",!0);switch(t.action){case"get":case"update":return Ct.handler({action:t.action,projectPath:t.projectPath,completedStep:t.completedStep,addEnvVar:t.addEnvVar});case"share":{let e=be(t.projectPath??process.cwd()),r=Se(e,"mistflow.json");if(!we(r))return V(e);let a;try{a=JSON.parse(ve(r,"utf-8"))}catch{return p("Could not read mistflow.json.",!0)}let n=a.projectId;if(!n)return p("No project ID found. Deploy the project first to register it.",!0);try{let o=await ot(n,{isTemplate:!0,description:t.templateDescription});return p(JSON.stringify({shareUrl:o.share_url,shareToken:o.share_token,message:`Your project is now a shareable template!
|
|
1690
|
-
|
|
1691
|
-
Anyone can fork it: ${o.share_url}
|
|
1692
|
-
|
|
1693
|
-
Others can use it in their AI editor:
|
|
1694
|
-
"build me something like ${o.share_url}"`}))}catch(o){let l=o instanceof Error?o.message:"Failed to share project";return p(l,!0)}}case"landing-designs":{if(t.presetId){let n=ye(t.presetId);if(!n)return p(`Preset '${t.presetId}' not found. Use mist_project action='presets' without presetId to list all available presets.`,!0);let o=kt(t.presetId);return p(JSON.stringify({preset:{id:n.id,title:n.title,category:n.category,description:o?.description??"",style:o?.style??"",theme:o?.theme??"dark",colors:o?.colors??[],tags:o?.tags??[],promptLength:n.prompt.length},message:`Landing design "${n.title}" (${n.category}) \u2014 ${o?.description??""}. To use it, pass landingDesign="${n.id}" when calling mist_plan.`}))}let e=_t(t.category??void 0),r=Be(t.category),a=Pt(r);return p(JSON.stringify({count:e.length,presets:e.map(n=>({id:n.id,title:n.title,category:n.category,description:n.description,style:n.style,theme:n.theme,colors:n.colors})),formatted:a,message:`${e.length} landing designs available.${t.category?` Filtered by: ${t.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 ${Je()}/designs?tab=landing-designs.`}))}case"integrations":{if(t.integrationId){let n=At(t.integrationId);if(!n)return p(`Integration '${t.integrationId}' not found. Use mist_project action='integrations' without integrationId to list all available integrations.`,!0);let o=Nt(n.id);return p(JSON.stringify({integration:{id:n.id,name:n.name,category:n.category,description:o?.description??"",packages:o?.packages??[],envVars:o?.envVars??[],docsUrl:o?.docsUrl??"",difficulty:o?.difficulty??"medium"},message:`Integration "${n.name}" (${n.category}) \u2014 ${o?.description??""}. This blueprint is auto-injected during implementation when your plan has a matching integration step. Required env vars: ${o?.envVars?.map(l=>l.key).join(", ")||"none"}. Docs: ${o?.docsUrl??"n/a"}.`}))}let e=Mt(t.category??void 0),r=Ke(t.category??void 0),a=jt(r);return p(JSON.stringify({count:e.length,integrations:e.map(n=>({id:n.id,name:n.name,category:n.category,description:n.description,packages:n.packages,difficulty:n.difficulty,envVars:n.envVars.map(o=>o.key)})),formatted:a,message:`${e.length} integration blueprints available.${t.category?` Filtered by: ${t.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 e=be(t.projectPath??process.cwd()),r=Se(e,"mistflow.json");if(!we(r))return V(e);let a;try{a=JSON.parse(ve(r,"utf-8"))}catch{return p("Could not read mistflow.json.",!0)}let n=a.projectId;if(!n)return p("No project ID found. Deploy the project first.",!0);try{let o=await tt(n,t.period??"7d");return o.total===0?p(JSON.stringify({total:0,period:o.period,message:`No runtime errors in the last ${o.period}. The app is running clean.`})):p(JSON.stringify({total:o.total,period:o.period,errors:o.errors,message:`${o.total} runtime error(s) in the last ${o.period}. Review the errors above and use mist_build debug to investigate.`}))}catch(o){let l=o instanceof Error?o.message:"Failed to fetch errors";return p(l,!0)}}case"logs":{let e=be(t.projectPath??process.cwd()),r=Se(e,"mistflow.json");if(!we(r))return V(e);let a;try{a=JSON.parse(ve(r,"utf-8"))}catch{return p("Could not read mistflow.json.",!0)}let n=a.projectId;if(!n)return p("No project ID found. Deploy the project first.",!0);let o=t.deploymentId;if(!o)try{let l=await Ce(n);if(l.length===0)return p("No deployments found for this project.",!0);o=l[0].id}catch(l){let c=l instanceof Error?l.message:"Failed to fetch deployments";return p(c,!0)}try{let[l,c]=await Promise.all([et(o),Qe(o)]),h=l.filter(d=>d.level==="error"),w=l.filter(d=>d.level==="warn");return p(JSON.stringify({deploymentId:o,status:c.status,errorMessage:c.error??null,totalLogs:l.length,errorCount:h.length,warnCount:w.length,logs:l.map(d=>({time:d.timestamp,level:d.level,phase:d.phase,message:d.message})),message:c.status==="failed"?`Deployment failed. ${h.length} error(s) found in logs. Review the logs above to diagnose the issue.`:`Deployment status: ${c.status}. ${l.length} log entries (${h.length} errors, ${w.length} warnings).`}))}catch(l){let c=l instanceof Error?l.message:"Failed to fetch deploy logs";return p(c,!0)}}case"deployments":{let e=be(t.projectPath??process.cwd()),r=Se(e,"mistflow.json");if(!we(r))return V(e);let a;try{a=JSON.parse(ve(r,"utf-8"))}catch{return p("Could not read mistflow.json.",!0)}let n=a.projectId;if(!n)return p("No project ID found. Deploy the project first.",!0);try{let o=await Ce(n);return p(JSON.stringify({total:o.length,deployments:o.map(l=>({id:l.id,status:l.status,errorMessage:l.error_message,durationSeconds:l.duration_seconds,isRollback:!!l.rollback_from_id,createdAt:l.created_at})),message:`${o.length} deployment(s) found. Use mist_project action='logs' deploymentId='<id>' to see detailed logs for any deployment.`}))}catch(o){let l=o instanceof Error?o.message:"Failed to fetch deployments";return p(l,!0)}}case"version":{Ee().backendSignalReceived||await Xe();let e=Ee(),r=e.severity==="none",a={none:"up to date",patch:"patch update available",minor:"minor update available",major:"major update available",unsupported:"UNSUPPORTED \u2014 upgrade required"};return p(JSON.stringify({current:e.current,latest:e.latest||"unknown",minSupported:e.minSupported||"unknown",severity:e.severity,upToDate:r,upgradeCmd:e.upgradeCmd,changelogUrl:e.changelogUrl,backendSignalReceived:e.backendSignalReceived,message:e.backendSignalReceived?`Mistflow MCP ${e.current} (${a[e.severity]??e.severity}). Latest: ${e.latest}.${r?"":` Run \`${e.upgradeCmd}\` and restart your editor to upgrade.`}`:`Mistflow MCP ${e.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 p(`Unknown action: ${t.action}. Use get, update, share, landing-designs, integrations, errors, logs, deployments, or version.`,!0)}}};import{z as W}from"zod";var qs=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.")}),Lt={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:qs,handler:async i=>{let t=i,s=await Ye();if(t.action==="navigate"){if(!t.url)return p("URL is required for 'navigate'.",!0);let a=[],n=c=>{c.type()==="error"&&a.push(c.text())};s.on("console",n),await s.goto(t.url,{waitUntil:"domcontentloaded",timeout:3e4}),await s.waitForLoadState("networkidle").catch(()=>{});let o=[],l=c=>o.push(c.message);if(s.on("pageerror",l),await s.waitForTimeout(500),s.removeListener("console",n),s.removeListener("pageerror",l),a.length>0||o.length>0){let c=await pe(s),h=[{type:"text",text:JSON.stringify({url:s.url(),title:await s.title(),snapshot:c,consoleErrors:a,pageErrors:o,hasErrors:!0})}];if(t.includeScreenshot){let w=await ue(s);h.push({type:"image",data:w.toString("base64"),mimeType:"image/png"})}return{content:h}}}else if(t.action==="go_back")await s.goBack({waitUntil:"domcontentloaded",timeout:1e4});else if(t.action==="go_forward")await s.goForward({waitUntil:"domcontentloaded",timeout:1e4});else if(t.action==="click"){if(!t.selector)return p("Selector is required for 'click'.",!0);await s.click(t.selector,{timeout:1e4}),await s.waitForLoadState("domcontentloaded").catch(()=>{}),await s.waitForTimeout(500)}else if(t.action==="type"){if(!t.selector)return p("Selector is required for 'type'.",!0);if(!t.value)return p("Value is required for 'type'.",!0);await s.type(t.selector,t.value,{delay:50})}else if(t.action==="fill"){if(!t.selector)return p("Selector is required for 'fill'.",!0);if(!t.value)return p("Value is required for 'fill'.",!0);await s.fill(t.selector,t.value)}else if(t.action==="select_option"){if(!t.selector)return p("Selector is required for 'select_option'.",!0);if(!t.value)return p("Value is required for 'select_option'.",!0);await s.selectOption(t.selector,t.value)}else if(t.action==="hover"){if(!t.selector)return p("Selector is required for 'hover'.",!0);await s.hover(t.selector,{timeout:1e4})}else if(t.action==="press_key"){if(!t.value)return p("Value is required for 'press_key' (e.g. 'Enter').",!0);await s.keyboard.press(t.value),await s.waitForLoadState("domcontentloaded").catch(()=>{}),await s.waitForTimeout(500)}else if(t.action==="screenshot"){t.url&&(await s.goto(t.url,{waitUntil:"domcontentloaded",timeout:3e4}),await s.waitForLoadState("networkidle").catch(()=>{}));let a;if(t.selector){let n=await s.$(t.selector);if(!n)return p(`Element not found: ${t.selector}`,!0);a=await n.screenshot({type:"png"})}else a=await ue(s,t.fullPage);return{content:[{type:"text",text:JSON.stringify({url:s.url(),title:await s.title(),message:`Screenshot captured (${t.fullPage?"full page":"viewport"})`})},{type:"image",data:a.toString("base64"),mimeType:"image/png"}]}}else if(t.action==="snapshot"){let a=await pe(s);return{content:[{type:"text",text:JSON.stringify({url:s.url(),title:await s.title(),snapshot:a})}]}}let e=await pe(s),r=[{type:"text",text:JSON.stringify({url:s.url(),title:await s.title(),snapshot:e})}];if(t.includeScreenshot){let a=await ue(s);r.push({type:"image",data:a.toString("base64"),mimeType:"image/png"})}return{content:r}}};import{z as Ut}from"zod";var Dt=`# Mistflow CLI reference
|
|
10
|
+
If you want to deploy an existing project, use your framework's deploy tools directly.`,!0)}import{z as P}from"zod";import{platform as W}from"os";import{execFile as M}from"child_process";var K=P.object({apiKey:P.string().optional().describe("API key (mist_...) for headless auth. Skips the device code flow entirely. Generate one at app.mistflow.ai/mcp-keys."),deviceCode:P.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 E(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 Y={fetch:globalThis.fetch,openBrowser:H,sleep:E};async function L(n,e,t,a){let o=t,l=a.sleep??E;for(let i=0;i<e;i++){await l(o);let s;try{let p=await a.fetch(`${j()}/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 T({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 G(n,e=Y){let t=n;if(t?.apiKey)try{let i=await e.fetch(`${j()}/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 T({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 L(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(`${j()}/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 L(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 O={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=>G(n)};import{z as m}from"zod";import{z as u}from"zod";import{resolve as Z,join as N}from"path";import{existsSync as ee,readFileSync as $,writeFileSync as te}from"fs";import{existsSync as X,readFileSync as Q}from"fs";function A(n){let e=new Set;if(!X(n))return e;let t=Q(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")}),q={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=N(t,"mistflow.json");if(!ee(a))return R(t);let o;try{o=JSON.parse($(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,"utf-8")))}catch{}let s=o.plan,d=s?.steps?.filter(c=>c.status==="completed").length??0,p=s?.steps?.length??0,g=A(N(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 h=[`Project: ${o.name}`];if(s){h.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":" ";h.push(` [${b}] ${c.number}. ${c.name}`)}}let w=y.filter(c=>!c.configured);w.length>0&&h.push(`Missing env vars: ${w.map(c=>c.name).join(", ")}`),o.deploy?.url?h.push(`Deployed: ${o.deploy.url} (${o.deploy.count??0} deploys)`):h.push("Not deployed yet");let v=[],S=s?.steps?.find(c=>c.status!=="completed");return S?v.push(`NEXT: Call mist_build with action='implement' to work on step ${S.number} (${S.name}).`):s&&d===p&&(o.deploy?.url||v.push("NEXT: All steps complete! Call mist_deploy with action='deploy' 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:h.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: Call mist_build with action='implement' to work on step ${d.number} (${d.name}). Do this now.`):i.push("NEXT: All steps complete! Call mist_deploy with action='deploy' 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")}),U={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 q.handler({action:e.action,projectPath:e.projectPath,completedStep:e.completedStep,addEnvVar:e.addEnvVar})}};import{z as f}from"zod";var ne=f.object({action:f.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:f.string().optional().describe("URL to navigate to. Required for 'navigate'; optional for 'screenshot' (navigates before capturing)."),selector:f.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:f.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:f.boolean().default(!1).describe("For 'screenshot': capture the full scrollable page instead of just the viewport."),includeScreenshot:f.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 _(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 _(t);return{content:[{type:"text",text:JSON.stringify({url:t.url(),title:await t.title(),snapshot:l})}]}}let a=await _(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
|
|
1695
17
|
|
|
1696
18
|
The Mistflow CLI handles local execution and long-running operations that
|
|
1697
19
|
would hit the MCP 60s tool-call ceiling. Every command below is invokable
|
|
@@ -1773,6 +95,6 @@ and decides the next command. Example end-to-end chain:
|
|
|
1773
95
|
echo '{...}' | mist plan --cid ... --pick-stdin --json
|
|
1774
96
|
# \u2192 {"status":"ready","plan":{...}}
|
|
1775
97
|
# Then: mist_build init (MCP), mist install (CLI), mist_build implement (MCP), etc.
|
|
1776
|
-
|
|
1777
|
-
`),
|
|
1778
|
-
`).trim())}};var
|
|
98
|
+
`,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(`
|
|
99
|
+
`),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(`
|
|
100
|
+
`).trim())}};var C=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 --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."}),z=[O,U,D,J];C.setRequestHandler(le,async()=>({tools:z.map(n=>({name:n.name,description:n.description,inputSchema:ce(n.inputSchema)}))}));C.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:C,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 C.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)});
|