#!/usr/bin/env python3
"""Code project harness for small business-owner app builds.
This harness creates a real multi-file project instead of a one-file demo. It
keeps the model out of fragile boilerplate and makes the product path return a
project with files, tests, a change summary, and deterministic validation.
"""
from __future__ import annotations
import difflib
import json
import re
from dataclasses import dataclass, field
from pathlib import Path
from typing import Any
@dataclass
class CodeProjectSpec:
project_name: str
project_type: str
description: str
features: list[str] = field(default_factory=list)
entities: list[str] = field(default_factory=list)
fields: list[str] = field(default_factory=list)
commands: list[str] = field(default_factory=list)
PROJECT_DEFAULTS: dict[str, dict[str, list[str] | str]] = {
"stripe_checkout": {
"description": "Stripe-ready checkout starter with server-side session creation and webhook notes.",
"features": ["Pricing cards", "Checkout session route", "Webhook verification notes", "Success and cancel states"],
"entities": ["product", "checkout_session", "customer"],
"fields": ["Product", "Price", "Billing email", "Status"],
"commands": ["npm install", "npm run lint", "npm run test"],
},
"booking_app": {
"description": "Appointment booking starter with customer/service/date capture and local persistence.",
"features": ["Booking form", "Appointment list", "Status updates", "CSV export"],
"entities": ["appointment", "customer", "service"],
"fields": ["Customer", "Service", "Date", "Time", "Status"],
"commands": ["npm install", "npm run lint", "npm run test"],
},
"crm_app": {
"description": "Lead tracker starter for small businesses that need follow-up discipline.",
"features": ["Lead form", "Pipeline status", "Follow-up date", "CSV export"],
"entities": ["lead", "company", "follow_up"],
"fields": ["Name", "Company", "Need", "Status", "Next Follow-up"],
"commands": ["npm install", "npm run lint", "npm run test"],
},
"dashboard": {
"description": "Small business dashboard starter with metrics, tasks, and next actions.",
"features": ["KPI cards", "Task list", "Recent activity", "Next action panel"],
"entities": ["metric", "task", "activity"],
"fields": ["Metric", "Value", "Owner", "Status"],
"commands": ["npm install", "npm run lint", "npm run test"],
},
"invoice_app": {
"description": "Invoice builder starter for small businesses that need fast billing and payment tracking.",
"features": ["Invoice form", "Client and service tracking", "Payment status", "CSV export"],
"entities": ["invoice", "client", "service"],
"fields": ["Client", "Service", "Amount", "Due Date", "Status"],
"commands": ["npm install", "npm run lint", "npm run test"],
},
"estimate_app": {
"description": "Estimate builder starter for service businesses that need clear quotes and follow-up tracking.",
"features": ["Estimate form", "Scope and price tracking", "Follow-up date", "CSV export"],
"entities": ["estimate", "client", "job"],
"fields": ["Client", "Job", "Scope", "Price", "Follow-up Date", "Status"],
"commands": ["npm install", "npm run lint", "npm run test"],
},
"content_calendar": {
"description": "Content calendar starter for creators and solo founders planning posts, hooks, and publishing dates.",
"features": ["Content idea capture", "Channel planning", "Publish date tracking", "CSV export"],
"entities": ["content_item", "channel", "campaign"],
"fields": ["Idea", "Channel", "Hook", "Publish Date", "Status"],
"commands": ["npm install", "npm run lint", "npm run test"],
},
"expense_tracker": {
"description": "Expense tracker starter for owners who need fast cash and vendor visibility.",
"features": ["Expense form", "Vendor and category tracking", "Payment method tracking", "CSV export"],
"entities": ["expense", "vendor", "category"],
"fields": ["Vendor", "Category", "Amount", "Payment Method", "Date", "Status"],
"commands": ["npm install", "npm run lint", "npm run test"],
},
"cloudflare_worker": {
"description": "Cloudflare Worker API starter with validation, CORS, health check, tests, and safe environment handling.",
"features": ["Health check", "Validated JSON endpoint", "CORS handling", "Worker tests", "Wrangler deployment notes"],
"entities": ["request", "lead", "response"],
"fields": ["Name", "Email", "Need", "Source"],
"commands": ["npm install", "npm run dev", "npm run build", "npm run test", "npm run deploy"],
},
}
def clean_text(value: Any, fallback: str) -> str:
if not isinstance(value, str):
return fallback
value = re.sub(r"\s+", " ", value).strip()
return value or fallback
def slugify(value: str) -> str:
return re.sub(r"[^a-z0-9]+", "-", value.lower()).strip("-") or "kaiju-project"
def pascal_case(value: str) -> str:
words = re.findall(r"[A-Za-z0-9]+", value)
return "".join(word[:1].upper() + word[1:] for word in words) or "KaijuProject"
def infer_project_type(prompt: str) -> str:
lower = prompt.lower()
if "content calendar" in lower or "content planner" in lower or "posting calendar" in lower:
return "content_calendar"
if "expense tracker" in lower or "budget tracker" in lower or "cash tracker" in lower:
return "expense_tracker"
if "estimate app" in lower or "estimate builder" in lower or "quote app" in lower or "quote builder" in lower:
return "estimate_app"
if "invoice app" in lower or "invoice builder" in lower or "invoice tracker" in lower or "invoice generator" in lower:
return "invoice_app"
if "stripe" in lower or "checkout" in lower or "payment" in lower:
return "stripe_checkout"
if any(term in lower for term in ["cloudflare worker", "worker api", "wrangler", "d1", "r2", "durable object", "api worker", "telegram", "webhook", "file upload", "search proxy", "perplexity", "rate limit", "license check", "api key"]):
return "cloudflare_worker"
if "booking" in lower or "appointment" in lower or "schedule" in lower:
return "booking_app"
if "crm" in lower or "lead" in lower or "pipeline" in lower or "prospect" in lower:
return "crm_app"
return "dashboard"
def infer_project_name(prompt: str, project_type: str) -> str:
stop_words = r"\s+(?:for|with|to|that|using|include|including|built|as)\b"
for marker in ["named", "called"]:
match = re.search(rf"{marker}\s+([A-Z][A-Za-z0-9 &'&-]{{2,70}}?)(?:\.|,|{stop_words}|$)", prompt, flags=re.IGNORECASE)
if match:
return clean_text(match.group(1), "Kaiju Project").rstrip(".")
names = {
"stripe_checkout": "Checkout Starter",
"booking_app": "Booking Desk",
"crm_app": "Pipeline Desk",
"dashboard": "Operator Dashboard",
"invoice_app": "Invoice Desk",
"estimate_app": "Estimate Desk",
"content_calendar": "Content Desk",
"expense_tracker": "Expense Desk",
"cloudflare_worker": "Worker API",
}
return names[project_type]
def spec_from_prompt(prompt: str) -> CodeProjectSpec:
project_type = infer_project_type(prompt)
defaults = PROJECT_DEFAULTS[project_type]
features = list(defaults["features"])
entities = list(defaults["entities"])
fields = list(defaults["fields"])
lower = prompt.lower()
if project_type == "cloudflare_worker":
if "telegram" in lower:
features.append("Telegram webhook route")
entities.append("telegram_update")
if "r2" in lower or "upload" in lower or "file" in lower:
features.append("R2 file upload route")
entities.append("uploaded_file")
if "d1" in lower or "database" in lower or "persist" in lower or "store" in lower:
features.append("D1 persistence notes")
entities.append("database_record")
if "search" in lower or "perplexity" in lower:
features.append("Server-side search proxy")
entities.append("search_query")
if "rate limit" in lower or "license" in lower or "api key" in lower or "auth" in lower:
features.append("Bearer auth and rate limit guard")
entities.append("api_client")
return CodeProjectSpec(
project_name=infer_project_name(prompt, project_type),
project_type=project_type,
description=str(defaults["description"]),
features=list(dict.fromkeys(features)),
entities=list(dict.fromkeys(entities)),
fields=fields,
commands=list(defaults["commands"]),
)
def normalize_spec(raw: dict[str, Any] | CodeProjectSpec, prompt: str = "") -> CodeProjectSpec:
if isinstance(raw, CodeProjectSpec):
spec = raw
else:
fallback = spec_from_prompt(prompt)
features = raw.get("features") if isinstance(raw.get("features"), list) else fallback.features
entities = raw.get("entities") if isinstance(raw.get("entities"), list) else fallback.entities
fields = raw.get("fields") if isinstance(raw.get("fields"), list) else fallback.fields
commands = raw.get("commands") if isinstance(raw.get("commands"), list) else fallback.commands
spec = CodeProjectSpec(
project_name=clean_text(raw.get("project_name"), fallback.project_name),
project_type=clean_text(raw.get("project_type"), fallback.project_type).lower(),
description=clean_text(raw.get("description"), fallback.description),
features=[clean_text(item, "") for item in features if isinstance(item, str)][:8] or fallback.features,
entities=[clean_text(item, "") for item in entities if isinstance(item, str)][:6] or fallback.entities,
fields=[clean_text(item, "") for item in fields if isinstance(item, str)][:8] or fallback.fields,
commands=[clean_text(item, "") for item in commands if isinstance(item, str)][:5] or fallback.commands,
)
if spec.project_type not in PROJECT_DEFAULTS:
spec.project_type = infer_project_type(prompt)
if len(spec.features) < 3:
spec.features = list(PROJECT_DEFAULTS[spec.project_type]["features"])
if len(spec.fields) < 3:
spec.fields = list(PROJECT_DEFAULTS[spec.project_type]["fields"])
return spec
def render_package_json(spec: CodeProjectSpec) -> str:
if spec.project_type == "cloudflare_worker":
package = {
"name": slugify(spec.project_name),
"version": "0.1.0",
"private": True,
"type": "module",
"scripts": {
"dev": "wrangler dev",
"build": "wrangler deploy --dry-run",
"deploy": "wrangler deploy",
"test": "vitest run",
"lint": "tsc --noEmit",
},
"dependencies": {
"zod": "^3.23.8",
},
"devDependencies": {
"@cloudflare/workers-types": "^4.20250501.0",
"typescript": "^5.6.0",
"vitest": "^2.1.0",
"wrangler": "^4.0.0",
},
}
return json.dumps(package, indent=2) + "\n"
package = {
"name": slugify(spec.project_name),
"version": "0.1.0",
"private": True,
"scripts": {
"dev": "next dev",
"build": "next build",
"lint": "tsc --noEmit",
"test": "vitest run",
},
"dependencies": {
"next": "^15.0.0",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"zod": "^3.23.8",
},
"devDependencies": {
"@types/node": "^22.0.0",
"@types/react": "^19.0.0",
"@types/react-dom": "^19.0.0",
"typescript": "^5.6.0",
"vitest": "^2.1.0",
},
}
if spec.project_type == "stripe_checkout":
package["dependencies"]["stripe"] = "^17.0.0"
return json.dumps(package, indent=2) + "\n"
def render_tsconfig() -> str:
return """{
"compilerOptions": {
"target": "ES2022",
"lib": ["dom", "dom.iterable", "es2022"],
"allowJs": false,
"skipLibCheck": true,
"strict": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true,
"plugins": [{ "name": "next" }]
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
"exclude": ["node_modules"]
}
"""
def render_next_config() -> str:
return """/** @type {import('next').NextConfig} */
const nextConfig = {
reactStrictMode: true
};
module.exports = nextConfig;
"""
def render_worker_tsconfig() -> str:
return """{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "Bundler",
"lib": ["ES2022"],
"types": ["@cloudflare/workers-types"],
"strict": true,
"skipLibCheck": true,
"noEmit": true
},
"include": ["src/**/*.ts", "tests/**/*.ts"]
}
"""
def worker_has(spec: CodeProjectSpec, *terms: str) -> bool:
text = " ".join([spec.project_name, spec.description, *spec.features, *spec.entities, *spec.fields]).lower()
return any(term.lower() in text for term in terms)
def render_wrangler_toml(spec: CodeProjectSpec) -> str:
bindings: list[str] = []
if worker_has(spec, "d1", "database", "persist"):
bindings.append("""
[[d1_databases]]
binding = "DB"
database_name = "kaiju_dev"
database_id = "replace-with-d1-database-id"
""")
if worker_has(spec, "r2", "upload", "file"):
bindings.append("""
[[r2_buckets]]
binding = "FILES_BUCKET"
bucket_name = "kaiju-dev-files"
""")
return f"""name = "{slugify(spec.project_name)}"
main = "src/index.ts"
compatibility_date = "2026-05-01"
workers_dev = true
[vars]
PUBLIC_APP_NAME = "{spec.project_name}"
""" + "".join(bindings)
def render_css() -> str:
return """* { box-sizing: border-box; }
body {
margin: 0;
font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
background: radial-gradient(circle at top right, rgba(244, 173, 50, 0.18), transparent 34%), #090d14;
color: #f8fafc;
}
a { color: inherit; }
button, input, select { font: inherit; }
.shell { max-width: 1120px; margin: 0 auto; padding: 42px 24px; }
.eyebrow { color: #f4ad32; text-transform: uppercase; letter-spacing: .16em; font-size: 12px; font-weight: 900; }
.hero { display: grid; grid-template-columns: 1fr 420px; gap: 24px; align-items: start; }
h1 { font-size: clamp(44px, 7vw, 78px); line-height: .95; letter-spacing: -.055em; margin: 12px 0; }
p { color: #a6b0c2; line-height: 1.65; font-size: 18px; }
.panel { background: rgba(255,255,255,.045); border: 1px solid rgba(148,163,184,.22); border-radius: 26px; padding: 22px; box-shadow: 0 24px 80px rgba(0,0,0,.22); }
.grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 14px; margin-top: 22px; }
.card { background: rgba(255,255,255,.045); border: 1px solid rgba(148,163,184,.18); border-radius: 20px; padding: 18px; }
.btn { border: 0; border-radius: 999px; padding: 13px 18px; background: #f4ad32; color: #111827; font-weight: 950; cursor: pointer; }
.field { display: block; margin-bottom: 12px; color: #a6b0c2; font-weight: 800; }
.input { width: 100%; margin-top: 7px; padding: 12px; border-radius: 14px; border: 1px solid rgba(148,163,184,.24); background: #0e1420; color: #f8fafc; }
.list { margin: 0; padding-left: 18px; color: #dbe3f0; line-height: 1.75; }
.status { display: inline-flex; padding: 8px 12px; border-radius: 999px; background: rgba(34,197,94,.14); color: #86efac; font-weight: 900; }
@media (max-width: 880px) {
.hero, .grid { grid-template-columns: 1fr; }
h1 { font-size: 44px; }
}
"""
def field_id(label: str) -> str:
return slugify(label).replace("-", "_")
def render_layout(spec: CodeProjectSpec) -> str:
return f"""import type {{ Metadata }} from "next";
import "./globals.css";
export const metadata: Metadata = {{
title: "{spec.project_name}",
description: "{spec.description}",
}};
export default function RootLayout({{ children }}: {{ children: React.ReactNode }}) {{
return (
{{children}}
);
}}
"""
def render_page(spec: CodeProjectSpec) -> str:
component = pascal_case(spec.project_name)
feature_cards = "\n".join(f' {feature}
Built into the first version so the owner can use it immediately.
' for feature in spec.features[:6])
fields = "\n".join(
f' '
for label in spec.fields[:6]
)
if spec.project_type == "stripe_checkout":
side_panel = """ """
else:
side_panel = f""" """
return f"""const features = {json.dumps(spec.features[:6], indent=2)};
export default function {component}Page() {{
return (
Kaiju project harness
{spec.project_name}
{spec.description}
{spec.project_type.replace("_", " ")}
{side_panel}
Implementation notes
{{features.map((feature) => - {{feature}}
)}}
);
}}
"""
def render_interactive_page(spec: CodeProjectSpec) -> str:
component = pascal_case(spec.project_name)
fields = [{"id": field_id(label), "label": label} for label in spec.fields[:6]]
field_labels = ", ".join(field["label"] for field in fields)
return f""""use client";
import type {{ FormEvent }} from "react";
import {{ useEffect, useState }} from "react";
import {{ toCsv, type CsvRow }} from "../lib/csv";
type RecordItem = {{
id: string;
createdAt: string;
}} & Record;
const fields = {json.dumps(fields, indent=2)};
const storageKey = "kaiju:{slugify(spec.project_name)}";
function emptyDraft(): Record {{
return Object.fromEntries(fields.map((field) => [field.id, ""])) as Record;
}}
function downloadText(filename: string, content: string): void {{
const blob = new Blob([content], {{ type: "text/csv;charset=utf-8" }});
const url = URL.createObjectURL(blob);
const link = document.createElement("a");
link.href = url;
link.download = filename;
link.click();
URL.revokeObjectURL(url);
}}
export default function {component}Page() {{
const [records, setRecords] = useState([]);
const [draft, setDraft] = useState>(emptyDraft);
useEffect(() => {{
const saved = window.localStorage.getItem(storageKey);
if (!saved) return;
try {{
const parsed = JSON.parse(saved) as RecordItem[];
if (Array.isArray(parsed)) setRecords(parsed);
}} catch (_error) {{
window.localStorage.removeItem(storageKey);
}}
}}, []);
useEffect(() => {{
window.localStorage.setItem(storageKey, JSON.stringify(records));
}}, [records]);
function saveRecord(event: FormEvent): void {{
event.preventDefault();
const hasValue = fields.some((field) => draft[field.id]?.trim());
if (!hasValue) return;
const record: RecordItem = {{
id: crypto.randomUUID(),
createdAt: new Date().toLocaleString(),
...draft,
}};
setRecords((current) => [record, ...current]);
setDraft(emptyDraft());
}}
function deleteRecord(id: string): void {{
setRecords((current) => current.filter((record) => record.id !== id));
}}
function exportRecords(): void {{
const columns = ["createdAt", ...fields.map((field) => field.id)];
const rows: CsvRow[] = records.map((record) =>
Object.fromEntries(columns.map((column) => [column, record[column] || ""])) as CsvRow,
);
downloadText("{slugify(spec.project_name)}.csv", toCsv(rows, columns));
}}
return (
Kaiju project harness
{spec.project_name}
{spec.description}
{spec.project_type.replace("_", " ")}
{{records.length === 0 ? (
No records yet
Add the first one to start using the app.
) : (
records.map((record) => (
{{record.createdAt}}
{{fields.map((field) => (
{{field.label}}: {{record[field.id] || "Not set"}}
))}}
))
)}}
);
}}
"""
def render_checkout_route() -> str:
return """import { NextRequest, NextResponse } from "next/server";
import Stripe from "stripe";
import { z } from "zod";
const CheckoutSchema = z.object({
priceId: z.string().min(1)
});
function stripeClient() {
const secret = process.env.STRIPE_SECRET_KEY;
if (!secret) throw new Error("Missing STRIPE_SECRET_KEY");
return new Stripe(secret, { apiVersion: "2025-02-24.acacia" });
}
export async function POST(request: NextRequest) {
const formData = await request.formData();
const parsed = CheckoutSchema.safeParse({ priceId: formData.get("priceId") });
if (!parsed.success) {
return NextResponse.json({ error: "Missing priceId" }, { status: 400 });
}
const origin = request.headers.get("origin") || "http://localhost:3000";
const session = await stripeClient().checkout.sessions.create({
mode: "payment",
line_items: [{ price: parsed.data.priceId, quantity: 1 }],
success_url: `${origin}/success?session_id={CHECKOUT_SESSION_ID}`,
cancel_url: `${origin}/cancel`
});
if (!session.url) {
return NextResponse.json({ error: "Stripe did not return a checkout URL" }, { status: 502 });
}
return NextResponse.redirect(session.url, { status: 303 });
}
"""
def render_webhook_route() -> str:
return """import { NextRequest, NextResponse } from "next/server";
import Stripe from "stripe";
function stripeClient() {
const secret = process.env.STRIPE_SECRET_KEY;
if (!secret) throw new Error("Missing STRIPE_SECRET_KEY");
return new Stripe(secret, { apiVersion: "2025-02-24.acacia" });
}
export async function POST(request: NextRequest) {
const signature = request.headers.get("stripe-signature");
const webhookSecret = process.env.STRIPE_WEBHOOK_SECRET;
if (!signature || !webhookSecret) {
return NextResponse.json({ error: "Webhook is not configured" }, { status: 400 });
}
const rawBody = await request.text();
let event: Stripe.Event;
try {
event = stripeClient().webhooks.constructEvent(rawBody, signature, webhookSecret);
} catch (error) {
return NextResponse.json({ error: "Invalid signature" }, { status: 400 });
}
if (event.type === "checkout.session.completed") {
const session = event.data.object as Stripe.Checkout.Session;
console.log("checkout complete", session.id);
}
return NextResponse.json({ received: true });
}
"""
def render_success_page(title: str, message: str) -> str:
return f"""export default function Page() {{
return (
);
}}
"""
def render_readme(spec: CodeProjectSpec) -> str:
commands = "\n".join(f"- `{command}`" for command in spec.commands)
features = "\n".join(f"- {feature}" for feature in spec.features)
env = ""
if spec.project_type == "stripe_checkout":
env = """
## Environment
Create `.env.local`:
```bash
STRIPE_SECRET_KEY=stripe_secret_key_replace_me
STRIPE_WEBHOOK_SECRET=whsec_replace_me
```
Do not expose Stripe secret keys in client components.
"""
return f"""# {spec.project_name}
{spec.description}
## Features
{features}
## Run
{commands}
{env}
## Notes
- This is a generated Kaiju project harness output.
- Review environment variables before using real payments.
- Run tests before shipping.
"""
def render_test(spec: CodeProjectSpec) -> str:
expected = json.dumps(spec.features[:3])
return f"""import {{ describe, expect, it }} from "vitest";
const features = {expected};
describe("{spec.project_name}", () => {{
it("has a practical first-version feature set", () => {{
expect(features.length).toBeGreaterThanOrEqual(3);
expect(features.join(" ").toLowerCase()).not.toContain("placeholder copy");
}});
}});
"""
def render_csv_utility() -> str:
return """export type CsvValue = string | number | boolean | null | undefined;
export type CsvRow = Record;
function escapeCsv(value: CsvValue): string {
const text = value === null || value === undefined ? "" : String(value);
if (/[",\\n]/.test(text)) {
return `"${text.replaceAll('"', '""')}"`;
}
return text;
}
export function toCsv(rows: CsvRow[], columns: string[]): string {
const header = columns.map(escapeCsv).join(",");
const body = rows.map((row) => columns.map((column) => escapeCsv(row[column])).join(","));
return [header, ...body].join("\\n");
}
"""
def render_csv_test() -> str:
return """import { describe, expect, it } from "vitest";
import { toCsv } from "../src/lib/csv";
describe("toCsv", () => {
it("escapes commas, quotes, and missing values", () => {
const csv = toCsv(
[{ name: "Ada, Inc.", note: 'Needs "premium"', missing: null }],
["name", "note", "missing"],
);
expect(csv).toContain('"Ada, Inc."');
expect(csv).toContain('"Needs ""premium""' + '"');
expect(csv.endsWith(",")).toBe(true);
});
});
"""
def render_worker_index(spec: CodeProjectSpec) -> str:
title = spec.project_name
routes = ["/health", "POST /leads"]
route_blocks: list[str] = []
d1_line = ""
if worker_has(spec, "d1", "database", "persist"):
d1_line = """
if (env.DB) {
await env.DB.prepare("CREATE TABLE IF NOT EXISTS leads (id TEXT PRIMARY KEY, email TEXT, need TEXT, created_at TEXT)").run();
await env.DB.prepare("INSERT INTO leads (id, email, need, created_at) VALUES (?, ?, ?, ?)").bind(lead.id, lead.email, lead.need, lead.createdAt).run();
}
"""
if worker_has(spec, "telegram"):
routes.append("POST /telegram/webhook")
route_blocks.append("""
if (url.pathname === "/telegram/webhook" && request.method === "POST") {
const update = await readJson(request);
if (!update || typeof update !== "object") {
return json({ ok: false, error: "Invalid Telegram update" }, { status: 400 });
}
return json({ ok: true, handled: true, nextStep: "Queue this update or send a reply with TELEGRAM_BOT_TOKEN server-side." });
}
""")
if worker_has(spec, "r2", "upload", "file"):
routes.append("POST /files")
route_blocks.append("""
if (url.pathname === "/files" && request.method === "POST") {
if (!env.FILES_BUCKET) {
return json({ ok: false, error: "FILES_BUCKET is not bound" }, { status: 500 });
}
const filename = url.searchParams.get("filename") || `upload-${crypto.randomUUID()}.bin`;
await env.FILES_BUCKET.put(filename, request.body);
return json({ ok: true, key: filename });
}
""")
if worker_has(spec, "search", "perplexity"):
routes.append("POST /search")
route_blocks.append("""
if (url.pathname === "/search" && request.method === "POST") {
if (!env.PERPLEXITY_API_KEY) {
return json({ ok: false, error: "Search provider is not configured" }, { status: 500 });
}
const payload = await readJson(request);
return json({ ok: true, query: payload, nextStep: "Call the provider server-side here; never expose PERPLEXITY_API_KEY to clients." });
}
""")
return f"""import {{ z }} from "zod";
export interface Env {{
PUBLIC_APP_NAME?: string;
API_TOKEN_HASH?: string;
TELEGRAM_BOT_TOKEN?: string;
PERPLEXITY_API_KEY?: string;
FILES_BUCKET?: R2Bucket;
DB?: D1Database;
}}
const LeadSchema = z.object({{
name: z.string().min(2),
email: z.string().email(),
need: z.string().min(3),
source: z.string().optional()
}});
const corsHeaders = {{
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Methods": "GET,POST,OPTIONS",
"Access-Control-Allow-Headers": "Content-Type, Authorization"
}};
function json(data: unknown, init: ResponseInit = {{}}): Response {{
return new Response(JSON.stringify(data, null, 2), {{
...init,
headers: {{
"Content-Type": "application/json; charset=utf-8",
...corsHeaders,
...(init.headers || {{}})
}}
}});
}}
async function readJson(request: Request): Promise {{
try {{
return await request.json();
}} catch (_error) {{
return null;
}}
}}
async function sha256Hex(value: string): Promise {{
const bytes = new TextEncoder().encode(value);
const digest = await crypto.subtle.digest("SHA-256", bytes);
return [...new Uint8Array(digest)].map((byte) => byte.toString(16).padStart(2, "0")).join("");
}}
function timingSafeEqual(a: string, b: string): boolean {{
if (a.length !== b.length) return false;
let mismatch = 0;
for (let index = 0; index < a.length; index += 1) {{
mismatch |= a.charCodeAt(index) ^ b.charCodeAt(index);
}}
return mismatch === 0;
}}
async function requireBearer(request: Request, env: Env): Promise {{
if (!env.API_TOKEN_HASH) return null;
const supplied = request.headers.get("authorization") || "";
if (!supplied.startsWith("Bearer ")) {{
return json({{ ok: false, error: "Missing bearer token" }}, {{ status: 401 }});
}}
const token = supplied.slice("Bearer ".length).trim();
const tokenHash = await sha256Hex(token);
if (!timingSafeEqual(tokenHash, env.API_TOKEN_HASH)) {{
return json({{ ok: false, error: "Unauthorized" }}, {{ status: 401 }});
}}
return null;
}}
export default {{
async fetch(request: Request, env: Env): Promise {{
const url = new URL(request.url);
const authError = await requireBearer(request, env);
if (authError) return authError;
if (request.method === "OPTIONS") {{
return new Response(null, {{ headers: corsHeaders }});
}}
if (url.pathname === "/health") {{
return json({{ ok: true, service: env.PUBLIC_APP_NAME || "{title}" }});
}}
if (url.pathname === "/leads" && request.method === "POST") {{
const parsed = LeadSchema.safeParse(await readJson(request));
if (!parsed.success) {{
return json({{ ok: false, error: "Invalid lead payload", issues: parsed.error.flatten() }}, {{ status: 400 }});
}}
const lead = {{
id: crypto.randomUUID(),
createdAt: new Date().toISOString(),
...parsed.data
}};
{d1_line}
return json({{ ok: true, lead, nextStep: "Send a confirmation email or store this in D1 when ready." }}, {{ status: 201 }});
}}
{''.join(route_blocks)}
return json({{
ok: true,
service: env.PUBLIC_APP_NAME || "{title}",
routes: {json.dumps(routes)},
features: {json.dumps(spec.features[:5])}
}});
}}
}};
"""
def render_worker_test(spec: CodeProjectSpec) -> str:
routes = ["/health", "POST /leads"]
if worker_has(spec, "telegram"):
routes.append("POST /telegram/webhook")
if worker_has(spec, "r2", "upload", "file"):
routes.append("POST /files")
if worker_has(spec, "search", "perplexity"):
routes.append("POST /search")
return f"""import {{ describe, expect, it }} from "vitest";
describe("{spec.project_name} worker contract", () => {{
it("documents the required routes", () => {{
const routes = {json.dumps(routes)};
expect(routes).toContain("/health");
expect(routes).toContain("POST /leads");
}});
it("keeps provider secrets out of generated code", () => {{
const forbidden = ["live provider key", "test provider key", "search provider key"];
expect(forbidden.join(" ")).not.toContain("real_secret_value");
}});
}});
"""
def render_change_summary(spec: CodeProjectSpec, files: dict[str, str]) -> str:
file_list = "\n".join(f"- `{path}`" for path in sorted(files))
return f"""# Kaiju Change Summary
## Project
{spec.project_name}
## Type
{spec.project_type}
## What Changed
Generated a complete starter project with app UI, styling, tests, documentation, and safe defaults.
## Files
{file_list}
## Verification
- Parse `package.json`.
- Confirm required source files exist.
- Confirm no provider secret is hardcoded.
- Run `npm run test` after dependencies are installed.
"""
def render_patch(files: dict[str, str]) -> str:
patch_chunks: list[str] = []
for path, content in sorted(files.items()):
diff = difflib.unified_diff(
[],
content.splitlines(keepends=True),
fromfile=f"a/{path}",
tofile=f"b/{path}",
)
patch_chunks.append("".join(diff))
return "\n".join(patch_chunks)
def render_files(raw_spec: dict[str, Any] | CodeProjectSpec, prompt: str = "") -> tuple[CodeProjectSpec, dict[str, str]]:
spec = normalize_spec(raw_spec, prompt)
if spec.project_type == "cloudflare_worker":
files = {
"package.json": render_package_json(spec),
"tsconfig.json": render_worker_tsconfig(),
"wrangler.toml": render_wrangler_toml(spec),
"src/index.ts": render_worker_index(spec),
"tests/worker.test.ts": render_worker_test(spec),
"README.md": render_readme(spec),
}
files["kaiju-change-summary.md"] = render_change_summary(spec, files)
files["kaiju.patch"] = render_patch(files)
return spec, files
files: dict[str, str] = {
"package.json": render_package_json(spec),
"tsconfig.json": render_tsconfig(),
"next.config.js": render_next_config(),
"src/app/layout.tsx": render_layout(spec),
"src/app/globals.css": render_css(),
"src/app/page.tsx": render_page(spec) if spec.project_type == "stripe_checkout" else render_interactive_page(spec),
"tests/smoke.test.ts": render_test(spec),
"README.md": render_readme(spec),
}
if spec.project_type == "stripe_checkout":
files["src/app/api/checkout/route.ts"] = render_checkout_route()
files["src/app/api/webhooks/stripe/route.ts"] = render_webhook_route()
files["src/app/success/page.tsx"] = render_success_page("Payment received", "Stripe returned a successful checkout session.")
files["src/app/cancel/page.tsx"] = render_success_page("Checkout canceled", "The customer can return and try again.")
else:
files["src/lib/csv.ts"] = render_csv_utility()
files["tests/csv.test.ts"] = render_csv_test()
files["kaiju-change-summary.md"] = render_change_summary(spec, files)
files["kaiju.patch"] = render_patch(files)
return spec, files
def validate_files(files: dict[str, str], spec: CodeProjectSpec | None = None) -> list[str]:
errors: list[str] = []
if spec and spec.project_type == "cloudflare_worker":
required = ["package.json", "tsconfig.json", "wrangler.toml", "src/index.ts", "tests/worker.test.ts", "README.md", "kaiju-change-summary.md", "kaiju.patch"]
else:
required = ["package.json", "tsconfig.json", "next.config.js", "src/app/layout.tsx", "src/app/page.tsx", "src/app/globals.css", "tests/smoke.test.ts", "README.md", "kaiju-change-summary.md", "kaiju.patch"]
for path in required:
if path not in files:
errors.append(f"missing file: {path}")
try:
package = json.loads(files.get("package.json", "{}"))
except json.JSONDecodeError as exc:
errors.append(f"invalid package.json: {exc}")
package = {}
scripts = package.get("scripts", {}) if isinstance(package, dict) else {}
for script in ["dev", "build", "lint", "test"]:
if script not in scripts:
errors.append(f"missing npm script: {script}")
combined = "\n".join(files.values()).lower()
forbidden = ["sk_live_", "sk_test_", "rk_live_", "pplx-", "AIza", "anthropic_api_key"]
for token in forbidden:
if token.lower() in combined:
errors.append(f"forbidden secret token: {token}")
if "lorem ipsum" in combined:
errors.append("lorem ipsum found")
if spec and spec.project_type == "stripe_checkout":
for path in ["src/app/api/checkout/route.ts", "src/app/api/webhooks/stripe/route.ts"]:
if path not in files:
errors.append(f"missing Stripe route: {path}")
if "process.env.stripe_secret_key" not in combined:
errors.append("Stripe secret is not server-side env based")
if "constructevent" not in combined:
errors.append("missing Stripe webhook signature verification")
if spec and spec.project_type in {"booking_app", "crm_app", "dashboard", "invoice_app", "estimate_app", "content_calendar", "expense_tracker"}:
page = files.get("src/app/page.tsx", "")
for path in ["src/lib/csv.ts", "tests/csv.test.ts"]:
if path not in files:
errors.append(f"missing interactive app support file: {path}")
for token in ['"use client"', "localStorage", "Export CSV", "Delete", "Save locally"]:
if token not in page:
errors.append(f"interactive app page missing token: {token}")
if "tocsv" not in combined:
errors.append("interactive app missing CSV export utility")
if spec and spec.project_type == "cloudflare_worker":
for path in ["wrangler.toml", "src/index.ts", "tests/worker.test.ts"]:
if path not in files:
errors.append(f"missing Worker file: {path}")
worker = files.get("src/index.ts", "")
wrangler = files.get("wrangler.toml", "")
if "export default" not in worker:
errors.append("missing Worker fetch export")
if "access-control-allow-origin" not in combined:
errors.append("missing CORS handling")
if "/health" not in combined or "/leads" not in combined:
errors.append("missing health/leads routes")
if worker_has(spec, "telegram") and ("/telegram/webhook" not in worker or "TELEGRAM_BOT_TOKEN" not in worker):
errors.append("missing Telegram webhook route or env")
if worker_has(spec, "r2", "upload", "file") and ("/files" not in worker or "FILES_BUCKET" not in worker or "[[r2_buckets]]" not in wrangler):
errors.append("missing R2 upload route or binding")
if worker_has(spec, "d1", "database", "persist") and ("env.DB" not in worker or "[[d1_databases]]" not in wrangler):
errors.append("missing D1 persistence route logic or binding")
if worker_has(spec, "search", "perplexity") and ("/search" not in worker or "PERPLEXITY_API_KEY" not in worker):
errors.append("missing server-side search proxy route or env")
if worker_has(spec, "auth", "license", "rate limit", "api key"):
auth_required = ["API_TOKEN_HASH", "requireBearer", "crypto.subtle.digest", "timingSafeEqual", "await requireBearer"]
if any(token not in worker for token in auth_required):
errors.append("missing verified bearer auth guard")
return errors
def write_project(root: Path, files: dict[str, str]) -> None:
root.mkdir(parents=True, exist_ok=True)
for relative, content in files.items():
path = root / relative
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(content, encoding="utf-8")
def render_from_prompt(prompt: str) -> tuple[CodeProjectSpec, dict[str, str], list[str]]:
spec, files = render_files(spec_from_prompt(prompt), prompt)
return spec, files, validate_files(files, spec)
def spec_to_json(spec: CodeProjectSpec) -> str:
return json.dumps(spec.__dict__, indent=2, ensure_ascii=False)