/** * PersonaForge — Agentify.help Admin Corpus Pipeline * * Admin tool for building WellAgent corpora from any expert's published work. * Paul Graham (essay corpus) is the first built-in subject. * * Routes (agentify.help host only, admin key protected): * GET /personaforge — SSR admin dashboard * GET /api/personaforge/subjects — list all subjects * POST /api/personaforge/subjects — add subject * POST /api/personaforge/subjects/:slug/fetch — fetch artifact list from source * POST /api/personaforge/subjects/:slug/build — build + upload corpus (async) * GET /api/personaforge/subjects/:slug/logs — SSE build log stream * POST /api/personaforge/subjects/:slug/overture — generate Overture letter via Claude * POST /api/personaforge/subjects/:slug/submit — submit to WellSpr.ing staging import * POST /api/personaforge/subjects/:slug/notify — send Overture email via Resend */ import type { Express, Request, Response } from "express"; import { pool } from "../db"; import crypto from "crypto"; import Anthropic from "@anthropic-ai/sdk"; import { Resend } from "resend"; import { S3Client, PutObjectCommand } from "@aws-sdk/client-s3"; import { Upload } from "@aws-sdk/lib-storage"; const ADMIN_KEY = process.env.ADMIN_KEY || "b0db7a87384fc814b0f46ea7bdc6ab6a81152be5b098718b"; const claude = new Anthropic({ apiKey: process.env.ANTHROPIC_API_KEY }); const resend = new Resend(process.env.RESEND_API_KEY); const S3_HOSTNAME = process.env.VULTR_S3_HOSTNAME || "ewr1.vultrobjects.com"; const S3_BUCKET = process.env.CORPUS_S3_BUCKET || "agentify-corpus"; const S3_ENDPOINT = `https://${S3_HOSTNAME}`; function getS3(): S3Client { return new S3Client({ endpoint: S3_ENDPOINT, region: "ewr1", forcePathStyle: true, credentials: { accessKeyId: process.env.VULTR_S3_ACCESS_KEY || "", secretAccessKey: process.env.VULTR_S3_SECRET_KEY || "", }, }); } // ── In-memory SSE broadcaster per slug ───────────────────────────────────── const liveStreams = new Map>(); function broadcast(slug: string, event: string, data: string) { const clients = liveStreams.get(slug); if (!clients) return; const line = `event: ${event}\ndata: ${data}\n\n`; clients.forEach(res => { try { res.write(line); } catch {} }); } function broadcastLog(slug: string, msg: string) { console.log(`[PersonaForge/${slug}] ${msg}`); broadcast(slug, "log", JSON.stringify({ ts: new Date().toISOString(), msg })); pool.query( `UPDATE personaforge_subjects SET build_log = COALESCE(build_log,'') || $2, updated_at=NOW() WHERE slug=$1`, [slug, `[${new Date().toISOString().slice(11,19)}] ${msg}\n`] ).catch(() => {}); } // ── DB init ───────────────────────────────────────────────────────────────── export async function initPersonaForge() { await pool.query(` CREATE TABLE IF NOT EXISTS personaforge_subjects ( id SERIAL PRIMARY KEY, slug TEXT UNIQUE NOT NULL, subject_name TEXT NOT NULL, domain TEXT, source_type TEXT NOT NULL DEFAULT 'essays', source_url TEXT, essay_count INT DEFAULT 0, chunk_count INT DEFAULT 0, build_status TEXT NOT NULL DEFAULT 'pending', build_log TEXT DEFAULT '', corpus_version TEXT DEFAULT '1.0.0', bundle_id TEXT, overture_text TEXT, overture_sent_at TIMESTAMPTZ, overture_email TEXT, created_at TIMESTAMPTZ DEFAULT NOW(), updated_at TIMESTAMPTZ DEFAULT NOW() ) `); await pool.query(` CREATE TABLE IF NOT EXISTS personaforge_artifacts ( id SERIAL PRIMARY KEY, subject_slug TEXT NOT NULL, artifact_id TEXT NOT NULL UNIQUE, title TEXT NOT NULL, source_url TEXT, pub_date TEXT, artifact_type TEXT DEFAULT 'essay', word_count INT, chunk_count INT DEFAULT 0, s3_key TEXT, created_at TIMESTAMPTZ DEFAULT NOW() ) `); // Seed Paul Graham as first subject await pool.query(` INSERT INTO personaforge_subjects (slug, subject_name, domain, source_type, source_url, corpus_version) VALUES ('paul-graham', 'Paul Graham', 'Essays / Startup / Technology / Philosophy', 'essays', 'https://paulgraham.com/articles.html', '1.0.0') ON CONFLICT (slug) DO NOTHING `); console.log("[PersonaForge] Tables ready — Paul Graham seeded as subject #1"); } // ── Host guard helper ──────────────────────────────────────────────────────── function isAgentifyAdmin(req: Request): boolean { const host = [ req.headers["x-forwarded-host"], req.headers["x-geo-node-host"], req.hostname, req.headers["host"], ].flat().filter(Boolean).map((h: any) => h.toString().toLowerCase().split(",")[0].trim())[0] || ""; return host.includes("agentify") && !host.startsWith("skills."); } function requireAdminKey(req: Request, res: Response): boolean { const key = (req.headers["x-admin-key"] as string) || req.query.admin_key as string; if (key !== ADMIN_KEY) { res.status(403).json({ error: "Admin key required" }); return false; } return true; } // ── Essay scraping (paulgraham.com-style HTML) ─────────────────────────────── async function fetchEssayList(sourceUrl: string): Promise<{ slug: string; title: string; url: string }[]> { const html = await fetch(sourceUrl, { headers: { "User-Agent": "WellSpr.ing PersonaForge/1.0" } }) .then(r => r.text()); const base = new URL(sourceUrl); const results: { slug: string; title: string; url: string }[] = []; // Match links to .html pages const re = /]*>([^<]+)<\/a>/gi; let m: RegExpExecArray | null; while ((m = re.exec(html)) !== null) { const href = m[1].trim(); const title = m[2].trim(); if (!title || title.length < 3) continue; try { const full = new URL(href, base).href; const slug = href.replace(/.*\//, "").replace(".html", "").toLowerCase().replace(/[^a-z0-9]+/g, "-"); if (!results.find(r => r.slug === slug)) results.push({ slug, title, url: full }); } catch {} } return results; } async function fetchEssayText(url: string): Promise { const html = await fetch(url, { headers: { "User-Agent": "WellSpr.ing PersonaForge/1.0" }, signal: AbortSignal.timeout(15000) }) .then(r => r.text()).catch(() => ""); // Strip tags, decode entities, collapse whitespace return html .replace(/]*>[\s\S]*?<\/script>/gi, " ") .replace(/]*>[\s\S]*?<\/style>/gi, " ") .replace(/<[^>]+>/g, " ") .replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/ /g, " ").replace(/&#[0-9]+;/g, " ") .replace(/\s+/g, " ").trim(); } function chunkText(text: string, maxWords = 400): string[] { const sentences = text.split(/(?<=[.!?])\s+/); const chunks: string[] = []; let current = ""; for (const s of sentences) { const proposed = current ? current + " " + s : s; if (proposed.split(/\s+/).length > maxWords && current) { chunks.push(current.trim()); current = s; } else { current = proposed; } } if (current.trim()) chunks.push(current.trim()); return chunks.filter(c => c.split(/\s+/).length > 20); } // ── Corpus build (runs async, streams logs) ────────────────────────────────── const activeBuilds = new Set(); async function runCorpusBuild(slug: string) { if (activeBuilds.has(slug)) { broadcastLog(slug, "Build already in progress"); return; } activeBuilds.add(slug); try { await pool.query(`UPDATE personaforge_subjects SET build_status='building', build_log='', chunk_count=0, updated_at=NOW() WHERE slug=$1`, [slug]); broadcastLog(slug, `Starting corpus build for ${slug}`); const subjectRow = (await pool.query(`SELECT * FROM personaforge_subjects WHERE slug=$1`, [slug])).rows[0]; if (!subjectRow) { broadcastLog(slug, "Subject not found"); return; } // Fetch artifact list const artifacts = (await pool.query(`SELECT * FROM personaforge_artifacts WHERE subject_slug=$1 ORDER BY id`, [slug])).rows; if (artifacts.length === 0) { broadcastLog(slug, "No artifacts found — run Fetch Essays first"); return; } broadcastLog(slug, `Found ${artifacts.length} artifacts to process`); const allChunks: any[] = []; let essaysProcessed = 0; for (const art of artifacts) { try { broadcastLog(slug, `[${essaysProcessed + 1}/${artifacts.length}] Fetching: ${art.title}`); const text = await fetchEssayText(art.source_url); const wordCount = text.split(/\s+/).length; if (wordCount < 50) { broadcastLog(slug, ` → Skipped (too short: ${wordCount}w)`); continue; } const textChunks = chunkText(text, 400); textChunks.forEach((content, i) => { allChunks.push({ chunk_id: `${art.artifact_id}-p${String(i + 1).padStart(3, "0")}`, artifact_id: art.artifact_id, title: art.title, source_url: art.source_url, pub_date: art.pub_date || null, content, word_count: content.split(/\s+/).length, }); }); await pool.query( `UPDATE personaforge_artifacts SET word_count=$2, chunk_count=$3 WHERE artifact_id=$4`, [wordCount, textChunks.length, art.artifact_id] ); broadcastLog(slug, ` → ${wordCount}w → ${textChunks.length} chunks`); essaysProcessed++; // Polite rate limit await new Promise(r => setTimeout(r, 300)); } catch (err: any) { broadcastLog(slug, ` → Error: ${err.message}`); } } broadcastLog(slug, `\nTotal: ${essaysProcessed} essays, ${allChunks.length} chunks`); if (allChunks.length === 0) { broadcastLog(slug, "No chunks produced — aborting"); return; } // Upload to Vultr S3 const bundleId = crypto.randomUUID(); const corpVer = subjectRow.corpus_version || "1.0.0"; const prefix = `staging/${slug}/v${corpVer}/${bundleId}`; const chunksKey = `${prefix}/chunks/index.ndjson`; const manifestKey = `${prefix}/manifest.json`; broadcastLog(slug, `\nUploading ${allChunks.length} chunks to Vultr S3…`); const s3 = getS3(); const ndjson = allChunks.map(c => JSON.stringify(c)).join("\n"); await new Upload({ client: s3, params: { Bucket: S3_BUCKET, Key: chunksKey, Body: Buffer.from(ndjson, "utf-8"), ContentType: "application/x-ndjson" } }).done(); broadcastLog(slug, `✓ Chunks uploaded — ${chunksKey}`); const manifest = { schema_version: "1.0", bundle_id: bundleId, expert_slug: slug, attestation_uri: `https://wellspr.ing/vault/agents/${slug}/attestation-v1.0.json`, attestation_version: "v1.0", corpus_version: corpVer, assembled_by: "personaforge", assembled_at: new Date().toISOString(), chunk_strategy: "paragraph-400w", chunk_count: allChunks.length, artifact_count: essaysProcessed, chunks_index_s3_key: chunksKey, manifest_sha256: "", }; const manifestBuf = Buffer.from(JSON.stringify(manifest, null, 2), "utf-8"); manifest.manifest_sha256 = crypto.createHash("sha256").update(manifestBuf).digest("hex"); const signedBuf = Buffer.from(JSON.stringify(manifest, null, 2), "utf-8"); await new Upload({ client: s3, params: { Bucket: S3_BUCKET, Key: manifestKey, Body: signedBuf, ContentType: "application/json" } }).done(); broadcastLog(slug, `✓ Manifest uploaded — ${manifestKey}`); // Update DB await pool.query( `UPDATE personaforge_subjects SET build_status='built', bundle_id=$2, chunk_count=$3, essay_count=$4, updated_at=NOW() WHERE slug=$1`, [slug, bundleId, allChunks.length, essaysProcessed] ); broadcastLog(slug, `\n✓ Corpus build complete — bundle ${bundleId}`); broadcast(slug, "done", JSON.stringify({ bundle_id: bundleId, chunk_count: allChunks.length })); } catch (err: any) { broadcastLog(slug, `\nFATAL: ${err.message}`); await pool.query(`UPDATE personaforge_subjects SET build_status='error', updated_at=NOW() WHERE slug=$1`, [slug]); broadcast(slug, "error", JSON.stringify({ error: err.message })); } finally { activeBuilds.delete(slug); } } // ── SSR page ───────────────────────────────────────────────────────────────── function renderPersonaForgePage(subjects: any[]): string { const sidebar = subjects.map(s => { const statusColor = { pending: "#64748b", fetching: "#d97706", building: "#3b82f6", built: "#10b981", submitted: "#8b5cf6", error: "#ef4444" }[s.build_status as string] || "#64748b"; const statusLabel = { pending: "Pending", fetching: "Fetching", building: "Building", built: "Built", submitted: "Submitted", overture_sent: "Overture Sent", error: "Error" }[s.build_status as string] || s.build_status; return `
${s.subject_name}
${s.domain || ""}
${statusLabel} ${s.essay_count ? `${s.essay_count} essays` : ""} ${s.chunk_count ? `${s.chunk_count} chunks` : ""}
`; }).join(""); return ` PersonaForge — Agentify.help Admin
PersonaForge Agentify.help · Corpus Pipeline Admin
${subjects.length} subjects ${subjects.filter(s => ["built","submitted"].includes(s.build_status)).length} built

Select a subject

Choose a subject from the sidebar or add a new one.

`; } // ── Route Registration ──────────────────────────────────────────────────────── export async function registerPersonaForgeRoutes(app: Express) { await initPersonaForge(); // ── GET /personaforge ── SSR admin page app.get("/personaforge", async (req: Request, res: Response, next: any) => { const host = [req.headers["x-forwarded-host"], req.hostname, req.headers["host"]] .flat().filter(Boolean).map((h: any) => h.toString().toLowerCase())[0] || ""; if (!host.includes("agentify") || host.startsWith("skills.")) return next(); const key = req.query.admin_key as string || req.headers["x-admin-key"] as string; if (key !== ADMIN_KEY) { return res.status(401).send(`
`); } try { const subjects = (await pool.query(`SELECT * FROM personaforge_subjects ORDER BY created_at`)).rows; res.type("text/html").send(renderPersonaForgePage(subjects)); } catch (err: any) { res.status(500).send("Error loading PersonaForge: " + err.message); } }); // ── GET /api/personaforge/subjects ────────────────────────────────────── app.get("/api/personaforge/subjects", async (req: Request, res: Response) => { if (!requireAdminKey(req, res)) return; const rows = (await pool.query(`SELECT * FROM personaforge_subjects ORDER BY created_at`)).rows; res.json({ subjects: rows }); }); // ── GET /api/personaforge/subjects/:slug ───────────────────────────────── app.get("/api/personaforge/subjects/:slug", async (req: Request, res: Response) => { if (!requireAdminKey(req, res)) return; const row = (await pool.query(`SELECT * FROM personaforge_subjects WHERE slug=$1`, [req.params.slug])).rows[0]; if (!row) return res.status(404).json({ error: "not_found" }); res.json(row); }); // ── POST /api/personaforge/subjects ── Add subject app.post("/api/personaforge/subjects", async (req: Request, res: Response) => { if (!requireAdminKey(req, res)) return; const { subject_name, domain, source_type = "essays", source_url } = req.body || {}; if (!subject_name) return res.status(400).json({ error: "subject_name required" }); const slug = subject_name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, ""); try { await pool.query( `INSERT INTO personaforge_subjects (slug, subject_name, domain, source_type, source_url) VALUES ($1,$2,$3,$4,$5) ON CONFLICT (slug) DO NOTHING`, [slug, subject_name, domain || null, source_type, source_url || null] ); res.json({ ok: true, slug }); } catch (err: any) { res.status(500).json({ error: err.message }); } }); // ── POST /api/personaforge/subjects/:slug/fetch ── Fetch artifact list app.post("/api/personaforge/subjects/:slug/fetch", async (req: Request, res: Response) => { if (!requireAdminKey(req, res)) return; const { slug } = req.params; const subject = (await pool.query(`SELECT * FROM personaforge_subjects WHERE slug=$1`, [slug])).rows[0]; if (!subject) return res.status(404).json({ error: "not_found" }); if (!subject.source_url) return res.status(400).json({ error: "No source_url set for this subject" }); try { await pool.query(`UPDATE personaforge_subjects SET build_status='fetching', updated_at=NOW() WHERE slug=$1`, [slug]); const essays = await fetchEssayList(subject.source_url); let inserted = 0; for (const e of essays) { const artifactId = `${slug}-${e.slug}`; await pool.query( `INSERT INTO personaforge_artifacts (subject_slug, artifact_id, title, source_url, artifact_type) VALUES ($1,$2,$3,$4,'essay') ON CONFLICT (artifact_id) DO NOTHING`, [slug, artifactId, e.title, e.url] ); inserted++; } await pool.query( `UPDATE personaforge_subjects SET essay_count=$2, build_status='pending', updated_at=NOW() WHERE slug=$1`, [slug, inserted] ); res.json({ ok: true, count: inserted, slug }); } catch (err: any) { await pool.query(`UPDATE personaforge_subjects SET build_status='error', updated_at=NOW() WHERE slug=$1`, [slug]); res.status(500).json({ error: err.message }); } }); // ── GET /api/personaforge/subjects/:slug/artifacts ──────────────────────── app.get("/api/personaforge/subjects/:slug/artifacts", async (req: Request, res: Response) => { if (!requireAdminKey(req, res)) return; const artifacts = (await pool.query( `SELECT * FROM personaforge_artifacts WHERE subject_slug=$1 ORDER BY id`, [req.params.slug] )).rows; res.json({ artifacts }); }); // ── POST /api/personaforge/subjects/:slug/build ── Start corpus build (async) app.post("/api/personaforge/subjects/:slug/build", async (req: Request, res: Response) => { if (!requireAdminKey(req, res)) return; const { slug } = req.params; res.json({ ok: true, message: "Build started — connect to /logs for progress" }); // Run async (don't await) runCorpusBuild(slug).catch(err => console.error("[PersonaForge] Build error:", err)); }); // ── GET /api/personaforge/subjects/:slug/logs ── SSE build log stream app.get("/api/personaforge/subjects/:slug/logs", (req: Request, res: Response) => { const key = (req.headers["x-admin-key"] as string) || req.query.admin_key as string; if (key !== ADMIN_KEY) return res.status(403).end(); const { slug } = req.params; res.setHeader("Content-Type", "text/event-stream"); res.setHeader("Cache-Control", "no-cache"); res.setHeader("Connection", "keep-alive"); res.flushHeaders(); res.write("event: connected\ndata: {}\n\n"); if (!liveStreams.has(slug)) liveStreams.set(slug, new Set()); liveStreams.get(slug)!.add(res); req.on("close", () => { const set = liveStreams.get(slug); if (set) { set.delete(res); if (!set.size) liveStreams.delete(slug); } }); }); // ── POST /api/personaforge/subjects/:slug/submit ── Submit to WellSpr.ing app.post("/api/personaforge/subjects/:slug/submit", async (req: Request, res: Response) => { if (!requireAdminKey(req, res)) return; const { slug } = req.params; const subject = (await pool.query(`SELECT * FROM personaforge_subjects WHERE slug=$1`, [slug])).rows[0]; if (!subject) return res.status(404).json({ error: "not_found" }); if (!subject.bundle_id) return res.status(400).json({ error: "No bundle built yet — run Build Corpus first" }); try { // Call the staging import endpoint internally const corpVer = subject.corpus_version || "1.0.0"; const importRes = await fetch(`http://localhost:${process.env.PORT || 5000}/api/agentify/corpus/staging/import/${slug}`, { method: "POST", headers: { "Content-Type": "application/json", "x-admin-key": ADMIN_KEY, }, body: JSON.stringify({ manifest_s3_key: `staging/${slug}/v${corpVer}/${subject.bundle_id}/manifest.json`, }), }); const result = await importRes.json(); if (importRes.ok || result.bundle_id) { await pool.query(`UPDATE personaforge_subjects SET build_status='submitted', updated_at=NOW() WHERE slug=$1`, [slug]); res.json({ ok: true, bundle_id: subject.bundle_id, staging_result: result }); } else { res.status(500).json({ error: result.error || "Import failed", detail: result }); } } catch (err: any) { res.status(500).json({ error: err.message }); } }); // ── POST /api/personaforge/subjects/:slug/overture ── Generate Overture via Claude app.post("/api/personaforge/subjects/:slug/overture", async (req: Request, res: Response) => { if (!requireAdminKey(req, res)) return; const { slug } = req.params; const subject = (await pool.query(`SELECT * FROM personaforge_subjects WHERE slug=$1`, [slug])).rows[0]; if (!subject) return res.status(404).json({ error: "not_found" }); const artifacts = (await pool.query( `SELECT title, source_url, pub_date FROM personaforge_artifacts WHERE subject_slug=$1 ORDER BY id LIMIT 10`, [slug] )).rows; const corpusSample = artifacts.map(a => ` - "${a.title}" (${a.source_url})`).join("\n"); const prompt = `You are composing a formal Overture letter on behalf of WellSpr.ing / Agentify.Help to ${subject.subject_name}. Agentify.Help is a one-per-person registry for AI expert personas grounded in real published work. It enforces a covenant standard: corpus-grounded, steward-named, with a 30-day notification window and a permanent opt-out mechanism. The situation: we have compiled a corpus of ${subject.subject_name}'s published work (${subject.essay_count || "many"} essays/artifacts) and built an AI persona that reasons within their published framework. We are notifying them per the covenant protocol. Sample corpus artifacts: ${corpusSample || " (essays from their published body of work)"} The Overture must: 1. Acknowledge that multiple inferior AI wrappers of this person already exist 2. Explain what distinguishes Agentify.Help (covenant standard, corpus-grounded, steward-named, chain of title) 3. Offer three paths: Claim (endorse and gain editorial control), Correct (send amendments), Decline (permanent registry entry honoring their refusal) 4. Make clear that the Decline option is first-class and durable — not a failure condition 5. Explain the 30-day notification window 6. Use a tone appropriate to someone who values clear thinking over pitch language — no sales tactics, no manufactured urgency 7. Be approximately 400-600 words 8. Sign from "The WellSpr.ing / Agentify.Help Team" with contact ody@wellspr.ing Write the complete letter now:`; try { const resp = await claude.messages.create({ model: "claude-opus-4-5", max_tokens: 1200, messages: [{ role: "user", content: prompt }], }); const overture = (resp.content[0] as any)?.text || ""; await pool.query(`UPDATE personaforge_subjects SET overture_text=$2, updated_at=NOW() WHERE slug=$1`, [slug, overture]); res.json({ ok: true, overture }); } catch (err: any) { res.status(500).json({ error: err.message }); } }); // ── POST /api/personaforge/subjects/:slug/notify ── Send Overture email app.post("/api/personaforge/subjects/:slug/notify", async (req: Request, res: Response) => { if (!requireAdminKey(req, res)) return; const { slug } = req.params; const { email } = req.body || {}; if (!email) return res.status(400).json({ error: "email required" }); const subject = (await pool.query(`SELECT * FROM personaforge_subjects WHERE slug=$1`, [slug])).rows[0]; if (!subject) return res.status(404).json({ error: "not_found" }); if (!subject.overture_text) return res.status(400).json({ error: "Generate Overture first" }); try { await resend.emails.send({ from: "Agentify.Help ", to: [email], subject: `Agentify.Help — WellAgent Notification for ${subject.subject_name}`, text: subject.overture_text, html: `
Agentify.Help · WellAgent Registry
${subject.overture_text.split("\n\n").map((p: string) => `

${p.replace(/\n/g, "
")}

`).join("")}
`, }); await pool.query( `UPDATE personaforge_subjects SET overture_sent_at=NOW(), overture_email=$2, build_status='overture_sent', updated_at=NOW() WHERE slug=$1`, [slug, email] ); res.json({ ok: true, to: email }); } catch (err: any) { res.status(500).json({ error: err.message }); } }); // ── POST /api/personaforge/subjects/:slug/registry-push ── Push to agentify_subject_registry app.post("/api/personaforge/subjects/:slug/registry-push", async (req: Request, res: Response) => { if (!requireAdminKey(req, res)) return; const { slug } = req.params; const s = (await pool.query(`SELECT * FROM personaforge_subjects WHERE slug=$1`, [slug])).rows[0]; if (!s) return res.status(404).json({ error: "not_found" }); try { await pool.query(` INSERT INTO agentify_subject_registry (subject_slug, subject_name, subject_domain, steward_name, steward_email, corpus_version, status, registered_via) VALUES ($1,$2,$3,'WellSpr.ing PersonaForge','ody@wellspr.ing',$4,'active','personaforge') ON CONFLICT (subject_slug) DO UPDATE SET subject_name=EXCLUDED.subject_name, subject_domain=EXCLUDED.subject_domain, corpus_version=EXCLUDED.corpus_version, status='active', updated_at=NOW() `, [slug, s.subject_name, s.domain || null, s.corpus_version || "1.0.0"]); res.json({ ok: true, slug }); } catch (err: any) { res.status(500).json({ error: err.message }); } }); console.log("[PersonaForge] Routes registered — agentify.help/personaforge"); }