restokes92's picture
Upload Kaiju Coder 7 OpenCode helper package
c75f885 verified
#!/usr/bin/env python3
"""Interactive app harness for simple business-owner apps.
This creates complete one-file web apps with local persistence. The goal is not
to replace real engineering for complex SaaS products. It is to make common
small-business utility apps fast, complete, and demonstrable.
"""
from __future__ import annotations
import html
import json
import re
from dataclasses import dataclass, field
from pathlib import Path
from typing import Any
@dataclass
class AppSpec:
app_name: str
app_type: str
headline: str
entities: list[str] = field(default_factory=list)
fields: list[str] = field(default_factory=list)
actions: list[str] = field(default_factory=list)
accent: str = "#f4ad32"
APP_DEFAULTS: dict[str, dict[str, list[str] | str]] = {
"booking": {
"headline": "Book appointments and keep the day organized.",
"entities": ["appointments"],
"fields": ["Customer", "Service", "Date", "Time", "Phone"],
"actions": ["Add appointment", "Mark complete", "Export CSV"],
},
"crm": {
"headline": "Track leads from first message to paid customer.",
"entities": ["leads"],
"fields": ["Name", "Company", "Need", "Status", "Next Follow-up"],
"actions": ["Add lead", "Update status", "Export CSV"],
},
"invoice_tracker": {
"headline": "Track invoices, payment status, and follow-ups.",
"entities": ["invoices"],
"fields": ["Client", "Invoice #", "Amount", "Status", "Due Date"],
"actions": ["Add invoice", "Mark paid", "Export CSV"],
},
"inventory": {
"headline": "Keep inventory clear without a heavy system.",
"entities": ["items"],
"fields": ["Item", "Category", "Quantity", "Reorder At", "Supplier"],
"actions": ["Add item", "Update quantity", "Export CSV"],
},
"task_board": {
"headline": "Turn scattered work into a clear action board.",
"entities": ["tasks"],
"fields": ["Task", "Owner", "Priority", "Status", "Due Date"],
"actions": ["Add task", "Mark complete", "Export CSV"],
},
"estimate_builder": {
"headline": "Build quotes and estimates without losing the details.",
"entities": ["estimates"],
"fields": ["Client", "Service", "Labor Hours", "Materials", "Estimate Total", "Status"],
"actions": ["Add estimate", "Mark approved", "Export CSV"],
},
"content_calendar": {
"headline": "Plan posts, hooks, channels, and publish dates in one place.",
"entities": ["content ideas"],
"fields": ["Idea", "Channel", "Hook", "Publish Date", "Status", "Owner"],
"actions": ["Add idea", "Mark published", "Export CSV"],
},
"expense_tracker": {
"headline": "Track expenses and cash movement before it gets messy.",
"entities": ["expenses"],
"fields": ["Vendor", "Category", "Amount", "Payment Method", "Date", "Status"],
"actions": ["Add expense", "Mark reviewed", "Export CSV"],
},
}
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 infer_app_type(prompt: str) -> str:
lower = prompt.lower()
if any(term in lower for term in ["content calendar", "post calendar", "social calendar", "marketing calendar", "content planner", "post flow"]):
return "content_calendar"
if any(term in lower for term in ["expense", "budget", "cash flow", "spend", "receipt", "payment method"]):
return "expense_tracker"
if any(term in lower for term in ["estimate", "quote", "bid", "proposal calculator", "price calculator"]):
return "estimate_builder"
if any(term in lower for term in ["inventory", "stock", "reorder"]):
return "inventory"
if any(term in lower for term in ["crm", "lead", "prospect", "pipeline"]):
return "crm"
if any(term in lower for term in ["invoice", "paid", "payment"]):
return "invoice_tracker"
if any(term in lower for term in ["booking", "appointment", "calendar", "schedule"]):
return "booking"
return "task_board"
def infer_app_name(prompt: str, app_type: str) -> str:
stop_words = r"\s+(?:for|with|to|that|using|include|including|built|as)\b"
patterns = [
rf"named\s+([A-Z][A-Za-z0-9 &'&-]{{2,70}}?)(?:\.|,|{stop_words}|$)",
rf"called\s+([A-Z][A-Za-z0-9 &'&-]{{2,70}}?)(?:\.|,|{stop_words}|$)",
]
for pattern in patterns:
match = re.search(pattern, prompt, flags=re.IGNORECASE)
if match:
return clean_text(match.group(1), "Business App").rstrip(".")
names = {
"booking": "Booking Desk",
"crm": "Lead Desk",
"invoice_tracker": "Invoice Desk",
"inventory": "Inventory Desk",
"task_board": "Work Desk",
"estimate_builder": "Estimate Desk",
"content_calendar": "Content Desk",
"expense_tracker": "Expense Desk",
}
return names.get(app_type, "Business App")
def spec_from_prompt(prompt: str) -> AppSpec:
app_type = infer_app_type(prompt)
defaults = APP_DEFAULTS[app_type]
return AppSpec(
app_name=infer_app_name(prompt, app_type),
app_type=app_type,
headline=str(defaults["headline"]),
entities=list(defaults["entities"]),
fields=list(defaults["fields"]),
actions=list(defaults["actions"]),
accent="#38bdf8" if app_type in {"crm", "invoice_tracker", "content_calendar"} else "#f4ad32",
)
def normalize_spec(raw: dict[str, Any] | AppSpec, prompt: str = "") -> AppSpec:
if isinstance(raw, AppSpec):
spec = raw
else:
fallback = spec_from_prompt(prompt)
fields = raw.get("fields") if isinstance(raw.get("fields"), list) else fallback.fields
actions = raw.get("actions") if isinstance(raw.get("actions"), list) else fallback.actions
entities = raw.get("entities") if isinstance(raw.get("entities"), list) else fallback.entities
spec = AppSpec(
app_name=clean_text(raw.get("app_name"), fallback.app_name),
app_type=clean_text(raw.get("app_type"), fallback.app_type).lower(),
headline=clean_text(raw.get("headline"), fallback.headline),
entities=[clean_text(item, "") for item in entities if isinstance(item, str)][:4] or fallback.entities,
fields=[clean_text(item, "") for item in fields if isinstance(item, str)][:8] or fallback.fields,
actions=[clean_text(item, "") for item in actions if isinstance(item, str)][:6] or fallback.actions,
accent=clean_text(raw.get("accent"), fallback.accent),
)
if spec.app_type not in APP_DEFAULTS:
spec.app_type = infer_app_type(prompt)
inferred_app_type = infer_app_type(prompt)
if prompt and inferred_app_type != spec.app_type:
previous_defaults = APP_DEFAULTS.get(spec.app_type, {})
previous_headline = str(previous_defaults.get("headline", ""))
spec.app_type = inferred_app_type
if spec.headline == previous_headline or "book appointments" in spec.headline.lower():
spec.headline = str(APP_DEFAULTS[spec.app_type]["headline"])
if len(spec.fields) < 3:
spec.fields = list(APP_DEFAULTS[spec.app_type]["fields"])
if len(spec.actions) < 2:
spec.actions = list(APP_DEFAULTS[spec.app_type]["actions"])
return spec
def esc(value: str) -> str:
return html.escape(value, quote=True)
def slugify(value: str) -> str:
return re.sub(r"[^a-z0-9]+", "-", value.lower()).strip("-") or "kaiju-app"
def sample_value(field: str, app_type: str) -> str:
lower = field.lower()
if "name" in lower or "client" in lower or "customer" in lower:
return "Jordan Lee"
if "company" in lower:
return "Bright Path Studio"
if "service" in lower:
return "Premium package"
if "date" in lower or "due" in lower or "follow" in lower or "publish" in lower:
return "2026-05-15"
if "time" in lower:
return "10:30 AM"
if "phone" in lower:
return "(404) 555-0199"
if "amount" in lower or "total" in lower or "materials" in lower:
return "$450"
if "hour" in lower:
return "4"
if "status" in lower:
return "Open"
if "priority" in lower:
return "High"
if "owner" in lower:
return "Richard"
if "category" in lower:
return "Retail"
if "quantity" in lower:
return "24"
if "reorder" in lower:
return "10"
if "supplier" in lower or "vendor" in lower:
return "Main Street Supply"
if "item" in lower:
return "Signature candles"
if "idea" in lower:
return "Before-and-after demo"
if "hook" in lower:
return "Stop losing leads in your notes"
if "channel" in lower:
return "YouTube"
if "payment" in lower:
return "Business card"
if "invoice" in lower:
return "INV-1007"
if "task" in lower:
return "Call warm leads"
if "need" in lower:
return "Website and checkout"
return f"{app_type.replace('_', ' ').title()} sample"
def render_field_inputs(fields: list[str]) -> str:
return "\n".join(
f'<label>{esc(field)}<input name="{esc(slugify(field))}" placeholder="{esc(field)}" required></label>'
for field in fields
)
def render_table_headers(fields: list[str]) -> str:
return "\n".join(f"<th>{esc(field)}</th>" for field in fields)
def render_html(raw_spec: dict[str, Any] | AppSpec, prompt: str = "") -> str:
spec = normalize_spec(raw_spec, prompt)
storage_key = f"kaiju-{slugify(spec.app_name)}"
inputs = render_field_inputs(spec.fields)
headers = render_table_headers(spec.fields)
field_names = json.dumps([slugify(field) for field in spec.fields])
field_labels = json.dumps(spec.fields)
sample = json.dumps({slugify(field): sample_value(field, spec.app_type) for field in spec.fields})
return f"""<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{esc(spec.app_name)} | Kaiju App</title>
<style>
:root{{--bg:#0b0f17;--panel:#121823;--card:#171f2d;--ink:#f8fafc;--muted:#94a3b8;--line:rgba(148,163,184,.2);--accent:{esc(spec.accent)}}}*{{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(56,189,248,.22),transparent 32%),var(--bg);color:var(--ink)}}button,input,select{{font:inherit}}.app{{max-width:1180px;margin:auto;padding:34px 28px}}header{{display:flex;justify-content:space-between;gap:20px;align-items:start;margin-bottom:34px}}.brand{{font-weight:950;letter-spacing:-.035em;font-size:28px}}.pill{{border:1px solid var(--line);border-radius:999px;padding:9px 13px;color:var(--muted);font-weight:800}}h1{{font-size:clamp(40px,5.4vw,64px);line-height:.98;letter-spacing:-.045em;margin:20px 0 12px;max-width:860px}}p{{color:var(--muted);line-height:1.65;font-size:18px}}.grid{{display:grid;grid-template-columns:380px 1fr;gap:20px;align-items:start}}.panel{{background:linear-gradient(180deg,rgba(255,255,255,.04),rgba(255,255,255,.015));border:1px solid var(--line);border-radius:28px;padding:22px;box-shadow:0 24px 80px rgba(0,0,0,.24)}}label{{display:block;color:var(--muted);font-weight:850;margin-bottom:12px}}input,select{{width:100%;margin-top:7px;border:1px solid var(--line);background:#0e1420;color:var(--ink);border-radius:14px;padding:13px}}button{{border:0;border-radius:15px;background:var(--accent);color:#07111f;font-weight:950;padding:13px 16px;cursor:pointer}}.secondary{{background:#253044;color:var(--ink)}}.actions{{display:flex;gap:10px;flex-wrap:wrap;margin-top:16px}}table{{width:100%;border-collapse:collapse;overflow:hidden;border-radius:20px}}th,td{{text-align:left;border-bottom:1px solid var(--line);padding:13px;vertical-align:top}}th{{color:var(--muted);font-size:13px;text-transform:uppercase;letter-spacing:.1em}}.empty{{padding:28px;border:1px dashed var(--line);border-radius:20px;color:var(--muted)}}.statbar{{display:grid;grid-template-columns:repeat(3,1fr);gap:12px;margin:22px 0}}.stat{{background:var(--card);border:1px solid var(--line);border-radius:18px;padding:16px}}.stat strong{{display:block;font-size:26px}}@media(max-width:880px){{header,.grid{{display:block}}.panel{{margin-bottom:18px}}h1{{font-size:42px}}.statbar{{grid-template-columns:1fr}}table{{display:block;overflow-x:auto}}}}
</style>
</head>
<body>
<div class="app">
<header><div class="brand">{esc(spec.app_name)}</div><div class="pill">Local-first {esc(spec.app_type.replace("_", " "))}</div></header>
<main>
<p class="pill" style="display:inline-block">Kaiju one-file app</p>
<h1>{esc(spec.headline)}</h1>
<p>Built for business owners who need a working tool now: form entry, saved records, clear status, and CSV export without a backend.</p>
<div class="statbar"><div class="stat"><strong id="totalCount">0</strong><span>Total records</span></div><div class="stat"><strong id="todayCount">0</strong><span>Added today</span></div><div class="stat"><strong id="storageState">Ready</strong><span>Local storage</span></div></div>
<div class="grid">
<section class="panel">
<h2>Add record</h2>
<form id="recordForm">
{inputs}
<div class="actions"><button type="submit">{esc(spec.actions[0])}</button><button class="secondary" type="button" id="loadSample">Load sample</button></div>
</form>
</section>
<section class="panel">
<div style="display:flex;justify-content:space-between;gap:14px;align-items:center;margin-bottom:16px"><h2 style="margin:0">Records</h2><div class="actions" style="margin:0"><button class="secondary" type="button" id="exportCsv">Export CSV</button><button class="secondary" type="button" id="clearAll">Clear all</button></div></div>
<div id="emptyState" class="empty">No records yet. Add the first one from the form.</div>
<table id="recordsTable" hidden><thead><tr>{headers}<th>Created</th><th>Action</th></tr></thead><tbody></tbody></table>
</section>
</div>
</main>
</div>
<script>
const STORAGE_KEY = "{storage_key}";
const FIELDS = {field_names};
const LABELS = {field_labels};
const SAMPLE = {sample};
const form = document.getElementById("recordForm");
const table = document.getElementById("recordsTable");
const tbody = table.querySelector("tbody");
const emptyState = document.getElementById("emptyState");
const totalCount = document.getElementById("totalCount");
const todayCount = document.getElementById("todayCount");
const storageState = document.getElementById("storageState");
function readRecords() {{
try {{
return JSON.parse(localStorage.getItem(STORAGE_KEY) || "[]");
}} catch (_error) {{
return [];
}}
}}
function writeRecords(records) {{
localStorage.setItem(STORAGE_KEY, JSON.stringify(records));
render();
}}
function formToRecord() {{
const data = new FormData(form);
const record = {{ id: crypto.randomUUID(), createdAt: new Date().toISOString() }};
for (const field of FIELDS) record[field] = String(data.get(field) || "").trim();
return record;
}}
function render() {{
const records = readRecords();
tbody.innerHTML = "";
const today = new Date().toISOString().slice(0, 10);
totalCount.textContent = String(records.length);
todayCount.textContent = String(records.filter(record => record.createdAt.slice(0, 10) === today).length);
storageState.textContent = "Ready";
emptyState.hidden = records.length > 0;
table.hidden = records.length === 0;
for (const record of records) {{
const row = document.createElement("tr");
row.innerHTML = FIELDS.map(field => `<td>${{escapeHtml(record[field] || "")}}</td>`).join("") +
`<td>${{new Date(record.createdAt).toLocaleString()}}</td><td><button class="secondary" data-delete="${{record.id}}">Delete</button></td>`;
tbody.appendChild(row);
}}
}}
function escapeHtml(value) {{
return value.replace(/[&<>"']/g, char => ({{ "&": "&amp;", "<": "&lt;", ">": "&gt;", "\\"": "&quot;", "'": "&#39;" }}[char]));
}}
form.addEventListener("submit", event => {{
event.preventDefault();
const record = formToRecord();
if (FIELDS.some(field => !record[field])) return;
writeRecords([record, ...readRecords()]);
form.reset();
}});
tbody.addEventListener("click", event => {{
const button = event.target.closest("[data-delete]");
if (!button) return;
writeRecords(readRecords().filter(record => record.id !== button.dataset.delete));
}});
document.getElementById("clearAll").addEventListener("click", () => {{
if (confirm("Clear all saved records for this app?")) writeRecords([]);
}});
document.getElementById("loadSample").addEventListener("click", () => {{
for (const field of FIELDS) {{
const input = Array.from(form.elements).find(element => element.name === field);
if (input) input.value = SAMPLE[field] || LABELS[FIELDS.indexOf(field)] + " example";
}}
}});
document.getElementById("exportCsv").addEventListener("click", () => {{
const records = readRecords();
const header = [...LABELS, "Created"].join(",");
const lines = records.map(record => [...FIELDS.map(field => record[field] || ""), record.createdAt].map(value => `"${{String(value).replaceAll('"', '""')}}"`).join(","));
const blob = new Blob([[header, ...lines].join("\\n")], {{ type: "text/csv" }});
const url = URL.createObjectURL(blob);
const link = document.createElement("a");
link.href = url;
link.download = "{slugify(spec.app_name)}.csv";
link.click();
URL.revokeObjectURL(url);
}});
render();
</script>
</body>
</html>"""
def validate_html(rendered: str, spec: AppSpec | None = None) -> list[str]:
lower = rendered.lower()
errors: list[str] = []
for token in ["<!doctype html", "<html", "<head", "</head>", "<body", "</body>", "</html>"]:
if token not in lower:
errors.append(f"missing {token}")
if not lower.strip().endswith("</html>"):
errors.append("document does not end with </html>")
for token in ["<form", "<script", "localstorage", "addeventlistener", "export csv"]:
if token not in lower:
errors.append(f"missing app token: {token}")
sample_match = re.search(r"const\s+SAMPLE\s*=\s*(\{.*?\});", rendered, flags=re.DOTALL)
if not sample_match:
errors.append("missing sample data")
else:
try:
sample_data = json.loads(sample_match.group(1))
if not sample_data or any(not str(value).strip() for value in sample_data.values()):
errors.append("sample data contains blank values")
except json.JSONDecodeError:
errors.append("sample data is not valid JSON")
if "@media" not in lower or "viewport" not in lower:
errors.append("missing responsive app styling")
if "```" in rendered:
errors.append("markdown fence found")
if spec:
for field in spec.fields[:3]:
if field.lower() not in lower:
errors.append(f"missing field: {field}")
return errors
def render_from_prompt(prompt: str) -> tuple[AppSpec, str, list[str]]:
spec = spec_from_prompt(prompt)
rendered = render_html(spec, prompt)
return spec, rendered, validate_html(rendered, spec)
def write_html(path: Path, rendered: str) -> None:
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(rendered, encoding="utf-8")
def spec_to_json(spec: AppSpec) -> str:
return json.dumps(spec.__dict__, indent=2, ensure_ascii=False)