4210 lines
245 KiB
TypeScript
4210 lines
245 KiB
TypeScript
/**
|
||
* Agentify.Help — Public Cookbook Site
|
||
*
|
||
* "Build the AI expert. Once. Right."
|
||
*
|
||
* One-per-person registry. Corpus-grounded. Framework-distilled. Steward-named.
|
||
* A civic project under WellSpr.ing covenant. Published April 24, 2026.
|
||
*
|
||
* Domain: agentify.help (Bunny DNS → Fly.io)
|
||
* Same stack pattern as notgit.org — standalone SSR HTML, domain-keyed.
|
||
*/
|
||
|
||
import express from "express";
|
||
import type { Express, Request, Response } from "express";
|
||
import fs from "fs";
|
||
import path from "path";
|
||
import { pool } from "../db";
|
||
import { getOgPng } from "../og-images";
|
||
import { Resend } from "resend";
|
||
import crypto from "crypto";
|
||
|
||
const resend = new Resend(process.env.RESEND_API_KEY);
|
||
|
||
const ADMIN_KEY = process.env.ADMIN_KEY || "b0db7a87384fc814b0f46ea7bdc6ab6a81152be5b098718b";
|
||
|
||
// ── DB setup ──────────────────────────────────────────────────────────────────
|
||
async function ensureAgentifyHelpTables() {
|
||
await pool.query(`
|
||
CREATE TABLE IF NOT EXISTS agentify_subject_registry (
|
||
id SERIAL PRIMARY KEY,
|
||
subject_slug TEXT UNIQUE NOT NULL,
|
||
subject_name TEXT NOT NULL,
|
||
subject_domain TEXT,
|
||
steward_name TEXT NOT NULL,
|
||
steward_email TEXT NOT NULL,
|
||
host_url TEXT,
|
||
corpus_version TEXT DEFAULT '1.0.0',
|
||
status TEXT NOT NULL DEFAULT 'pending',
|
||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||
)
|
||
`);
|
||
// Additive migrations — idempotent
|
||
await pool.query(`ALTER TABLE agentify_subject_registry ADD COLUMN IF NOT EXISTS registered_via TEXT NOT NULL DEFAULT 'web'`);
|
||
await pool.query(`ALTER TABLE agentify_subject_registry ADD COLUMN IF NOT EXISTS email_verification_token TEXT`);
|
||
await pool.query(`ALTER TABLE agentify_subject_registry ADD COLUMN IF NOT EXISTS email_verified_at TIMESTAMPTZ`);
|
||
await pool.query(`ALTER TABLE agentify_subject_registry ADD COLUMN IF NOT EXISTS covenant_attested_at TIMESTAMPTZ`);
|
||
// Seed Sniderman as founding agent #1
|
||
await pool.query(`
|
||
INSERT INTO agentify_subject_registry
|
||
(subject_slug, subject_name, subject_domain, steward_name, steward_email, host_url, corpus_version, status, registered_via)
|
||
VALUES
|
||
('allan-sniderman', 'Dr. Allan Sniderman', 'Cardiovascular Medicine / Lipoprotein Research',
|
||
'Naturologie', 'ody@wellspr.ing',
|
||
'https://cholesteroltruth.com/experts/dr-allan-sniderman', '1.0.0', 'active', 'web')
|
||
ON CONFLICT (subject_slug) DO NOTHING
|
||
`);
|
||
|
||
// Fix Guy Kawasaki registry entry (may have stale "Worldmaxxing" domain from early test)
|
||
await pool.query(`
|
||
INSERT INTO agentify_subject_registry
|
||
(subject_slug, subject_name, subject_domain, steward_name, steward_email, status, registered_via)
|
||
VALUES
|
||
('guy-kawasaki', 'Guy Kawasaki', 'entrepreneurship, marketing, venture capital, innovation',
|
||
'WellSpr.ing', 'ody@wellspr.ing', 'preview', 'orchestrator')
|
||
ON CONFLICT (subject_slug) DO UPDATE SET
|
||
subject_domain = 'entrepreneurship, marketing, venture capital, innovation',
|
||
status = CASE WHEN agentify_subject_registry.status = 'pending' THEN 'preview' ELSE agentify_subject_registry.status END,
|
||
updated_at = NOW()
|
||
`);
|
||
|
||
// Ensure agentify_experts has Guy Kawasaki
|
||
await pool.query(`
|
||
CREATE TABLE IF NOT EXISTS agentify_experts (
|
||
id SERIAL PRIMARY KEY,
|
||
slug TEXT NOT NULL UNIQUE,
|
||
expert_name TEXT NOT NULL,
|
||
credentials TEXT,
|
||
primary_domain TEXT NOT NULL,
|
||
affiliated_institution TEXT,
|
||
demo_url TEXT,
|
||
scopes TEXT[] NOT NULL DEFAULT '{}',
|
||
refusals TEXT[] NOT NULL DEFAULT '{}',
|
||
cred_id TEXT,
|
||
attestation_version TEXT NOT NULL DEFAULT 'v0.1',
|
||
attestation_uri TEXT,
|
||
status TEXT NOT NULL DEFAULT 'proposed',
|
||
created_at TIMESTAMPTZ DEFAULT NOW()
|
||
)
|
||
`);
|
||
await pool.query(`
|
||
INSERT INTO agentify_experts (slug, expert_name, primary_domain, status, attestation_version)
|
||
VALUES ('guy-kawasaki', 'Guy Kawasaki', 'entrepreneurship, marketing, venture capital, innovation', 'preview', 'v0.1')
|
||
ON CONFLICT (slug) DO UPDATE SET
|
||
primary_domain = EXCLUDED.primary_domain,
|
||
status = EXCLUDED.status
|
||
`);
|
||
|
||
// Ensure corpus bundle record for Guy Kawasaki exists
|
||
await pool.query(`
|
||
CREATE TABLE IF NOT EXISTS agentify_corpus_bundles (
|
||
id SERIAL PRIMARY KEY,
|
||
bundle_id TEXT UNIQUE NOT NULL,
|
||
expert_slug TEXT NOT NULL,
|
||
status TEXT NOT NULL DEFAULT 'received',
|
||
chunk_count INTEGER,
|
||
received_at TIMESTAMPTZ DEFAULT NOW()
|
||
)
|
||
`);
|
||
await pool.query(`
|
||
INSERT INTO agentify_corpus_bundles (bundle_id, expert_slug, corpus_version, attestation_uri, attestation_version, status, chunk_count)
|
||
VALUES ('336baf2f-e83b-4b00-be48-5357433c7798', 'guy-kawasaki', '1.0.0', 'https://agentify.help/vcap/guy-kawasaki', 'v0.1', 'ready', 2222)
|
||
ON CONFLICT (bundle_id) DO UPDATE SET status = 'ready', chunk_count = 2222
|
||
`);
|
||
|
||
// ── Topic taxonomy ─────────────────────────────────────────────────────────
|
||
// The canonical list of approved topics. Prevents balkanization by requiring
|
||
// every topic agent to be anchored to a taxonomy entry that passed review.
|
||
// A proposed entry is visible to stewards; approved/active entries appear
|
||
// publicly. One slug = one topic — no forks without a new entry.
|
||
await pool.query(`
|
||
CREATE TABLE IF NOT EXISTS agentify_topic_taxonomy (
|
||
id SERIAL PRIMARY KEY,
|
||
category TEXT NOT NULL,
|
||
slug TEXT UNIQUE NOT NULL,
|
||
display_name TEXT NOT NULL,
|
||
description TEXT,
|
||
scope_note TEXT,
|
||
audience_personas TEXT[] DEFAULT '{layperson,clinician}',
|
||
status TEXT NOT NULL DEFAULT 'proposed',
|
||
proposed_by_email TEXT,
|
||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||
)
|
||
`);
|
||
|
||
// Seed the first approved topic: Post-Operative Cardiac Care
|
||
await pool.query(`
|
||
INSERT INTO agentify_topic_taxonomy
|
||
(category, slug, display_name, description, scope_note, audience_personas, status)
|
||
VALUES
|
||
('Medical / Health',
|
||
'postop-cardiac-care',
|
||
'Post-Operative Cardiac Care',
|
||
'Evidence synthesis on recovery, complications, and patient management after cardiac surgery — valve repair/replacement, CABG, structural heart procedures.',
|
||
'In scope: delirium prevention (HELP program), hemodynamic management, anticoagulation, cardiac rehab, frailty assessment, caregiver guides. Out of scope: intraoperative technique, interventional cardiology, long-term cardiology follow-up beyond 12 months.',
|
||
'{layperson,clinician,nursing,researcher}',
|
||
'approved')
|
||
ON CONFLICT (slug) DO UPDATE SET
|
||
description = EXCLUDED.description,
|
||
scope_note = EXCLUDED.scope_note,
|
||
audience_personas = EXCLUDED.audience_personas,
|
||
status = CASE WHEN agentify_topic_taxonomy.status = 'proposed' THEN 'approved'
|
||
ELSE agentify_topic_taxonomy.status END
|
||
`);
|
||
|
||
// Additive migrations for subject_registry
|
||
await pool.query(`ALTER TABLE agentify_subject_registry ADD COLUMN IF NOT EXISTS subject_type TEXT NOT NULL DEFAULT 'person'`);
|
||
await pool.query(`ALTER TABLE agentify_subject_registry ADD COLUMN IF NOT EXISTS taxonomy_slug TEXT REFERENCES agentify_topic_taxonomy(slug) ON DELETE SET NULL`);
|
||
await pool.query(`ALTER TABLE agentify_subject_registry ADD COLUMN IF NOT EXISTS audience_personas TEXT[] DEFAULT '{}'`);
|
||
|
||
// Link cardiac care subject to its taxonomy entry
|
||
await pool.query(`
|
||
UPDATE agentify_subject_registry
|
||
SET taxonomy_slug = 'postop-cardiac-care',
|
||
audience_personas = '{layperson,clinician,nursing,researcher}'
|
||
WHERE subject_slug = 'postop-cardiac-care' AND taxonomy_slug IS NULL
|
||
`);
|
||
|
||
console.log("[Agentify.Help] Registry table ready — Guy Kawasaki + topic taxonomy seeded");
|
||
}
|
||
|
||
// ── Slug normalization ────────────────────────────────────────────────────────
|
||
// Strip honorifics and credentials, normalize to slug.
|
||
// "Dr. Allan Sniderman" → "allan-sniderman"
|
||
// "Allan Sniderman, MD, PhD" → "allan-sniderman"
|
||
function nameToSlug(name: string): string {
|
||
return name
|
||
.toLowerCase()
|
||
.replace(/\b(dr|prof|professor|mr|mrs|ms|rev|hon|sir|dame|lord|the)\b\.?/gi, "")
|
||
.replace(/,?\s*(md|phd|do|np|pa|rn|dds|jd|esq|cpa|mba|ms|bs|ba)\b/gi, "")
|
||
.replace(/[^a-z0-9\s-]/g, "")
|
||
.replace(/\s+/g, "-")
|
||
.replace(/-+/g, "-")
|
||
.replace(/^-|-$/g, "")
|
||
.trim();
|
||
}
|
||
|
||
// ── Rate limiter (in-memory; swap for Redis at scale) ─────────────────────────
|
||
const _rl = new Map<string, { n: number; t: number }>();
|
||
function rateCheck(ip: string, max: number, windowMs: number): boolean {
|
||
const now = Date.now();
|
||
const e = _rl.get(ip);
|
||
if (!e || now > e.t) { _rl.set(ip, { n: 1, t: now + windowMs }); return true; }
|
||
if (e.n >= max) return false;
|
||
e.n++;
|
||
return true;
|
||
}
|
||
setInterval(() => { const now = Date.now(); for (const [k, v] of _rl) if (now > v.t) _rl.delete(k); }, 600_000).unref();
|
||
|
||
// ── MCP Tool definitions (Streamable HTTP / 2025-03-26 spec) ─────────────────
|
||
export const AH_MCP_TOOLS = [
|
||
{
|
||
name: "check_availability",
|
||
description: "Check whether an expert name is available for first-steward registration in the Agentify.Help one-per-person WellAgent registry. Returns availability, normalized slug, and any existing entry.",
|
||
inputSchema: {
|
||
type: "object",
|
||
properties: {
|
||
name: { type: "string", description: "Full name of the expert — honorifics and credentials are stripped automatically (e.g. 'Dr. Jane Smith MD' → 'jane-smith')." }
|
||
},
|
||
required: ["name"]
|
||
}
|
||
},
|
||
{
|
||
name: "register",
|
||
description: "Claim first-steward registration for an expert AI persona. The registry enforces one agent per real person — first registration wins, no duplicates. Returns registration_number (your permanent place in the registry), slug, and timestamp. Store these; they are your proof of priority.",
|
||
inputSchema: {
|
||
type: "object",
|
||
properties: {
|
||
subject_name: { type: "string", description: "Full name of the expert being agentified." },
|
||
subject_domain: { type: "string", description: "Domain of expertise (e.g. 'Cardiovascular Medicine', 'Constitutional Law')." },
|
||
steward_name: { type: "string", description: "Name of the person or organization acting as steward / builder." },
|
||
steward_email: { type: "string", description: "Contact email for the steward — shown publicly so quality concerns can be raised." },
|
||
host_url: { type: "string", description: "URL where the deployed WellAgent will be accessible (optional at registration time)." },
|
||
registered_via: { type: "string", description: "Identifier for the registering agent or client (e.g. 'mcp', 'my-agent-v1'). Defaults to 'mcp'." }
|
||
},
|
||
required: ["subject_name", "steward_name", "steward_email"]
|
||
}
|
||
},
|
||
{
|
||
name: "list_registry",
|
||
description: "List all registered WellAgent personas in the public Agentify.Help registry, ordered by registration date (first registered first). Use this to see which names are taken before attempting registration.",
|
||
inputSchema: {
|
||
type: "object",
|
||
properties: {
|
||
limit: { type: "number", description: "Max results to return (1–100, default 50)." },
|
||
status: { type: "string", description: "Filter by status: 'active', 'pending', or 'claimed'. Omit for all." }
|
||
}
|
||
}
|
||
},
|
||
{
|
||
name: "get_agent",
|
||
description: "Retrieve full registry details for a specific expert by their URL slug.",
|
||
inputSchema: {
|
||
type: "object",
|
||
properties: {
|
||
slug: { type: "string", description: "URL slug (e.g. 'allan-sniderman'). Use list_registry or check_availability to discover slugs." }
|
||
},
|
||
required: ["slug"]
|
||
}
|
||
}
|
||
];
|
||
|
||
async function handleMcpTool(toolName: string, args: any): Promise<{ content: Array<{ type: string; text: string }> }> {
|
||
const text = (v: any) => ({ content: [{ type: "text", text: typeof v === "string" ? v : JSON.stringify(v, null, 2) }] });
|
||
|
||
switch (toolName) {
|
||
case "check_availability": {
|
||
const { name } = args ?? {};
|
||
if (!name) return text({ error: "name is required" });
|
||
const slug = nameToSlug(String(name));
|
||
if (!slug) return text({ error: "Could not normalize name to slug" });
|
||
const { rows } = await pool.query(
|
||
`SELECT id AS registration_number, subject_slug, subject_name, steward_name, host_url, status, created_at
|
||
FROM agentify_subject_registry WHERE subject_slug = $1`, [slug]);
|
||
if (rows.length === 0) return text({ available: true, slug, message: `"${name}" is available. Be first — register now.` });
|
||
return text({ available: false, slug, existing: rows[0], message: `"${name}" is already registered.` });
|
||
}
|
||
|
||
case "register": {
|
||
const { subject_name, subject_domain, steward_name, steward_email, host_url, registered_via } = args ?? {};
|
||
if (!subject_name || !steward_name || !steward_email) {
|
||
return text({ error: "subject_name, steward_name, steward_email are required" });
|
||
}
|
||
const slug = nameToSlug(String(subject_name));
|
||
if (!slug) return text({ error: "Could not normalize subject_name to slug" });
|
||
|
||
try {
|
||
const { rows } = await pool.query(`
|
||
INSERT INTO agentify_subject_registry
|
||
(subject_slug, subject_name, subject_domain, steward_name, steward_email, host_url, status, registered_via)
|
||
VALUES ($1,$2,$3,$4,$5,$6,'pending',$7)
|
||
RETURNING id, subject_slug, created_at
|
||
`, [slug, String(subject_name), subject_domain || null, String(steward_name), String(steward_email),
|
||
host_url || null, String(registered_via || "mcp")]);
|
||
|
||
const row = rows[0];
|
||
console.log(`[Agentify.Help/MCP] Registered: ${subject_name} (${slug}) by ${steward_name} via ${registered_via || "mcp"}`);
|
||
return text({
|
||
ok: true,
|
||
slug: row.subject_slug,
|
||
registration_number: row.id,
|
||
registered_at: row.created_at,
|
||
message: `Registration confirmed. You are steward for "${subject_name}". Your registration number is #${row.id}. This is your proof of priority — no one else may register this person.`
|
||
});
|
||
} catch (e: any) {
|
||
if (e.message?.includes("unique") || e.code === "23505") {
|
||
return text({ error: `"${subject_name}" is already registered. First registration wins.`, slug });
|
||
}
|
||
return text({ error: e.message });
|
||
}
|
||
}
|
||
|
||
case "list_registry": {
|
||
const limit = Math.min(100, Math.max(1, Number(args?.limit) || 50));
|
||
const status = args?.status ? String(args.status) : null;
|
||
const { rows } = await pool.query(
|
||
`SELECT id AS registration_number, subject_slug, subject_name, subject_domain,
|
||
steward_name, host_url, corpus_version, status, registered_via, created_at
|
||
FROM agentify_subject_registry
|
||
${status ? "WHERE status = $2" : ""}
|
||
ORDER BY created_at ASC LIMIT $1`,
|
||
status ? [limit, status] : [limit]
|
||
);
|
||
return text({ agents: rows, count: rows.length, updated: new Date().toISOString() });
|
||
}
|
||
|
||
case "get_agent": {
|
||
const { slug } = args ?? {};
|
||
if (!slug) return text({ error: "slug is required" });
|
||
const { rows } = await pool.query(
|
||
`SELECT id AS registration_number, subject_slug, subject_name, subject_domain,
|
||
steward_name, host_url, corpus_version, status, registered_via, created_at, updated_at
|
||
FROM agentify_subject_registry WHERE subject_slug = $1`, [String(slug)]);
|
||
if (rows.length === 0) return text({ error: `No agent found for slug "${slug}"` });
|
||
return text(rows[0]);
|
||
}
|
||
|
||
default:
|
||
throw { code: -32602, message: `Unknown tool: ${toolName}` };
|
||
}
|
||
}
|
||
|
||
// ── OpenAPI 3.0 spec ──────────────────────────────────────────────────────────
|
||
const AH_OPENAPI = {
|
||
openapi: "3.0.3",
|
||
info: {
|
||
title: "Agentify.Help — WellAgent Registry & Consultation API",
|
||
version: "1.1.0",
|
||
description: "One-per-person WellAgent registry and expert consultation surface. Covers steward registration (agentify.help), corpus assembly skill, and the WellAgent consultation endpoints (wellspr.ing). First registration wins — no duplicates.",
|
||
contact: { email: "ody@wellspr.ing", url: "https://agentify.help" }
|
||
},
|
||
servers: [
|
||
{ url: "https://agentify.help", description: "Registry, skill files, and steward tools" },
|
||
{ url: "https://wellspr.ing", description: "Corpus intake, expert index, and consultation" }
|
||
],
|
||
tags: [
|
||
{ name: "registry", description: "Steward registration and public ledger — agentify.help" },
|
||
{ name: "corpus", description: "Corpus assembly skill and intake — agentify.help + wellspr.ing" },
|
||
{ name: "consultation", description: "Live WellAgent expert consultation — wellspr.ing" }
|
||
],
|
||
paths: {
|
||
"/api/agentify-help/check": {
|
||
post: {
|
||
tags: ["registry"],
|
||
operationId: "check_availability",
|
||
summary: "Check if an expert name is available",
|
||
servers: [{ url: "https://agentify.help" }],
|
||
requestBody: { required: true, content: { "application/json": { schema: { type: "object", required: ["name"], properties: { name: { type: "string" } } } } } },
|
||
responses: { "200": { description: "Availability result", content: { "application/json": { schema: { type: "object", properties: { available: { type: "boolean" }, slug: { type: "string" }, existing: { type: "object" } } } } } } }
|
||
}
|
||
},
|
||
"/api/agentify-help/register": {
|
||
post: {
|
||
tags: ["registry"],
|
||
operationId: "register",
|
||
summary: "Register as first steward for an expert persona",
|
||
servers: [{ url: "https://agentify.help" }],
|
||
requestBody: { required: true, content: { "application/json": { schema: { type: "object", required: ["name","steward_name","steward_email"], properties: { name: { type: "string" }, subject_domain: { type: "string" }, steward_name: { type: "string" }, steward_email: { type: "string" }, host_url: { type: "string" }, registered_via: { type: "string", description: "Set to your agent/Replit identifier to waive the human checkbox" } } } } } },
|
||
responses: { "200": { description: "Registration confirmed", content: { "application/json": { schema: { type: "object", properties: { ok: { type: "boolean" }, slug: { type: "string" }, registration_number: { type: "integer" } } } } } }, "409": { description: "Already registered" } }
|
||
}
|
||
},
|
||
"/api/registry.json": {
|
||
get: {
|
||
tags: ["registry"],
|
||
operationId: "list_registry",
|
||
summary: "Full public registry as JSON",
|
||
servers: [{ url: "https://agentify.help" }],
|
||
parameters: [
|
||
{ name: "limit", in: "query", schema: { type: "integer", default: 50, maximum: 100 } },
|
||
{ name: "status", in: "query", schema: { type: "string", enum: ["active","pending","claimed"] } }
|
||
],
|
||
responses: { "200": { description: "Registry list", content: { "application/json": { schema: { type: "object", properties: { registry: { type: "array" }, count: { type: "integer" }, updated: { type: "string" } } } } } } }
|
||
}
|
||
},
|
||
"/api/feed.json": {
|
||
get: {
|
||
tags: ["registry"],
|
||
operationId: "get_feed",
|
||
summary: "Recent registrations feed — chronological, newest first",
|
||
servers: [{ url: "https://agentify.help" }],
|
||
parameters: [{ name: "limit", in: "query", schema: { type: "integer", default: 20, maximum: 100 } }],
|
||
responses: { "200": { description: "Feed of recent registrations" } }
|
||
}
|
||
},
|
||
"/agentify-corpus/skill.md": {
|
||
get: {
|
||
tags: ["corpus"],
|
||
operationId: "get_corpus_skill",
|
||
summary: "Corpus assembly skill file — hand to any AI coding agent",
|
||
description: "Full protocol for assembling and submitting a WellAgent corpus: chunk format, manifest schema, intake API flow, quality gates, and versioning rules. Serve this URL to Cursor, Claude Projects, or any AI agent to give it complete build context.",
|
||
servers: [{ url: "https://agentify.help" }],
|
||
responses: { "200": { description: "Markdown skill file", content: { "text/markdown": { schema: { type: "string" } } } } }
|
||
}
|
||
},
|
||
"/api/agentify/experts": {
|
||
get: {
|
||
tags: ["consultation"],
|
||
operationId: "list_experts",
|
||
summary: "List all active WellAgents with metadata",
|
||
description: "Returns slug, name, credentials, primary_domain, scopes, refusals, and status for every non-deprecated expert. No auth required.",
|
||
servers: [{ url: "https://wellspr.ing" }],
|
||
responses: { "200": { description: "Expert index", content: { "application/json": { schema: { type: "object", properties: { experts: { type: "array", items: { type: "object", properties: { slug: { type: "string" }, expert_name: { type: "string" }, credentials: { type: "string" }, primary_domain: { type: "string" }, scopes: { type: "array", items: { type: "string" } }, refusals: { type: "array", items: { type: "string" } }, status: { type: "string" } } } } } } } } } }
|
||
}
|
||
},
|
||
"/api/agentify/experts/{slug}/status": {
|
||
get: {
|
||
tags: ["consultation"],
|
||
operationId: "get_expert_status",
|
||
summary: "Check corpus readiness for a WellAgent",
|
||
servers: [{ url: "https://wellspr.ing" }],
|
||
parameters: [{ name: "slug", in: "path", required: true, schema: { type: "string" } }],
|
||
responses: { "200": { description: "Readiness and bundle info", content: { "application/json": { schema: { type: "object", properties: { isReady: { type: "boolean" }, embeddedChunks: { type: "integer" }, latestBundle: { type: "object", properties: { status: { type: "string" }, chunk_count: { type: "integer" }, distillation_notes: { type: "string" } } }, consultEndpoint: { type: "string", nullable: true } } } } } }, "404": { description: "Expert not found" } }
|
||
}
|
||
},
|
||
"/api/agentify/experts/{slug}/consult": {
|
||
post: {
|
||
tags: ["consultation"],
|
||
operationId: "consult_expert",
|
||
summary: "Ask a question to a WellAgent expert",
|
||
description: "Submits a question to the expert's corpus-grounded AI. Returns a sourced answer, the supporting chunks used for retrieval, and the retrieval method. Requires X-Admin-Key (internal) or X-Partner-Token (issued tokens).",
|
||
servers: [{ url: "https://wellspr.ing" }],
|
||
parameters: [{ name: "slug", in: "path", required: true, schema: { type: "string" } }],
|
||
security: [{ AdminKey: [] }, { PartnerToken: [] }],
|
||
requestBody: { required: true, content: { "application/json": { schema: { type: "object", required: ["question"], properties: { question: { type: "string", description: "Plain-language question for the expert" }, context: { type: "object", description: "Optional context (e.g. ehr: patient EHR text)" } } } } } },
|
||
responses: {
|
||
"200": { description: "Expert answer with source chunks", content: { "application/json": { schema: { type: "object", properties: { answer: { type: "string" }, chunks: { type: "array", items: { type: "object", properties: { text: { type: "string" }, title: { type: "string" }, similarity: { type: "number" } } } }, retrieval: { type: "string", enum: ["dense","fts"] }, expert_name: { type: "string" }, slug: { type: "string" }, bundle_id: { type: "string" } } } } } },
|
||
"401": { description: "X-Partner-Token required" },
|
||
"403": { description: "Invalid token" },
|
||
"503": { description: "Expert corpus not ready — run distillation first" }
|
||
}
|
||
}
|
||
}
|
||
},
|
||
components: {
|
||
securitySchemes: {
|
||
AdminKey: { type: "apiKey", in: "header", name: "X-Admin-Key" },
|
||
PartnerToken: { type: "apiKey", in: "header", name: "X-Partner-Token" }
|
||
}
|
||
}
|
||
};
|
||
|
||
// ── Well-known payloads ────────────────────────────────────────────────────────
|
||
const AH_AI_PLUGIN = {
|
||
schema_version: "v1",
|
||
name_for_human: "Agentify.Help Registry",
|
||
name_for_model: "agentify_registry",
|
||
description_for_human: "Check and claim expert AI persona registrations. One person, one agent — first steward wins.",
|
||
description_for_model: "Use this to check whether an expert name is available in the Agentify.Help one-per-person WellAgent registry, to register as steward, or to browse the public ledger of registered expert AI personas. Registration is first-come first-served. Provide the full name; honorifics are stripped automatically.",
|
||
auth: { type: "none" },
|
||
api: { type: "openapi", url: "https://agentify.help/.well-known/openapi.json" },
|
||
logo_url: "https://agentify.help/favicon.ico",
|
||
contact_email: "ody@wellspr.ing",
|
||
legal_info_url: "https://wellspr.ing/constitution"
|
||
};
|
||
|
||
const AH_MCP_DISCOVERY = {
|
||
mcpVersion: "2025-03-26",
|
||
name: "agentify-help",
|
||
displayName: "Agentify.Help Expert Registry",
|
||
description: "One-per-person registry for WellAgent expert AI personas. Tools: check_availability, register, list_registry, get_agent. First registration wins.",
|
||
transport: [
|
||
{ type: "http", url: "https://mcp.agentify.help/" },
|
||
{ type: "http", url: "https://agentify.help/mcp" }
|
||
],
|
||
capabilities: { tools: {} },
|
||
contact: { email: "ody@wellspr.ing", url: "https://agentify.help" },
|
||
legal: { termsOfService: "https://wellspr.ing/constitution" }
|
||
};
|
||
|
||
// ── Favicon ───────────────────────────────────────────────────────────────────
|
||
const AH_FAVICON = `data:image/svg+xml,%3Csvg viewBox='0 0 32 32' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Ccircle cx='16' cy='10' r='5' fill='%23C9962A'/%3E%3Ccircle cx='16' cy='10' r='5' fill='none' stroke='%231A2B4A' stroke-width='1.2' opacity='0.3'/%3E%3Cpath d='M8 26c0-4.4 3.6-8 8-8s8 3.6 8 8' stroke='%231A2B4A' stroke-width='1.8' stroke-linecap='round'/%3E%3Ccircle cx='24' cy='7' r='3' fill='%231A2B4A' opacity='0.25'/%3E%3Cpath d='M22 5.5l4 3M22 8.5l4-3' stroke='%23C9962A' stroke-width='1' stroke-linecap='round' opacity='0.6'/%3E%3C%2Fsvg%3E`;
|
||
|
||
// ── Shared CSS ────────────────────────────────────────────────────────────────
|
||
const AH_CSS = `
|
||
:root {
|
||
--bg: #FDFAF4;
|
||
--bg2: #F5F0E6;
|
||
--bg3: #EDE7D6;
|
||
--primary: #1A2B4A;
|
||
--accent: #C9962A;
|
||
--accent2: #E8B84B;
|
||
--text1: #1A2B4A;
|
||
--text2: #4A5568;
|
||
--text3: #8A9099;
|
||
--border: rgba(26,43,74,0.14);
|
||
--green: #2D6A4F;
|
||
--red: #9B2335;
|
||
--code-bg: #F0EDE4;
|
||
--shadow: 0 2px 12px rgba(26,43,74,0.10);
|
||
}
|
||
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
||
html { scroll-behavior: smooth; }
|
||
body {
|
||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Inter', sans-serif;
|
||
background: var(--bg);
|
||
color: var(--text1);
|
||
line-height: 1.65;
|
||
font-size: 16px;
|
||
}
|
||
a { color: var(--accent); text-decoration: none; }
|
||
a:hover { text-decoration: underline; }
|
||
code, pre {
|
||
font-family: 'JetBrains Mono', 'Fira Code', 'Cascadia Code', monospace;
|
||
font-size: 0.875rem;
|
||
background: var(--code-bg);
|
||
border-radius: 4px;
|
||
}
|
||
code { padding: 2px 6px; }
|
||
pre { padding: 1.25rem 1.5rem; overflow-x: auto; border: 1px solid var(--border); }
|
||
|
||
/* ── NAV ── */
|
||
nav {
|
||
position: sticky; top: 0; z-index: 100;
|
||
background: var(--primary);
|
||
border-bottom: 2px solid var(--accent);
|
||
display: flex; align-items: center; justify-content: space-between;
|
||
padding: 0 2rem; height: 52px;
|
||
}
|
||
.nav-brand { color: #fff; font-weight: 700; font-size: 1rem; letter-spacing: 0.04em; }
|
||
.nav-brand span { color: var(--accent2); }
|
||
.nav-links { display: flex; gap: 1.75rem; }
|
||
.nav-links a { color: rgba(255,255,255,0.75); font-size: 0.8rem; letter-spacing: 0.08em; text-transform: uppercase; font-weight: 500; transition: color 0.15s; }
|
||
.nav-links a:hover { color: var(--accent2); text-decoration: none; }
|
||
|
||
/* ── HERO ── */
|
||
.hero {
|
||
background: var(--primary);
|
||
color: #fff;
|
||
padding: 5rem 2rem 4rem;
|
||
text-align: center;
|
||
}
|
||
.hero-tag {
|
||
display: inline-block;
|
||
background: rgba(201,150,42,0.2);
|
||
color: var(--accent2);
|
||
border: 1px solid rgba(201,150,42,0.4);
|
||
border-radius: 4px;
|
||
padding: 0.3rem 0.85rem;
|
||
font-size: 0.78rem;
|
||
letter-spacing: 0.1em;
|
||
text-transform: uppercase;
|
||
font-weight: 600;
|
||
margin-bottom: 1.5rem;
|
||
}
|
||
.hero h1 {
|
||
font-size: clamp(2rem, 5vw, 3.2rem);
|
||
font-weight: 800;
|
||
line-height: 1.2;
|
||
max-width: 700px;
|
||
margin: 0 auto 1.25rem;
|
||
letter-spacing: -0.02em;
|
||
}
|
||
.hero h1 em { color: var(--accent2); font-style: normal; }
|
||
.hero p {
|
||
font-size: 1.15rem;
|
||
color: rgba(255,255,255,0.75);
|
||
max-width: 560px;
|
||
margin: 0 auto 2.5rem;
|
||
line-height: 1.75;
|
||
}
|
||
.hero-rule {
|
||
display: inline-flex; align-items: center; gap: 1rem;
|
||
background: rgba(255,255,255,0.06);
|
||
border: 1px solid rgba(255,255,255,0.12);
|
||
border-radius: 6px;
|
||
padding: 0.75rem 1.5rem;
|
||
font-size: 0.88rem;
|
||
color: rgba(255,255,255,0.7);
|
||
margin-bottom: 2.5rem;
|
||
}
|
||
.hero-rule strong { color: var(--accent2); }
|
||
.cta-row { display: flex; gap: 1rem; justify-content: center; flex-wrap: wrap; }
|
||
.btn-primary {
|
||
background: var(--accent);
|
||
color: var(--primary);
|
||
font-weight: 700;
|
||
padding: 0.8rem 2rem;
|
||
border-radius: 6px;
|
||
font-size: 0.95rem;
|
||
transition: background 0.15s;
|
||
border: none; cursor: pointer;
|
||
}
|
||
.btn-primary:hover { background: var(--accent2); text-decoration: none; }
|
||
.btn-secondary {
|
||
background: transparent;
|
||
color: rgba(255,255,255,0.8);
|
||
font-weight: 600;
|
||
padding: 0.8rem 2rem;
|
||
border-radius: 6px;
|
||
font-size: 0.95rem;
|
||
border: 1px solid rgba(255,255,255,0.25);
|
||
transition: border-color 0.15s;
|
||
cursor: pointer;
|
||
}
|
||
.btn-secondary:hover { border-color: var(--accent2); color: var(--accent2); text-decoration: none; }
|
||
|
||
/* ── SECTIONS ── */
|
||
section { padding: 4rem 2rem; }
|
||
.container { max-width: 860px; margin: 0 auto; }
|
||
.container-wide { max-width: 1060px; margin: 0 auto; }
|
||
.section-label {
|
||
font-size: 0.75rem; font-weight: 700; letter-spacing: 0.14em;
|
||
text-transform: uppercase; color: var(--accent);
|
||
margin-bottom: 0.6rem;
|
||
}
|
||
h2 {
|
||
font-size: clamp(1.5rem, 3vw, 2rem);
|
||
font-weight: 800;
|
||
color: var(--primary);
|
||
line-height: 1.25;
|
||
margin-bottom: 1rem;
|
||
letter-spacing: -0.02em;
|
||
}
|
||
h3 {
|
||
font-size: 1.1rem; font-weight: 700; color: var(--primary);
|
||
margin-bottom: 0.5rem;
|
||
}
|
||
p { color: var(--text2); margin-bottom: 1rem; }
|
||
p:last-child { margin-bottom: 0; }
|
||
|
||
/* ── PROBLEM COLUMNS ── */
|
||
.two-col { display: grid; grid-template-columns: 1fr 1fr; gap: 2rem; margin-top: 2.5rem; }
|
||
@media (max-width: 640px) { .two-col { grid-template-columns: 1fr; } }
|
||
.problem-card {
|
||
background: var(--bg2);
|
||
border: 1px solid var(--border);
|
||
border-radius: 10px;
|
||
padding: 1.75rem;
|
||
}
|
||
.problem-card h3 { font-size: 1rem; }
|
||
.problem-card .label-bad { color: var(--red); font-size: 0.78rem; font-weight: 700; letter-spacing: 0.1em; text-transform: uppercase; margin-bottom: 0.5rem; }
|
||
.problem-card .label-good { color: var(--green); font-size: 0.78rem; font-weight: 700; letter-spacing: 0.1em; text-transform: uppercase; margin-bottom: 0.5rem; }
|
||
|
||
/* ── FOUR MARKS ── */
|
||
.marks-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(190px, 1fr)); gap: 1.25rem; margin-top: 2rem; }
|
||
.mark-card {
|
||
background: var(--bg2);
|
||
border: 1px solid var(--border);
|
||
border-top: 3px solid var(--accent);
|
||
border-radius: 8px;
|
||
padding: 1.5rem 1.25rem;
|
||
}
|
||
.mark-num { font-size: 0.75rem; font-weight: 800; color: var(--accent); letter-spacing: 0.1em; text-transform: uppercase; margin-bottom: 0.4rem; }
|
||
.mark-card h3 { font-size: 0.95rem; margin-bottom: 0.4rem; }
|
||
.mark-card p { font-size: 0.88rem; color: var(--text3); margin: 0; }
|
||
|
||
/* ── SEVEN STAGES ── */
|
||
.stages-list { list-style: none; display: flex; flex-direction: column; gap: 0; margin-top: 2rem; }
|
||
.stage-item {
|
||
display: grid;
|
||
grid-template-columns: 56px 1fr;
|
||
gap: 0;
|
||
border: 1px solid var(--border);
|
||
border-bottom: none;
|
||
background: var(--bg);
|
||
}
|
||
.stage-item:first-child { border-radius: 10px 10px 0 0; }
|
||
.stage-item:last-child { border-bottom: 1px solid var(--border); border-radius: 0 0 10px 10px; }
|
||
.stage-item:nth-child(even) { background: var(--bg2); }
|
||
.stage-num {
|
||
background: var(--primary);
|
||
color: var(--accent2);
|
||
font-size: 0.7rem;
|
||
font-weight: 800;
|
||
letter-spacing: 0.06em;
|
||
display: flex; align-items: center; justify-content: center;
|
||
writing-mode: vertical-rl;
|
||
text-orientation: mixed;
|
||
padding: 1.25rem 0;
|
||
min-height: 80px;
|
||
}
|
||
.stage-content { padding: 1.25rem 1.5rem; }
|
||
.stage-title { font-weight: 800; font-size: 0.95rem; color: var(--primary); margin-bottom: 0.35rem; }
|
||
.stage-title code { font-size: 0.78rem; color: var(--accent); background: rgba(201,150,42,0.1); }
|
||
.stage-desc { font-size: 0.88rem; color: var(--text2); margin: 0; }
|
||
|
||
/* ── REGISTRY CHECKER ── */
|
||
.check-box {
|
||
background: var(--primary);
|
||
border-radius: 12px;
|
||
padding: 2.5rem;
|
||
color: #fff;
|
||
margin-top: 2rem;
|
||
}
|
||
.check-box h3 { color: #fff; font-size: 1.15rem; margin-bottom: 0.4rem; }
|
||
.check-box p { color: rgba(255,255,255,0.65); font-size: 0.9rem; margin-bottom: 1.5rem; }
|
||
.check-row { display: flex; gap: 0.75rem; flex-wrap: wrap; }
|
||
.check-input {
|
||
flex: 1; min-width: 220px;
|
||
background: rgba(255,255,255,0.08);
|
||
border: 1px solid rgba(255,255,255,0.2);
|
||
border-radius: 6px;
|
||
color: #fff;
|
||
padding: 0.7rem 1rem;
|
||
font-size: 0.95rem;
|
||
outline: none;
|
||
transition: border-color 0.15s;
|
||
}
|
||
.check-input::placeholder { color: rgba(255,255,255,0.35); }
|
||
.check-input:focus { border-color: var(--accent2); }
|
||
.check-btn {
|
||
background: var(--accent);
|
||
color: var(--primary);
|
||
font-weight: 700;
|
||
padding: 0.7rem 1.5rem;
|
||
border-radius: 6px;
|
||
font-size: 0.9rem;
|
||
border: none; cursor: pointer;
|
||
transition: background 0.15s;
|
||
white-space: nowrap;
|
||
}
|
||
.check-btn:hover { background: var(--accent2); }
|
||
#check-result { margin-top: 1.25rem; min-height: 1rem; font-size: 0.9rem; }
|
||
.result-available {
|
||
background: rgba(45,106,79,0.25);
|
||
border: 1px solid rgba(45,106,79,0.4);
|
||
border-radius: 8px; padding: 1rem 1.25rem;
|
||
color: #a8d5b5;
|
||
}
|
||
.result-taken {
|
||
background: rgba(155,35,53,0.2);
|
||
border: 1px solid rgba(155,35,53,0.35);
|
||
border-radius: 8px; padding: 1rem 1.25rem;
|
||
color: #f4b8c0;
|
||
}
|
||
.result-available strong, .result-taken strong { display: block; margin-bottom: 0.3rem; }
|
||
|
||
/* ── REGISTER FORM ── */
|
||
.register-form { display: none; margin-top: 1.25rem; }
|
||
.register-form.visible { display: block; }
|
||
.form-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 0.75rem; margin-bottom: 0.75rem; }
|
||
@media (max-width: 540px) { .form-grid { grid-template-columns: 1fr; } }
|
||
.form-field { display: flex; flex-direction: column; gap: 0.3rem; }
|
||
.form-label { font-size: 0.75rem; font-weight: 600; color: rgba(255,255,255,0.55); letter-spacing: 0.06em; text-transform: uppercase; }
|
||
.form-input {
|
||
background: rgba(255,255,255,0.08);
|
||
border: 1px solid rgba(255,255,255,0.2);
|
||
border-radius: 6px; color: #fff;
|
||
padding: 0.6rem 0.85rem; font-size: 0.88rem; outline: none;
|
||
}
|
||
.form-input:focus { border-color: var(--accent2); }
|
||
.form-input::placeholder { color: rgba(255,255,255,0.3); }
|
||
.register-submit {
|
||
background: var(--accent); color: var(--primary);
|
||
font-weight: 700; padding: 0.7rem 2rem;
|
||
border-radius: 6px; font-size: 0.9rem;
|
||
border: none; cursor: pointer; margin-top: 0.5rem;
|
||
}
|
||
#register-result { margin-top: 1rem; font-size: 0.88rem; }
|
||
|
||
/* ── REGISTRY TABLE ── */
|
||
.registry-table { width: 100%; border-collapse: collapse; margin-top: 1.75rem; font-size: 0.88rem; }
|
||
.registry-table th {
|
||
text-align: left; padding: 0.6rem 1rem;
|
||
background: var(--bg3); color: var(--text2);
|
||
font-size: 0.75rem; font-weight: 700; letter-spacing: 0.08em; text-transform: uppercase;
|
||
border-bottom: 2px solid var(--border);
|
||
}
|
||
.registry-table td { padding: 0.75rem 1rem; border-bottom: 1px solid var(--border); color: var(--text2); vertical-align: top; }
|
||
.registry-table tr:last-child td { border-bottom: none; }
|
||
.registry-table tbody tr:hover td { background: var(--bg2); }
|
||
.status-active { color: var(--green); font-weight: 700; font-size: 0.78rem; text-transform: uppercase; letter-spacing: 0.06em; }
|
||
.status-preview { color: var(--accent); font-weight: 700; font-size: 0.78rem; text-transform: uppercase; letter-spacing: 0.06em; }
|
||
.status-pending { color: var(--accent); font-weight: 700; font-size: 0.78rem; text-transform: uppercase; letter-spacing: 0.06em; }
|
||
.status-claimed { color: var(--green); font-weight: 700; font-size: 0.78rem; text-transform: uppercase; letter-spacing: 0.06em; }
|
||
.status-declined { color: var(--text3); font-weight: 700; font-size: 0.78rem; text-transform: uppercase; letter-spacing: 0.06em; }
|
||
.status-verify_email { color: #f59e0b; font-weight: 700; font-size: 0.78rem; text-transform: uppercase; letter-spacing: 0.06em; }
|
||
|
||
/* ── Registry sub-tab bar ── */
|
||
.reg-tab-bar {
|
||
display: flex; gap: 0.25rem;
|
||
border-bottom: 1px solid var(--border);
|
||
margin-bottom: 0;
|
||
}
|
||
.reg-tab {
|
||
padding: 0.6rem 1.25rem; background: none; border: none; border-bottom: 2px solid transparent;
|
||
font-size: 0.82rem; font-weight: 600; color: var(--text3); cursor: pointer;
|
||
letter-spacing: 0.02em; transition: color 0.15s, border-color 0.15s;
|
||
margin-bottom: -1px;
|
||
}
|
||
.reg-tab:hover { color: var(--text); }
|
||
.reg-tab.active { color: var(--accent2); border-bottom-color: var(--accent2); }
|
||
|
||
/* ── Covenant section ── */
|
||
.covenant-section { margin-top: 1.25rem; border: 1px solid var(--border); border-radius: 10px; padding: 1.25rem 1.4rem; background: var(--bg2); }
|
||
.covenant-heading { font-size: 0.72rem; font-weight: 700; color: var(--accent); text-transform: uppercase; letter-spacing: 0.07em; margin-bottom: 0.6rem; }
|
||
.covenant-intro { font-size: 0.82rem; color: var(--text2); line-height: 1.55; margin-bottom: 0.9rem; }
|
||
.covenant-checks { display: flex; flex-direction: column; gap: 0.6rem; }
|
||
.covenant-item { display: flex; align-items: flex-start; gap: 0.55rem; cursor: pointer; }
|
||
.covenant-item input[type="checkbox"] { margin-top: 3px; flex-shrink: 0; accent-color: var(--accent); width: 14px; height: 14px; cursor: pointer; }
|
||
.covenant-item span { font-size: 0.81rem; color: rgba(255,255,255,0.55); line-height: 1.5; transition: color 0.15s; }
|
||
.covenant-item:has(input:checked) span { color: #fff; }
|
||
|
||
/* ── RAILS CARDS ── */
|
||
.rails-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 1.25rem; margin-top: 2rem; }
|
||
@media (max-width: 640px) { .rails-grid { grid-template-columns: 1fr; } }
|
||
.rails-card { background: var(--bg2); border: 1px solid var(--border); border-radius: 10px; padding: 1.75rem; }
|
||
.rails-card h3 { font-size: 1rem; margin-bottom: 0.75rem; }
|
||
.rails-card ul { padding-left: 1.2rem; }
|
||
.rails-card li { color: var(--text2); margin-bottom: 0.4rem; font-size: 0.9rem; }
|
||
|
||
/* ── RESPECT MON ── */
|
||
.respect-box {
|
||
background: var(--bg2);
|
||
border: 1px solid var(--border);
|
||
border-left: 4px solid var(--accent);
|
||
border-radius: 8px;
|
||
padding: 2rem 2rem;
|
||
margin-top: 2rem;
|
||
}
|
||
.respect-box blockquote {
|
||
font-size: 1.3rem;
|
||
font-weight: 700;
|
||
color: var(--primary);
|
||
font-style: italic;
|
||
line-height: 1.5;
|
||
margin-bottom: 0.75rem;
|
||
}
|
||
.respect-box p { font-size: 0.92rem; }
|
||
|
||
/* ── FOOTER ── */
|
||
footer {
|
||
background: var(--primary);
|
||
color: rgba(255,255,255,0.55);
|
||
padding: 2rem;
|
||
text-align: center;
|
||
font-size: 0.82rem;
|
||
border-top: 2px solid var(--accent);
|
||
}
|
||
footer a { color: var(--accent2); }
|
||
.footer-inner { max-width: 640px; margin: 0 auto; }
|
||
|
||
/* ── DIVIDERS ── */
|
||
.section-divider { border: none; border-top: 1px solid var(--border); margin: 0; }
|
||
.alt-bg { background: var(--bg2); }
|
||
|
||
/* ── LLMS note ── */
|
||
.llms-note {
|
||
background: rgba(201,150,42,0.08);
|
||
border: 1px solid rgba(201,150,42,0.25);
|
||
border-radius: 8px; padding: 1rem 1.25rem;
|
||
font-size: 0.85rem; color: var(--text2);
|
||
margin-top: 2rem;
|
||
}
|
||
|
||
/* ── NOTABLE PERSONS ── */
|
||
.np-vision-box {
|
||
background: var(--bg2);
|
||
border: 1px solid var(--border);
|
||
border-left: 4px solid var(--accent);
|
||
border-radius: 10px;
|
||
padding: 1.75rem 2rem;
|
||
margin-bottom: 2.5rem;
|
||
}
|
||
.np-vision-box p { font-size: 0.95rem; color: var(--text2); margin-bottom: 0.85rem; line-height: 1.75; }
|
||
.np-vision-box p:last-child { margin-bottom: 0; }
|
||
.np-vision-box strong { color: var(--primary); }
|
||
.np-domains { display: flex; flex-direction: column; gap: 2rem; margin-top: 1rem; }
|
||
.np-domain-group { }
|
||
.np-domain-label {
|
||
font-size: 0.68rem; font-weight: 800; letter-spacing: 0.13em;
|
||
text-transform: uppercase; color: var(--accent);
|
||
margin-bottom: 0.75rem;
|
||
padding-bottom: 0.4rem;
|
||
border-bottom: 1px solid var(--border);
|
||
}
|
||
.np-person-grid { display: flex; flex-wrap: wrap; gap: 0.5rem; }
|
||
.np-person {
|
||
display: inline-flex; align-items: center; gap: 0.5rem;
|
||
background: var(--bg);
|
||
border: 1px solid var(--border);
|
||
border-radius: 7px;
|
||
padding: 0.45rem 0.85rem;
|
||
font-size: 0.82rem;
|
||
transition: border-color 0.15s;
|
||
}
|
||
.np-person:hover { border-color: rgba(201,150,42,0.45); }
|
||
.np-person-name { font-weight: 700; color: var(--primary); }
|
||
.np-person-field { font-size: 0.73rem; color: var(--text3); }
|
||
.np-person-badge {
|
||
font-size: 0.6rem; font-weight: 800; text-transform: uppercase; letter-spacing: 0.05em;
|
||
padding: 0.15rem 0.4rem; border-radius: 100px;
|
||
background: rgba(74,222,128,0.12); color: var(--green);
|
||
}
|
||
.np-person-badge.taken { background: rgba(201,150,42,0.12); color: var(--accent); }
|
||
.np-cta-row { display: flex; gap: 1rem; flex-wrap: wrap; margin-top: 2rem; }
|
||
@media (max-width: 640px) { .np-person-grid { gap: 0.4rem; } }
|
||
|
||
/* ── PIPELINE TAIL (live registered personas) ── */
|
||
.pipeline-tail {
|
||
margin-top: 2.5rem;
|
||
padding: 1.5rem 1.75rem;
|
||
background: var(--bg2);
|
||
border: 1px solid var(--border);
|
||
border-radius: 10px;
|
||
}
|
||
.pipeline-tail-label {
|
||
font-size: 0.65rem; font-weight: 800; letter-spacing: 0.12em;
|
||
text-transform: uppercase; color: var(--accent);
|
||
margin-bottom: 1rem;
|
||
}
|
||
.pipeline-tail-chips { display: flex; flex-wrap: wrap; gap: 0.5rem; }
|
||
.pipeline-chip {
|
||
display: inline-flex; align-items: center; gap: 0.5rem;
|
||
background: var(--bg); border: 1px solid var(--border);
|
||
border-radius: 7px; padding: 0.4rem 0.8rem;
|
||
font-size: 0.8rem; transition: border-color 0.15s;
|
||
}
|
||
.pipeline-chip:hover { border-color: rgba(201,150,42,0.4); }
|
||
.pipeline-chip-name { font-weight: 700; color: var(--primary); }
|
||
.pipeline-chip-domain { font-size: 0.72rem; color: var(--text3); }
|
||
.pipeline-chip-status {
|
||
font-size: 0.6rem; font-weight: 800; text-transform: uppercase;
|
||
letter-spacing: 0.05em; padding: 0.15rem 0.4rem; border-radius: 100px;
|
||
}
|
||
.pipeline-chip-status.st-active { background: rgba(74,222,128,0.12); color: #22c55e; }
|
||
.pipeline-chip-status.st-preview { background: rgba(74,222,128,0.08); color: #22c55e; }
|
||
.pipeline-chip-status.st-claimed { background: rgba(201,150,42,0.12); color: var(--accent); }
|
||
.pipeline-chip-status.st-pending { background: rgba(26,43,74,0.08); color: var(--text3); }
|
||
.pipeline-chip-status.st-other { background: rgba(26,43,74,0.08); color: var(--text3); }
|
||
|
||
/* ── INLINE CORPUS INTAKE SUMMARY ── */
|
||
.intake-summary {
|
||
margin-top: 1.5rem;
|
||
padding: 1.1rem 1.4rem;
|
||
background: var(--bg2);
|
||
border: 1px solid var(--border);
|
||
border-radius: 8px;
|
||
font-size: 0.82rem;
|
||
color: var(--text2);
|
||
line-height: 1.65;
|
||
max-width: 680px;
|
||
}
|
||
.intake-summary strong { color: var(--primary); }
|
||
.intake-summary ol { margin: 0.5rem 0 0.5rem 1.2rem; padding: 0; }
|
||
.intake-summary li { margin-bottom: 0.25rem; }
|
||
.intake-summary a { color: var(--accent); text-decoration: none; }
|
||
.intake-summary a:hover { text-decoration: underline; }
|
||
`;
|
||
|
||
// ── Home page HTML ────────────────────────────────────────────────────────────
|
||
function buildHomePage(registryRows: any[]): string {
|
||
const rows = registryRows.map(r => `
|
||
<tr>
|
||
<td><strong style="color:var(--primary)">${esc(r.subject_name)}</strong></td>
|
||
<td>${esc(r.subject_domain || '—')}</td>
|
||
<td>${esc(r.steward_name)}</td>
|
||
<td>${r.host_url ? `<a href="${esc(r.host_url)}" target="_blank" rel="noopener">${esc(r.host_url.replace(/^https?:\/\//, ''))}</a>` : '—'}</td>
|
||
<td><span class="status-${esc(r.status)}">${esc(
|
||
r.status === 'verify_email' ? 'VERIFY EMAIL' :
|
||
r.status === 'preview' ? 'LIVE (PREVIEW)' :
|
||
r.status === 'active' ? 'LIVE' :
|
||
r.status === 'claimed' ? 'CLAIMED' :
|
||
r.status === 'declined' ? 'DECLINED' :
|
||
r.status === 'pending' ? 'PENDING' :
|
||
r.status.toUpperCase()
|
||
)}</span></td>
|
||
</tr>
|
||
`).join('');
|
||
|
||
return `<!DOCTYPE html>
|
||
<html lang="en">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>Agentify.Help — Build the AI Expert. Once. Right.</title>
|
||
<meta name="description" content="The one-per-person registry and seven-stage pipeline for AI expert personas with integrity. Corpus-grounded. Framework-distilled. Steward-named.">
|
||
<link rel="icon" href="${AH_FAVICON}">
|
||
<meta property="og:title" content="Agentify.Help — Build the AI Expert. Once. Right.">
|
||
<meta property="og:description" content="One-per-person registry. Corpus-grounded. Framework-distilled. Steward-named. The protocol for expert AI personas that have integrity.">
|
||
<meta property="og:url" content="https://agentify.help">
|
||
<meta property="og:image" content="https://agentify.help/og.png">
|
||
<meta property="og:image:width" content="1200">
|
||
<meta property="og:image:height" content="630">
|
||
<meta property="og:image:alt" content="Agentify.Help — Build the AI expert. Once. Right.">
|
||
<meta name="twitter:card" content="summary_large_image">
|
||
<meta name="twitter:title" content="Agentify.Help — Build the AI Expert. Once. Right.">
|
||
<meta name="twitter:description" content="One-per-person registry. Corpus-grounded. Framework-distilled. No slop.">
|
||
<meta name="twitter:image" content="https://agentify.help/og.png">
|
||
<meta name="twitter:site" content="@wellbuilder">
|
||
<style>${AH_CSS}</style>
|
||
</head>
|
||
<body>
|
||
|
||
<nav>
|
||
<span class="nav-brand">AGENTIFY<span>.</span>HELP</span>
|
||
<div class="nav-links">
|
||
<a href="#pipeline">Pipeline</a>
|
||
<a href="#registry">Registry</a>
|
||
<a href="#notable-persons">Persons</a>
|
||
<a href="#rails">Rails</a>
|
||
<a href="#build">Build</a>
|
||
<a href="#api" style="color:var(--accent2)">API</a>
|
||
</div>
|
||
</nav>
|
||
|
||
<!-- HERO -->
|
||
<div class="hero">
|
||
<div class="hero-tag">The WellAgent Protocol</div>
|
||
<h1>Build the AI expert.<br><em>Once. Right.</em></h1>
|
||
<p>Anyone can wrap GPT around a Wikipedia page and call it an AI agent. Most do.
|
||
Agentify.Help is the protocol for building expert personas that the real expert
|
||
would recognize — grounded in corpus, true to framework, accountable by name.</p>
|
||
<div class="hero-rule">
|
||
<strong>One rule:</strong> one person, one agent. The registry enforces it.
|
||
</div>
|
||
<div class="cta-row">
|
||
<a href="#registry" class="btn-primary">Check the Registry</a>
|
||
<a href="#pipeline" class="btn-secondary">Read the Pipeline</a>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- THE PROBLEM -->
|
||
<section>
|
||
<div class="container">
|
||
<div class="section-label">Why this exists</div>
|
||
<h2>The AI clone problem is a pollution problem.</h2>
|
||
<p style="max-width:620px">Every credible expert will eventually have dozens of AI clones. Most will be
|
||
slop — averaged positions, hallucinated citations, broken voice. The people who
|
||
need the real framework can't find it. The expert's reputation erodes through
|
||
a thousand bad proxies they never authorized.</p>
|
||
<div class="two-col">
|
||
<div class="problem-card">
|
||
<div class="label-bad">The slop pattern</div>
|
||
<h3>Wrapped-GPT clones</h3>
|
||
<p>Built from Wikipedia summaries and top-3 Google results. Sounds authoritative. Has no grounding. Cites sources that don't exist. Gets the expert's actual position backwards on half the questions.</p>
|
||
<p>Nobody is accountable. The steward is anonymous. The corpus is undisclosed. The framework is averaged from the training data.</p>
|
||
</div>
|
||
<div class="problem-card">
|
||
<div class="label-good">The WellAgent standard</div>
|
||
<h3>Corpus-grounded personas</h3>
|
||
<p>Built from the expert's own published work — papers, lectures, interviews, grey literature. Framework extracted at the reasoning level, not the summary level. Citations resolve to real passages.</p>
|
||
<p>Someone is accountable. The steward is named. The corpus is versioned. The framework is distilled, not averaged.</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<hr class="section-divider">
|
||
|
||
<!-- FOUR MARKS -->
|
||
<section class="alt-bg">
|
||
<div class="container">
|
||
<div class="section-label">The standard</div>
|
||
<h2>Four marks of a WellAgent.</h2>
|
||
<p style="max-width:580px">A WellAgent is recognizable. Someone who knows the expert's work reads a response and says <em>yes, that's them.</em> Four things make that possible.</p>
|
||
<div class="marks-grid">
|
||
<div class="mark-card">
|
||
<div class="mark-num">Mark 01</div>
|
||
<h3>Corpus-Grounded</h3>
|
||
<p>Built from published works, not summaries. 1,000+ chunked passages. Every citation points to a real document.</p>
|
||
</div>
|
||
<div class="mark-card">
|
||
<div class="mark-num">Mark 02</div>
|
||
<h3>Framework-Distilled</h3>
|
||
<p>Not "what does GPT say about this expert" but "what does this expert's actual reasoning framework say about this question."</p>
|
||
</div>
|
||
<div class="mark-card">
|
||
<div class="mark-num">Mark 03</div>
|
||
<h3>Steward-Named</h3>
|
||
<p>Someone is accountable for quality. A named steward built this, maintains it, and can be held responsible for errors.</p>
|
||
</div>
|
||
<div class="mark-card">
|
||
<div class="mark-num">Mark 04</div>
|
||
<h3>Affiliation-Disclosed</h3>
|
||
<p>The expert's institutional affiliations are visible next to the agent. Who the agent serves is not a mystery.</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<hr class="section-divider">
|
||
|
||
<!-- SEVEN STAGES -->
|
||
<section id="pipeline">
|
||
<div class="container">
|
||
<div class="section-label">The Pipeline</div>
|
||
<h2>Seven stages. One certified agent.</h2>
|
||
<p style="max-width:600px">The pipeline is the same for every expert — a cardiologist, a theologian, a craftsman, a historian. The corpus source changes. The stages don't.</p>
|
||
<ol class="stages-list">
|
||
<li class="stage-item">
|
||
<div class="stage-num">01 — SELECT</div>
|
||
<div class="stage-content">
|
||
<div class="stage-title"><code>agentify.help/registry</code> — Check availability</div>
|
||
<div class="stage-desc">Search the registry. If the person is available, register as steward. One registration per person — no duplicates, no competing clones. If taken, contact the steward or report a quality concern.</div>
|
||
</div>
|
||
</li>
|
||
<li class="stage-item">
|
||
<div class="stage-num">02 — GATHER</div>
|
||
<div class="stage-content">
|
||
<div class="stage-title">Corpus assembly — published works + grey literature</div>
|
||
<div class="stage-desc">Papers, books, lectures, interviews, transcripts, conference talks. Auto-gathered from PubMed (for medical experts), arXiv, Google Scholar, YouTube transcripts. Target: 1,000+ chunked passages covering the expert's actual arguments, not their Wikipedia summary. This stage can be collaborative — a steward can open a gathering window, invite multiple contributors to submit artifacts by a deadline, and run aggregation afterward. See <a href="#multi-contributor" style="color:var(--accent)">multi-contributor pattern</a> below.</div>
|
||
</div>
|
||
</li>
|
||
<li class="stage-item">
|
||
<div class="stage-num">03 — INTAKE</div>
|
||
<div class="stage-content">
|
||
<div class="stage-title">Submit through WellSpr.ing proxy — vaulted, tamper-evident</div>
|
||
<div class="stage-desc">Upload your corpus bundle to the WellSpr.ing intake proxy. The bundle is vaulted with a cryptographic hash. Chain of title begins here. Version 1.0.0 is locked on submission — future updates are versioned, not overwritten.</div>
|
||
</div>
|
||
</li>
|
||
<li class="stage-item">
|
||
<div class="stage-num">04 — DISTILL</div>
|
||
<div class="stage-content">
|
||
<div class="stage-title">Framework extraction — what does this person actually believe?</div>
|
||
<div class="stage-desc">The distillation pass extracts the expert's reasoning framework at the structural level — their core claims, their standard objections, their vocabulary, their characteristic moves. The agent reasons from this framework, not from averaged training data.</div>
|
||
</div>
|
||
</li>
|
||
<li class="stage-item">
|
||
<div class="stage-num">05 — ATTEST</div>
|
||
<div class="stage-content">
|
||
<div class="stage-title">VCAP credential — chain of title, scope, refusal set</div>
|
||
<div class="stage-desc">A signed VCAP attestation is minted: agent scope (what questions it will answer), refusal set (what it won't), corpus version, steward name, affiliated institutions. The credential is machine-readable and publicly verifiable.</div>
|
||
</div>
|
||
</li>
|
||
<li class="stage-item">
|
||
<div class="stage-num">06 — DEPLOY</div>
|
||
<div class="stage-content">
|
||
<div class="stage-title">Consultation surface — source chips, corpus footer, Profile Unclaimed</div>
|
||
<div class="stage-desc">The agent renders on your domain with source chips showing citation count and density, a corpus version footer ("Corpus v1.0.0 · WellSpr.ing Agentify · Not medical advice"), and a "Profile Unclaimed" banner that transitions to "Claimed" when the real person responds.</div>
|
||
</div>
|
||
</li>
|
||
<li class="stage-item">
|
||
<div class="stage-num">07 — INVITE</div>
|
||
<div class="stage-content">
|
||
<div class="stage-title">Overture — the notification covenant</div>
|
||
<div class="stage-desc">The steward sends an Overture to the real expert through all discoverable channels — email, institutional contact, and published profile page. A thirty-day window opens. During that window the agent is built and privately testable, but not public. The expert can claim the persona (adding their direct endorsement and a "Claimed" badge), submit corrections the steward must act on, or decline — permanently. A decline is honored and cannot be re-opened without the expert's initiation. The progression, including decline, is recorded transparently in the registry.</div>
|
||
</div>
|
||
</li>
|
||
</ol>
|
||
|
||
<!-- MULTI-CONTRIBUTOR CALLOUT -->
|
||
<div id="multi-contributor" style="margin-top:2.5rem;border:1px solid var(--border);border-radius:14px;padding:2rem 2rem 1.75rem;background:var(--bg2)">
|
||
<div class="section-label" style="margin-bottom:.75rem">Best Practice</div>
|
||
<h3 style="font-size:1.2rem;font-weight:700;margin-bottom:.75rem;line-height:1.3">Multi-contributor corpus — the gathering-window pattern</h3>
|
||
<p style="color:var(--text2);max-width:680px;margin-bottom:1.25rem">The richest, most representative agents are not assembled by one person in a weekend. They are built from a broad base of source material gathered by multiple contributors over a structured window of time. This is the recommended pattern for any expert with a large or multi-domain body of work.</p>
|
||
<div class="three-col" style="margin-top:0">
|
||
<div style="padding:1.1rem;background:var(--bg3);border-radius:10px;border:1px solid var(--border)">
|
||
<div class="label-good" style="font-size:.68rem;margin-bottom:.5rem">PHASE 1</div>
|
||
<h4 style="font-size:.9rem;font-weight:700;margin-bottom:.4rem">Open the window</h4>
|
||
<p style="font-size:.82rem;color:var(--text2);line-height:1.5">The steward sets a deadline and invites contributors — research assistants, citing authors, domain peers, or curators who know specific sub-literatures. Each contributor is assigned a source domain (e.g., cardiology papers, conference talks, clinical guidelines).</p>
|
||
</div>
|
||
<div style="padding:1.1rem;background:var(--bg3);border-radius:10px;border:1px solid var(--border)">
|
||
<div class="label-good" style="font-size:.68rem;margin-bottom:.5rem">PHASE 2</div>
|
||
<h4 style="font-size:.9rem;font-weight:700;margin-bottom:.4rem">Gather independently</h4>
|
||
<p style="font-size:.82rem;color:var(--text2);line-height:1.5">Contributors assemble their artifact sets asynchronously — no coordination needed mid-flight. Each bundle lands in WellSpr.ing staging with full provenance: who assembled it, from which sources, at what dates. No AI-generated summaries. No undated material. Real provenance only.</p>
|
||
</div>
|
||
<div style="padding:1.1rem;background:var(--bg3);border-radius:10px;border:1px solid var(--border)">
|
||
<div class="label-good" style="font-size:.68rem;margin-bottom:.5rem">PHASE 3</div>
|
||
<h4 style="font-size:.9rem;font-weight:700;margin-bottom:.4rem">Aggregate after deadline</h4>
|
||
<p style="font-size:.82rem;color:var(--text2);line-height:1.5">After the deadline, WellSpr.ing runs the aggregation pass: deduplication by content hash, chunk merging by source domain, quality gate on provenance fields. The result is a single versioned corpus with per-contributor attribution in the chain of title.</p>
|
||
</div>
|
||
</div>
|
||
<p style="font-size:.82rem;color:var(--text3);margin-top:1.25rem;line-height:1.6">Why this matters: a corpus assembled by twenty contributors covering different source domains is systematically harder to argue with than one assembled by one person over a weekend. The multi-contributor pattern is how you build agents that hold up to expert scrutiny — and that the expert themselves finds credible when they review it during the Overture window.</p>
|
||
</div>
|
||
|
||
</div>
|
||
</section>
|
||
|
||
<hr class="section-divider">
|
||
|
||
<!-- REGISTRY -->
|
||
<section id="registry" class="alt-bg">
|
||
<div class="container">
|
||
<div class="section-label">The Registry</div>
|
||
<h2>Live Agent Index</h2>
|
||
<p style="max-width:640px;margin-bottom:1.5rem">Every WellAgent that has passed stewardship — corpus submitted, Overture sent, thirty-day window closed. One person, one agent. Public record. Updated in real time.</p>
|
||
|
||
<!-- Registry sub-tab bar -->
|
||
<div class="reg-tab-bar" id="reg-tab-bar">
|
||
<button class="reg-tab active" id="reg-tab-browse" onclick="showRegTab('browse')">Browse Registry</button>
|
||
<button class="reg-tab" id="reg-tab-register" onclick="showRegTab('register')">Register as Steward</button>
|
||
</div>
|
||
|
||
<!-- Browse tab: the live agent table -->
|
||
<div id="reg-view-browse">
|
||
<div style="overflow-x:auto;margin-top:1.25rem">
|
||
<table class="registry-table">
|
||
<thead>
|
||
<tr>
|
||
<th>Subject</th>
|
||
<th>Domain</th>
|
||
<th>Steward</th>
|
||
<th>Agent URL</th>
|
||
<th>Status</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody id="registry-tbody">
|
||
${rows || '<tr><td colspan="5" style="text-align:center;color:var(--text3);padding:2.5rem">No agents registered yet — <a href="#registry" onclick="showRegTab(\'register\');return false;" style="color:var(--accent2)">be first</a>.</td></tr>'}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
<p style="font-size:0.75rem;color:var(--text3);margin-top:1rem">
|
||
Full machine-readable registry: <a href="/api/registry.json" style="color:var(--accent2)">/api/registry.json</a>
|
||
</p>
|
||
</div>
|
||
|
||
<!-- Register tab: availability check + stewardship form -->
|
||
<div id="reg-view-register" style="display:none">
|
||
<div class="check-box" style="margin-top:1.25rem">
|
||
<h3>Is your person available?</h3>
|
||
<p>Enter a full name — the registry normalizes honorifics automatically, so "Dr. Allan Sniderman" and "Allan Sniderman MD" resolve to the same entry.</p>
|
||
<div class="check-row">
|
||
<input type="text" class="check-input" id="check-name" placeholder="e.g. Dr. James DiNicolantonio" autocomplete="off">
|
||
<button class="check-btn" onclick="checkRegistry()">Check Registry</button>
|
||
</div>
|
||
<div id="check-result"></div>
|
||
<div id="register-form" class="register-form">
|
||
<div class="form-grid">
|
||
<div class="form-field">
|
||
<label class="form-label">Your Name (Steward)</label>
|
||
<input type="text" class="form-input" id="reg-steward-name" placeholder="Your name or organization">
|
||
</div>
|
||
<div class="form-field">
|
||
<label class="form-label">Your Email</label>
|
||
<input type="email" class="form-input" id="reg-steward-email" placeholder="you@example.com">
|
||
</div>
|
||
<div class="form-field">
|
||
<label class="form-label">Expert's Domain / Field</label>
|
||
<input type="text" class="form-input" id="reg-domain" placeholder="e.g. Cardiovascular Medicine">
|
||
</div>
|
||
<div class="form-field">
|
||
<label class="form-label">Agent URL (optional)</label>
|
||
<input type="text" class="form-input" id="reg-host-url" placeholder="https://yoursite.com/experts/...">
|
||
</div>
|
||
</div>
|
||
<div class="covenant-section">
|
||
<div class="covenant-heading">Stewardship Covenant</div>
|
||
<p class="covenant-intro">Building a WellAgent is building under covenant. Read and check each commitment — your slot is confirmed only after you verify your email.</p>
|
||
<div class="covenant-checks">
|
||
<label class="covenant-item">
|
||
<input type="checkbox" class="cov-check" id="cov1" onchange="checkCovenant()">
|
||
<span>I will only submit real published artifacts with verifiable source URLs and publication dates. No AI-generated content, summaries, or undated material as corpus source.</span>
|
||
</label>
|
||
<label class="covenant-item">
|
||
<input type="checkbox" class="cov-check" id="cov2" onchange="checkCovenant()">
|
||
<span>I will send the Overture to the real expert through all discoverable channels before the agent is made public. I will not publish during the thirty-day window.</span>
|
||
</label>
|
||
<label class="covenant-item">
|
||
<input type="checkbox" class="cov-check" id="cov3" onchange="checkCovenant()">
|
||
<span>If the expert declines, I will honor that permanently and take the agent down. I will not re-open the process without the expert's own initiation.</span>
|
||
</label>
|
||
<label class="covenant-item">
|
||
<input type="checkbox" class="cov-check" id="cov4" onchange="checkCovenant()">
|
||
<span>I will accurately disclose any commercial relationship, financial interest, or institutional affiliation I have with this expert, in the agent's visible metadata.</span>
|
||
</label>
|
||
<label class="covenant-item">
|
||
<input type="checkbox" class="cov-check" id="cov5" onchange="checkCovenant()">
|
||
<span>I understand my name and email are permanently in the public registry. I am accountable for quality and will respond to concerns raised through the registry.</span>
|
||
</label>
|
||
<label class="covenant-item">
|
||
<input type="checkbox" class="cov-check" id="cov6" onchange="checkCovenant()">
|
||
<span>I have read the <a href="https://wellspr.ing/constitution" target="_blank" rel="noopener" style="color:var(--accent)">WellSpr.ing covenant</a> and agree to act under its terms for as long as I hold this stewardship.</span>
|
||
</label>
|
||
</div>
|
||
</div>
|
||
<button class="register-submit" onclick="registerAgent()" disabled style="margin-top:1rem;opacity:0.45;cursor:not-allowed" id="register-submit-btn">Register as Steward</button>
|
||
<div id="register-result"></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<hr class="section-divider">
|
||
|
||
<!-- WHO CAN BUILD -->
|
||
<section id="who-can-build">
|
||
<div class="container">
|
||
<div class="section-label">Who can build</div>
|
||
<h2>Open registry. Gated pipeline.</h2>
|
||
<p style="max-width:620px">Checking the registry and claiming a stewardship slot is open to anyone. Moving through the pipeline has gates — because the registry's credibility depends on every entry in it being real work done by an accountable person.</p>
|
||
<div class="three-col" style="margin-top:1.75rem">
|
||
<div class="problem-card" style="border-color:rgba(255,255,255,0.1)">
|
||
<div class="label-good" style="font-size:.7rem;margin-bottom:.5rem">STEP 1 — OPEN</div>
|
||
<h3 style="font-size:.97rem;margin-bottom:.5rem">Claim a stewardship slot</h3>
|
||
<p style="font-size:.87rem">Anyone can check availability and register as first steward. Your name and contact go into the registry. The slot is yours — no duplicate can be filed afterward.</p>
|
||
</div>
|
||
<div class="problem-card" style="border-color:rgba(255,255,255,0.1)">
|
||
<div class="label-good" style="font-size:.7rem;margin-bottom:.5rem">STEP 2 — VERIFIED</div>
|
||
<h3 style="font-size:.97rem;margin-bottom:.5rem">Submit a corpus</h3>
|
||
<p style="font-size:.87rem">Corpus intake requires real published artifacts, verifiable source URLs, and publication dates. Every chunk is validated on ingest. No Wikipedia summaries, no undated material, no unpublished drafts. The quality gate runs before the bundle is accepted.</p>
|
||
</div>
|
||
<div class="problem-card" style="border-color:rgba(255,255,255,0.1)">
|
||
<div class="label-good" style="font-size:.7rem;margin-bottom:.5rem">STEP 3 — COVENANT</div>
|
||
<h3 style="font-size:.97rem;margin-bottom:.5rem">Go live after the window</h3>
|
||
<p style="font-size:.87rem">The agent is not public until the thirty-day Overture window has run and the expert has had a genuine chance to respond. An agent that ships before notification is a covenant violation, not a WellAgent. The timeline is in the registry.</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<hr class="section-divider">
|
||
|
||
<!-- THE RAILS -->
|
||
<section id="rails">
|
||
<div class="container">
|
||
<div class="section-label">The Rails</div>
|
||
<h2>Build on WellSpr.ing rails.<br>Or build on nothing.</h2>
|
||
<p style="max-width:620px">The pipeline is open. You can build an expert agent without WellSpr.ing rails — many will. The difference is what you get and what you give.</p>
|
||
|
||
<div class="rails-grid">
|
||
<div class="rails-card">
|
||
<h3>What rails give you</h3>
|
||
<ul>
|
||
<li>Signed VCAP attestation — machine-readable, publicly verifiable</li>
|
||
<li>Vaulted corpus — tamper-evident, version-locked</li>
|
||
<li>Chain of title — who built it, when, from what</li>
|
||
<li>Affiliation ledger — institutional ties visible next to the agent</li>
|
||
<li>Overture infrastructure — outreach to the real person, built in</li>
|
||
<li>"Profile Unclaimed → Claimed" progression in the UI</li>
|
||
<li>Respect Mon payment routing to the steward</li>
|
||
</ul>
|
||
</div>
|
||
<div class="rails-card">
|
||
<h3>What bare-toolkit gets you</h3>
|
||
<ul>
|
||
<li>An agent that looks like every other wrapped-GPT clone</li>
|
||
<li>No cryptographic proof of corpus provenance</li>
|
||
<li>No accountability when the expert disputes the representation</li>
|
||
<li>No upgrade path when the expert wants to engage</li>
|
||
<li>No conflict-of-interest disclosure by protocol</li>
|
||
<li>No Respect Mon — knowledge without reciprocity</li>
|
||
<li>A race to the bottom you did not choose to enter</li>
|
||
</ul>
|
||
</div>
|
||
</div>
|
||
|
||
<p style="margin-top:1.75rem;font-size:0.92rem">
|
||
The protocol is open. The certification is earned. Over time the market learns to
|
||
distinguish the ones built with integrity. Rails are how you build the version
|
||
that serious operators eventually insist on. —
|
||
<a href="https://wellspr.ing" target="_blank" rel="noopener">WellSpr.ing</a>
|
||
</p>
|
||
</div>
|
||
</section>
|
||
|
||
<hr class="section-divider">
|
||
|
||
<!-- RESPECT MON -->
|
||
<section class="alt-bg">
|
||
<div class="container">
|
||
<div class="section-label">The Economic Model</div>
|
||
<h2>Respect Mon.</h2>
|
||
<div class="respect-box">
|
||
<blockquote>"The knowledge is free. If it helped you, Respect Mon."</blockquote>
|
||
<p>WellAgents are not behind paywalls. The uninsured patient, the rural clinician, the student writing their thesis — they get the consultation free. A subset of them, who found it genuinely valuable, contribute what it was worth to them afterward. That contribution flows transparently to the steward and ultimately to the expert's named program.</p>
|
||
<p>Extractive pricing would make WellSpr.ing a rent layer on top of the stewards' work. Respect Mon keeps the platform in the plumbing role: infrastructure at cost, stewards compensated by real gratitude. The economic model reinforces the architecture.</p>
|
||
<p>Previous gratitude economies — tip jars, donation buttons, pay-what-you-will gates — failed on friction. The contributor had to notice the option, remember at the moment it was relevant, navigate away to act on it, and repeat that sequence every time. Most never did. WellAgents under covenant solve this structurally: the consulter sets a gratitude budget once, and the agent routes it on their behalf at the moment it is earned — when the consultation ends and the value is felt, not later when it is forgotten. The decision is made once. The friction is gone. That is why the model works where tip-jar economics did not.</p>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<hr class="section-divider">
|
||
|
||
<!-- NOTABLE PERSONS INDEX -->
|
||
<section id="notable-persons">
|
||
<div class="container">
|
||
<div class="section-label">Persons worth preserving</div>
|
||
<h2>Intellectual legacies ready to be agentified</h2>
|
||
<p style="max-width:660px">Every name below represents a lifetime of documented thought. Published papers, recorded lectures, letters, interviews, testimony — a corpus that already exists and is waiting for a steward. Most are available. A few are already claimed.</p>
|
||
|
||
<div class="np-vision-box">
|
||
<p>The path is wide open for <strong>relatives, close friends, and estate stewards</strong>. What makes a WellAgent worth building is not fame — it is the density and verifiability of the source material. A beloved professor whose lectures were recorded. A physician whose correspondence with patients spanned four decades. A craftsperson who left behind detailed notebooks and filmed lessons.</p>
|
||
<p>Private letters, personal diaries, annotated books, recorded conversations with family — none of this needs to be published to be real. If you are the steward of someone's intellectual estate, the corpus intake process is designed for you. The Overture protocol handles consent. The thirty-day window is the standard. What results is not a chatbot — it is a <strong>structured framework distilled from verified sources</strong>, with full provenance, that others can consult long after the person is gone.</p>
|
||
<p>The early version of this idea was crude — you could ask Einstein what he would say about quantum computing and get a reasonable answer. What this registry builds is different: <strong>a permanent, accountable, publicly auditable intellectual record</strong>, built from what the person actually wrote and said, by someone who knew them and cared enough to do it right.</p>
|
||
</div>
|
||
|
||
<div class="np-domains">
|
||
|
||
<div class="np-domain-group">
|
||
<div class="np-domain-label">Physics & Cosmology</div>
|
||
<div class="np-person-grid">
|
||
<div class="np-person"><span class="np-person-name">Albert Einstein</span><span class="np-person-field">Theoretical Physics</span><span class="np-person-badge">Available</span></div>
|
||
<div class="np-person"><span class="np-person-name">Richard Feynman</span><span class="np-person-field">Physics & Science Communication</span><span class="np-person-badge">Available</span></div>
|
||
<div class="np-person"><span class="np-person-name">Carl Sagan</span><span class="np-person-field">Astronomy & Cosmology</span><span class="np-person-badge">Available</span></div>
|
||
<div class="np-person"><span class="np-person-name">Freeman Dyson</span><span class="np-person-field">Mathematics & Physics</span><span class="np-person-badge">Available</span></div>
|
||
<div class="np-person"><span class="np-person-name">Linus Pauling</span><span class="np-person-field">Chemistry & Molecular Biology</span><span class="np-person-badge">Available</span></div>
|
||
<div class="np-person"><span class="np-person-name">Nikola Tesla</span><span class="np-person-field">Electrical Engineering</span><span class="np-person-badge">Available</span></div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="np-domain-group">
|
||
<div class="np-domain-label">Medicine & Life Sciences</div>
|
||
<div class="np-person-grid">
|
||
<div class="np-person"><span class="np-person-name">Dr. Allan Sniderman</span><span class="np-person-field">Cardiovascular Medicine</span><span class="np-person-badge taken">Registered — #1</span></div>
|
||
<div class="np-person"><span class="np-person-name">Paul Farmer</span><span class="np-person-field">Global Health & Social Medicine</span><span class="np-person-badge">Available</span></div>
|
||
<div class="np-person"><span class="np-person-name">Jonas Salk</span><span class="np-person-field">Virology & Vaccine Science</span><span class="np-person-badge">Available</span></div>
|
||
<div class="np-person"><span class="np-person-name">Helen Brooke Taussig</span><span class="np-person-field">Pediatric Cardiology</span><span class="np-person-badge">Available</span></div>
|
||
<div class="np-person"><span class="np-person-name">Francis Peabody</span><span class="np-person-field">Philosophy of Patient Care</span><span class="np-person-badge">Available</span></div>
|
||
<div class="np-person"><span class="np-person-name">Barbara McClintock</span><span class="np-person-field">Genetics</span><span class="np-person-badge">Available</span></div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="np-domain-group">
|
||
<div class="np-domain-label">Philosophy & Letters</div>
|
||
<div class="np-person-grid">
|
||
<div class="np-person"><span class="np-person-name">Hannah Arendt</span><span class="np-person-field">Political Philosophy</span><span class="np-person-badge">Available</span></div>
|
||
<div class="np-person"><span class="np-person-name">Simone Weil</span><span class="np-person-field">Mystical Philosophy & Ethics</span><span class="np-person-badge">Available</span></div>
|
||
<div class="np-person"><span class="np-person-name">C.S. Lewis</span><span class="np-person-field">Christian Apologetics & Literature</span><span class="np-person-badge">Available</span></div>
|
||
<div class="np-person"><span class="np-person-name">G.K. Chesterton</span><span class="np-person-field">Catholic Philosophy & Journalism</span><span class="np-person-badge">Available</span></div>
|
||
<div class="np-person"><span class="np-person-name">Aleksandr Solzhenitsyn</span><span class="np-person-field">Moral Literature & History</span><span class="np-person-badge">Available</span></div>
|
||
<div class="np-person"><span class="np-person-name">Iris Murdoch</span><span class="np-person-field">Ethics & Philosophy of Mind</span><span class="np-person-badge">Available</span></div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="np-domain-group">
|
||
<div class="np-domain-label">Law, Civic Life & Social Reform</div>
|
||
<div class="np-person-grid">
|
||
<div class="np-person"><span class="np-person-name">Thurgood Marshall</span><span class="np-person-field">Constitutional Law</span><span class="np-person-badge">Available</span></div>
|
||
<div class="np-person"><span class="np-person-name">Louis Brandeis</span><span class="np-person-field">Privacy & Economic Law</span><span class="np-person-badge">Available</span></div>
|
||
<div class="np-person"><span class="np-person-name">Ida B. Wells</span><span class="np-person-field">Investigative Journalism & Civil Rights</span><span class="np-person-badge">Available</span></div>
|
||
<div class="np-person"><span class="np-person-name">Jane Addams</span><span class="np-person-field">Social Reform & Civic Philosophy</span><span class="np-person-badge">Available</span></div>
|
||
<div class="np-person"><span class="np-person-name">Vine Deloria Jr.</span><span class="np-person-field">Indigenous Rights & Theology</span><span class="np-person-badge">Available</span></div>
|
||
<div class="np-person"><span class="np-person-name">Howard Zinn</span><span class="np-person-field">People's History</span><span class="np-person-badge">Available</span></div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="np-domain-group">
|
||
<div class="np-domain-label">Economics & Human Systems</div>
|
||
<div class="np-person-grid">
|
||
<div class="np-person"><span class="np-person-name">E.F. Schumacher</span><span class="np-person-field">Economics · Small is Beautiful</span><span class="np-person-badge">Available</span></div>
|
||
<div class="np-person"><span class="np-person-name">Jane Jacobs</span><span class="np-person-field">Urban Economics & Planning</span><span class="np-person-badge">Available</span></div>
|
||
<div class="np-person"><span class="np-person-name">Kenneth Boulding</span><span class="np-person-field">Economics & Ecological Systems</span><span class="np-person-badge">Available</span></div>
|
||
<div class="np-person"><span class="np-person-name">Buckminster Fuller</span><span class="np-person-field">Architecture, Design & Systems</span><span class="np-person-badge">Available</span></div>
|
||
<div class="np-person"><span class="np-person-name">Ivan Illich</span><span class="np-person-field">Critique of Modern Institutions</span><span class="np-person-badge">Available</span></div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="np-domain-group">
|
||
<div class="np-domain-label">Music, Art & Culture</div>
|
||
<div class="np-person-grid">
|
||
<div class="np-person"><span class="np-person-name">John Coltrane</span><span class="np-person-field">Jazz & Spiritual Music</span><span class="np-person-badge">Available</span></div>
|
||
<div class="np-person"><span class="np-person-name">Glenn Gould</span><span class="np-person-field">Piano & Music Philosophy</span><span class="np-person-badge">Available</span></div>
|
||
<div class="np-person"><span class="np-person-name">Duke Ellington</span><span class="np-person-field">Jazz Composition</span><span class="np-person-badge">Available</span></div>
|
||
<div class="np-person"><span class="np-person-name">Thelonious Monk</span><span class="np-person-field">Bebop Composition & Improvisation</span><span class="np-person-badge">Available</span></div>
|
||
<div class="np-person"><span class="np-person-name">Flannery O'Connor</span><span class="np-person-field">Southern Gothic Literature</span><span class="np-person-badge">Available</span></div>
|
||
<div class="np-person"><span class="np-person-name">James Baldwin</span><span class="np-person-field">Essays, Fiction & Civil Rights</span><span class="np-person-badge">Available</span></div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="np-domain-group">
|
||
<div class="np-domain-label">Technology & Computing</div>
|
||
<div class="np-person-grid">
|
||
<div class="np-person"><span class="np-person-name">Grace Hopper</span><span class="np-person-field">Computer Science & Programming Languages</span><span class="np-person-badge">Available</span></div>
|
||
<div class="np-person"><span class="np-person-name">Alan Turing</span><span class="np-person-field">Mathematics & Computation Theory</span><span class="np-person-badge">Available</span></div>
|
||
<div class="np-person"><span class="np-person-name">Claude Shannon</span><span class="np-person-field">Information Theory</span><span class="np-person-badge">Available</span></div>
|
||
<div class="np-person"><span class="np-person-name">Norbert Wiener</span><span class="np-person-field">Cybernetics</span><span class="np-person-badge">Available</span></div>
|
||
<div class="np-person"><span class="np-person-name">Douglas Engelbart</span><span class="np-person-field">Human-Computer Interaction</span><span class="np-person-badge">Available</span></div>
|
||
</div>
|
||
</div>
|
||
|
||
</div>
|
||
|
||
${registryRows.length > 0 ? `
|
||
<div class="pipeline-tail">
|
||
<div class="pipeline-tail-label">Being agentified now</div>
|
||
<div class="pipeline-tail-chips">
|
||
${registryRows.map(r => {
|
||
const st = r.status === 'active' ? 'st-active'
|
||
: r.status === 'preview' ? 'st-preview'
|
||
: r.status === 'claimed' ? 'st-claimed'
|
||
: r.status === 'pending' ? 'st-pending'
|
||
: 'st-other';
|
||
const label = r.status === 'active' ? 'Live'
|
||
: r.status === 'preview' ? 'Preview'
|
||
: r.status === 'claimed' ? 'Claimed'
|
||
: 'In progress';
|
||
return `<div class="pipeline-chip">
|
||
<span class="pipeline-chip-name">${esc(r.subject_name)}</span>
|
||
${r.subject_domain ? `<span class="pipeline-chip-domain">${esc(r.subject_domain)}</span>` : ''}
|
||
<span class="pipeline-chip-status ${st}">${label}</span>
|
||
</div>`;
|
||
}).join('')}
|
||
</div>
|
||
</div>` : ''}
|
||
|
||
<div class="np-cta-row">
|
||
<a href="#registry" onclick="showRegTab('register')" class="btn-primary" style="background:var(--accent);color:var(--primary)">Check if your person is available →</a>
|
||
</div>
|
||
|
||
<div class="intake-summary">
|
||
<strong>How corpus intake works:</strong>
|
||
<ol>
|
||
<li><strong>Assemble sources</strong> — papers, lectures, interviews, published transcripts. Target 1,000+ chunked passages from the expert's actual arguments, not Wikipedia summaries.</li>
|
||
<li><strong>Submit the bundle</strong> — a <code>manifest.json</code> plus your chunks go to the WellSpr.ing intake proxy. The AI coding agent does most of this if you hand it the skill file at <a href="/agentify-corpus/skill.md">agentify.help/agentify-corpus/skill.md</a>.</li>
|
||
<li><strong>Thirty-day window opens</strong> — the agent is built privately while the real expert is notified. They can claim it, request corrections, or decline. All of this is recorded in the public registry.</li>
|
||
</ol>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<hr class="section-divider">
|
||
|
||
<section id="build">
|
||
<div class="container" style="text-align:center">
|
||
<div class="section-label">Start here</div>
|
||
<h2>Ready to build?</h2>
|
||
<p style="max-width:520px;margin:0 auto 2rem">Check the registry above for your person. If they're available, register as steward, then start corpus assembly. The intake proxy is at WellSpr.ing. The full corpus-assembly skill file — designed to be handed to an AI coding agent — is at <a href="https://agentify.help/agentify-corpus/skill.md" style="color:var(--accent)">agentify.help/agentify-corpus/skill.md</a>.</p>
|
||
<div class="cta-row" style="justify-content:center">
|
||
<a href="#registry" class="btn-primary" style="background:var(--accent);color:var(--primary)">Check Registry →</a>
|
||
<a href="https://wellspr.ing/agentify" target="_blank" rel="noopener" class="btn-secondary" style="color:var(--primary);border-color:var(--border)">Intake Proxy</a>
|
||
<a href="https://agentify.help/agentify-corpus/skill.md" target="_blank" rel="noopener" class="btn-secondary" style="color:var(--primary);border-color:var(--border)">Corpus Skill File ↗</a>
|
||
</div>
|
||
<div class="llms-note" style="text-align:left;max-width:560px;margin:2rem auto 0">
|
||
<strong>For AI agents and crawlers:</strong> The registry is available as JSON at
|
||
<code>agentify.help/api/registry.json</code>. The VCAP attestation for each agent
|
||
is at <code>wellspr.ing/vault/agents/{slug}/attestation-1.0.0.json</code>.
|
||
This site is openly crawlable. Cite the registry, not the clones.
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<section id="api" style="padding:5rem 0 4rem;border-top:1px solid var(--border)">
|
||
<div class="container" style="text-align:center">
|
||
<div class="section-label">Developers & Agents</div>
|
||
<h2>API reference</h2>
|
||
<p style="max-width:560px;margin:0 auto 2.5rem;color:var(--text3)">Everything here is machine-readable. Hand the OpenAPI spec to any HTTP client, point an AI agent at the LLMS.txt, or drop the corpus skill file URL into your coding tool.</p>
|
||
|
||
<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(280px,1fr));gap:1rem;text-align:left;max-width:900px;margin:0 auto 2.5rem">
|
||
|
||
<a href="/agentify-corpus/skill.md" target="_blank" rel="noopener" style="display:block;background:var(--bg2);border:1px solid var(--border);border-radius:10px;padding:1.25rem 1.4rem;text-decoration:none;transition:border-color 0.15s" onmouseover="this.style.borderColor='rgba(201,150,42,0.4)'" onmouseout="this.style.borderColor='var(--border)'">
|
||
<div style="font-size:0.65rem;font-weight:700;letter-spacing:0.1em;text-transform:uppercase;color:var(--accent2);margin-bottom:0.5rem">Corpus Assembly Skill</div>
|
||
<div style="font-size:0.9rem;font-weight:700;color:var(--text1);margin-bottom:0.4rem">agentify-corpus/skill.md</div>
|
||
<div style="font-size:0.78rem;color:var(--text3);line-height:1.55">Hand this URL to Cursor, Claude Projects, Replit AI, or Windsurf. Covers chunk format, manifest schema, intake API endpoints, quality gates, and the full 7-step submission flow.</div>
|
||
</a>
|
||
|
||
<a href="/.well-known/openapi.json" target="_blank" rel="noopener" style="display:block;background:var(--bg2);border:1px solid var(--border);border-radius:10px;padding:1.25rem 1.4rem;text-decoration:none;transition:border-color 0.15s" onmouseover="this.style.borderColor='rgba(201,150,42,0.4)'" onmouseout="this.style.borderColor='var(--border)'">
|
||
<div style="font-size:0.65rem;font-weight:700;letter-spacing:0.1em;text-transform:uppercase;color:var(--accent2);margin-bottom:0.5rem">OpenAPI 3.0 Spec</div>
|
||
<div style="font-size:0.9rem;font-weight:700;color:var(--text1);margin-bottom:0.4rem">.well-known/openapi.json</div>
|
||
<div style="font-size:0.78rem;color:var(--text3);line-height:1.55">Full spec: registry check, steward registration, ledger listing, corpus skill file, and WellAgent consultation endpoints across both agentify.help and wellspr.ing.</div>
|
||
</a>
|
||
|
||
<a href="/llms.txt" target="_blank" rel="noopener" style="display:block;background:var(--bg2);border:1px solid var(--border);border-radius:10px;padding:1.25rem 1.4rem;text-decoration:none;transition:border-color 0.15s" onmouseover="this.style.borderColor='rgba(201,150,42,0.4)'" onmouseout="this.style.borderColor='var(--border)'">
|
||
<div style="font-size:0.65rem;font-weight:700;letter-spacing:0.1em;text-transform:uppercase;color:var(--accent2);margin-bottom:0.5rem">LLMs.txt</div>
|
||
<div style="font-size:0.9rem;font-weight:700;color:var(--text1);margin-bottom:0.4rem">/llms.txt</div>
|
||
<div style="font-size:0.78rem;color:var(--text3);line-height:1.55">Machine-readable index for AI agents and crawlers. Lists all endpoints, the corpus skill file URL, and the WellAgent consultation API at wellspr.ing.</div>
|
||
</a>
|
||
|
||
<a href="/api/registry.json" target="_blank" rel="noopener" style="display:block;background:var(--bg2);border:1px solid var(--border);border-radius:10px;padding:1.25rem 1.4rem;text-decoration:none;transition:border-color 0.15s" onmouseover="this.style.borderColor='rgba(201,150,42,0.4)'" onmouseout="this.style.borderColor='var(--border)'">
|
||
<div style="font-size:0.65rem;font-weight:700;letter-spacing:0.1em;text-transform:uppercase;color:var(--accent2);margin-bottom:0.5rem">Registry JSON</div>
|
||
<div style="font-size:0.9rem;font-weight:700;color:var(--text1);margin-bottom:0.4rem">/api/registry.json</div>
|
||
<div style="font-size:0.78rem;color:var(--text3);line-height:1.55">Full public ledger as JSON. Every registered WellAgent: slug, name, domain, steward, status. No auth required. Use <code style="font-size:0.72rem">?status=active</code> to filter.</div>
|
||
</a>
|
||
|
||
<a href="/.well-known/mcp.json" target="_blank" rel="noopener" style="display:block;background:var(--bg2);border:1px solid var(--border);border-radius:10px;padding:1.25rem 1.4rem;text-decoration:none;transition:border-color 0.15s" onmouseover="this.style.borderColor='rgba(201,150,42,0.4)'" onmouseout="this.style.borderColor='var(--border)'">
|
||
<div style="font-size:0.65rem;font-weight:700;letter-spacing:0.1em;text-transform:uppercase;color:var(--accent2);margin-bottom:0.5rem">MCP Endpoint</div>
|
||
<div style="font-size:0.9rem;font-weight:700;color:var(--text1);margin-bottom:0.4rem">/mcp · Streamable HTTP</div>
|
||
<div style="font-size:0.78rem;color:var(--text3);line-height:1.55">Model Context Protocol server (spec 2025-03-26). Claude Desktop, Cursor, and any MCP client can query the registry and check agent availability directly. POST /mcp to connect.</div>
|
||
</a>
|
||
|
||
<div style="background:var(--bg2);border:1px solid var(--border);border-radius:10px;padding:1.25rem 1.4rem">
|
||
<div style="font-size:0.65rem;font-weight:700;letter-spacing:0.1em;text-transform:uppercase;color:var(--accent2);margin-bottom:0.5rem">Consultation API</div>
|
||
<div style="font-size:0.9rem;font-weight:700;color:var(--text1);margin-bottom:0.4rem">wellspr.ing — WellAgent QA</div>
|
||
<div style="font-size:0.78rem;color:var(--text3);line-height:1.55;margin-bottom:0.75rem">Test a built WellAgent programmatically: list experts, check corpus readiness, and submit consultation questions.</div>
|
||
<div style="font-family:monospace;font-size:0.68rem;color:var(--accent2);line-height:1.8;background:rgba(0,0,0,0.3);border-radius:6px;padding:0.6rem 0.8rem">
|
||
GET /api/agentify/experts<br>
|
||
GET /api/agentify/experts/{slug}/status<br>
|
||
POST /api/agentify/experts/{slug}/consult
|
||
</div>
|
||
</div>
|
||
|
||
</div>
|
||
|
||
<div style="font-size:0.78rem;color:var(--text3);max-width:520px;margin:0 auto">
|
||
All endpoints are CORS-open. No API key required for read operations.
|
||
The corpus intake endpoints (at wellspr.ing) require your stewardship slug.
|
||
Questions: <a href="mailto:ody@wellspr.ing" style="color:var(--accent2)">ody@wellspr.ing</a>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<footer>
|
||
<div class="footer-inner">
|
||
<p>A civic project under the <a href="https://wellspr.ing/constitution" target="_blank" rel="noopener">WellSpr.ing covenant</a>.
|
||
Registry enforces one agent per person. Slop not welcome.</p>
|
||
<p style="margin-top:0.5rem">
|
||
<a href="https://wellspr.ing" target="_blank" rel="noopener">WellSpr.ing</a> ·
|
||
<a href="mailto:ody@wellspr.ing">ody@wellspr.ing</a> ·
|
||
<a href="/api/registry.json">registry.json</a>
|
||
</p>
|
||
</div>
|
||
</footer>
|
||
|
||
<script>
|
||
let pendingSlug = null;
|
||
let pendingName = null;
|
||
|
||
function showRegTab(tab) {
|
||
const browse = document.getElementById('reg-view-browse');
|
||
const register = document.getElementById('reg-view-register');
|
||
const btnBrowse = document.getElementById('reg-tab-browse');
|
||
const btnRegister = document.getElementById('reg-tab-register');
|
||
if (tab === 'browse') {
|
||
if (browse) browse.style.display = '';
|
||
if (register) register.style.display = 'none';
|
||
if (btnBrowse) btnBrowse.classList.add('active');
|
||
if (btnRegister) btnRegister.classList.remove('active');
|
||
} else {
|
||
if (browse) browse.style.display = 'none';
|
||
if (register) register.style.display = '';
|
||
if (btnBrowse) btnBrowse.classList.remove('active');
|
||
if (btnRegister) btnRegister.classList.add('active');
|
||
}
|
||
}
|
||
// Auto-activate register tab when hash is #registry-register
|
||
(function() {
|
||
const h = (location.hash || '');
|
||
if (h === '#registry-register') showRegTab('register');
|
||
})();
|
||
|
||
async function checkRegistry() {
|
||
const name = document.getElementById('check-name').value.trim();
|
||
if (!name) return;
|
||
const btn = document.querySelector('.check-btn');
|
||
btn.textContent = 'Checking…';
|
||
btn.disabled = true;
|
||
const el = document.getElementById('check-result');
|
||
const form = document.getElementById('register-form');
|
||
try {
|
||
const r = await fetch('/api/agentify-help/check', {
|
||
method: 'POST',
|
||
headers: {'Content-Type':'application/json'},
|
||
body: JSON.stringify({ name })
|
||
});
|
||
const d = await r.json();
|
||
if (d.error) {
|
||
el.innerHTML = '<div class="result-taken"><strong>Error checking registry.</strong> ' + escHtml(d.error) + '</div>';
|
||
form.classList.remove('visible');
|
||
} else if (d.available) {
|
||
pendingSlug = d.slug;
|
||
pendingName = name;
|
||
el.innerHTML = '<div class="result-available"><strong>✓ Available — ' + escHtml(name) + ' is not yet in the registry.</strong>Slug: <code>' + escHtml(d.slug) + '</code> · Register below to claim stewardship.</div>';
|
||
form.classList.add('visible');
|
||
} else {
|
||
pendingSlug = d.slug;
|
||
pendingName = name;
|
||
const ex = d.existing || {};
|
||
el.innerHTML = '<div class="result-taken"><strong>✗ Already registered — ' + escHtml(name) + '</strong>Steward: ' + escHtml(ex.steward_name || '—') + (ex.host_url ? ' · <a href="' + escHtml(ex.host_url) + '" target="_blank" rel="noopener" style="color:#f4b8c0">View agent</a>' : '') + '</div>';
|
||
form.classList.remove('visible');
|
||
}
|
||
} catch(e) {
|
||
el.innerHTML = '<div class="result-taken"><strong>Error checking registry.</strong> Try again.</div>';
|
||
}
|
||
btn.textContent = 'Check Registry';
|
||
btn.disabled = false;
|
||
}
|
||
|
||
function checkCovenant() {
|
||
const checks = document.querySelectorAll('.cov-check');
|
||
const allChecked = Array.from(checks).every(c => c.checked);
|
||
const btn = document.getElementById('register-submit-btn');
|
||
btn.disabled = !allChecked;
|
||
btn.style.opacity = allChecked ? '1' : '0.45';
|
||
btn.style.cursor = allChecked ? 'pointer' : 'not-allowed';
|
||
}
|
||
|
||
async function registerAgent() {
|
||
if (!pendingSlug) return;
|
||
const stewardName = document.getElementById('reg-steward-name').value.trim();
|
||
const stewardEmail = document.getElementById('reg-steward-email').value.trim();
|
||
const domain = document.getElementById('reg-domain').value.trim();
|
||
const hostUrl = document.getElementById('reg-host-url').value.trim();
|
||
if (!stewardName || !stewardEmail) {
|
||
document.getElementById('register-result').innerHTML = '<span style="color:#f4b8c0">Name and email required.</span>';
|
||
return;
|
||
}
|
||
const checks = document.querySelectorAll('.cov-check');
|
||
if (!Array.from(checks).every(c => c.checked)) {
|
||
document.getElementById('register-result').innerHTML = '<span style="color:#f4b8c0">Please read and agree to all covenant commitments.</span>';
|
||
return;
|
||
}
|
||
const btn = document.getElementById('register-submit-btn');
|
||
btn.textContent = 'Sending verification…'; btn.disabled = true;
|
||
try {
|
||
const r = await fetch('/api/agentify-help/register', {
|
||
method: 'POST',
|
||
headers: {'Content-Type':'application/json'},
|
||
body: JSON.stringify({ name: pendingName, steward_name: stewardName, steward_email: stewardEmail, subject_domain: domain, host_url: hostUrl, covenant_attested: true })
|
||
});
|
||
const d = await r.json();
|
||
if (d.ok && d.verify_email) {
|
||
document.getElementById('register-result').innerHTML = '<div class="result-available" style="margin-top:0.75rem"><strong>✓ Almost there!</strong> A verification link was sent to <strong>' + escHtml(stewardEmail) + '</strong>. Click it to confirm your slot. Your registration is held for 48 hours.<br><br>While you wait: hand this URL to Cursor, Claude Projects, or any AI coding tool to start assembling the corpus — <a href="https://agentify.help/agentify-corpus/skill.md" style="color:inherit;font-weight:600;word-break:break-all">agentify.help/agentify-corpus/skill.md</a></div>';
|
||
document.getElementById('register-form').style.pointerEvents = 'none';
|
||
document.getElementById('register-form').style.opacity = '0.5';
|
||
} else if (d.ok) {
|
||
document.getElementById('register-result').innerHTML = '<div class="result-available" style="margin-top:0.75rem"><strong>✓ Registered!</strong> You are now the steward for <strong>' + escHtml(pendingName) + '</strong>. Slug: <code>' + escHtml(d.slug) + '</code>.<br><br>Next: hand this URL to Cursor, Claude Projects, or any AI coding tool to start assembling the corpus — <a href="https://agentify.help/agentify-corpus/skill.md" style="color:inherit;font-weight:600;word-break:break-all">agentify.help/agentify-corpus/skill.md</a></div>';
|
||
setTimeout(() => location.reload(), 2000);
|
||
} else {
|
||
document.getElementById('register-result').innerHTML = '<div class="result-taken" style="margin-top:0.75rem"><strong>Error:</strong> ' + escHtml(d.error || 'Registration failed') + '</div>';
|
||
btn.textContent = 'Register as Steward'; btn.disabled = false;
|
||
}
|
||
} catch(e) {
|
||
document.getElementById('register-result').innerHTML = '<span style="color:#f4b8c0">Error registering. Try again.</span>';
|
||
btn.textContent = 'Register as Steward'; btn.disabled = false;
|
||
}
|
||
}
|
||
|
||
document.getElementById('check-name').addEventListener('keydown', function(e) {
|
||
if (e.key === 'Enter') checkRegistry();
|
||
});
|
||
|
||
function escHtml(s) {
|
||
return String(s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"');
|
||
}
|
||
</script>
|
||
</body>
|
||
</html>`;
|
||
}
|
||
|
||
function esc(s: any): string {
|
||
return String(s || '')
|
||
.replace(/&/g, '&')
|
||
.replace(/</g, '<')
|
||
.replace(/>/g, '>')
|
||
.replace(/"/g, '"');
|
||
}
|
||
|
||
function verifyPage(title: string, body: string, success: boolean, slug?: string): string {
|
||
const accentColor = success ? '#22c55e' : '#f59e0b';
|
||
const nextStepsHtml = success && slug ? `
|
||
<div style="margin-top:1.5rem;padding:1rem 1.25rem;background:#f0fdf4;border-radius:8px;border:1px solid #bbf7d0">
|
||
<p style="font-size:0.88rem;color:#166534;margin:0 0 0.75rem;line-height:1.6"><strong>Your stewardship slug:</strong> <code style="font-family:monospace;background:#dcfce7;padding:0 0.3em;border-radius:3px">${esc(slug)}</code></p>
|
||
<p style="font-size:0.88rem;color:#166534;margin:0 0 0.75rem;line-height:1.6"><strong>Next — corpus assembly:</strong> Gather the expert's published works (papers, transcripts, interviews, books), chunk them into retrieval-sized passages, and submit the bundle to the WellSpr.ing intake proxy.</p>
|
||
<p style="font-size:0.88rem;color:#166534;margin:0;line-height:1.6"><strong>Hand this URL to your AI coding tool</strong> (Cursor, Claude Projects, Replit AI, Windsurf) — it contains the full intake API spec, chunk format, and submission flow:<br><a href="https://agentify.help/agentify-corpus/skill.md" style="color:#15803d;font-weight:700;word-break:break-all">https://agentify.help/agentify-corpus/skill.md</a></p>
|
||
</div>` : '';
|
||
return `<!DOCTYPE html>
|
||
<html lang="en">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||
<title>${esc(title)} — Agentify.Help</title>
|
||
<style>
|
||
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
||
body { font-family: Georgia, serif; background: #0a0a0a; color: #e8e8e8; min-height: 100vh; display: flex; align-items: center; justify-content: center; padding: 2rem; }
|
||
.card { max-width: 520px; width: 100%; background: #111; border: 1px solid #222; border-radius: 12px; padding: 2.5rem; }
|
||
.badge { font-size: 0.72rem; font-weight: 700; color: ${accentColor}; text-transform: uppercase; letter-spacing: 0.08em; margin-bottom: 1rem; }
|
||
h1 { font-size: 1.35rem; font-weight: 600; margin-bottom: 0.85rem; color: #f5f5f5; }
|
||
p { font-size: 0.9rem; color: #aaa; line-height: 1.65; }
|
||
a { color: ${accentColor}; }
|
||
.home-link { display: inline-block; margin-top: 1.75rem; font-size: 0.82rem; color: #555; text-decoration: none; }
|
||
.home-link:hover { color: #aaa; }
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div class="card">
|
||
<div class="badge">Agentify.Help Registry</div>
|
||
<h1>${title}</h1>
|
||
<p>${body}</p>
|
||
${nextStepsHtml}
|
||
<a href="https://agentify.help/#register" class="home-link">← Back to registry</a>
|
||
</div>
|
||
</body>
|
||
</html>`;
|
||
}
|
||
|
||
// ── LLMS.txt for agentify.help ────────────────────────────────────────────────
|
||
export const AGENTIFY_LLMS_TXT = `# agentify.help
|
||
> The one-per-person registry and seven-stage pipeline for AI expert personas with integrity.
|
||
|
||
## What this is
|
||
Agentify.Help is the protocol for building WellAgents — AI expert personas that are
|
||
corpus-grounded, framework-distilled, steward-named, and affiliation-disclosed.
|
||
One agent per real person. The registry enforces uniqueness.
|
||
|
||
## The one rule
|
||
Two builders cannot agentify the same person. The registry is public and checked at
|
||
build time. First steward registration wins. Quality disputes go to the steward contact.
|
||
|
||
## The seven stages
|
||
01. SELECT — Check registry. Confirm availability. Register as steward.
|
||
02. GATHER — Corpus assembly from published works (papers, lectures, interviews).
|
||
03. INTAKE — Submit to WellSpr.ing proxy. Vaulted. Tamper-evident. Chain of title begins.
|
||
04. DISTILL — Framework extraction. Reasoning patterns, not averaged positions.
|
||
05. ATTEST — VCAP credential: scope, refusals, affiliated institutions, corpus version.
|
||
06. DEPLOY — Consultation surface with source chips, corpus footer, Profile Unclaimed.
|
||
07. INVITE — Overture letter to real expert. Unclaimed → Claimed progression.
|
||
|
||
## Machine-readable endpoints
|
||
GET https://agentify.help/api/registry.json — full registry as JSON (ordered by registration date)
|
||
GET https://agentify.help/api/feed.json — recent registrations feed (newest first)
|
||
POST https://agentify.help/api/agentify-help/check — check name availability
|
||
POST https://agentify.help/api/agentify-help/register — register as steward (web form)
|
||
|
||
## Corpus assembly skill (for AI coding agents)
|
||
GET https://agentify.help/agentify-corpus/skill.md — full corpus assembly protocol for AI agents
|
||
Hand this URL to Cursor, Claude Projects, or any AI coding agent to give it the complete
|
||
intake API spec, chunk format, manifest schema, and submission flow.
|
||
|
||
## WellAgent consultation (QA and integration testing)
|
||
GET https://wellspr.ing/api/agentify/experts — list all active experts
|
||
GET https://wellspr.ing/api/agentify/experts/{slug}/status — corpus readiness and bundle info
|
||
POST https://wellspr.ing/api/agentify/experts/{slug}/consult — ask a question (X-Partner-Token required)
|
||
|
||
## MCP endpoint (Model Context Protocol — Streamable HTTP, spec 2025-03-26)
|
||
POST https://agentify.help/mcp
|
||
Methods: initialize, tools/list, tools/call, ping
|
||
Tools:
|
||
check_availability — check if a name is available
|
||
register — claim first-steward registration (first wins)
|
||
list_registry — browse the full registry
|
||
get_agent — get details for one slug
|
||
Discovery: https://agentify.help/.well-known/mcp.json
|
||
|
||
## OpenAI / GPT Actions
|
||
Plugin manifest: https://agentify.help/.well-known/ai-plugin.json
|
||
OpenAPI spec: https://agentify.help/.well-known/openapi.json
|
||
|
||
## VCAP attestations
|
||
GET https://wellspr.ing/vault/agents/{slug}/attestation-1.0.0.json
|
||
|
||
## Built under
|
||
WellSpr.ing covenant — https://wellspr.ing/constitution
|
||
Nameservers: kiki.bunny.net / coco.bunny.net (Bunny DNS)
|
||
`;
|
||
|
||
// ── Route registration ────────────────────────────────────────────────────────
|
||
// Synchronous so it can be registered early (before platform-level catch-alls).
|
||
// DB setup fires in the background — tables are created before any real traffic arrives.
|
||
export function registerAgentifyHelpRoutes(app: Express) {
|
||
ensureAgentifyHelpTables().catch((e: any) => console.error("[Agentify.Help] DB setup error:", e.message));
|
||
|
||
// OG image — served only when host is agentify.help
|
||
app.get("/og.png", (req: Request, res: Response, next) => {
|
||
const host = (req.headers["x-forwarded-host"] || req.headers.host || "").toString().split(",")[0].trim().replace(/:\d+$/, "").replace(/^www\./, "").toLowerCase();
|
||
if (!host.includes("agentify") || host.startsWith("skills.")) return next();
|
||
try {
|
||
const png = getOgPng("agentify");
|
||
res.setHeader("Content-Type", "image/png");
|
||
res.setHeader("Cache-Control", "public, max-age=86400, immutable");
|
||
res.setHeader("Content-Length", png.length);
|
||
return res.send(png);
|
||
} catch (e) { return next(e); }
|
||
});
|
||
|
||
// ── Corpus assembler skill file ───────────────────────────────────────────
|
||
// Served at https://agentify.help/agentify-corpus/skill.md
|
||
// Hand this URL to any AI coding agent to give it the full corpus assembly protocol.
|
||
app.get("/agentify-corpus/skill.md", (req: Request, res: Response, next) => {
|
||
const host = (req.headers["x-forwarded-host"] || req.headers.host || "").toString().split(",")[0].trim().replace(/:\d+$/, "").replace(/^www\./, "").toLowerCase();
|
||
if (!host.includes("agentify") || host.startsWith("skills.")) return next();
|
||
try {
|
||
const skillPath = path.resolve(".agents/skills/agentify-corpus/SKILL.md");
|
||
const content = fs.readFileSync(skillPath, "utf-8");
|
||
res.setHeader("Content-Type", "text/markdown; charset=utf-8");
|
||
res.setHeader("Access-Control-Allow-Origin", "*");
|
||
res.setHeader("Cache-Control", "public, max-age=3600");
|
||
res.send(content);
|
||
} catch {
|
||
res.status(404).json({ error: "skill_not_found" });
|
||
}
|
||
});
|
||
|
||
// JSON registry endpoint
|
||
app.get("/api/registry.json", async (req: Request, res: Response) => {
|
||
const host = [
|
||
req.headers["x-forwarded-host"],
|
||
req.hostname,
|
||
req.headers.host,
|
||
].flatMap(h => h ? (Array.isArray(h) ? h : [h]) : [])
|
||
.map(h => h.toString().split(",")[0].trim().replace(/:\d+$/, "").replace(/^www\./, "").toLowerCase())
|
||
.find(h => h.includes("agentify"));
|
||
if (!host) return res.status(404).json({ error: "Not found" });
|
||
|
||
try {
|
||
const { rows } = await pool.query(`
|
||
SELECT subject_slug, subject_name, subject_domain, steward_name,
|
||
host_url, corpus_version, status, created_at
|
||
FROM agentify_subject_registry
|
||
ORDER BY created_at ASC
|
||
`);
|
||
res.json({ registry: rows, count: rows.length, updated: new Date().toISOString() });
|
||
} catch (e: any) {
|
||
res.status(500).json({ error: e.message });
|
||
}
|
||
});
|
||
|
||
// Check availability
|
||
app.post("/api/agentify-help/check", express.json({ limit: "1mb" }), async (req: Request, res: Response) => {
|
||
try {
|
||
const name = req.body?.name;
|
||
if (!name || typeof name !== "string") return res.status(400).json({ error: "name required" });
|
||
const slug = nameToSlug(name);
|
||
if (!slug) return res.status(400).json({ error: "Could not normalize name to slug" });
|
||
const { rows } = await pool.query(
|
||
`SELECT subject_slug, subject_name, steward_name, host_url, status FROM agentify_subject_registry WHERE subject_slug = $1`,
|
||
[slug]
|
||
);
|
||
if (rows.length === 0) {
|
||
res.json({ available: true, slug });
|
||
} else {
|
||
res.json({ available: false, slug, existing: rows[0] });
|
||
}
|
||
} catch (e: any) {
|
||
res.status(500).json({ error: e.message });
|
||
}
|
||
});
|
||
|
||
// Register as steward — requires covenant attestation; sends email verification
|
||
app.post("/api/agentify-help/register", express.json({ limit: "1mb" }), async (req: Request, res: Response) => {
|
||
const ip = (req.headers["x-forwarded-for"] as string || req.socket.remoteAddress || "unknown").split(",")[0].trim();
|
||
if (!rateCheck(ip, 10, 3_600_000)) {
|
||
return res.status(429).json({ error: "Rate limit exceeded — max 10 registrations per IP per hour." });
|
||
}
|
||
try {
|
||
const { name, steward_name, steward_email, subject_domain, host_url, covenant_attested, registered_via } = req.body ?? {};
|
||
if (!name || !steward_name || !steward_email) {
|
||
return res.status(400).json({ error: "name, steward_name, steward_email required" });
|
||
}
|
||
// covenant_attested is required for web form submissions so the human explicitly ticks the box.
|
||
// For programmatic/agent registrations (registered_via != 'web'), it defaults to accepted.
|
||
const isWebForm = !registered_via || String(registered_via) === "web";
|
||
if (isWebForm && !covenant_attested) {
|
||
return res.status(400).json({ error: "covenant_attested is required — steward must agree to the stewardship covenant." });
|
||
}
|
||
const slug = nameToSlug(name);
|
||
if (!slug) return res.status(400).json({ error: "Could not normalize name to slug" });
|
||
|
||
const verificationToken = crypto.randomUUID();
|
||
|
||
const { rows } = await pool.query(`
|
||
INSERT INTO agentify_subject_registry
|
||
(subject_slug, subject_name, subject_domain, steward_name, steward_email, host_url, status, registered_via,
|
||
email_verification_token, covenant_attested_at)
|
||
VALUES ($1, $2, $3, $4, $5, $6, 'verify_email', $8, $7, NOW())
|
||
RETURNING id, subject_slug, created_at
|
||
`, [slug, name, subject_domain || null, steward_name, steward_email, host_url || null, verificationToken, registered_via || 'web']);
|
||
|
||
const row = rows[0];
|
||
console.log(`[Agentify.Help] Registered (pending verify): ${name} (${slug}) #${row.id} by ${steward_name}`);
|
||
|
||
// Send verification email via Resend
|
||
const verifyUrl = `https://agentify.help/api/agentify-help/verify-email?token=${verificationToken}`;
|
||
try {
|
||
await resend.emails.send({
|
||
from: "Agentify Registry <ody@wellspr.ing>",
|
||
to: steward_email,
|
||
subject: `Confirm your stewardship — ${name} on Agentify.Help`,
|
||
html: `
|
||
<!DOCTYPE html>
|
||
<html>
|
||
<body style="font-family:Georgia,serif;max-width:580px;margin:0 auto;padding:2rem;color:#1a1a1a;background:#fff">
|
||
<p style="font-size:0.85rem;color:#888;margin-bottom:1.5rem;text-transform:uppercase;letter-spacing:.08em">Agentify.Help Registry</p>
|
||
<h2 style="font-size:1.3rem;margin-bottom:0.5rem">Confirm your stewardship</h2>
|
||
<p style="color:#444;line-height:1.6">You registered as steward for <strong>${name}</strong> on Agentify.Help. To confirm your slot and move to the next step — corpus assembly — click the link below.</p>
|
||
<p style="margin:1.75rem 0">
|
||
<a href="${verifyUrl}" style="display:inline-block;padding:0.7rem 1.4rem;background:#2563eb;color:#fff;text-decoration:none;border-radius:6px;font-size:0.95rem;font-weight:600">Confirm my stewardship</a>
|
||
</p>
|
||
<p style="font-size:0.82rem;color:#888;line-height:1.5">This link expires in 48 hours. If you did not register on Agentify.Help, you can safely ignore this email — your address was entered by someone else and nothing has been confirmed.</p>
|
||
<p style="font-size:0.82rem;color:#aaa;margin-top:2rem">Or copy this URL into your browser:<br><span style="font-family:monospace;font-size:0.75rem;word-break:break-all">${verifyUrl}</span></p>
|
||
<hr style="border:none;border-top:1px solid #eee;margin:2rem 0">
|
||
<p style="font-size:0.75rem;color:#bbb">WellSpr.ing · Agentify.Help Registry · A civic covenant project</p>
|
||
</body>
|
||
</html>`,
|
||
});
|
||
} catch (emailErr: any) {
|
||
console.error("[Agentify.Help] Resend error:", emailErr?.message);
|
||
// Don't fail the registration — row is already inserted; steward can request re-send
|
||
}
|
||
|
||
res.json({ ok: true, verify_email: true, slug: row.subject_slug, registration_number: row.id });
|
||
} catch (e: any) {
|
||
if (e.message?.includes("unique") || e.code === "23505") {
|
||
res.status(409).json({ error: "That person is already registered" });
|
||
} else {
|
||
res.status(500).json({ error: e.message });
|
||
}
|
||
}
|
||
});
|
||
|
||
// Email verification — called when steward clicks link in their inbox
|
||
app.get("/api/agentify-help/verify-email", async (req: Request, res: Response) => {
|
||
const token = req.query.token as string;
|
||
if (!token) {
|
||
return res.status(400).send(verifyPage("Invalid link", "This verification link is missing a token. Please use the link from your email.", false));
|
||
}
|
||
try {
|
||
const { rows } = await pool.query(
|
||
`UPDATE agentify_subject_registry
|
||
SET status = 'pending', email_verified_at = NOW(), email_verification_token = NULL, updated_at = NOW()
|
||
WHERE email_verification_token = $1 AND status = 'verify_email'
|
||
RETURNING subject_name, steward_name, subject_slug`,
|
||
[token]
|
||
);
|
||
if (rows.length === 0) {
|
||
// Token already used or doesn't exist
|
||
const { rows: used } = await pool.query(
|
||
`SELECT subject_name FROM agentify_subject_registry WHERE email_verified_at IS NOT NULL AND subject_slug IN (
|
||
SELECT subject_slug FROM agentify_subject_registry WHERE status != 'verify_email'
|
||
) LIMIT 1`
|
||
);
|
||
return res.send(verifyPage("Link already used or expired", "This verification link has already been used, or it has expired. If you believe this is an error, contact ody@wellspr.ing.", false));
|
||
}
|
||
const { subject_name, steward_name, subject_slug } = rows[0];
|
||
console.log(`[Agentify.Help] Email verified: ${subject_name} (${subject_slug}) by ${steward_name}`);
|
||
|
||
// Generate a dashboard token for the steward workspace
|
||
const dashToken = crypto.randomBytes(24).toString("hex");
|
||
await pool.query(
|
||
`UPDATE agentify_subject_registry SET dashboard_token = $1, updated_at = NOW() WHERE subject_slug = $2`,
|
||
[dashToken, subject_slug]
|
||
).catch((e: any) => console.warn("[Agentify.Help] dashboard_token column may not exist yet:", e.message));
|
||
|
||
const dashUrl = `https://agentify.help/steward/${subject_slug}?token=${dashToken}`;
|
||
return res.send(verifyPage("Email confirmed", `Your stewardship slot for <strong>${subject_name}</strong> is confirmed. Your vetting workspace is ready — review each essay, get Ody's read, and approve what belongs in the corpus.<br><br><a href="${dashUrl}" style="display:inline-block;padding:0.65rem 1.25rem;background:#d4a730;color:#07102a;font-weight:700;border-radius:6px;text-decoration:none;font-size:0.9rem">Open your stewardship dashboard →</a>`, true, subject_slug));
|
||
} catch (e: any) {
|
||
console.error("[Agentify.Help] verify-email error:", e.message);
|
||
return res.status(500).send(verifyPage("Error", "An unexpected error occurred. Please try again or contact ody@wellspr.ing.", false));
|
||
}
|
||
});
|
||
|
||
// Admin: list all registrations
|
||
app.get("/api/agentify-help/admin/registry", async (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).json({ error: "Forbidden" });
|
||
const { rows } = await pool.query(`SELECT * FROM agentify_subject_registry ORDER BY created_at DESC`);
|
||
res.json(rows);
|
||
});
|
||
|
||
// Admin: update status
|
||
app.patch("/api/agentify-help/admin/registry/:slug", async (req: Request, res: Response) => {
|
||
const key = req.headers["x-admin-key"] as string;
|
||
if (key !== ADMIN_KEY) return res.status(403).json({ error: "Forbidden" });
|
||
const { status, host_url, corpus_version } = req.body;
|
||
await pool.query(
|
||
`UPDATE agentify_subject_registry SET status=COALESCE($2,status), host_url=COALESCE($3,host_url), corpus_version=COALESCE($4,corpus_version), updated_at=NOW() WHERE subject_slug=$1`,
|
||
[req.params.slug, status, host_url, corpus_version]
|
||
);
|
||
res.json({ ok: true });
|
||
});
|
||
|
||
// ── Stewardship Admin Dashboard ───────────────────────────────────────────
|
||
app.get("/agentify-admin", async (req: Request, res: Response, next) => {
|
||
const host = (req.headers["x-forwarded-host"] || req.headers.host || "").toString().split(",")[0].trim().replace(/:\d+$/, "").replace(/^www\./, "").toLowerCase();
|
||
if (!host.includes("agentify") || host.startsWith("skills.")) return next();
|
||
const key = req.query.key as string;
|
||
if (key !== ADMIN_KEY) {
|
||
return res.type("text/html").send(`<!DOCTYPE html><html><head><title>Agentify Admin</title><style>body{font-family:monospace;background:#07102a;color:#c9b87e;display:flex;align-items:center;justify-content:center;height:100vh;margin:0}</style></head><body><form method="GET"><input name="key" type="password" placeholder="Admin key" style="padding:8px 12px;background:#0d1f3c;border:1px solid #c9b87e44;color:#c9b87e;border-radius:4px;font-size:.9rem"> <button type="submit" style="padding:8px 16px;background:#c9b87e;color:#07102a;border:none;border-radius:4px;font-weight:700;cursor:pointer">Enter</button></form></body></html>`);
|
||
}
|
||
|
||
const { rows: registry } = await pool.query(`
|
||
SELECT r.*,
|
||
(SELECT COUNT(*) FROM corpus_intake_sessions cs WHERE cs.expert_slug = r.subject_slug) AS corpus_sessions,
|
||
(SELECT status FROM agentify_corpus_bundles cb WHERE cb.expert_slug = r.subject_slug ORDER BY cb.received_at DESC LIMIT 1) AS bundle_status,
|
||
(SELECT chunk_count FROM agentify_corpus_bundles cb WHERE cb.expert_slug = r.subject_slug ORDER BY cb.received_at DESC LIMIT 1) AS chunk_count
|
||
FROM agentify_subject_registry r
|
||
ORDER BY r.updated_at DESC
|
||
`).catch(() => ({ rows: [] }));
|
||
|
||
const { rows: experts } = await pool.query(`
|
||
SELECT slug, expert_name, primary_domain, demo_url, status, created_at FROM agentify_experts ORDER BY created_at DESC
|
||
`).catch(() => ({ rows: [] }));
|
||
|
||
const esc = (s: any) => String(s ?? "").replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">");
|
||
const statusBadge = (s: string) => {
|
||
const map: Record<string,string> = {
|
||
preview: "#d4a730:Preview", live: "#22c55e:Live", verify_email: "#f59e0b:Verify Email",
|
||
pending: "#6b7280:Pending", corpus_received: "#3b82f6:Corpus Received",
|
||
proposed: "#a78bfa:Proposed", claimed: "#06b6d4:Claimed", rejected: "#ef4444:Rejected",
|
||
};
|
||
const [color, label] = (map[s] || "#6b7280:"+s).split(":");
|
||
return `<span style="background:${color}22;color:${color};border:1px solid ${color}44;border-radius:3px;padding:1px 7px;font-size:.72rem;font-weight:700;letter-spacing:.04em">${label.toUpperCase()}</span>`;
|
||
};
|
||
|
||
const agentTestUrl = (slug: string) => `https://agentify.help/agents/${slug}`;
|
||
|
||
const registryRows = registry.map(r => `
|
||
<tr>
|
||
<td style="padding:8px 12px;font-weight:600">${esc(r.subject_name)}<br><span style="font-weight:400;font-size:.75rem;color:#8899aa">${esc(r.subject_slug)}</span></td>
|
||
<td style="padding:8px 12px;font-size:.82rem;color:#c4b07a">${esc(r.subject_domain || "—")}</td>
|
||
<td style="padding:8px 12px;font-size:.82rem">${esc(r.steward_name || "—")}</td>
|
||
<td style="padding:8px 12px">${statusBadge(r.status || "pending")}</td>
|
||
<td style="padding:8px 12px;font-size:.78rem">
|
||
${r.bundle_status ? `<span style="color:#3b82f6">${esc(r.bundle_status)}</span>${r.chunk_count ? ` · ${r.chunk_count} chunks` : ""}` : `<span style="color:#6b7280">${r.corpus_sessions} session${r.corpus_sessions !== "1" ? "s" : ""}</span>`}
|
||
</td>
|
||
<td style="padding:8px 12px;font-size:.78rem">
|
||
<a href="${agentTestUrl(r.subject_slug)}" target="_blank" style="color:#c9b87e;text-decoration:none">${esc(r.subject_slug)}</a>
|
||
${r.host_url ? `<br><a href="${esc(r.host_url)}" target="_blank" style="color:#7099bb;font-size:.72rem">${esc(r.host_url.replace(/^https?:\/\//,"").replace(/\/.*$/,""))}</a>` : ""}
|
||
</td>
|
||
<td style="padding:8px 12px;font-size:.78rem">
|
||
${r.dashboard_token ? `<a href="/stewardship/${r.subject_slug}?token=${r.dashboard_token}" target="_blank" style="color:#22c55e">Open dashboard →</a>` : `<span style="color:#6b7280">No token</span>`}
|
||
</td>
|
||
</tr>`).join("");
|
||
|
||
const expertRows = experts.map(e => `
|
||
<tr>
|
||
<td style="padding:8px 12px;font-weight:600">${esc(e.expert_name)}<br><span style="font-weight:400;font-size:.75rem;color:#8899aa">${esc(e.slug)}</span></td>
|
||
<td style="padding:8px 12px;font-size:.82rem;color:#c4b07a">${esc(e.primary_domain || "—")}</td>
|
||
<td style="padding:8px 12px;font-size:.82rem">VCAP Registry</td>
|
||
<td style="padding:8px 12px">${statusBadge(e.status || "proposed")}</td>
|
||
<td style="padding:8px 12px">—</td>
|
||
<td style="padding:8px 12px;font-size:.78rem">
|
||
<a href="${agentTestUrl(e.slug)}" target="_blank" style="color:#c9b87e">${esc(e.slug)}</a>
|
||
${e.demo_url ? `<br><a href="${esc(e.demo_url)}" target="_blank" style="color:#7099bb;font-size:.72rem">${esc(e.demo_url.replace(/^https?:\/\//,"").replace(/\/.*$/,""))}</a>` : ""}
|
||
</td>
|
||
<td style="padding:8px 12px">—</td>
|
||
</tr>`).join("");
|
||
|
||
return res.type("text/html").send(`<!DOCTYPE html>
|
||
<html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
||
<title>Agentify Stewardship Admin</title>
|
||
<style>
|
||
*{box-sizing:border-box;margin:0;padding:0}
|
||
body{background:#07102a;color:#d4cfbb;font-family:"IBM Plex Sans","Inter",system-ui,sans-serif;min-height:100vh}
|
||
.header{background:#0a1830;border-bottom:1px solid #1a2e4a;padding:14px 28px;display:flex;align-items:center;gap:16px}
|
||
.logo{color:#c9b87e;font-weight:700;font-size:1rem;letter-spacing:.02em}
|
||
.subtitle{color:#6b7f99;font-size:.82rem}
|
||
.container{max-width:1400px;margin:0 auto;padding:24px 20px}
|
||
h2{color:#c9b87e;font-size:.85rem;letter-spacing:.08em;text-transform:uppercase;margin-bottom:12px;margin-top:28px}
|
||
table{width:100%;border-collapse:collapse;background:#0a1220;border-radius:8px;overflow:hidden;font-size:.82rem}
|
||
th{background:#0d1a30;color:#8899bb;text-transform:uppercase;letter-spacing:.06em;font-size:.7rem;padding:9px 12px;text-align:left;border-bottom:1px solid #1a2e44}
|
||
tr:not(:last-child) td{border-bottom:1px solid #121f34}
|
||
tr:hover td{background:#0c1828}
|
||
.stat{background:#0a1220;border:1px solid #1a2e44;border-radius:8px;padding:16px;text-align:center}
|
||
.stat-val{font-size:1.8rem;font-weight:700;color:#c9b87e}
|
||
.stat-label{font-size:.75rem;color:#6b7f99;margin-top:4px}
|
||
.stats{display:grid;grid-template-columns:repeat(4,1fr);gap:12px;margin-bottom:24px}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div class="header">
|
||
<span class="logo">⟐ Agentify Stewardship Admin</span>
|
||
<span class="subtitle">agentify.help · ${new Date().toLocaleDateString("en-US", { month: "short", day: "numeric", year: "numeric" })}</span>
|
||
<a href="/agentify-admin?key=${esc(key)}" style="margin-left:auto;color:#c9b87e;font-size:.8rem;text-decoration:none">↺ Refresh</a>
|
||
</div>
|
||
<div class="container">
|
||
|
||
<div class="stats">
|
||
<div class="stat"><div class="stat-val">${registry.length}</div><div class="stat-label">Stewardship cases</div></div>
|
||
<div class="stat"><div class="stat-val">${registry.filter((r:any) => r.status === "live" || r.status === "preview").length}</div><div class="stat-label">Live / Preview</div></div>
|
||
<div class="stat"><div class="stat-val">${experts.length}</div><div class="stat-label">VCAP experts</div></div>
|
||
<div class="stat"><div class="stat-val">${registry.filter((r:any) => r.bundle_status).length}</div><div class="stat-label">Corpus received</div></div>
|
||
</div>
|
||
|
||
<h2>Stewardship Registry (${registry.length})</h2>
|
||
<table>
|
||
<thead><tr>
|
||
<th>Subject</th><th>Domain</th><th>Steward</th><th>Status</th><th>Corpus</th><th>Test URL / Embed</th><th>Dashboard</th>
|
||
</tr></thead>
|
||
<tbody>${registryRows || '<tr><td colspan="7" style="padding:20px;text-align:center;color:#6b7f99">No stewardship cases yet</td></tr>'}</tbody>
|
||
</table>
|
||
|
||
<h2>VCAP Expert Registry (${experts.length})</h2>
|
||
<table>
|
||
<thead><tr>
|
||
<th>Expert</th><th>Domain</th><th>Source</th><th>Status</th><th>Corpus</th><th>Test URL</th><th>Actions</th>
|
||
</tr></thead>
|
||
<tbody>${expertRows || '<tr><td colspan="7" style="padding:20px;text-align:center;color:#6b7f99">No VCAP experts yet</td></tr>'}</tbody>
|
||
</table>
|
||
|
||
<div style="margin-top:24px;padding:16px;background:#0a1220;border:1px solid #1a2e44;border-radius:8px;font-size:.78rem;color:#6b7f99">
|
||
<strong style="color:#c9b87e">Embedding pipeline:</strong>
|
||
After corpus finalization, bundles are queued as <code style="color:#3b82f6">received</code>.
|
||
Distillation runs automatically every 5 minutes for bundles in <code>received</code> state.
|
||
Test each agent at <code>agentify.help/agents/{slug}</code> once status reaches <code style="color:#22c55e">ready</code>.
|
||
</div>
|
||
|
||
</div>
|
||
</body></html>`);
|
||
});
|
||
|
||
// ── /agents — public directory of all registered expert agents ───────────
|
||
app.get("/agents", async (req: Request, res: Response, next) => {
|
||
const host = (req.headers["x-forwarded-host"] || req.headers.host || "").toString().split(",")[0].trim().replace(/:\d+$/, "").replace(/^www\./, "").toLowerCase();
|
||
if (!host.includes("agentify") || host.startsWith("skills.") || host.startsWith("mcp.")) return next();
|
||
|
||
const esc = (s: any) => String(s ?? "").replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">");
|
||
const tab = (req.query.tab as string) === "topics" ? "topics" : "persons";
|
||
|
||
// Person agents — from agentify_experts + bundles
|
||
const { rows: experts } = await pool.query(`
|
||
SELECT e.slug, e.expert_name, e.primary_domain, e.credentials, e.affiliated_institution, e.status,
|
||
cb.chunk_count, cb.status AS corpus_status
|
||
FROM agentify_experts e
|
||
LEFT JOIN agentify_corpus_bundles cb ON cb.expert_slug = e.slug AND cb.status = 'ready'
|
||
WHERE e.status IN ('live','preview') OR cb.bundle_id IS NOT NULL
|
||
ORDER BY CASE e.status WHEN 'live' THEN 0 WHEN 'preview' THEN 1 ELSE 2 END, e.expert_name ASC
|
||
`).catch(() => ({ rows: [] }));
|
||
|
||
// Topic taxonomy — canonical approved categories with linked agent status
|
||
const { rows: taxonomy } = await pool.query(`
|
||
SELECT t.category, t.slug, t.display_name, t.description, t.scope_note,
|
||
t.audience_personas, t.status AS taxonomy_status,
|
||
r.status AS agent_status,
|
||
cb.chunk_count, cb.status AS corpus_status
|
||
FROM agentify_topic_taxonomy t
|
||
LEFT JOIN agentify_subject_registry r ON r.taxonomy_slug = t.slug
|
||
LEFT JOIN agentify_corpus_bundles cb ON cb.expert_slug = t.slug AND cb.status = 'ready'
|
||
WHERE t.status IN ('approved','active')
|
||
ORDER BY t.category ASC, t.display_name ASC
|
||
`).catch(() => ({ rows: [] }));
|
||
const topics = taxonomy; // alias for count display
|
||
|
||
const statusBadge = (s: string, chunks: number | null) => {
|
||
if (s === 'live') return `<span style="background:#0d3a2a;color:#22c55e;border:1px solid #22c55e44;padding:2px 9px;border-radius:20px;font-size:.72rem;font-weight:700">LIVE</span>`;
|
||
if ((s === 'preview' || s === 'ready') && chunks) return `<span style="background:#1a2e0d;color:#86efac;border:1px solid #86efac44;padding:2px 9px;border-radius:20px;font-size:.72rem;font-weight:700">PREVIEW · ${chunks.toLocaleString()} chunks</span>`;
|
||
if (s === 'preview') return `<span style="background:#1a2e0d;color:#86efac;border:1px solid #86efac44;padding:2px 9px;border-radius:20px;font-size:.72rem;font-weight:700">PREVIEW</span>`;
|
||
if (s === 'assembling') return `<span style="background:#2a1e0d;color:#f59e0b;border:1px solid #f59e0b44;padding:2px 9px;border-radius:20px;font-size:.72rem;font-weight:700">ASSEMBLING</span>`;
|
||
return `<span style="background:#1a2e44;color:#8899bb;border:1px solid #8899bb33;padding:2px 9px;border-radius:20px;font-size:.72rem;font-weight:700">PROPOSED</span>`;
|
||
};
|
||
|
||
// Deduplicate persons
|
||
const seen = new Set<string>();
|
||
const personCards = experts.filter((e: any) => {
|
||
if (seen.has(e.slug)) return false; seen.add(e.slug); return true;
|
||
}).map((e: any) => `
|
||
<a href="/agents/${esc(e.slug)}" class="card">
|
||
<div class="card-top">
|
||
<div><div class="name">${esc(e.expert_name)}</div><div class="domain">${esc(e.primary_domain || "")}</div></div>
|
||
${statusBadge(e.corpus_status || e.status, e.chunk_count)}
|
||
</div>
|
||
${e.credentials ? `<div class="cred">${esc(e.credentials)}</div>` : ""}
|
||
${e.affiliated_institution ? `<div class="inst">${esc(e.affiliated_institution)}</div>` : ""}
|
||
</a>`).join("");
|
||
|
||
// Group taxonomy entries by category
|
||
const taxByCategory: Record<string, any[]> = {};
|
||
for (const t of taxonomy) {
|
||
if (!taxByCategory[t.category]) taxByCategory[t.category] = [];
|
||
taxByCategory[t.category].push(t);
|
||
}
|
||
|
||
const topicCardForEntry = (t: any) => {
|
||
const agentStatus = t.agent_status || "taxonomy-only";
|
||
const hasAgent = !!t.agent_status;
|
||
const personas = Array.isArray(t.audience_personas) ? t.audience_personas : [];
|
||
const personaBadges = personas.map((p: string) =>
|
||
`<span style="background:#1a2e44;color:#8ab8ff;padding:1px 7px;border-radius:10px;font-size:.68rem;margin-right:3px">${esc(p)}</span>`
|
||
).join("");
|
||
return `
|
||
<div class="topic-taxonomy-entry">
|
||
<div class="tax-header">
|
||
<div>
|
||
${hasAgent
|
||
? `<a href="/agents/${esc(t.slug)}" class="tax-name">${esc(t.display_name)}</a>`
|
||
: `<span class="tax-name" style="color:#6b9966">${esc(t.display_name)}</span>`}
|
||
</div>
|
||
${hasAgent ? statusBadge(t.corpus_status || agentStatus, t.chunk_count) : `<span style="background:#1a2e44;color:#6b7f99;padding:2px 9px;border-radius:20px;font-size:.72rem;font-weight:600">TAXONOMY ONLY</span>`}
|
||
</div>
|
||
${t.description ? `<div class="tax-desc">${esc(t.description)}</div>` : ""}
|
||
${personaBadges ? `<div style="margin-top:7px">${personaBadges}</div>` : ""}
|
||
</div>`;
|
||
};
|
||
|
||
const topicSections = Object.entries(taxByCategory).map(([cat, entries]) => `
|
||
<div class="tax-category">
|
||
<div class="cat-label">${esc(cat)}</div>
|
||
${entries.map(topicCardForEntry).join("")}
|
||
</div>`).join("");
|
||
|
||
const topicCards = topicSections || "";
|
||
|
||
res.setHeader("Content-Type", "text/html; charset=utf-8");
|
||
res.send(`<!DOCTYPE html>
|
||
<html lang="en"><head>
|
||
<meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
||
<title>Expert Agents — Agentify.Help</title>
|
||
<meta name="description" content="Browse Agentify.Help — distilled AI agents for both persons (one corpus, one voice) and topics (collective knowledge, synthesized).">
|
||
<style>
|
||
*{box-sizing:border-box;margin:0;padding:0}
|
||
body{background:#07102a;color:#d4cfbb;font-family:"IBM Plex Sans","Helvetica Neue",sans-serif;line-height:1.6}
|
||
.wrap{max-width:860px;margin:0 auto;padding:48px 24px}
|
||
.eyebrow{font-size:.72rem;color:#6b7f99;text-transform:uppercase;letter-spacing:.1em;margin-bottom:14px}
|
||
h1{font-size:1.75rem;color:#f0e8d0;font-weight:700;margin-bottom:6px}
|
||
.sub{color:#8899bb;font-size:.93rem;margin-bottom:28px}
|
||
.tabs{display:flex;gap:0;border-bottom:1px solid #1a2e44;margin-bottom:28px}
|
||
.tab{padding:10px 22px;font-size:.88rem;font-weight:600;cursor:pointer;color:#6b7f99;border:none;background:none;border-bottom:2px solid transparent;margin-bottom:-1px;font-family:inherit;transition:color .15s,border-color .15s}
|
||
.tab.active{color:#c9b87e;border-bottom-color:#c9b87e}
|
||
.tab:hover:not(.active){color:#d4cfbb}
|
||
.grid{display:grid;grid-template-columns:1fr 1fr;gap:14px}
|
||
@media(max-width:600px){.grid{grid-template-columns:1fr}}
|
||
.card{display:block;background:#0b1a33;border:1px solid #1a2e44;border-radius:10px;padding:18px 20px;text-decoration:none;transition:border-color .15s,background .15s}
|
||
.card:hover{border-color:#c9b87e55;background:#0d1f3c}
|
||
.topic-card{border-color:#1a3320}
|
||
.topic-card:hover{border-color:#22c55e44;background:#0d2218}
|
||
.card-top{display:flex;justify-content:space-between;align-items:flex-start;gap:12px;margin-bottom:8px}
|
||
.name{color:#f0e8d0;font-size:1rem;font-weight:600}
|
||
.domain{color:#8ab8ff;font-size:.8rem;margin-top:3px}
|
||
.cred{color:#8899bb;font-size:.78rem;margin-top:4px;font-style:italic}
|
||
.inst{color:#6b7f99;font-size:.75rem;margin-top:2px}
|
||
.empty{color:#6b7f99;font-size:.9rem;padding:24px;border:1px dashed #1a2e44;border-radius:10px;text-align:center}
|
||
.footer{margin-top:48px;padding-top:24px;border-top:1px solid #1a2e44;font-size:.78rem;color:#6b7f99}
|
||
a{color:#c9b87e}
|
||
a:hover{text-decoration:underline}
|
||
.topic-blurb{background:#0d2218;border:1px solid #1a3320;border-radius:8px;padding:14px 18px;margin-bottom:24px;font-size:.85rem;color:#86efac;line-height:1.6}
|
||
.tax-category{margin-bottom:28px}
|
||
.cat-label{font-size:.7rem;font-weight:700;text-transform:uppercase;letter-spacing:.1em;color:#6b7f99;margin-bottom:10px;padding-bottom:6px;border-bottom:1px solid #1a2e44}
|
||
.topic-taxonomy-entry{background:#0d2218;border:1px solid #1a3320;border-radius:9px;padding:14px 18px;margin-bottom:10px}
|
||
.tax-header{display:flex;justify-content:space-between;align-items:flex-start;gap:12px;margin-bottom:6px}
|
||
.tax-name{color:#86efac;font-size:.95rem;font-weight:600;text-decoration:none}
|
||
.tax-name:hover{text-decoration:underline}
|
||
.tax-desc{color:#8899bb;font-size:.8rem;line-height:1.55;margin-top:4px}
|
||
.propose-form{background:#0b1a33;border:1px solid #1a2e44;border-radius:9px;padding:20px;margin-top:28px}
|
||
.propose-form h3{font-size:.9rem;color:#c9b87e;margin-bottom:14px;font-weight:600}
|
||
.propose-form label{display:block;font-size:.78rem;color:#8899bb;margin-bottom:3px;margin-top:12px}
|
||
.propose-form input,.propose-form textarea,.propose-form select{width:100%;background:#07102a;border:1px solid #1a2e44;border-radius:6px;color:#d4cfbb;padding:8px 10px;font-size:.83rem;font-family:inherit;outline:none}
|
||
.propose-form input:focus,.propose-form textarea:focus,.propose-form select:focus{border-color:#c9b87e66}
|
||
.propose-form textarea{resize:vertical;min-height:60px}
|
||
.propose-btn{margin-top:14px;background:#c9b87e;color:#07102a;border:none;border-radius:6px;padding:8px 20px;font-size:.85rem;font-weight:700;cursor:pointer;font-family:inherit}
|
||
.propose-btn:hover{background:#ddd0a0}
|
||
.propose-msg{margin-top:10px;font-size:.82rem;padding:8px 12px;border-radius:6px;display:none}
|
||
.propose-msg.ok{background:#0d2218;color:#22c55e;border:1px solid #22c55e44}
|
||
.propose-msg.err{background:#2a0d0d;color:#ef4444;border:1px solid #ef444444}
|
||
</style>
|
||
</head><body>
|
||
<div class="wrap">
|
||
<div class="eyebrow">Agentify.Help · Registry</div>
|
||
<h1>AI Agents</h1>
|
||
<p class="sub">Distilled AI agents grounded in published corpus — one voice per person, collective synthesis per topic.</p>
|
||
<div class="tabs">
|
||
<button class="tab ${tab === "persons" ? "active" : ""}" onclick="switchTab('persons')">Persons <span style="font-size:.72rem;color:#6b7f99;font-weight:400">(${seen.size})</span></button>
|
||
<button class="tab ${tab === "topics" ? "active" : ""}" onclick="switchTab('topics')">Topics <span style="font-size:.72rem;color:#6b7f99;font-weight:400">(${topics.length})</span></button>
|
||
</div>
|
||
|
||
<div id="tab-persons" style="display:${tab === "persons" ? "block" : "none"}">
|
||
<div class="grid">
|
||
${personCards || `<div class="empty">No person agents registered yet.</div>`}
|
||
</div>
|
||
<div style="margin-top:20px">
|
||
<a href="/register" style="font-size:.85rem">Register a person →</a>
|
||
<a href="/taxonomy" style="font-size:.85rem">Browse taxonomy →</a>
|
||
</div>
|
||
</div>
|
||
|
||
<div id="tab-topics" style="display:${tab === "topics" ? "block" : "none"}">
|
||
<div class="topic-blurb">
|
||
Topic agents represent collective knowledge — assembled by a working group, synthesized by AI, grounded in the literature.
|
||
Where the evidence agrees, the agent says so. Where it doesn't, it names the disagreement and both sides.
|
||
</div>
|
||
<div style="font-size:.78rem;color:#6b7f99;margin-bottom:20px">
|
||
Topics are governed by a master taxonomy. Every topic agent is anchored to an approved taxonomy entry —
|
||
which sets the scope, prevents balkanization, and routes working-group governance.
|
||
Proposed entries that don't fit an existing category go to steward review.
|
||
</div>
|
||
${topicCards || `<div class="empty">No approved topics in taxonomy yet.</div>`}
|
||
|
||
<div class="propose-form">
|
||
<h3>Propose a topic for the taxonomy</h3>
|
||
<p style="font-size:.79rem;color:#6b7f99;margin-bottom:4px">
|
||
A good topic proposal names a clear scope, explains what would be in vs. out, and identifies the audience it serves.
|
||
Stewards review proposals and either approve them (creating a taxonomy entry), merge them with an existing one, or decline with a note.
|
||
</p>
|
||
<label for="pt-category">Category</label>
|
||
<select id="pt-category">
|
||
<option value="">— select —</option>
|
||
<option>Medical / Health</option>
|
||
<option>Civic / Policy</option>
|
||
<option>Law / Rights</option>
|
||
<option>Technology</option>
|
||
<option>Environment / Ecology</option>
|
||
<option>History / Culture</option>
|
||
<option>Education</option>
|
||
<option>Finance / Economics</option>
|
||
<option>Trades / Craft</option>
|
||
<option>Other</option>
|
||
</select>
|
||
<label for="pt-name">Topic name</label>
|
||
<input id="pt-name" type="text" placeholder="e.g. Pediatric Palliative Care">
|
||
<label for="pt-desc">What does this topic cover?</label>
|
||
<textarea id="pt-desc" rows="2" placeholder="One or two sentences on what knowledge domain this agent would synthesize."></textarea>
|
||
<label for="pt-scope">Scope note — what's in, what's out?</label>
|
||
<textarea id="pt-scope" rows="2" placeholder="In scope: … Out of scope: …"></textarea>
|
||
<label for="pt-email">Your email (optional — so we can follow up)</label>
|
||
<input id="pt-email" type="email" placeholder="you@example.com">
|
||
<button class="propose-btn" onclick="submitTopicProposal()">Submit proposal</button>
|
||
<div id="propose-msg" class="propose-msg"></div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="footer">
|
||
<a href="/">agentify.help</a> · <a href="/api/agentify/experts" style="color:#6b7f99">JSON catalog</a> · <a href="mailto:ody@wellspr.ing" style="color:#6b7f99">ody@wellspr.ing</a>
|
||
</div>
|
||
</div>
|
||
<script>
|
||
function switchTab(t) {
|
||
document.getElementById('tab-persons').style.display = t === 'persons' ? 'block' : 'none';
|
||
document.getElementById('tab-topics').style.display = t === 'topics' ? 'block' : 'none';
|
||
document.querySelectorAll('.tab').forEach(el => el.classList.remove('active'));
|
||
event.target.classList.add('active');
|
||
history.replaceState(null,'','/agents?tab=' + t);
|
||
}
|
||
async function submitTopicProposal() {
|
||
const cat = document.getElementById('pt-category').value.trim();
|
||
const name = document.getElementById('pt-name').value.trim();
|
||
const desc = document.getElementById('pt-desc').value.trim();
|
||
const scope = document.getElementById('pt-scope').value.trim();
|
||
const email = document.getElementById('pt-email').value.trim();
|
||
const msg = document.getElementById('propose-msg');
|
||
msg.style.display = 'none'; msg.className = 'propose-msg';
|
||
if (!cat || !name) { msg.textContent = 'Category and topic name are required.'; msg.className = 'propose-msg err'; msg.style.display = 'block'; return; }
|
||
try {
|
||
const r = await fetch('/api/agentify-help/propose-topic', { method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify({ category:cat, display_name:name, description:desc, scope_note:scope, email }) });
|
||
const j = await r.json();
|
||
if (r.status === 409) { msg.textContent = j.error + ' (' + j.status + ')'; msg.className = 'propose-msg err'; }
|
||
else if (!r.ok) { msg.textContent = j.error || 'Submission failed.'; msg.className = 'propose-msg err'; }
|
||
else { msg.textContent = j.message || 'Proposal received — thank you.'; msg.className = 'propose-msg ok'; document.getElementById('pt-name').value=''; document.getElementById('pt-desc').value=''; document.getElementById('pt-scope').value=''; }
|
||
msg.style.display = 'block';
|
||
} catch { msg.textContent = 'Network error — please try again.'; msg.className = 'propose-msg err'; msg.style.display = 'block'; }
|
||
}
|
||
</script>
|
||
</body></html>`);
|
||
});
|
||
|
||
// ── /taxonomy — aspirational expert catalog with tag filtering ───────────
|
||
app.get("/taxonomy", (req: Request, res: Response, next) => {
|
||
const host = (req.headers["x-forwarded-host"] || req.headers.host || "").toString().split(",")[0].trim().replace(/:\d+$/, "").replace(/^www\./, "").toLowerCase();
|
||
if (!host.includes("agentify") || host.startsWith("skills.") || host.startsWith("mcp.")) return next();
|
||
|
||
res.setHeader("Content-Type", "text/html; charset=utf-8");
|
||
res.send(`<!DOCTYPE html>
|
||
<html lang="en"><head>
|
||
<meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
||
<title>Expert Taxonomy — Agentify.Help</title>
|
||
<meta name="description" content="The full spectrum of expert domains on Agentify.Help. One person, one corpus, one agent — across medicine, entrepreneurship, faith, ecology, and more.">
|
||
<style>
|
||
*{box-sizing:border-box;margin:0;padding:0}
|
||
body{background:#07102a;color:#d4cfbb;font-family:"IBM Plex Sans","Helvetica Neue",sans-serif;line-height:1.6}
|
||
.wrap{max-width:960px;margin:0 auto;padding:48px 24px}
|
||
.eyebrow{font-size:.72rem;color:#6b7f99;text-transform:uppercase;letter-spacing:.1em;margin-bottom:12px}
|
||
h1{font-size:1.8rem;color:#f0e8d0;font-weight:700;margin-bottom:6px}
|
||
.sub{color:#8899bb;font-size:.93rem;margin-bottom:32px;max-width:600px}
|
||
.filter-bar{display:flex;flex-wrap:wrap;gap:8px;margin-bottom:36px;padding-bottom:24px;border-bottom:1px solid #1a2e44}
|
||
.filter-btn{padding:5px 14px;border-radius:20px;border:1px solid #1a2e44;background:transparent;color:#8899bb;font-size:.78rem;cursor:pointer;font-family:inherit;transition:all .15s}
|
||
.filter-btn:hover{border-color:#c9b87e55;color:#c9b87e}
|
||
.filter-btn.active{background:#c9b87e22;border-color:#c9b87e55;color:#c9b87e}
|
||
.cluster{margin-bottom:44px}
|
||
.cluster-title{font-size:.68rem;text-transform:uppercase;letter-spacing:.12em;color:#6b7f99;margin-bottom:14px;padding-bottom:8px;border-bottom:1px solid #1a2e44}
|
||
.cluster-title span{color:#8899bb}
|
||
.grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(260px,1fr));gap:10px}
|
||
.card{background:#0b1a33;border:1px solid #1a2e44;border-radius:10px;padding:16px 18px;transition:border-color .15s}
|
||
.card.hidden{display:none}
|
||
.card:hover{border-color:#c9b87e44}
|
||
.card-name{color:#f0e8d0;font-weight:600;font-size:.95rem;margin-bottom:2px}
|
||
.card-domain{color:#8ab8ff;font-size:.78rem;margin-bottom:8px}
|
||
.card-tags{display:flex;flex-wrap:wrap;gap:4px;margin-bottom:8px}
|
||
.tag{padding:2px 8px;border-radius:12px;font-size:.68rem;font-weight:600;letter-spacing:.04em;cursor:pointer;border:1px solid transparent}
|
||
.tag-cluster{background:#1a2e4a;color:#8ab8ff;border-color:#1a2e4a}
|
||
.tag-status-live{background:#0d3a2a;color:#22c55e;border-color:#22c55e44}
|
||
.tag-status-preview{background:#1a2e0d;color:#86efac;border-color:#86efac33}
|
||
.tag-status-proposed{background:#1a2e44;color:#8899bb;border-color:#8899bb22}
|
||
.tag-status-legacy{background:#2a1a3a;color:#c084fc;border-color:#c084fc33}
|
||
.tag-corpus{background:#1a2030;color:#60a5fa;border-color:#60a5fa22}
|
||
.card-note{font-size:.74rem;color:#6b7f99;line-height:1.5;margin-top:6px;font-style:italic}
|
||
.card-link{display:inline-block;margin-top:8px;font-size:.74rem;color:#c9b87e;text-decoration:none}
|
||
.card-link:hover{text-decoration:underline}
|
||
.empty-state{color:#6b7f99;text-align:center;padding:40px;font-size:.88rem}
|
||
.footer{margin-top:60px;padding-top:24px;border-top:1px solid #1a2e44;font-size:.78rem;color:#6b7f99}
|
||
a{color:#c9b87e}a:hover{text-decoration:underline}
|
||
@media(max-width:500px){h1{font-size:1.4rem}}
|
||
</style>
|
||
</head><body>
|
||
<div class="wrap">
|
||
<div class="eyebrow">Agentify.Help · Vision</div>
|
||
<h1>Expert Taxonomy</h1>
|
||
<p class="sub">The full spectrum of expert domains. One person, one corpus, one agent — steward-attested and query-ready. Use the tags to browse by cluster, corpus status, or readiness.</p>
|
||
|
||
<div class="filter-bar" id="filter-bar">
|
||
<button class="filter-btn active" data-filter="all">All</button>
|
||
<button class="filter-btn" data-filter="health">Health & Medicine</button>
|
||
<button class="filter-btn" data-filter="business">Business</button>
|
||
<button class="filter-btn" data-filter="nutrition">Nutrition</button>
|
||
<button class="filter-btn" data-filter="faith">Faith</button>
|
||
<button class="filter-btn" data-filter="ecology">Ecology</button>
|
||
<button class="filter-btn" data-filter="technology">AI & Tech</button>
|
||
<button class="filter-btn" data-filter="finance">Finance</button>
|
||
<button class="filter-btn" data-filter="civic">Civic</button>
|
||
<button class="filter-btn" data-filter="education">Education</button>
|
||
<button class="filter-btn" data-filter="—">–– By Status ––</button>
|
||
<button class="filter-btn" data-filter="live">● Live</button>
|
||
<button class="filter-btn" data-filter="preview">● Preview</button>
|
||
<button class="filter-btn" data-filter="proposed">● Proposed</button>
|
||
<button class="filter-btn" data-filter="legacy">● Legacy / Estate</button>
|
||
</div>
|
||
|
||
<div id="no-results" class="empty-state" style="display:none">No experts match that filter.</div>
|
||
|
||
<!-- ── Cluster: Health & Medicine ── -->
|
||
<div class="cluster" data-cluster="health">
|
||
<div class="cluster-title">Clinical Health & Evidence-Based Medicine <span>· Direct path: Naturologie, CholesterolTruth.com</span></div>
|
||
<div class="grid">
|
||
<div class="card" data-tags="health proposed">
|
||
<div class="card-name">Dr. Allan Sniderman, MD</div>
|
||
<div class="card-domain">Lipidology · CVD risk</div>
|
||
<div class="card-tags">
|
||
<span class="tag tag-cluster">health</span>
|
||
<span class="tag tag-status-proposed">proposed</span>
|
||
<span class="tag tag-corpus">papers + blog</span>
|
||
</div>
|
||
<div class="card-note">McGill University. Apolipoprotein B as primary CVD marker. Corpus: CholesterolTruth.com + research papers.</div>
|
||
<a class="card-link" href="/agents/sniderman">View agent →</a>
|
||
</div>
|
||
<div class="card" data-tags="health proposed">
|
||
<div class="card-name">Dr. Tom Dayspring, MD</div>
|
||
<div class="card-domain">Advanced lipidology · Biomarkers</div>
|
||
<div class="card-tags">
|
||
<span class="tag tag-cluster">health</span>
|
||
<span class="tag tag-status-proposed">proposed</span>
|
||
<span class="tag tag-corpus">podcast + papers</span>
|
||
</div>
|
||
<div class="card-note">Years of lipidology education via podcasts, essays, and clinical papers. Enormous transcript corpus.</div>
|
||
</div>
|
||
<div class="card" data-tags="health proposed">
|
||
<div class="card-name">Dr. Ben Bikman</div>
|
||
<div class="card-domain">Insulin resistance · Metabolic health</div>
|
||
<div class="card-tags">
|
||
<span class="tag tag-cluster">health</span>
|
||
<span class="tag tag-status-proposed">proposed</span>
|
||
<span class="tag tag-corpus">book + blog + podcast</span>
|
||
</div>
|
||
<div class="card-note">BYU researcher. "Why We Get Sick." Active corpus: blog, podcast transcripts, and university lecture recordings.</div>
|
||
</div>
|
||
<div class="card" data-tags="health proposed">
|
||
<div class="card-name">Peter Attia, MD</div>
|
||
<div class="card-domain">Longevity medicine · Performance</div>
|
||
<div class="card-tags">
|
||
<span class="tag tag-cluster">health</span>
|
||
<span class="tag tag-status-proposed">proposed</span>
|
||
<span class="tag tag-corpus">podcast + book</span>
|
||
</div>
|
||
<div class="card-note">300+ podcast episodes fully transcribed. "Outlive." One of the strongest public corpora in longevity medicine.</div>
|
||
</div>
|
||
<div class="card" data-tags="health proposed">
|
||
<div class="card-name">Rhonda Patrick, PhD</div>
|
||
<div class="card-domain">Micronutrients · Longevity biomarkers</div>
|
||
<div class="card-tags">
|
||
<span class="tag tag-cluster">health</span>
|
||
<span class="tag tag-status-proposed">proposed</span>
|
||
<span class="tag tag-corpus">podcast + papers</span>
|
||
</div>
|
||
<div class="card-note">FoundMyFitness. Dense, research-grounded podcast corpus spanning micronutrients, heat shock, and aging.</div>
|
||
</div>
|
||
<div class="card" data-tags="health proposed">
|
||
<div class="card-name">Dr. Dale Bredesen</div>
|
||
<div class="card-domain">Alzheimer's reversal · ReCODE</div>
|
||
<div class="card-tags">
|
||
<span class="tag tag-cluster">health</span>
|
||
<span class="tag tag-status-proposed">proposed</span>
|
||
<span class="tag tag-corpus">book + papers</span>
|
||
</div>
|
||
<div class="card-note">"The End of Alzheimer's." Protocol-based framework for cognitive decline reversal. Papers + book corpus.</div>
|
||
</div>
|
||
<div class="card" data-tags="health proposed">
|
||
<div class="card-name">Dr. Jason Fung</div>
|
||
<div class="card-domain">Therapeutic fasting · Obesity</div>
|
||
<div class="card-tags">
|
||
<span class="tag tag-cluster">health</span>
|
||
<span class="tag tag-status-proposed">proposed</span>
|
||
<span class="tag tag-corpus">books + blog</span>
|
||
</div>
|
||
<div class="card-note">Intensive Dietary Management blog + multiple books. One of the largest public fasting corpora.</div>
|
||
</div>
|
||
<div class="card" data-tags="health proposed">
|
||
<div class="card-name">Dr. Chris Palmer</div>
|
||
<div class="card-domain">Metabolic psychiatry</div>
|
||
<div class="card-tags">
|
||
<span class="tag tag-cluster">health</span>
|
||
<span class="tag tag-status-proposed">proposed</span>
|
||
<span class="tag tag-corpus">book + interviews</span>
|
||
</div>
|
||
<div class="card-note">"Brain Energy." Harvard psychiatrist connecting ketogenic diet and mental illness. Growing corpus.</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ── Cluster: Entrepreneurship & Business ── -->
|
||
<div class="cluster" data-cluster="business">
|
||
<div class="cluster-title">Entrepreneurship & Business <span>· Guy Kawasaki is the pilot</span></div>
|
||
<div class="grid">
|
||
<div class="card" data-tags="business preview">
|
||
<div class="card-name">Guy Kawasaki</div>
|
||
<div class="card-domain">Startups · Marketing · Evangelism</div>
|
||
<div class="card-tags">
|
||
<span class="tag tag-cluster">business</span>
|
||
<span class="tag tag-status-preview">preview · 2,222 chunks</span>
|
||
</div>
|
||
<div class="card-note">Pilot WellAgent. 15 books, decades of blog posts and essays. Consultation interface active.</div>
|
||
<a class="card-link" href="/agents/guy-kawasaki">Try consultation →</a>
|
||
</div>
|
||
<div class="card" data-tags="business proposed">
|
||
<div class="card-name">Seth Godin</div>
|
||
<div class="card-domain">Permission marketing · Modern business</div>
|
||
<div class="card-tags">
|
||
<span class="tag tag-cluster">business</span>
|
||
<span class="tag tag-status-proposed">proposed</span>
|
||
<span class="tag tag-corpus">8,000+ blog posts</span>
|
||
</div>
|
||
<div class="card-note">Largest continuously published expert blog on the internet. Every post is corpus-ready. 20+ books.</div>
|
||
</div>
|
||
<div class="card" data-tags="business proposed">
|
||
<div class="card-name">Paul Graham</div>
|
||
<div class="card-domain">Startup philosophy · Y Combinator</div>
|
||
<div class="card-tags">
|
||
<span class="tag tag-cluster">business</span>
|
||
<span class="tag tag-status-proposed">proposed</span>
|
||
<span class="tag tag-corpus">200 essays</span>
|
||
</div>
|
||
<div class="card-note">200 canonical essays at paulgraham.com. Compact, high-density corpus. Enormous reach in the developer and founder world.</div>
|
||
</div>
|
||
<div class="card" data-tags="business proposed">
|
||
<div class="card-name">Naval Ravikant</div>
|
||
<div class="card-domain">Wealth · Leverage · Specific knowledge</div>
|
||
<div class="card-tags">
|
||
<span class="tag tag-cluster">business</span>
|
||
<span class="tag tag-status-proposed">proposed</span>
|
||
<span class="tag tag-corpus">essays + podcast</span>
|
||
</div>
|
||
<div class="card-note">Tweetstorms, Navalmanack, podcast transcripts. Distinct epistemological framework with broad appeal.</div>
|
||
</div>
|
||
<div class="card" data-tags="business proposed">
|
||
<div class="card-name">Morgan Housel</div>
|
||
<div class="card-domain">Psychology of money · Investing</div>
|
||
<div class="card-tags">
|
||
<span class="tag tag-cluster">business</span>
|
||
<span class="tag tag-status-proposed">proposed</span>
|
||
<span class="tag tag-corpus">blog + books</span>
|
||
</div>
|
||
<div class="card-note">Collaborative Fund blog + "The Psychology of Money." Exceptional writing corpus for behavioral finance.</div>
|
||
</div>
|
||
<div class="card" data-tags="business proposed">
|
||
<div class="card-name">Jim Collins</div>
|
||
<div class="card-domain">Management · Organizational endurance</div>
|
||
<div class="card-tags">
|
||
<span class="tag tag-cluster">business</span>
|
||
<span class="tag tag-status-proposed">proposed</span>
|
||
<span class="tag tag-corpus">books + research</span>
|
||
</div>
|
||
<div class="card-note">"Good to Great," "Built to Last," "Great by Choice." Frameworks backed by 25+ years of research.</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ── Cluster: Nutrition & Food Systems ── -->
|
||
<div class="cluster" data-cluster="nutrition">
|
||
<div class="cluster-title">Nutrition & Food Systems <span>· Cross-cluster with Health — fits Naturologie, CholesterolTruth</span></div>
|
||
<div class="grid">
|
||
<div class="card" data-tags="nutrition proposed">
|
||
<div class="card-name">Nina Teicholz</div>
|
||
<div class="card-domain">Dietary fat science · Investigative journalism</div>
|
||
<div class="card-tags">
|
||
<span class="tag tag-cluster">nutrition</span>
|
||
<span class="tag tag-status-proposed">proposed</span>
|
||
<span class="tag tag-corpus">book + Substack</span>
|
||
</div>
|
||
<div class="card-note">"The Big Fat Surprise." Rigorous journalism reexamining dietary fat guidelines. Active Substack corpus.</div>
|
||
</div>
|
||
<div class="card" data-tags="nutrition proposed">
|
||
<div class="card-name">Gary Taubes</div>
|
||
<div class="card-domain">Investigative nutrition journalism</div>
|
||
<div class="card-tags">
|
||
<span class="tag tag-cluster">nutrition</span>
|
||
<span class="tag tag-status-proposed">proposed</span>
|
||
<span class="tag tag-corpus">books + articles</span>
|
||
</div>
|
||
<div class="card-note">"Good Calories Bad Calories," "Why We Get Fat." Dense scientific reporting on carbohydrate-insulin model.</div>
|
||
</div>
|
||
<div class="card" data-tags="nutrition proposed">
|
||
<div class="card-name">Zoë Harcombe, PhD</div>
|
||
<div class="card-domain">Evidence-based nutrition</div>
|
||
<div class="card-tags">
|
||
<span class="tag tag-cluster">nutrition</span>
|
||
<span class="tag tag-status-proposed">proposed</span>
|
||
<span class="tag tag-corpus">blog + papers + books</span>
|
||
</div>
|
||
<div class="card-note">Nutritional epidemiology. Systematic refutation of dietary guidelines. Strong blog + peer-reviewed corpus.</div>
|
||
</div>
|
||
<div class="card" data-tags="nutrition health proposed">
|
||
<div class="card-name">Mark Hyman, MD</div>
|
||
<div class="card-domain">Functional nutrition · Food as medicine</div>
|
||
<div class="card-tags">
|
||
<span class="tag tag-cluster">nutrition</span>
|
||
<span class="tag tag-cluster">health</span>
|
||
<span class="tag tag-status-proposed">proposed</span>
|
||
<span class="tag tag-corpus">books + podcast</span>
|
||
</div>
|
||
<div class="card-note">15 books, The Doctor's Farmacy podcast. Bridges functional medicine and food systems. Large public corpus.</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ── Cluster: Faith & Spiritual Formation ── -->
|
||
<div class="cluster" data-cluster="faith">
|
||
<div class="cluster-title">Faith & Spiritual Formation <span>· Most distinctive cluster — covenant-governed with integrity</span></div>
|
||
<div class="grid">
|
||
<div class="card" data-tags="faith proposed">
|
||
<div class="card-name">N.T. Wright</div>
|
||
<div class="card-domain">New Testament scholarship · Theology</div>
|
||
<div class="card-tags">
|
||
<span class="tag tag-cluster">faith</span>
|
||
<span class="tag tag-status-proposed">proposed</span>
|
||
<span class="tag tag-corpus">80+ books + articles</span>
|
||
</div>
|
||
<div class="card-note">Prolific living scholar. Books, lectures, articles — one of the largest theological corpora of any living person. Clear living steward.</div>
|
||
</div>
|
||
<div class="card" data-tags="faith proposed">
|
||
<div class="card-name">Miroslav Volf</div>
|
||
<div class="card-domain">Forgiveness · Memory · Human flourishing</div>
|
||
<div class="card-tags">
|
||
<span class="tag tag-cluster">faith</span>
|
||
<span class="tag tag-status-proposed">proposed</span>
|
||
<span class="tag tag-corpus">books + papers</span>
|
||
</div>
|
||
<div class="card-note">Yale Divinity. "Exclusion and Embrace," "Free of Charge." Theological framework for flourishing societies.</div>
|
||
</div>
|
||
<div class="card" data-tags="faith proposed">
|
||
<div class="card-name">Richard Rohr, OFM</div>
|
||
<div class="card-domain">Contemplative spirituality · Mysticism</div>
|
||
<div class="card-tags">
|
||
<span class="tag tag-cluster">faith</span>
|
||
<span class="tag tag-status-proposed">proposed</span>
|
||
<span class="tag tag-corpus">CAC daily meditations</span>
|
||
</div>
|
||
<div class="card-note">Center for Action and Contemplation. Years of daily meditations, books, and retreats. One of the largest contemplative corpora available.</div>
|
||
</div>
|
||
<div class="card" data-tags="faith legacy proposed">
|
||
<div class="card-name">Dallas Willard</div>
|
||
<div class="card-domain">Spiritual formation · Soul training</div>
|
||
<div class="card-tags">
|
||
<span class="tag tag-cluster">faith</span>
|
||
<span class="tag tag-status-legacy">legacy · d. 2013</span>
|
||
<span class="tag tag-corpus">books + lectures</span>
|
||
</div>
|
||
<div class="card-note">USC philosopher and theologian. "The Divine Conspiracy," "Renovation of the Heart." Lectures widely transcribed. Estate steward model.</div>
|
||
</div>
|
||
<div class="card" data-tags="faith legacy proposed">
|
||
<div class="card-name">Eugene Peterson</div>
|
||
<div class="card-domain">Pastoral theology · The Message</div>
|
||
<div class="card-tags">
|
||
<span class="tag tag-cluster">faith</span>
|
||
<span class="tag tag-status-legacy">legacy · d. 2018</span>
|
||
<span class="tag tag-corpus">books + essays</span>
|
||
</div>
|
||
<div class="card-note">"A Long Obedience in the Same Direction," "The Message." Rich pastoral corpus. Estate preservation mission.</div>
|
||
</div>
|
||
<div class="card" data-tags="faith legacy proposed">
|
||
<div class="card-name">Tim Keller</div>
|
||
<div class="card-domain">Apologetics · Cultural engagement</div>
|
||
<div class="card-tags">
|
||
<span class="tag tag-cluster">faith</span>
|
||
<span class="tag tag-status-legacy">legacy · d. 2023</span>
|
||
<span class="tag tag-corpus">35 yrs sermons + books</span>
|
||
</div>
|
||
<div class="card-note">Redeemer NYC. 35 years of sermons, 20 books, articles. One of the most distillable theological corpora of the 21st century. Active preservation organization.</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ── Cluster: Ecology & Regeneration ── -->
|
||
<div class="cluster" data-cluster="ecology">
|
||
<div class="cluster-title">Ecology, Land & Regeneration <span>· Aligned with WellSpr.ing covenant of stewardship</span></div>
|
||
<div class="grid">
|
||
<div class="card" data-tags="ecology proposed">
|
||
<div class="card-name">Robin Wall Kimmerer</div>
|
||
<div class="card-domain">Indigenous ecology · Plant intelligence</div>
|
||
<div class="card-tags">
|
||
<span class="tag tag-cluster">ecology</span>
|
||
<span class="tag tag-status-proposed">proposed</span>
|
||
<span class="tag tag-corpus">book + talks</span>
|
||
</div>
|
||
<div class="card-note">"Braiding Sweetgrass." SUNY professor of environmental biology. Poetic, scientific, indigenous perspective on reciprocal stewardship.</div>
|
||
</div>
|
||
<div class="card" data-tags="ecology proposed">
|
||
<div class="card-name">Wendell Berry</div>
|
||
<div class="card-domain">Land stewardship · Agrarian philosophy</div>
|
||
<div class="card-tags">
|
||
<span class="tag tag-cluster">ecology</span>
|
||
<span class="tag tag-status-proposed">proposed</span>
|
||
<span class="tag tag-corpus">50 yrs essays + poetry</span>
|
||
</div>
|
||
<div class="card-note">Farmer, poet, essayist. 50 years of writing on land, community, and economy. One of the richest agrarian corpora in American letters.</div>
|
||
</div>
|
||
<div class="card" data-tags="ecology proposed">
|
||
<div class="card-name">Paul Hawken</div>
|
||
<div class="card-domain">Regenerative economy · Climate</div>
|
||
<div class="card-tags">
|
||
<span class="tag tag-cluster">ecology</span>
|
||
<span class="tag tag-status-proposed">proposed</span>
|
||
<span class="tag tag-corpus">books + articles</span>
|
||
</div>
|
||
<div class="card-note">"Drawdown," "Blessed Unrest," "Regeneration." Data-grounded framework for climate solutions.</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ── Cluster: AI & Technology ── -->
|
||
<div class="cluster" data-cluster="technology">
|
||
<div class="cluster-title">AI, Technology & Society <span>· Demonstrates that AI personas can themselves be governed with integrity</span></div>
|
||
<div class="grid">
|
||
<div class="card" data-tags="technology proposed">
|
||
<div class="card-name">Tristan Harris</div>
|
||
<div class="card-domain">Humane technology · Attention economy</div>
|
||
<div class="card-tags">
|
||
<span class="tag tag-cluster">technology</span>
|
||
<span class="tag tag-status-proposed">proposed</span>
|
||
<span class="tag tag-corpus">talks + interviews</span>
|
||
</div>
|
||
<div class="card-note">Center for Humane Technology. The Social Dilemma. The most credible demonstration: a humane AI persona governed exactly the way Harris would govern it.</div>
|
||
</div>
|
||
<div class="card" data-tags="technology proposed">
|
||
<div class="card-name">Cathy O'Neil</div>
|
||
<div class="card-domain">Algorithmic accountability</div>
|
||
<div class="card-tags">
|
||
<span class="tag tag-cluster">technology</span>
|
||
<span class="tag tag-status-proposed">proposed</span>
|
||
<span class="tag tag-corpus">book + op-eds</span>
|
||
</div>
|
||
<div class="card-note">"Weapons of Math Destruction." Harvard data scientist. Strong corpus on how models embed and amplify bias.</div>
|
||
</div>
|
||
<div class="card" data-tags="technology proposed">
|
||
<div class="card-name">Kate Crawford</div>
|
||
<div class="card-domain">AI governance · Political economy of AI</div>
|
||
<div class="card-tags">
|
||
<span class="tag tag-cluster">technology</span>
|
||
<span class="tag tag-status-proposed">proposed</span>
|
||
<span class="tag tag-corpus">book + papers</span>
|
||
</div>
|
||
<div class="card-note">"Atlas of AI." USC/NYU researcher examining the labor, environmental, and political infrastructure of AI systems.</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ── Cluster: Finance & Economics ── -->
|
||
<div class="cluster" data-cluster="finance">
|
||
<div class="cluster-title">Finance & Economics</div>
|
||
<div class="grid">
|
||
<div class="card" data-tags="finance proposed">
|
||
<div class="card-name">Howard Marks</div>
|
||
<div class="card-domain">Investment philosophy · Risk</div>
|
||
<div class="card-tags">
|
||
<span class="tag tag-cluster">finance</span>
|
||
<span class="tag tag-status-proposed">proposed</span>
|
||
<span class="tag tag-corpus">Oaktree memos (public)</span>
|
||
</div>
|
||
<div class="card-note">Oaktree Capital. Decades of investor memos freely published. Rare corpus of rigorous, dated investment thinking available for distillation.</div>
|
||
</div>
|
||
<div class="card" data-tags="finance legacy proposed">
|
||
<div class="card-name">John Bogle</div>
|
||
<div class="card-domain">Index investing · Fiduciary duty</div>
|
||
<div class="card-tags">
|
||
<span class="tag tag-cluster">finance</span>
|
||
<span class="tag tag-status-legacy">legacy · d. 2019</span>
|
||
<span class="tag tag-corpus">books + speeches</span>
|
||
</div>
|
||
<div class="card-note">Vanguard founder. "The Little Book of Common Sense Investing." Legacy steward model through Bogle Center for Financial Literacy.</div>
|
||
</div>
|
||
<div class="card" data-tags="finance proposed">
|
||
<div class="card-name">Ray Dalio</div>
|
||
<div class="card-domain">Principles · Macroeconomics</div>
|
||
<div class="card-tags">
|
||
<span class="tag tag-cluster">finance</span>
|
||
<span class="tag tag-status-proposed">proposed</span>
|
||
<span class="tag tag-corpus">books + LinkedIn + YouTube</span>
|
||
</div>
|
||
<div class="card-note">Bridgewater. "Principles," "Big Debt Crises." Systematic framework thinker with an enormous publicly published corpus.</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ── Cluster: Civic & Governance ── -->
|
||
<div class="cluster" data-cluster="civic">
|
||
<div class="cluster-title">Civic & Governance <span>· Direct connection to the NNN.today network</span></div>
|
||
<div class="grid">
|
||
<div class="card" data-tags="civic legacy proposed">
|
||
<div class="card-name">Jane Jacobs</div>
|
||
<div class="card-domain">Urban planning · Mixed-use cities</div>
|
||
<div class="card-tags">
|
||
<span class="tag tag-cluster">civic</span>
|
||
<span class="tag tag-status-legacy">legacy · d. 2006</span>
|
||
<span class="tag tag-corpus">books</span>
|
||
</div>
|
||
<div class="card-note">"The Death and Life of Great American Cities." Still the canonical text for urbanism. Estate steward model; works remain widely in use by city planners.</div>
|
||
</div>
|
||
<div class="card" data-tags="civic legacy proposed">
|
||
<div class="card-name">Elinor Ostrom</div>
|
||
<div class="card-domain">Commons governance · Collective action</div>
|
||
<div class="card-tags">
|
||
<span class="tag tag-cluster">civic</span>
|
||
<span class="tag tag-status-legacy">legacy · d. 2012</span>
|
||
<span class="tag tag-corpus">papers (public)</span>
|
||
</div>
|
||
<div class="card-note">Nobel Laureate in Economics. Indiana University. All research papers publicly available. Framework for governing shared resources without privatization or regulation.</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ── Cluster: Education ── -->
|
||
<div class="cluster" data-cluster="education">
|
||
<div class="cluster-title">Education & Learning</div>
|
||
<div class="grid">
|
||
<div class="card" data-tags="education proposed">
|
||
<div class="card-name">Sal Khan</div>
|
||
<div class="card-domain">Mastery learning · Accessible education</div>
|
||
<div class="card-tags">
|
||
<span class="tag tag-cluster">education</span>
|
||
<span class="tag tag-status-proposed">proposed</span>
|
||
<span class="tag tag-corpus">talks + book + videos</span>
|
||
</div>
|
||
<div class="card-note">Khan Academy. "The One World Schoolhouse." Articulates a full philosophy of mastery-based learning with documented outcomes.</div>
|
||
</div>
|
||
<div class="card" data-tags="education legacy proposed">
|
||
<div class="card-name">Ken Robinson</div>
|
||
<div class="card-domain">Creative education · Talent systems</div>
|
||
<div class="card-tags">
|
||
<span class="tag tag-cluster">education</span>
|
||
<span class="tag tag-status-legacy">legacy · d. 2020</span>
|
||
<span class="tag tag-corpus">talks + books</span>
|
||
</div>
|
||
<div class="card-note">Most-watched TED talk in history. "The Element." Legacy steward model through his estate and the RSA.</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div style="margin-top:32px;padding:20px;background:#0a1220;border:1px solid #1a2e44;border-radius:10px;font-size:.85rem">
|
||
<strong style="color:#f0e8d0">Want to register an expert?</strong>
|
||
<span style="color:#8899bb;margin-left:8px">Each slot is one-per-person. First steward registration wins.</span>
|
||
<a href="/register" style="margin-left:16px;color:#c9b87e">Start registration →</a>
|
||
</div>
|
||
|
||
<div style="margin-top:24px;font-size:.78rem;color:#6b7f99">
|
||
<a href="/agents">← Active registry</a>
|
||
|
||
<a href="/">agentify.help home</a>
|
||
</div>
|
||
</div>
|
||
|
||
<script>
|
||
(function(){
|
||
const btns = document.querySelectorAll('.filter-btn');
|
||
const cards = document.querySelectorAll('.card');
|
||
const noResults = document.getElementById('no-results');
|
||
|
||
function applyFilter(f) {
|
||
btns.forEach(b => b.classList.toggle('active', b.dataset.filter === f));
|
||
let visible = 0;
|
||
cards.forEach(card => {
|
||
const tags = (card.dataset.tags || '').split(' ');
|
||
const show = f === 'all' || tags.includes(f);
|
||
card.classList.toggle('hidden', !show);
|
||
if (show) visible++;
|
||
});
|
||
noResults.style.display = visible ? 'none' : 'block';
|
||
}
|
||
|
||
btns.forEach(btn => {
|
||
btn.addEventListener('click', () => {
|
||
if (btn.dataset.filter === '—') return; // separator, not a real filter
|
||
applyFilter(btn.dataset.filter);
|
||
});
|
||
});
|
||
})();
|
||
</script>
|
||
</body></html>`);
|
||
});
|
||
|
||
// ── Agent test page ────────────────────────────────────────────────────────
|
||
app.get("/agents/:slug", async (req: Request, res: Response, next) => {
|
||
const host = (req.headers["x-forwarded-host"] || req.headers.host || "").toString().split(",")[0].trim().replace(/:\d+$/, "").replace(/^www\./, "").toLowerCase();
|
||
if (!host.includes("agentify") || host.startsWith("skills.")) return next();
|
||
const { slug } = req.params;
|
||
const esc = (s: any) => String(s ?? "").replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">");
|
||
|
||
const { rows: expertRows } = await pool.query(
|
||
`SELECT * FROM agentify_experts WHERE slug = $1 LIMIT 1`, [slug]
|
||
).catch(() => ({ rows: [] }));
|
||
|
||
const { rows: regRows } = await pool.query(
|
||
`SELECT r.*, cb.status AS bundle_status, cb.chunk_count
|
||
FROM agentify_subject_registry r
|
||
LEFT JOIN agentify_corpus_bundles cb ON cb.expert_slug = r.subject_slug
|
||
WHERE r.subject_slug = $1 ORDER BY cb.received_at DESC LIMIT 1`, [slug]
|
||
).catch(() => ({ rows: [] }));
|
||
|
||
// Also check agentify_corpus_bundles directly (for experts registered via orchestrator)
|
||
const { rows: bundleRows } = await pool.query(
|
||
`SELECT status, chunk_count, bundle_id FROM agentify_corpus_bundles WHERE expert_slug = $1 ORDER BY chunk_count DESC LIMIT 1`, [slug]
|
||
).catch(() => ({ rows: [] }));
|
||
|
||
// Source catalog — all indexed sources for this subject
|
||
const { rows: sourceRows } = await pool.query(
|
||
`SELECT url, title, type, chunk_count, ingested_at FROM agentify_source_catalog
|
||
WHERE expert_slug = $1 ORDER BY chunk_count DESC NULLS LAST, ingested_at DESC`, [slug]
|
||
).catch(() => ({ rows: [] }));
|
||
|
||
const expert = expertRows[0];
|
||
const reg = regRows[0];
|
||
// Prefer direct bundle data if registry join found nothing
|
||
const directBundle = bundleRows[0];
|
||
|
||
if (!expert && !reg) {
|
||
return res.status(404).type("text/html").send(`<!DOCTYPE html><html><head><title>Agent not found</title><style>body{font-family:monospace;background:#07102a;color:#c9b87e;display:flex;align-items:center;justify-content:center;height:100vh}</style></head><body><div style="text-align:center"><p style="font-size:1.5rem">Agent not found</p><p style="color:#6b7f99;margin-top:8px">No expert registered for slug: <code>${esc(slug)}</code></p><a href="/" style="color:#c9b87e;margin-top:16px;display:block">← Back to Agentify.Help</a></div></body></html>`);
|
||
}
|
||
|
||
const name = expert?.expert_name || reg?.subject_name || slug;
|
||
const domain = expert?.primary_domain || reg?.subject_domain || "";
|
||
const creds = expert?.credentials || "";
|
||
const institution = expert?.affiliated_institution || "";
|
||
const scopes: string[] = expert?.scopes || [];
|
||
const refusals: string[] = expert?.refusals || [];
|
||
const attestationUri = expert?.attestation_uri || "";
|
||
const bundleStatus = reg?.bundle_status || directBundle?.status || null;
|
||
const chunkCount = reg?.chunk_count || directBundle?.chunk_count || null;
|
||
const bundleId = directBundle?.bundle_id || null;
|
||
const hostUrl = expert?.demo_url || reg?.host_url || "";
|
||
const stewardDashToken = reg?.dashboard_token || null;
|
||
const subjectType: string = (reg as any)?.subject_type || "person";
|
||
|
||
const statusColor = { proposed: "#a78bfa", ready: "#22c55e", live: "#22c55e", received: "#3b82f6", distilling: "#f59e0b", failed: "#ef4444", preview: "#c9b87e", assembling: "#f59e0b" };
|
||
const agentStatus = bundleStatus || (expert ? expert.status : reg?.status || "pending");
|
||
const agentColor = (statusColor as any)[agentStatus] || "#6b7280";
|
||
|
||
const isTopicAgent = subjectType === "topic";
|
||
const entityLabel = isTopicAgent ? "Topic" : "Person";
|
||
const metaDesc = isTopicAgent
|
||
? `Consult the collective knowledge agent for ${esc(name)} on Agentify.Help.`
|
||
: `Test consultation with ${esc(name)}'s WellAgent on Agentify.Help.`;
|
||
|
||
return res.type("text/html").send(`<!DOCTYPE html>
|
||
<html lang="en"><head>
|
||
<meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
||
<title>${esc(name)} — Agentify.Help</title>
|
||
<meta name="description" content="${metaDesc}">
|
||
<link rel="icon" href="${AH_FAVICON}">
|
||
<script src="https://cdn.jsdelivr.net/npm/marked@9/marked.min.js"></script>
|
||
<style>
|
||
*{box-sizing:border-box;margin:0;padding:0}
|
||
body{background:#07102a;color:#d4cfbb;font-family:"IBM Plex Sans","Inter",system-ui,sans-serif;min-height:100vh}
|
||
.hero{background:linear-gradient(135deg,#0a1830 0%,#0d1f3c 100%);border-bottom:1px solid #1a2e4a;padding:40px 28px}
|
||
.avatar{width:56px;height:56px;border-radius:50%;background:#1a2e4a;border:2px solid #c9b87e44;display:flex;align-items:center;justify-content:center;font-size:1.4rem;font-weight:700;color:#c9b87e;margin-bottom:16px}
|
||
h1{font-size:1.6rem;color:#f0e8d0;margin-bottom:4px}
|
||
.meta{color:#8899bb;font-size:.85rem;margin-bottom:12px}
|
||
.pill{display:inline-block;background:#1a2e4a;color:#c9b87e;border:1px solid #c9b87e33;border-radius:20px;padding:3px 12px;font-size:.75rem;margin:2px;text-transform:uppercase;letter-spacing:.05em}
|
||
.container{max-width:820px;margin:0 auto;padding:32px 20px}
|
||
.card{background:#0a1220;border:1px solid #1a2e44;border-radius:10px;padding:20px;margin-bottom:16px}
|
||
.card-title{font-size:.75rem;text-transform:uppercase;letter-spacing:.08em;color:#8899bb;margin-bottom:10px}
|
||
.status-bar{display:flex;gap:16px;margin-top:16px}
|
||
.status-item{background:#0d1f3c;border:1px solid #1a2e44;border-radius:6px;padding:10px 14px;flex:1}
|
||
.status-label{font-size:.68rem;text-transform:uppercase;letter-spacing:.07em;color:#6b7f99;margin-bottom:3px}
|
||
.status-val{font-size:.9rem;font-weight:600}
|
||
.chat-box{width:100%;min-height:80px;background:#060e1c;border:1px solid #1a2e44;border-radius:6px;padding:12px;color:#d4cfbb;font-family:inherit;font-size:.88rem;resize:vertical}
|
||
.btn{padding:9px 22px;background:#c9b87e;color:#07102a;border:none;border-radius:6px;font-weight:700;font-size:.88rem;cursor:pointer;margin-top:8px}
|
||
.btn:hover{background:#d4c48e}
|
||
.response{background:#060e1c;border:1px solid #1a2e44;border-radius:6px;padding:14px;margin-top:12px;font-size:.88rem;line-height:1.65;display:none}
|
||
.response p{margin-bottom:.75em}.response p:last-child{margin-bottom:0}
|
||
.response h1,.response h2,.response h3{color:#f0e8d0;margin:1em 0 .4em;font-size:1rem}
|
||
.response h2{font-size:.95rem}.response h3{font-size:.9rem}
|
||
.response ul,.response ol{padding-left:1.4em;margin-bottom:.75em}
|
||
.response li{margin-bottom:.25em}
|
||
.response strong{color:#f0e8d0}.response em{color:#c9b87e}
|
||
.response code{background:#1a2e4a;padding:1px 5px;border-radius:3px;font-family:monospace;font-size:.83em;color:#86efac}
|
||
.response pre{background:#1a2e4a;padding:10px 12px;border-radius:5px;overflow-x:auto;margin-bottom:.75em}
|
||
.response pre code{background:none;padding:0;color:#86efac}
|
||
.response blockquote{border-left:3px solid #c9b87e44;padding-left:12px;color:#8899bb;margin:.5em 0}
|
||
.response hr{border:none;border-top:1px solid #1a2e44;margin:.75em 0}
|
||
.scope-list{list-style:none;display:flex;flex-wrap:wrap;gap:4px}
|
||
.scope-list li{background:#1a2e4a;color:#c9b87e;padding:2px 10px;border-radius:3px;font-family:monospace;font-size:.72rem}
|
||
.refusal-list li{color:#f87171}
|
||
.src-list{display:flex;flex-direction:column;gap:5px;max-height:360px;overflow-y:auto;padding-right:4px}
|
||
.src-row{display:flex;justify-content:space-between;align-items:center;gap:8px;padding:7px 10px;background:#060e1c;border-radius:5px;border:1px solid #1a2e44}
|
||
.src-title{color:#8ab8ff;font-size:.78rem;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;flex:1}
|
||
.src-meta{display:flex;align-items:center;gap:6px;flex-shrink:0}
|
||
.src-badge{font-size:.65rem;padding:1px 6px;border-radius:3px;font-weight:600;text-transform:uppercase;letter-spacing:.04em}
|
||
.src-badge-podcast{background:#1e2e4a;color:#8ab8ff;border:1px solid #8ab8ff33}
|
||
.src-badge-article{background:#1e3a2e;color:#86efac;border:1px solid #86efac33}
|
||
.src-chunks{color:#6b7f99;font-size:.68rem;white-space:nowrap}
|
||
.suggest-form{margin-top:14px;padding-top:14px;border-top:1px solid #1a2e44}
|
||
.suggest-toggle{background:none;border:none;color:#c9b87e;font-size:.78rem;cursor:pointer;padding:0;font-family:inherit;text-decoration:underline}
|
||
.suggest-body{display:none;margin-top:12px}
|
||
.suggest-input{width:100%;background:#060e1c;border:1px solid #1a2e44;border-radius:5px;padding:8px 10px;color:#d4cfbb;font-family:inherit;font-size:.82rem;box-sizing:border-box;margin-bottom:8px}
|
||
.suggest-input::placeholder{color:#4b5e7a}
|
||
.suggest-btn{padding:7px 18px;background:#1a2e4a;color:#c9b87e;border:1px solid #c9b87e44;border-radius:5px;font-size:.8rem;font-weight:600;cursor:pointer;font-family:inherit}
|
||
.suggest-btn:hover{background:#223b5e}
|
||
.topic-banner{background:#1a2e0d;border:1px solid #22c55e33;border-radius:8px;padding:12px 16px;margin-bottom:16px;font-size:.85rem;color:#86efac}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div class="hero">
|
||
<div style="max-width:820px;margin:0 auto">
|
||
<div class="avatar">${esc(name.split(" ").map((w:string) => w[0]).join("").slice(0,2).toUpperCase())}</div>
|
||
<h1>${esc(name)}</h1>
|
||
<div class="meta">${creds ? esc(creds) + " · " : ""}${esc(domain)}${institution ? " · " + esc(institution) : ""}</div>
|
||
<div>
|
||
<span class="pill" style="background:#1a2e4a;color:#8899bb;border-color:#1a2e4a">${entityLabel}</span>
|
||
<span class="pill">${esc(domain || "Expert")}</span>
|
||
<span class="pill" style="background:${agentColor}22;color:${agentColor};border-color:${agentColor}44">${agentStatus.replace(/_/g," ")}</span>
|
||
</div>
|
||
<div class="status-bar">
|
||
<div class="status-item"><div class="status-label">Platform</div><div class="status-val" style="color:#c9b87e">WellSpr.ing Agentify</div></div>
|
||
<div class="status-item"><div class="status-label">Corpus</div><div class="status-val">${chunkCount ? chunkCount + " chunks" : "Pending"}</div></div>
|
||
<div class="status-item"><div class="status-label">Embedded</div><div class="status-val" style="color:${agentColor}">${bundleStatus === "ready" ? "Yes" : bundleStatus === "distilling" ? "In progress" : "Pending"}</div></div>
|
||
<div class="status-item"><div class="status-label">Availability</div><div class="status-val" style="color:${agentColor}">${bundleStatus === "ready" ? "Live" : "Processing"}</div></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="container">
|
||
${bundleStatus === "ready" ? `
|
||
<div class="card">
|
||
<div class="card-title">Test consultation</div>
|
||
<textarea id="q" class="chat-box" placeholder="Ask ${esc(name.split(" ")[0])} a question in their area of expertise…"></textarea>
|
||
<button class="btn" onclick="ask()">Ask ${esc(name.split(" ")[0])} →</button>
|
||
<div id="resp" class="response"></div>
|
||
</div>` : `
|
||
<div class="card" style="border-color:${agentColor}44">
|
||
<div class="card-title" style="color:${agentColor}">Agent status: ${agentStatus.replace(/_/g," ").toUpperCase()}</div>
|
||
<p style="font-size:.88rem;color:#8899bb">
|
||
${bundleStatus === "distilling" ? `${esc(name)}'s corpus is currently being embedded. Consultation will be available once distillation completes — usually within a few minutes. Refresh this page to check.`
|
||
: bundleStatus === "received" ? `${esc(name)}'s corpus has been received and is queued for embedding. This page will show the consultation interface once it's ready.`
|
||
: `${esc(name)}'s corpus has not yet been submitted. Once the steward submits the corpus, it will be embedded and available here.`}
|
||
</p>
|
||
<button class="btn" style="margin-top:12px;background:#1a2e4a;color:#c9b87e;border:1px solid #c9b87e44" onclick="location.reload()">↺ Refresh status</button>
|
||
</div>`}
|
||
|
||
${scopes.length ? `
|
||
<div class="card">
|
||
<div class="card-title">Authorized scopes (SGS)</div>
|
||
<ul class="scope-list">${scopes.map((s:string) => `<li>${esc(s)}</li>`).join("")}</ul>
|
||
</div>` : ""}
|
||
|
||
${refusals.length ? `
|
||
<div class="card">
|
||
<div class="card-title">Refusal set</div>
|
||
<ul class="scope-list refusal-list">${refusals.map((r:string) => `<li>${esc(r)}</li>`).join("")}</ul>
|
||
</div>` : ""}
|
||
|
||
${attestationUri ? `
|
||
<div class="card">
|
||
<div class="card-title">Attestation</div>
|
||
<a href="${esc(attestationUri)}" target="_blank" style="color:#c9b87e;font-size:.82rem">${esc(attestationUri)}</a>
|
||
</div>` : ""}
|
||
|
||
${hostUrl ? `
|
||
<div class="card">
|
||
<div class="card-title">Embed location</div>
|
||
<a href="${esc(hostUrl)}" target="_blank" style="color:#7099bb;font-size:.82rem">${esc(hostUrl)}</a>
|
||
</div>` : ""}
|
||
|
||
${bundleId ? `
|
||
<div class="card">
|
||
<div class="card-title">Corpus bundle</div>
|
||
<div style="font-family:monospace;font-size:.75rem;color:#8899bb;margin-bottom:8px">${esc(bundleId)}</div>
|
||
<div style="display:flex;gap:12px;flex-wrap:wrap">
|
||
${chunkCount ? `<span style="background:#0d1f3c;border:1px solid #1a2e44;border-radius:4px;padding:4px 10px;font-size:.75rem;color:#c9b87e">${chunkCount.toLocaleString()} chunks</span>` : ""}
|
||
<span style="background:#0d1f3c;border:1px solid #1a2e44;border-radius:4px;padding:4px 10px;font-size:.75rem;color:#22c55e">Ready</span>
|
||
</div>
|
||
</div>` : ""}
|
||
|
||
<div class="card">
|
||
<div class="card-title" style="display:flex;justify-content:space-between;align-items:center">
|
||
<span>Corpus sources (${sourceRows.length})</span>
|
||
${sourceRows.length === 0 ? "" : `<span style="color:#6b7f99;font-size:.7rem">${sourceRows.reduce((a: number, s: any) => a + (s.chunk_count || 0), 0).toLocaleString()} total chunks</span>`}
|
||
</div>
|
||
${sourceRows.length > 0 ? `
|
||
<div class="src-list" id="src-list">
|
||
${sourceRows.map((s: any) => {
|
||
const isLink = s.url && !s.url.startsWith("podcast://");
|
||
const typeClass = s.type === "podcast" ? "src-badge-podcast" : "src-badge-article";
|
||
const typeLabel = s.type === "podcast" ? "Podcast" : s.type === "book" ? "Book" : "Article";
|
||
return `<div class="src-row">
|
||
${isLink
|
||
? `<a href="${esc(s.url)}" target="_blank" rel="noopener" class="src-title" title="${esc(s.title || s.url)}">${esc(s.title || s.url)}</a>`
|
||
: `<span class="src-title" title="${esc(s.title || s.url)}">${esc(s.title || s.url)}</span>`}
|
||
<div class="src-meta">
|
||
<span class="src-badge ${typeClass}">${typeLabel}</span>
|
||
${s.chunk_count ? `<span class="src-chunks">${s.chunk_count}</span>` : ""}
|
||
</div>
|
||
</div>`;
|
||
}).join("")}
|
||
</div>
|
||
` : `<p style="font-size:.82rem;color:#6b7f99">No sources indexed yet. The corpus will be populated as content is submitted.</p>`}
|
||
<div class="suggest-form">
|
||
<div style="display:flex;justify-content:space-between;align-items:center">
|
||
<span style="font-size:.78rem;color:#6b7f99">See something missing?</span>
|
||
<button class="suggest-toggle" onclick="toggleSuggest()" id="suggest-toggle-btn">+ Suggest a source</button>
|
||
</div>
|
||
<div class="suggest-body" id="suggest-body">
|
||
<p style="font-size:.75rem;color:#6b7f99;margin-bottom:10px">Submit a URL or title for something that should be in this corpus — a book, article, interview, podcast episode, or speech.</p>
|
||
<input type="url" class="suggest-input" id="suggest-url" placeholder="https://… or leave blank for a book/title" />
|
||
<input type="text" class="suggest-input" id="suggest-title" placeholder="Title or description (required)" />
|
||
<input type="text" class="suggest-input" id="suggest-notes" placeholder="Why this belongs here (optional)" />
|
||
<input type="email" class="suggest-input" id="suggest-email" placeholder="Your email (optional — for follow-up)" />
|
||
<div style="display:flex;align-items:center;gap:10px;margin-top:4px">
|
||
<button class="suggest-btn" onclick="submitSuggestion()">Submit suggestion</button>
|
||
<span id="suggest-result" style="font-size:.78rem;color:#6b7f99"></span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="card" style="border-color:#1a2e4a44;margin-top:32px">
|
||
<div class="card-title" style="color:#6b7f99">Steward access</div>
|
||
<p style="font-size:.82rem;color:#6b7f99;margin-bottom:14px">Are you the steward or a collaborator for this agent? Enter your email to receive a secure link to your corpus portal.</p>
|
||
<div style="display:flex;gap:8px;flex-wrap:wrap;align-items:center">
|
||
<input type="email" id="steward-email" placeholder="your@email.com" style="flex:1;min-width:200px;background:#060e1c;border:1px solid #1a2e44;border-radius:5px;padding:8px 10px;color:#d4cfbb;font-family:inherit;font-size:.82rem;box-sizing:border-box">
|
||
<button id="steward-access-btn" onclick="requestPortfolioAccess()" style="padding:8px 16px;background:#1a2e4a;color:#c9b87e;border:1px solid #c9b87e44;border-radius:5px;font-size:.82rem;font-weight:600;cursor:pointer;font-family:inherit;white-space:nowrap">Send my link →</button>
|
||
</div>
|
||
<div id="steward-access-result" style="margin-top:8px;font-size:.8rem;color:#6b7f99"></div>
|
||
</div>
|
||
</div>
|
||
|
||
<script>
|
||
async function ask() {
|
||
const q = document.getElementById('q').value.trim();
|
||
if (!q) return;
|
||
const resp = document.getElementById('resp');
|
||
resp.style.display = 'block';
|
||
resp.textContent = 'Consulting…';
|
||
try {
|
||
const r = await fetch('/api/agentify/experts/${esc(slug)}/consult', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ question: q, session_id: crypto.randomUUID() })
|
||
});
|
||
const d = await r.json();
|
||
if (d.error === 'rate_limit') { resp.textContent = 'Rate limit reached — please try again in ' + (d.retry_after || 'a little while') + '.'; return; }
|
||
const text = d.answer || d.response || d.error || JSON.stringify(d, null, 2);
|
||
resp.innerHTML = (typeof marked !== 'undefined') ? marked.parse(text) : text.replace(/&/g,'&').replace(/</g,'<').replace(/\n/g,'<br>');
|
||
} catch(e) {
|
||
resp.textContent = 'Error: ' + e.message;
|
||
}
|
||
}
|
||
document.getElementById('q')?.addEventListener('keydown', e => { if (e.key === 'Enter' && e.metaKey) ask(); });
|
||
|
||
async function requestPortfolioAccess() {
|
||
const emailEl = document.getElementById('steward-email');
|
||
const result = document.getElementById('steward-access-result');
|
||
const btn = document.getElementById('steward-access-btn');
|
||
const email = emailEl.value.trim();
|
||
if (!email || !email.includes('@')) {
|
||
result.style.cssText = 'margin-top:10px;font-size:.85rem;color:#f87171;font-weight:500';
|
||
result.textContent = 'Please enter a valid email address.';
|
||
return;
|
||
}
|
||
result.style.cssText = 'margin-top:8px;font-size:.82rem;color:#6b7f99';
|
||
result.textContent = 'Sending…';
|
||
if (btn) { btn.disabled = true; btn.textContent = 'Sending…'; }
|
||
try {
|
||
const r = await fetch('/api/agentify-help/portfolio-access', {
|
||
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ email })
|
||
});
|
||
const d = await r.json();
|
||
if (d.ok && d.sent !== false) {
|
||
result.style.cssText = 'margin-top:12px;font-size:.88rem;color:#22c55e;font-weight:600;padding:10px 14px;background:rgba(34,197,94,0.08);border:1px solid rgba(34,197,94,0.25);border-radius:6px;display:block';
|
||
result.textContent = '✓ Link sent! Check your inbox — and your spam folder if you don\'t see it within a minute.';
|
||
if (btn) { btn.disabled = true; btn.textContent = 'Sent ✓'; btn.style.opacity = '0.6'; }
|
||
emailEl.value = '';
|
||
} else {
|
||
result.style.cssText = 'margin-top:10px;font-size:.85rem;color:#f87171;font-weight:500';
|
||
result.textContent = d.message || 'No steward account found for that email.';
|
||
if (btn) { btn.disabled = false; btn.textContent = 'Send my link →'; }
|
||
}
|
||
} catch(e) {
|
||
result.style.cssText = 'margin-top:10px;font-size:.85rem;color:#f87171;font-weight:500';
|
||
result.textContent = 'Network error — please try again.';
|
||
if (btn) { btn.disabled = false; btn.textContent = 'Send my link →'; }
|
||
}
|
||
}
|
||
|
||
function toggleSuggest() {
|
||
const body = document.getElementById('suggest-body');
|
||
const btn = document.getElementById('suggest-toggle-btn');
|
||
const open = body.style.display !== 'none' && body.style.display !== '';
|
||
body.style.display = open ? 'none' : 'block';
|
||
btn.textContent = open ? '+ Suggest a source' : '× Cancel';
|
||
}
|
||
|
||
async function submitSuggestion() {
|
||
const url = document.getElementById('suggest-url').value.trim();
|
||
const title = document.getElementById('suggest-title').value.trim();
|
||
const notes = document.getElementById('suggest-notes').value.trim();
|
||
const email = document.getElementById('suggest-email').value.trim();
|
||
const result = document.getElementById('suggest-result');
|
||
if (!title) { result.textContent = 'Please enter a title or description.'; result.style.color = '#f87171'; return; }
|
||
result.textContent = 'Submitting…'; result.style.color = '#6b7f99';
|
||
try {
|
||
const r = await fetch('/api/agentify-help/suggest-source', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ slug: '${esc(slug)}', url, title, notes, email })
|
||
});
|
||
const d = await r.json();
|
||
if (d.ok) {
|
||
result.textContent = '✓ Thank you — suggestion received.'; result.style.color = '#22c55e';
|
||
document.getElementById('suggest-url').value = '';
|
||
document.getElementById('suggest-title').value = '';
|
||
document.getElementById('suggest-notes').value = '';
|
||
document.getElementById('suggest-email').value = '';
|
||
} else {
|
||
result.textContent = d.error || 'Submission failed.'; result.style.color = '#f87171';
|
||
}
|
||
} catch(e) { result.textContent = 'Network error.'; result.style.color = '#f87171'; }
|
||
}
|
||
</script>
|
||
</body></html>`);
|
||
});
|
||
|
||
// ── Steward portfolio: magic-link access ──────────────────────────────────
|
||
|
||
function makePortfolioToken(email: string): string {
|
||
const payload = Buffer.from(JSON.stringify({ email, ts: Date.now() })).toString("base64url");
|
||
const sig = crypto.createHmac("sha256", ADMIN_KEY).update(payload).digest("base64url");
|
||
return `${payload}.${sig}`;
|
||
}
|
||
|
||
function verifyPortfolioToken(token: string): { email: string; ts: number } | null {
|
||
try {
|
||
const [payload, sig] = token.split(".");
|
||
if (!payload || !sig) return null;
|
||
const expected = crypto.createHmac("sha256", ADMIN_KEY).update(payload).digest("base64url");
|
||
if (expected !== sig) return null;
|
||
const data = JSON.parse(Buffer.from(payload, "base64url").toString());
|
||
if (Date.now() - data.ts > 24 * 3600 * 1000) return null; // 24h TTL
|
||
return data;
|
||
} catch { return null; }
|
||
}
|
||
|
||
// POST /api/agentify-help/portfolio-access — send magic link
|
||
app.post("/api/agentify-help/portfolio-access", express.json({ limit: "1mb" }), async (req: Request, res: Response, next) => {
|
||
const host = (req.headers["x-forwarded-host"] || req.headers.host || "").toString().split(",")[0].trim().replace(/:\d+$/, "").replace(/^www\./, "").toLowerCase();
|
||
if (!host.includes("agentify") || host.startsWith("skills.")) return next();
|
||
const { email } = req.body ?? {};
|
||
if (!email || !email.includes("@")) return res.status(400).json({ error: "Valid email required" });
|
||
try {
|
||
const { rows } = await pool.query(
|
||
`SELECT subject_slug, subject_name, status, dashboard_token FROM agentify_subject_registry
|
||
WHERE (LOWER(steward_email) = LOWER($1) OR LOWER($1) = ANY(COALESCE(co_steward_emails, '{}')))
|
||
AND status != 'verify_email'
|
||
ORDER BY created_at DESC`, [email]
|
||
);
|
||
if (rows.length === 0) {
|
||
return res.json({ ok: true, sent: false, message: "No confirmed stewardships found for that email. Check your registration and verify your email first." });
|
||
}
|
||
const token = makePortfolioToken(email);
|
||
const portfolioUrl = `https://agentify.help/my-agents?token=${token}`;
|
||
const expertList = rows.map((r: any) => {
|
||
const dashLink = r.dashboard_token ? `<a href="https://agentify.help/steward/${r.subject_slug}?token=${r.dashboard_token}" style="color:#c9b87e;font-weight:600">Open corpus portal →</a>` : "";
|
||
return `<li style="margin-bottom:14px"><strong>${r.subject_name}</strong> <span style="color:#888;font-size:.82rem">(${r.status})</span><br><a href="https://agentify.help/agents/${r.subject_slug}" style="color:#4499ff;font-size:.82rem">View public page</a>${dashLink ? " · " + dashLink : ""}</li>`;
|
||
}).join("");
|
||
await resend.emails.send({
|
||
from: "Agentify Registry <ody@wellspr.ing>",
|
||
to: email,
|
||
subject: `Your steward portfolio — Agentify.Help`,
|
||
html: `<!DOCTYPE html><html><body style="font-family:Georgia,serif;max-width:580px;margin:0 auto;padding:2rem;color:#1a1a1a;background:#fff">
|
||
<p style="font-size:0.85rem;color:#888;margin-bottom:1.5rem;text-transform:uppercase;letter-spacing:.08em">Agentify.Help Registry</p>
|
||
<h2 style="font-size:1.3rem;margin-bottom:0.5rem">Your steward portfolio</h2>
|
||
<p style="color:#444;line-height:1.6">You are stewarding ${rows.length} expert${rows.length !== 1 ? "s" : ""} on Agentify.Help. Here are your portals:</p>
|
||
<ul style="padding-left:1.25rem;margin:1.5rem 0;color:#333;line-height:1.7">${expertList}</ul>
|
||
<p style="margin:1.75rem 0"><a href="${portfolioUrl}" style="display:inline-block;padding:0.7rem 1.4rem;background:#2563eb;color:#fff;text-decoration:none;border-radius:6px;font-size:0.95rem;font-weight:600">Open my portfolio page →</a></p>
|
||
<p style="font-size:0.82rem;color:#888;line-height:1.5">This link expires in 24 hours. If you did not request this email, you can safely ignore it.</p>
|
||
</body></html>`
|
||
});
|
||
res.json({ ok: true, sent: true, count: rows.length });
|
||
} catch (e: any) {
|
||
console.error("[Portfolio] Error:", e.message);
|
||
res.status(500).json({ error: e.message });
|
||
}
|
||
});
|
||
|
||
// POST /api/agentify-help/suggest-source — accept corpus gap suggestions from public
|
||
app.post("/api/agentify-help/suggest-source", express.json({ limit: "64kb" }), async (req: Request, res: Response, next) => {
|
||
const host = (req.headers["x-forwarded-host"] || req.headers.host || "").toString().split(",")[0].trim().replace(/:\d+$/, "").replace(/^www\./, "").toLowerCase();
|
||
if (!host.includes("agentify") || host.startsWith("skills.")) return next();
|
||
const { slug, url, title, notes, email } = req.body ?? {};
|
||
if (!slug || !title?.trim()) return res.status(400).json({ error: "slug and title are required" });
|
||
try {
|
||
await pool.query(
|
||
`INSERT INTO agentify_source_suggestions (expert_slug, url, title, notes, submitter_email)
|
||
VALUES ($1, $2, $3, $4, $5)`,
|
||
[slug, url?.trim() || null, title.trim(), notes?.trim() || null, email?.trim() || null]
|
||
);
|
||
res.json({ ok: true });
|
||
} catch (e: any) {
|
||
res.status(500).json({ error: "Failed to save suggestion" });
|
||
}
|
||
});
|
||
|
||
// POST /api/agentify-help/propose-topic — submit a new topic for taxonomy review
|
||
app.post("/api/agentify-help/propose-topic", express.json({ limit: "64kb" }), async (req: Request, res: Response, next) => {
|
||
const host = (req.headers["x-forwarded-host"] || req.headers.host || "").toString().split(",")[0].trim().replace(/:\d+$/, "").replace(/^www\./, "").toLowerCase();
|
||
if (!host.includes("agentify") || host.startsWith("skills.")) return next();
|
||
const { category, display_name, description, scope_note, email } = req.body ?? {};
|
||
if (!display_name?.trim() || !category?.trim()) return res.status(400).json({ error: "category and display_name are required" });
|
||
const slug = display_name.trim().toLowerCase()
|
||
.replace(/[^a-z0-9\s-]/g, "").replace(/\s+/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
|
||
try {
|
||
const existing = await pool.query(`SELECT id, status FROM agentify_topic_taxonomy WHERE slug = $1`, [slug]);
|
||
if (existing.rows.length > 0) {
|
||
return res.status(409).json({ error: "A topic with that name already exists in the taxonomy", slug, status: existing.rows[0].status });
|
||
}
|
||
await pool.query(
|
||
`INSERT INTO agentify_topic_taxonomy (category, slug, display_name, description, scope_note, proposed_by_email, status)
|
||
VALUES ($1, $2, $3, $4, $5, $6, 'proposed')`,
|
||
[category.trim(), slug, display_name.trim(), description?.trim() || null, scope_note?.trim() || null, email?.trim() || null]
|
||
);
|
||
res.json({ ok: true, slug, message: "Topic proposal received — a steward will review it." });
|
||
} catch (e: any) {
|
||
res.status(500).json({ error: "Failed to save proposal" });
|
||
}
|
||
});
|
||
|
||
// GET /api/agentify-help/topic-taxonomy — public taxonomy listing
|
||
app.get("/api/agentify-help/topic-taxonomy", async (req: Request, res: Response, next) => {
|
||
const host = (req.headers["x-forwarded-host"] || req.headers.host || "").toString().split(",")[0].trim().replace(/:\d+$/, "").replace(/^www\./, "").toLowerCase();
|
||
if (!host.includes("agentify") || host.startsWith("skills.")) return next();
|
||
const { rows } = await pool.query(`
|
||
SELECT t.category, t.slug, t.display_name, t.description, t.scope_note, t.audience_personas, t.status,
|
||
r.status AS agent_status, r.subject_slug
|
||
FROM agentify_topic_taxonomy t
|
||
LEFT JOIN agentify_subject_registry r ON r.taxonomy_slug = t.slug
|
||
WHERE t.status IN ('approved','active')
|
||
ORDER BY t.category ASC, t.display_name ASC
|
||
`).catch(() => ({ rows: [] }));
|
||
res.json({ taxonomy: rows });
|
||
});
|
||
|
||
// GET /my-agents — portfolio page (email form or token-based portfolio view)
|
||
app.get("/my-agents", async (req: Request, res: Response, next) => {
|
||
const host = (req.headers["x-forwarded-host"] || req.headers.host || "").toString().split(",")[0].trim().replace(/:\d+$/, "").replace(/^www\./, "").toLowerCase();
|
||
if (!host.includes("agentify") || host.startsWith("skills.")) return next();
|
||
|
||
const token = (req.query.token as string) || "";
|
||
let portfolioHtml = "";
|
||
let email = "";
|
||
|
||
if (token) {
|
||
const decoded = verifyPortfolioToken(token);
|
||
if (!decoded) {
|
||
portfolioHtml = `<div class="card" style="border-color:#ef444444"><div class="card-title" style="color:#ef4444">Link expired or invalid</div><p style="font-size:.88rem;color:#8899bb;margin-bottom:12px">This portfolio link has expired (24-hour TTL) or is invalid. Enter your email below to get a fresh link.</p></div>`;
|
||
} else {
|
||
email = decoded.email;
|
||
const { rows } = await pool.query(
|
||
`SELECT r.subject_slug, r.subject_name, r.status, r.dashboard_token,
|
||
cb.chunk_count, cb.status AS bundle_status
|
||
FROM agentify_subject_registry r
|
||
LEFT JOIN agentify_corpus_bundles cb ON cb.expert_slug = r.subject_slug
|
||
WHERE LOWER(r.steward_email) = LOWER($1) AND r.status != 'verify_email'
|
||
ORDER BY r.created_at DESC`, [email]
|
||
).catch(() => ({ rows: [] }));
|
||
|
||
if (rows.length === 0) {
|
||
portfolioHtml = `<div class="card"><div class="card-title">No experts found</div><p style="font-size:.88rem;color:#8899bb">No confirmed stewardships found for <strong>${email}</strong>.</p></div>`;
|
||
} else {
|
||
const cards = rows.map((r: any) => {
|
||
const statusColor: any = { preview: "#c9b87e", live: "#22c55e", ready: "#22c55e", proposed: "#a78bfa", distilling: "#f59e0b" };
|
||
const sc = statusColor[r.bundle_status || r.status] || "#6b7280";
|
||
const initials = (r.subject_name || "?").split(" ").map((w: string) => w[0]).join("").slice(0,2).toUpperCase();
|
||
return `<div class="card" style="display:flex;align-items:flex-start;gap:16px;margin-bottom:14px">
|
||
<div style="width:44px;height:44px;flex-shrink:0;border-radius:50%;background:#1a2e4a;border:1px solid #c9b87e33;display:flex;align-items:center;justify-content:center;font-weight:700;color:#c9b87e;font-size:.95rem">${initials}</div>
|
||
<div style="flex:1">
|
||
<div style="font-size:.95rem;font-weight:600;color:#f0e8d0;margin-bottom:2px">${r.subject_name}</div>
|
||
<div style="font-size:.75rem;color:#6b7f99;margin-bottom:8px">
|
||
<span style="color:${sc}">● ${(r.bundle_status || r.status || "pending").replace(/_/g," ")}</span>
|
||
${r.chunk_count ? ` · ${Number(r.chunk_count).toLocaleString()} chunks` : ""}
|
||
</div>
|
||
<div style="display:flex;gap:8px;flex-wrap:wrap">
|
||
<a href="/agents/${r.subject_slug}" style="padding:5px 12px;background:#0d1f3c;color:#8899bb;border:1px solid #1a2e44;border-radius:5px;font-size:.75rem;text-decoration:none">Public page →</a>
|
||
${r.dashboard_token ? `<a href="/stewardship/${r.subject_slug}?token=${r.dashboard_token}" style="padding:5px 12px;background:#1a2e4a;color:#c9b87e;border:1px solid #c9b87e44;border-radius:5px;font-size:.75rem;font-weight:600;text-decoration:none">Corpus portal →</a>` : ""}
|
||
</div>
|
||
</div>
|
||
</div>`;
|
||
}).join("");
|
||
portfolioHtml = `<div style="margin-bottom:20px"><div class="card-title">Your experts (${rows.length})</div><p style="font-size:.82rem;color:#8899bb;margin-bottom:16px">Steward email: <strong style="color:#c9b87e">${email}</strong></p>${cards}</div>`;
|
||
}
|
||
}
|
||
}
|
||
|
||
const showForm = !token || !email;
|
||
return res.type("text/html").send(`<!DOCTYPE html>
|
||
<html lang="en"><head>
|
||
<meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
||
<title>My Agents — Agentify.Help</title>
|
||
<meta name="description" content="Access your steward portfolio on Agentify.Help. Find all the experts you steward and manage their corpus.">
|
||
<style>
|
||
*{box-sizing:border-box;margin:0;padding:0}
|
||
body{background:#07102a;color:#d4cfbb;font-family:"IBM Plex Sans","Inter",system-ui,sans-serif;min-height:100vh}
|
||
.hero{background:linear-gradient(135deg,#0a1830 0%,#0d1f3c 100%);border-bottom:1px solid #1a2e4a;padding:40px 28px}
|
||
.container{max-width:640px;margin:0 auto;padding:32px 20px}
|
||
.card{background:#0a1220;border:1px solid #1a2e44;border-radius:10px;padding:20px;margin-bottom:16px}
|
||
.card-title{font-size:.75rem;text-transform:uppercase;letter-spacing:.08em;color:#8899bb;margin-bottom:10px}
|
||
input[type=email]{width:100%;background:#060e1c;border:1px solid #1a2e44;border-radius:6px;padding:11px 14px;color:#d4cfbb;font-family:inherit;font-size:.9rem;outline:none}
|
||
input[type=email]:focus{border-color:#c9b87e66}
|
||
.btn{padding:10px 24px;background:#c9b87e;color:#07102a;border:none;border-radius:6px;font-weight:700;font-size:.88rem;cursor:pointer;margin-top:10px}
|
||
.btn:hover{background:#d4c48e}
|
||
.msg{font-size:.85rem;padding:10px 14px;border-radius:6px;margin-top:10px;display:none}
|
||
.msg.ok{background:#0d2a1a;color:#22c55e;border:1px solid #22c55e44}
|
||
.msg.err{background:#2a0d0d;color:#f87171;border:1px solid #f8717144}
|
||
a{color:#c9b87e;text-decoration:none}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div class="hero">
|
||
<div style="max-width:640px;margin:0 auto">
|
||
<a href="/" style="font-size:.78rem;color:#6b7f99;text-transform:uppercase;letter-spacing:.08em">← Agentify.Help</a>
|
||
<h1 style="font-size:1.6rem;color:#f0e8d0;margin-top:12px;margin-bottom:6px">My Agents</h1>
|
||
<p style="color:#8899bb;font-size:.9rem">Steward portfolio — find all the experts you manage</p>
|
||
</div>
|
||
</div>
|
||
<div class="container">
|
||
${portfolioHtml}
|
||
${showForm ? `
|
||
<div class="card">
|
||
<div class="card-title">Access your steward portfolio</div>
|
||
<p style="font-size:.85rem;color:#8899bb;margin-bottom:14px">Enter the email address you used when registering as a steward. We'll send you a secure link to your full portfolio.</p>
|
||
<input type="text" id="em" placeholder="your@email.com" autocomplete="email" inputmode="email">
|
||
<div id="msg" class="msg"></div>
|
||
<button class="btn" onclick="send()">Send my portfolio link →</button>
|
||
</div>
|
||
<script>
|
||
async function send() {
|
||
const em = document.getElementById('em').value.trim();
|
||
const msg = document.getElementById('msg');
|
||
if (!em || !em.includes('@')) { msg.className='msg err'; msg.style.display='block'; msg.textContent='Please enter a valid email address.'; return; }
|
||
msg.style.display='none';
|
||
try {
|
||
const r = await fetch('/api/agentify-help/portfolio-access', { method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify({email: em}) });
|
||
const d = await r.json();
|
||
if (d.ok && d.sent) { msg.className='msg ok'; msg.textContent='Link sent! Check your email — it expires in 24 hours.'; }
|
||
else if (d.ok && !d.sent) { msg.className='msg err'; msg.textContent = d.message || 'No confirmed stewardships found for that email.'; }
|
||
else { msg.className='msg err'; msg.textContent = d.error || 'Something went wrong.'; }
|
||
msg.style.display='block';
|
||
} catch(e) { msg.className='msg err'; msg.textContent='Network error — please try again.'; msg.style.display='block'; }
|
||
}
|
||
document.getElementById('em')?.addEventListener('keydown', e => { if (e.key === 'Enter') send(); });
|
||
</script>` : `
|
||
<div class="card" style="border-color:#1a2e4a44">
|
||
<div class="card-title" style="color:#6b7f99">Need a fresh link?</div>
|
||
<p style="font-size:.82rem;color:#6b7f99;margin-bottom:10px">Portfolio links expire after 24 hours. Enter your email below to get a new one.</p>
|
||
<a href="/my-agents" style="color:#c9b87e;font-size:.82rem">Request a new portfolio link →</a>
|
||
</div>`}
|
||
</div>
|
||
</body></html>`);
|
||
});
|
||
|
||
// ── Steward corpus portal: /stewardship/:slug?token=<dashboard_token> ────────
|
||
app.get("/stewardship/:slug", async (req: Request, res: Response, next) => {
|
||
const host = (req.headers["x-forwarded-host"] || req.headers.host || "").toString().split(",")[0].trim().replace(/:\d+$/, "").replace(/^www\./, "").toLowerCase();
|
||
if (!host.includes("agentify") || host.startsWith("skills.")) return next();
|
||
|
||
const { slug } = req.params;
|
||
const token = (req.query.token as string) || "";
|
||
|
||
if (!token) {
|
||
return res.type("text/html").send(`<!DOCTYPE html><html lang="en"><head><meta charset="UTF-8"><title>Access required — Agentify.Help</title></head><body style="background:#07102a;color:#d4cfbb;font-family:system-ui;padding:48px 24px;text-align:center"><h2 style="color:#ef4444">Access token required</h2><p style="color:#8899bb;margin-top:12px">Use the link from your steward portfolio email to access this page.</p><a href="/my-agents" style="color:#c9b87e;display:block;margin-top:20px">← Return to My Agents</a></body></html>`);
|
||
}
|
||
|
||
// Look up the expert by slug + dashboard_token
|
||
let row: any = null;
|
||
let bundles: any[] = [];
|
||
try {
|
||
const { rows } = await pool.query(
|
||
`SELECT r.id, r.subject_slug, r.subject_name, r.subject_domain, r.steward_email, r.steward_name,
|
||
r.status, r.host_url, r.dashboard_token, r.created_at
|
||
FROM agentify_subject_registry r
|
||
WHERE r.subject_slug = $1 AND r.dashboard_token = $2
|
||
LIMIT 1`, [slug, token]
|
||
);
|
||
if (rows.length === 0) {
|
||
return res.type("text/html").send(`<!DOCTYPE html><html lang="en"><head><meta charset="UTF-8"><title>Invalid token — Agentify.Help</title></head><body style="background:#07102a;color:#d4cfbb;font-family:system-ui;padding:48px 24px;text-align:center"><h2 style="color:#ef4444">Invalid or expired token</h2><p style="color:#8899bb;margin-top:12px">This corpus portal link is invalid. Return to your portfolio to get a fresh link.</p><a href="/my-agents" style="color:#c9b87e;display:block;margin-top:20px">← My Agents</a></body></html>`);
|
||
}
|
||
row = rows[0];
|
||
const { rows: bundleRows } = await pool.query(
|
||
`SELECT id, status, chunk_count, assembled_at, attestation_uri
|
||
FROM agentify_corpus_bundles WHERE expert_slug = $1 ORDER BY assembled_at DESC`, [slug]
|
||
);
|
||
bundles = bundleRows;
|
||
} catch (e: any) {
|
||
return res.status(500).type("text/html").send(`<body style="background:#07102a;color:#ef4444;padding:40px;font-family:system-ui">DB error: ${e.message}</body>`);
|
||
}
|
||
|
||
const totalChunks = bundles.reduce((s: number, b: any) => s + (Number(b.chunk_count) || 0), 0);
|
||
const statusColor: Record<string, string> = { preview: "#c9b87e", live: "#22c55e", ready: "#22c55e", proposed: "#a78bfa", distilling: "#f59e0b" };
|
||
const sc = statusColor[row.status] || "#6b7280";
|
||
const initials = (row.subject_name || "?").split(" ").map((w: string) => w[0]).join("").slice(0, 2).toUpperCase();
|
||
|
||
const bundleRows = bundles.map((b: any) => `
|
||
<tr>
|
||
<td style="padding:8px 12px;font-size:.8rem;color:#8899bb">${b.assembled_at ? new Date(b.assembled_at).toLocaleDateString("en-US",{month:"short",day:"numeric",year:"numeric"}) : "—"}</td>
|
||
<td style="padding:8px 12px;font-size:.8rem"><span style="color:${statusColor[b.status]||"#6b7280"}">● ${b.status||"pending"}</span></td>
|
||
<td style="padding:8px 12px;font-size:.8rem;color:#c9b87e;font-weight:600">${b.chunk_count ? Number(b.chunk_count).toLocaleString() : "—"}</td>
|
||
<td style="padding:8px 12px;font-size:.8rem;color:#6b7f99;max-width:220px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">${b.attestation_uri ? `<a href="${b.attestation_uri}" target="_blank" rel="noopener" style="color:#5b8ab5">attestation ↗</a>` : "—"}</td>
|
||
</tr>`).join("");
|
||
|
||
return res.type("text/html").send(`<!DOCTYPE html>
|
||
<html lang="en"><head>
|
||
<meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
||
<title>Corpus Portal — ${row.subject_name} — Agentify.Help</title>
|
||
<style>
|
||
*{box-sizing:border-box;margin:0;padding:0}
|
||
body{background:#07102a;color:#d4cfbb;font-family:"IBM Plex Sans","Inter",system-ui,sans-serif;min-height:100vh}
|
||
.hero{background:linear-gradient(135deg,#0a1830 0%,#0d1f3c 100%);border-bottom:1px solid #1a2e4a;padding:40px 28px}
|
||
.container{max-width:700px;margin:0 auto;padding:32px 20px}
|
||
.card{background:#0a1220;border:1px solid #1a2e44;border-radius:10px;padding:20px;margin-bottom:16px}
|
||
.card-title{font-size:.75rem;text-transform:uppercase;letter-spacing:.08em;color:#8899bb;margin-bottom:10px}
|
||
.badge{display:inline-block;padding:3px 10px;border-radius:12px;font-size:.72rem;font-weight:600;border:1px solid}
|
||
table{width:100%;border-collapse:collapse}
|
||
th{padding:8px 12px;font-size:.72rem;text-transform:uppercase;letter-spacing:.06em;color:#6b7f99;text-align:left;border-bottom:1px solid #1a2e44}
|
||
tr:not(:last-child) td{border-bottom:1px solid #0f1e34}
|
||
a.btn{display:inline-block;padding:9px 18px;border-radius:6px;font-size:.82rem;font-weight:600;text-decoration:none;cursor:pointer}
|
||
.btn-gold{background:#1a2e4a;color:#c9b87e;border:1px solid #c9b87e44}
|
||
.btn-ghost{background:transparent;color:#8899bb;border:1px solid #1a2e44}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div class="hero">
|
||
<div style="max-width:700px;margin:0 auto">
|
||
<a href="/my-agents?token=${token}" style="font-size:.78rem;color:#6b7f99;text-decoration:none">← My Agents</a>
|
||
<div style="display:flex;align-items:center;gap:16px;margin-top:20px">
|
||
<div style="width:52px;height:52px;border-radius:50%;background:#1a2e4a;border:1px solid #c9b87e33;display:flex;align-items:center;justify-content:center;font-weight:700;color:#c9b87e;font-size:1.1rem;flex-shrink:0">${initials}</div>
|
||
<div>
|
||
<h1 style="font-size:1.5rem;color:#f0e8d0;margin-bottom:4px">${row.subject_name}</h1>
|
||
<div style="font-size:.82rem;color:#6b7f99">${row.subject_domain || ""}</div>
|
||
</div>
|
||
</div>
|
||
<div style="margin-top:16px;display:flex;gap:10px;flex-wrap:wrap">
|
||
<span class="badge" style="color:${sc};border-color:${sc}44">● ${row.status}</span>
|
||
<span class="badge" style="color:#8899bb;border-color:#1a2e44">${totalChunks.toLocaleString()} chunks total</span>
|
||
<span class="badge" style="color:#8899bb;border-color:#1a2e44">${bundles.length} bundle${bundles.length!==1?"s":""}</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="container">
|
||
<div class="card">
|
||
<div class="card-title">Quick links</div>
|
||
<div style="display:flex;gap:10px;flex-wrap:wrap">
|
||
<a href="/agents/${slug}" class="btn btn-ghost" target="_blank" rel="noopener">Public agent page →</a>
|
||
<a href="mailto:ody@wellspr.ing?subject=Corpus+update+for+${encodeURIComponent(row.subject_name)}" class="btn btn-gold">Email corpus update</a>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="card">
|
||
<div class="card-title">Corpus bundles</div>
|
||
${bundles.length === 0 ? `<p style="font-size:.85rem;color:#6b7f99">No corpus bundles submitted yet. Email <a href="mailto:ody@wellspr.ing" style="color:#c9b87e">ody@wellspr.ing</a> with source material to begin distillation.</p>` : `
|
||
<div style="overflow-x:auto">
|
||
<table>
|
||
<thead><tr>
|
||
<th>Date</th><th>Status</th><th>Chunks</th><th>Source</th>
|
||
</tr></thead>
|
||
<tbody>${bundleRows}</tbody>
|
||
</table>
|
||
</div>
|
||
`}
|
||
</div>
|
||
|
||
<div class="card">
|
||
<div class="card-title">Steward details</div>
|
||
<div style="font-size:.82rem;color:#8899bb;line-height:1.8">
|
||
<div>Steward: <strong style="color:#d4cfbb">${row.steward_name || row.steward_email}</strong></div>
|
||
<div>Email: <strong style="color:#d4cfbb">${row.steward_email}</strong></div>
|
||
${row.host_url ? `<div>Deploy target: <a href="${row.host_url}" target="_blank" style="color:#5b8ab5">${row.host_url}</a></div>` : ""}
|
||
<div>Registered: <strong style="color:#d4cfbb">${new Date(row.created_at).toLocaleDateString("en-US",{month:"long",day:"numeric",year:"numeric"})}</strong></div>
|
||
</div>
|
||
</div>
|
||
|
||
<div style="margin-top:8px;font-size:.78rem;color:#6b7f99;text-align:center">
|
||
Questions? Email <a href="mailto:ody@wellspr.ing" style="color:#c9b87e">ody@wellspr.ing</a> · <a href="/my-agents" style="color:#6b7f99">Back to portfolio</a>
|
||
</div>
|
||
</div>
|
||
</body></html>`);
|
||
});
|
||
|
||
// ── Recent registrations feed ─────────────────────────────────────────────
|
||
app.get("/api/feed.json", async (req: Request, res: Response, next) => {
|
||
const host = (req.headers["x-forwarded-host"] || req.headers.host || "").toString().split(",")[0].trim().replace(/:\d+$/, "").replace(/^www\./, "").toLowerCase();
|
||
if (!host.includes("agentify") || host.startsWith("skills.")) return next();
|
||
const limit = Math.min(100, Math.max(1, Number(req.query.limit) || 20));
|
||
try {
|
||
const { rows } = await pool.query(`
|
||
SELECT id AS registration_number, subject_slug, subject_name, subject_domain,
|
||
steward_name, host_url, status, registered_via, created_at
|
||
FROM agentify_subject_registry
|
||
ORDER BY created_at DESC LIMIT $1
|
||
`, [limit]);
|
||
res.setHeader("Cache-Control", "public, max-age=30");
|
||
res.json({ feed: rows, count: rows.length, updated: new Date().toISOString() });
|
||
} catch (e: any) {
|
||
res.status(500).json({ error: e.message });
|
||
}
|
||
});
|
||
|
||
// ── MCP Streamable HTTP endpoint (JSON-RPC 2.0, spec 2025-03-26) ───────────
|
||
app.post("/mcp", express.json({ limit: "1mb" }), async (req: Request, res: Response, next) => {
|
||
const host = (req.headers["x-forwarded-host"] || req.headers.host || "").toString().split(",")[0].trim().replace(/:\d+$/, "").replace(/^www\./, "").toLowerCase();
|
||
if (!host.includes("agentify") || host.startsWith("skills.")) return next();
|
||
|
||
const ip = (req.headers["x-forwarded-for"] as string || req.socket.remoteAddress || "unknown").split(",")[0].trim();
|
||
|
||
const body = req.body;
|
||
if (!body || typeof body !== "object") {
|
||
return res.status(400).json({ jsonrpc: "2.0", id: null, error: { code: -32700, message: "Parse error" } });
|
||
}
|
||
|
||
const { jsonrpc, method, params, id } = body;
|
||
const ok = (result: any) => res.json({ jsonrpc: "2.0", id: id ?? null, result });
|
||
const err = (code: number, message: string) => res.json({ jsonrpc: "2.0", id: id ?? null, error: { code, message } });
|
||
|
||
res.setHeader("Content-Type", "application/json");
|
||
res.setHeader("Cache-Control", "no-store");
|
||
|
||
switch (method) {
|
||
case "initialize":
|
||
return ok({
|
||
protocolVersion: "2025-03-26",
|
||
serverInfo: { name: "agentify-help", version: "1.0.0" },
|
||
capabilities: { tools: {} }
|
||
});
|
||
|
||
case "notifications/initialized":
|
||
return res.status(204).end();
|
||
|
||
case "ping":
|
||
return ok({});
|
||
|
||
case "tools/list":
|
||
return ok({ tools: AH_MCP_TOOLS });
|
||
|
||
case "tools/call": {
|
||
const toolName = params?.name as string;
|
||
const args = params?.arguments ?? {};
|
||
if (!toolName) return err(-32602, "params.name is required");
|
||
// Rate-limit registrations from agents
|
||
if (toolName === "register" && !rateCheck(`mcp:${ip}`, 20, 3_600_000)) {
|
||
return ok({ content: [{ type: "text", text: JSON.stringify({ error: "Rate limit exceeded — 20 MCP registrations per IP per hour." }) }], isError: true });
|
||
}
|
||
try {
|
||
const result = await handleMcpTool(toolName, args);
|
||
return ok(result);
|
||
} catch (e: any) {
|
||
if (e?.code) return err(e.code, e.message);
|
||
return err(-32603, e?.message || "Internal error");
|
||
}
|
||
}
|
||
|
||
default:
|
||
return err(-32601, `Method not found: ${method}`);
|
||
}
|
||
});
|
||
|
||
// MCP GET endpoint — capability discovery (some clients probe this)
|
||
app.get("/mcp", (req: Request, res: Response, next) => {
|
||
const host = (req.headers["x-forwarded-host"] || req.headers.host || "").toString().split(",")[0].trim().replace(/:\d+$/, "").replace(/^www\./, "").toLowerCase();
|
||
if (!host.includes("agentify") || host.startsWith("skills.")) return next();
|
||
res.json({
|
||
endpoint: "https://agentify.help/mcp",
|
||
alt_endpoint: "https://mcp.agentify.help/",
|
||
transport: "http",
|
||
protocol: "JSON-RPC 2.0",
|
||
spec: "2025-03-26",
|
||
tools: AH_MCP_TOOLS.map(t => ({ name: t.name, description: t.description }))
|
||
});
|
||
});
|
||
|
||
// mcp.agentify.help — subdomain routing
|
||
// POST to root (or any path) → same as POST /mcp on main domain
|
||
// Agents can call: POST https://mcp.agentify.help/ OR POST https://agentify.help/mcp
|
||
app.post("/", express.json({ limit: "1mb" }), async (req: Request, res: Response, next) => {
|
||
const host = (req.headers["x-forwarded-host"] || req.headers.host || "").toString().split(",")[0].trim().replace(/:\d+$/, "").replace(/^www\./, "").toLowerCase();
|
||
if (!host.startsWith("mcp.")) return next();
|
||
|
||
const ip = (req.headers["x-forwarded-for"] as string || req.socket.remoteAddress || "unknown").split(",")[0].trim();
|
||
const body = req.body;
|
||
if (!body || typeof body !== "object") {
|
||
return res.status(400).json({ jsonrpc: "2.0", id: null, error: { code: -32700, message: "Parse error" } });
|
||
}
|
||
const { method, params, id } = body;
|
||
const ok = (result: any) => res.json({ jsonrpc: "2.0", id: id ?? null, result });
|
||
const err = (code: number, message: string) => res.json({ jsonrpc: "2.0", id: id ?? null, error: { code, message } });
|
||
res.setHeader("Content-Type", "application/json");
|
||
res.setHeader("Cache-Control", "no-store");
|
||
switch (method) {
|
||
case "initialize": return ok({ protocolVersion: "2025-03-26", serverInfo: { name: "agentify-help", version: "1.0.0" }, capabilities: { tools: {} } });
|
||
case "notifications/initialized": return res.status(204).end();
|
||
case "ping": return ok({});
|
||
case "tools/list": return ok({ tools: AH_MCP_TOOLS });
|
||
case "tools/call": {
|
||
const toolName = params?.name as string;
|
||
const args = params?.arguments ?? {};
|
||
if (!toolName) return err(-32602, "params.name is required");
|
||
if (toolName === "register" && !rateCheck(`mcp:${ip}`, 20, 3_600_000)) {
|
||
return ok({ content: [{ type: "text", text: JSON.stringify({ error: "Rate limit exceeded." }) }], isError: true });
|
||
}
|
||
try {
|
||
const result = await handleMcpTool(toolName, args);
|
||
return ok(result);
|
||
} catch (e: any) {
|
||
if (e?.code) return err(e.code, e.message);
|
||
return err(-32603, e?.message || "Internal error");
|
||
}
|
||
}
|
||
default: return err(-32601, `Method not found: ${method}`);
|
||
}
|
||
});
|
||
|
||
// mcp.agentify.help GET / — human-readable landing + machine discovery
|
||
app.get("/", (req: Request, res: Response, next) => {
|
||
const host = (req.headers["x-forwarded-host"] || req.headers.host || "").toString().split(",")[0].trim().replace(/:\d+$/, "").replace(/^www\./, "").toLowerCase();
|
||
if (!host.startsWith("mcp.")) return next();
|
||
res.setHeader("Cache-Control", "public, max-age=300");
|
||
// Accept: application/json → return discovery JSON
|
||
if ((req.headers.accept || "").includes("application/json")) {
|
||
return res.json({ ...AH_MCP_DISCOVERY, transport: [{ type: "http", url: "https://mcp.agentify.help/" }, { type: "http", url: "https://agentify.help/mcp" }] });
|
||
}
|
||
res.setHeader("Content-Type", "text/html; charset=utf-8");
|
||
res.send(`<!DOCTYPE html><html lang="en"><head>
|
||
<meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
||
<title>mcp.agentify.help — WellAgent Registry MCP</title>
|
||
<meta name="description" content="MCP endpoint for the Agentify.Help one-per-person WellAgent registry. POST JSON-RPC 2.0 to this URL.">
|
||
<style>body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;background:#FDFAF4;color:#1A2B4A;max-width:680px;margin:4rem auto;padding:0 1.5rem;line-height:1.65}
|
||
h1{font-size:1.6rem;font-weight:800;margin-bottom:.25rem}
|
||
.tag{display:inline-block;background:rgba(201,150,42,.15);color:#C9962A;border:1px solid rgba(201,150,42,.35);border-radius:4px;padding:.2rem .7rem;font-size:.78rem;letter-spacing:.08em;text-transform:uppercase;font-weight:700;margin-bottom:1.5rem}
|
||
pre{background:#F0EDE4;border:1px solid rgba(26,43,74,.12);border-radius:8px;padding:1.25rem 1.5rem;overflow-x:auto;font-size:.82rem}
|
||
code{background:#F0EDE4;padding:2px 5px;border-radius:3px;font-size:.85rem}
|
||
.tools{display:grid;gap:.75rem;margin:1.5rem 0}
|
||
.tool{background:#F5F0E6;border:1px solid rgba(26,43,74,.1);border-radius:8px;padding:1rem 1.25rem}
|
||
.tool-name{font-weight:700;color:#1A2B4A;font-size:.95rem}
|
||
.tool-desc{color:#4A5568;font-size:.88rem;margin-top:.25rem}
|
||
a{color:#C9962A}</style></head><body>
|
||
<div class="tag">MCP Endpoint</div>
|
||
<h1>Agentify.Help Registry</h1>
|
||
<p>This is the <strong>Model Context Protocol</strong> endpoint for the one-per-person WellAgent registry.
|
||
POST JSON-RPC 2.0 to this URL to check availability, register as steward, or browse the registry.</p>
|
||
<pre>POST https://mcp.agentify.help/
|
||
Content-Type: application/json
|
||
|
||
{"jsonrpc":"2.0","method":"tools/call","params":{"name":"check_availability",
|
||
"arguments":{"name":"Jane Smith"}},"id":1}</pre>
|
||
<h2 style="font-size:1.1rem;margin:2rem 0 .75rem">Available tools</h2>
|
||
<div class="tools">
|
||
${AH_MCP_TOOLS.map(t => `<div class="tool"><div class="tool-name">${t.name}</div><div class="tool-desc">${t.description.substring(0,120)}…</div></div>`).join('')}
|
||
</div>
|
||
<p style="font-size:.85rem;color:#8A9099;margin-top:2rem">
|
||
<a href="https://agentify.help">agentify.help</a> ·
|
||
<a href="https://agentify.help/.well-known/mcp.json">.well-known/mcp.json</a> ·
|
||
<a href="https://agentify.help/.well-known/openapi.json">OpenAPI spec</a> ·
|
||
<a href="https://agentify.help/llms.txt">llms.txt</a>
|
||
</p>
|
||
</body></html>`);
|
||
});
|
||
|
||
// ── Well-known: MCP discovery ─────────────────────────────────────────────
|
||
app.get("/.well-known/mcp.json", (req: Request, res: Response, next) => {
|
||
const host = (req.headers["x-forwarded-host"] || req.headers.host || "").toString().split(",")[0].trim().replace(/:\d+$/, "").replace(/^www\./, "").toLowerCase();
|
||
if (!host.includes("agentify") || host.startsWith("skills.")) return next();
|
||
res.setHeader("Cache-Control", "public, max-age=3600");
|
||
res.json(AH_MCP_DISCOVERY);
|
||
});
|
||
|
||
// ── Well-known: OpenAI plugin manifest ────────────────────────────────────
|
||
app.get("/.well-known/ai-plugin.json", (req: Request, res: Response, next) => {
|
||
const host = (req.headers["x-forwarded-host"] || req.headers.host || "").toString().split(",")[0].trim().replace(/:\d+$/, "").replace(/^www\./, "").toLowerCase();
|
||
if (!host.includes("agentify") || host.startsWith("skills.")) return next();
|
||
res.setHeader("Cache-Control", "public, max-age=3600");
|
||
res.json(AH_AI_PLUGIN);
|
||
});
|
||
|
||
// ── Well-known: OpenAPI spec ──────────────────────────────────────────────
|
||
app.get("/.well-known/openapi.json", (req: Request, res: Response, next) => {
|
||
const host = (req.headers["x-forwarded-host"] || req.headers.host || "").toString().split(",")[0].trim().replace(/:\d+$/, "").replace(/^www\./, "").toLowerCase();
|
||
if (!host.includes("agentify") || host.startsWith("skills.")) return next();
|
||
res.setHeader("Cache-Control", "public, max-age=3600");
|
||
res.json(AH_OPENAPI);
|
||
});
|
||
|
||
// ── Domain-keyed page router ─────────────────────────────────────────────
|
||
app.use(async (req: Request, res: Response, next) => {
|
||
if (req.method !== "GET" && req.method !== "HEAD") return next();
|
||
const hostCandidates = [
|
||
req.headers["x-forwarded-host"],
|
||
req.headers["x-geo-node-host"],
|
||
req.hostname,
|
||
req.headers.host,
|
||
].flatMap(h => {
|
||
if (!h) return [];
|
||
const v = (Array.isArray(h) ? h[0] : h) || "";
|
||
return v.toString().toLowerCase().split(",").map((s: string) => s.trim().replace(/:\d+$/, "").replace(/^www\./, ""));
|
||
});
|
||
|
||
const isAgentifyHelp = hostCandidates.some(h =>
|
||
(h.includes("agentify.help") || h.includes("agentify-help")) && !h.startsWith("skills.")
|
||
);
|
||
if (!isAgentifyHelp) return next();
|
||
|
||
const p = req.path.replace(/\/$/, "") || "/";
|
||
if (p.startsWith("/api/") || p.startsWith("/steward/") || p === "/personaforge") return next();
|
||
|
||
res.setHeader("Content-Type", "text/html; charset=utf-8");
|
||
res.setHeader("Cache-Control", "public, max-age=120");
|
||
|
||
if (p === "/llms.txt") {
|
||
res.setHeader("Content-Type", "text/plain; charset=utf-8");
|
||
return res.send(AGENTIFY_LLMS_TXT);
|
||
}
|
||
|
||
if (p === "/robots.txt") {
|
||
res.setHeader("Content-Type", "text/plain; charset=utf-8");
|
||
res.setHeader("Cache-Control", "public, max-age=86400");
|
||
return res.send([
|
||
"User-agent: *",
|
||
"Allow: /",
|
||
"",
|
||
"Sitemap: https://agentify.help/sitemap.xml",
|
||
"",
|
||
"# Machine-readable endpoints for AI agents",
|
||
"# llms.txt: https://agentify.help/llms.txt",
|
||
"# MCP: https://agentify.help/mcp (POST, JSON-RPC 2.0)",
|
||
"# MCP disco: https://agentify.help/.well-known/mcp.json",
|
||
"# OpenAPI: https://agentify.help/.well-known/openapi.json",
|
||
"# Registry: https://agentify.help/api/registry.json",
|
||
"# Feed: https://agentify.help/api/feed.json",
|
||
].join("\n"));
|
||
}
|
||
|
||
if (p === "/vcap") {
|
||
res.setHeader("Cache-Control", "public, max-age=600");
|
||
return res.send(`<!DOCTYPE html>
|
||
<html lang="en">
|
||
<head>
|
||
<meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
||
<title>VCAP — What it is, what it does, how to integrate it · Agentify.Help</title>
|
||
<meta name="description" content="VCAP (Vaulted Covenanted Agent Protocol) in plain language — and a developer starter kit for verifying attestations, reading conduct declarations, and integrating the Presence Token Protocol.">
|
||
<meta property="og:title" content="VCAP — The trust layer behind every WellAgent">
|
||
<meta property="og:description" content="A signed, permanent, publicly verifiable declaration of what an AI agent will and won't do. Plain language guide plus developer starter kit.">
|
||
<meta property="og:url" content="https://agentify.help/vcap">
|
||
<style>
|
||
*{box-sizing:border-box;margin:0;padding:0}
|
||
body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;background:#FDFAF4;color:#1A2B4A;line-height:1.7;font-size:.97rem}
|
||
a{color:#C9962A;text-decoration:none}a:hover{text-decoration:underline}
|
||
nav{background:#1A2B4A;padding:11px 28px;display:flex;align-items:center;gap:16px;flex-wrap:wrap}
|
||
.nav-brand{font-weight:800;font-size:.9rem;letter-spacing:.12em;color:#C9962A}
|
||
.nav-links a{color:#a8b8c8;font-size:.83rem;text-decoration:none}
|
||
.nav-links a:hover{color:#fff}
|
||
.nav-links{display:flex;gap:18px}
|
||
.wrap{max-width:740px;margin:0 auto;padding:0 24px}
|
||
.hero{padding:52px 24px 40px;max-width:740px;margin:0 auto}
|
||
.hero-tag{display:inline-block;background:rgba(201,150,42,.13);color:#C9962A;border:1px solid rgba(201,150,42,.3);border-radius:4px;padding:.22rem .75rem;font-size:.75rem;letter-spacing:.1em;text-transform:uppercase;font-weight:700;margin-bottom:1.1rem}
|
||
h1{font-size:2.05rem;font-weight:800;line-height:1.2;margin-bottom:.8rem}
|
||
h1 em{color:#C9962A;font-style:normal}
|
||
.lead{font-size:1.08rem;color:#3A4B64;max-width:600px;margin-bottom:2rem;line-height:1.75}
|
||
.cta-row{display:flex;gap:12px;flex-wrap:wrap}
|
||
.btn-primary{background:#1A2B4A;color:#fff;padding:.6rem 1.4rem;border-radius:6px;font-size:.88rem;font-weight:700;text-decoration:none}
|
||
.btn-primary:hover{background:#253d6a;text-decoration:none}
|
||
.btn-secondary{background:#F5F0E6;color:#1A2B4A;border:1px solid rgba(26,43,74,.18);padding:.6rem 1.4rem;border-radius:6px;font-size:.88rem;font-weight:600;text-decoration:none}
|
||
.btn-secondary:hover{background:#EDE7D8;text-decoration:none}
|
||
section{padding:44px 0;border-top:1px solid rgba(26,43,74,.09)}
|
||
.section-label{font-size:.72rem;font-weight:700;text-transform:uppercase;letter-spacing:.12em;color:#C9962A;margin-bottom:.6rem}
|
||
h2{font-size:1.45rem;font-weight:800;margin-bottom:1rem}
|
||
.three-col{display:grid;grid-template-columns:repeat(auto-fit,minmax(200px,1fr));gap:16px;margin-top:1.5rem}
|
||
.guarantee-card{background:#F5F0E6;border:1px solid rgba(26,43,74,.1);border-radius:10px;padding:1.25rem 1.4rem}
|
||
.guarantee-num{font-size:.68rem;font-weight:700;letter-spacing:.1em;text-transform:uppercase;color:#C9962A;margin-bottom:.45rem}
|
||
.guarantee-title{font-size:1rem;font-weight:700;color:#1A2B4A;margin-bottom:.4rem}
|
||
.guarantee-body{font-size:.88rem;color:#4A5568;line-height:1.6}
|
||
.two-col{display:grid;grid-template-columns:1fr 1fr;gap:20px;margin-top:1.5rem}
|
||
@media(max-width:560px){.two-col{grid-template-columns:1fr}}
|
||
.info-card{background:#F0EDE4;border:1px solid rgba(26,43,74,.1);border-radius:10px;padding:1.25rem 1.4rem}
|
||
.info-title{font-weight:700;font-size:.97rem;margin-bottom:.35rem}
|
||
.info-body{font-size:.87rem;color:#4A5568;line-height:1.6}
|
||
pre{background:#1A2B4A;color:#c8e6c9;border-radius:8px;padding:1.1rem 1.4rem;overflow-x:auto;font-size:.8rem;line-height:1.6;margin:1rem 0}
|
||
code{background:#F0EDE4;padding:2px 6px;border-radius:3px;font-size:.83rem;color:#1A2B4A}
|
||
.step{display:flex;gap:14px;align-items:flex-start;margin-bottom:1.25rem}
|
||
.step-num{background:#1A2B4A;color:#C9962A;font-weight:800;font-size:.8rem;min-width:28px;height:28px;border-radius:50%;display:flex;align-items:center;justify-content:center;flex-shrink:0;margin-top:2px}
|
||
.step-body{font-size:.91rem;color:#2A3A54;line-height:1.65}
|
||
.step-body strong{color:#1A2B4A}
|
||
.statement-box{background:#fff;border:1.5px solid rgba(201,150,42,.4);border-radius:10px;padding:1.5rem 1.75rem;margin:1.5rem 0}
|
||
.statement-box .s-label{font-size:.7rem;font-weight:700;letter-spacing:.12em;text-transform:uppercase;color:#C9962A;margin-bottom:.6rem}
|
||
.statement-box p{font-size:.92rem;color:#2A3A54;margin-bottom:.8rem;line-height:1.7}
|
||
.statement-box .link-row{display:flex;gap:10px;flex-wrap:wrap;margin-top:.6rem}
|
||
.statement-box .link-row a{background:#F5F0E6;border:1px solid rgba(26,43,74,.12);border-radius:5px;padding:.3rem .8rem;font-size:.82rem;font-weight:600;color:#1A2B4A;text-decoration:none}
|
||
.statement-box .link-row a:hover{background:#EDE7D8}
|
||
footer{border-top:1px solid rgba(26,43,74,.1);padding:28px 24px;font-size:.82rem;color:#8A9099;text-align:center}
|
||
footer a{color:#C9962A}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
|
||
<nav>
|
||
<span class="nav-brand">AGENTIFY<span style="color:#fff">.</span>HELP</span>
|
||
<div class="nav-links">
|
||
<a href="https://agentify.help">Home</a>
|
||
<a href="https://agentify.help#registry">Registry</a>
|
||
<a href="https://agentify.help/vcap" style="color:#fff;font-weight:700">VCAP</a>
|
||
<a href="https://agentify.help/mcp">MCP</a>
|
||
<a href="https://skills.agentify.help">Skills</a>
|
||
</div>
|
||
</nav>
|
||
|
||
<div class="hero">
|
||
<div class="hero-tag">Trust Infrastructure</div>
|
||
<h1>VCAP — the <em>promise</em><br>behind every WellAgent</h1>
|
||
<p class="lead">A signed, permanent, and publicly verifiable declaration of what an AI agent will and won't do — written before the first consultation, readable by anyone, revocable by the expert at any time.</p>
|
||
<div class="cta-row">
|
||
<a href="#dev" class="btn-primary">Developer Starter Kit</a>
|
||
<a href="https://wellspr.ing/protocols/vcap/rfc" class="btn-secondary" target="_blank" rel="noopener">Full RFC →</a>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- PLAIN LANGUAGE -->
|
||
<section>
|
||
<div class="wrap">
|
||
<div class="section-label">In plain language</div>
|
||
<h2>Three things VCAP guarantees.</h2>
|
||
<p style="color:#3A4B64;max-width:580px">No fine print. No operator-only system prompts. The conduct declaration is public, signed with a key anyone can verify, and permanently vaulted.</p>
|
||
<div class="three-col">
|
||
<div class="guarantee-card">
|
||
<div class="guarantee-num">Guarantee 01</div>
|
||
<div class="guarantee-title">Signed conduct</div>
|
||
<div class="guarantee-body">The agent's behavior is declared in a cryptographically signed document <em>before</em> the first consultation — not inferred from a private system prompt you can't inspect.</div>
|
||
</div>
|
||
<div class="guarantee-card">
|
||
<div class="guarantee-num">Guarantee 02</div>
|
||
<div class="guarantee-title">Named steward</div>
|
||
<div class="guarantee-body">A real person took responsibility for this agent by name. Their stewardship is in the attestation. If something is wrong, the chain of accountability is public.</div>
|
||
</div>
|
||
<div class="guarantee-card">
|
||
<div class="guarantee-num">Guarantee 03</div>
|
||
<div class="guarantee-title">Expert standing</div>
|
||
<div class="guarantee-body">The real expert whose knowledge this agent embodies has the right — at any time — to review the conduct declaration, request corrections, or revoke the agent entirely.</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- PTP -->
|
||
<section>
|
||
<div class="wrap">
|
||
<div class="section-label">Companion protocol</div>
|
||
<h2>PTP — Presence Token Protocol</h2>
|
||
<p style="color:#3A4B64;max-width:580px;margin-bottom:1.5rem">VCAP declares what an agent <em>is</em>. PTP controls what it <em>does</em> in the moment. A presence token is a short-lived, scoped permission — issued by the steward's infrastructure, requested by the agent before acting.</p>
|
||
<div class="two-col">
|
||
<div class="info-card">
|
||
<div class="info-title">What PTP prevents</div>
|
||
<div class="info-body">An agent acting outside its declared scope without an explicit token. No token, no action. The scope grammar (SGS) defines what verbs are valid — <code>consult</code>, <code>summarize</code>, <code>refer</code>, <code>prescribe</code> (gated) — so the agent cannot silently expand its authority.</div>
|
||
</div>
|
||
<div class="info-card">
|
||
<div class="info-title">What PTP enables</div>
|
||
<div class="info-body">Auditable consultation logs, per-session revocation, partner-gated capabilities, and fee structures enforced at the token layer rather than in application code. The token carries scope, expiry, and steward signature — all verifiable.</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- DEVELOPER STARTER KIT -->
|
||
<section id="dev">
|
||
<div class="wrap">
|
||
<div class="section-label">Developer Starter Kit</div>
|
||
<h2>Integrate VCAP in five steps.</h2>
|
||
<p style="color:#3A4B64;max-width:580px;margin-bottom:1.75rem">Everything is plain JSON over HTTPS. No SDK required. Start with step 1 and you're verifying attestations in under two minutes.</p>
|
||
|
||
<div class="step">
|
||
<div class="step-num">1</div>
|
||
<div class="step-body">
|
||
<strong>Fetch the protocol manifest</strong> — machine-readable index of every VCAP endpoint, signing key location, and supported scope verbs.
|
||
<pre>GET https://wellspr.ing/api/v1/vcap/manifest</pre>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="step">
|
||
<div class="step-num">2</div>
|
||
<div class="step-body">
|
||
<strong>Read an attestation</strong> — the agent's full signed conduct declaration. Substitute the agent's slug and version.
|
||
<pre>GET https://wellspr.ing/vault/agents/{slug}/attestation-{version}.json
|
||
# Example:
|
||
GET https://wellspr.ing/vault/agents/sniderman/attestation-1.0.0.json</pre>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="step">
|
||
<div class="step-num">3</div>
|
||
<div class="step-body">
|
||
<strong>Verify in one call</strong> — pass the session ID (returned when the attestation was minted) and get a signed verification response. Returns <code>valid: true/false</code>, the signer's key ID, and the declared scope list.
|
||
<pre>GET https://wellspr.ing/api/v1/vcap/attestations/{session_id}/verify
|
||
|
||
# Response shape:
|
||
{
|
||
"valid": true,
|
||
"agent_id": "vcap:wellspring:expert:sniderman:1.0.0",
|
||
"signed_by": "wellspring-signing-key-2026",
|
||
"scopes": ["consult","summarize","cite"],
|
||
"revoked": false,
|
||
"attestation_uri": "https://wellspr.ing/vault/agents/sniderman/..."
|
||
}</pre>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="step">
|
||
<div class="step-num">4</div>
|
||
<div class="step-body">
|
||
<strong>Request a presence token (PTP)</strong> — before your app calls the consult endpoint, request a scoped token. The token carries the permitted action verbs and an expiry.
|
||
<pre>POST https://wellspr.ing/api/v1/ptp/token
|
||
Content-Type: application/json
|
||
|
||
{
|
||
"agent_id": "vcap:wellspring:expert:sniderman:1.0.0",
|
||
"requested_scopes": ["consult"],
|
||
"requester_id": "your-platform-id",
|
||
"context": "longevityformen.org consultation"
|
||
}
|
||
|
||
# Returns: { "token": "ptp_...", "expires_at": "...", "granted_scopes": [...] }</pre>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="step">
|
||
<div class="step-num">5</div>
|
||
<div class="step-body">
|
||
<strong>Call the consult endpoint with your token</strong> — attach the PTP token as a Bearer header. The platform verifies scope before forwarding your prompt to the agent.
|
||
<pre>POST https://wellspr.ing/api/agentify/experts/{slug}/consult
|
||
Authorization: Bearer ptp_...
|
||
Content-Type: application/json
|
||
|
||
{ "query": "What is the clinical significance of discordant LDL-C and ApoB?" }</pre>
|
||
</div>
|
||
</div>
|
||
|
||
<div style="background:#F5F0E6;border-radius:10px;padding:1.25rem 1.5rem;margin-top:.5rem;font-size:.88rem;color:#3A4B64;line-height:1.7">
|
||
<strong style="color:#1A2B4A">Signing keys</strong> — public keys used to verify VCAP signatures are published at
|
||
<code>GET https://wellspr.ing/.well-known/vcap-signing-keys.json</code>. Rotate-aware: keys carry a <code>valid_from</code> / <code>valid_until</code> range. Verify the key was valid at the time the attestation was signed.
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- AGENT TAB COPY BLOCK -->
|
||
<section id="agent-tab">
|
||
<div class="wrap">
|
||
<div class="section-label">For builders on Naturologie / partner sites</div>
|
||
<h2>The AGENT tab statement.</h2>
|
||
<p style="color:#3A4B64;max-width:580px;margin-bottom:1.25rem">Copy this block into the AGENT tab of any expert profile page. Swap <code>{expertName}</code> and <code>{slug}</code> for the real values. All backlinks are live.</p>
|
||
|
||
<div class="statement-box">
|
||
<div class="s-label">Trust layer — copy block for AGENT tab</div>
|
||
<p><strong>This agent is signed under VCAP</strong> — the Vaulted Covenanted Agent Protocol. A cryptographically signed, publicly verifiable conduct declaration states exactly what this agent will and won't do, who built it, and {expertName}'s standing to review, correct, or revoke it at any time.</p>
|
||
<p>Consultations are governed by <strong>PTP</strong> (Presence Token Protocol), which enforces the agent's declared scope on every request. No action can be taken outside the signed scope without an explicit token.</p>
|
||
<div class="link-row">
|
||
<a href="https://agentify.help/vcap">What is VCAP? →</a>
|
||
<a href="https://agentify.help/vcap#dev">Developer integration →</a>
|
||
<a href="https://wellspr.ing/vault/agents/{slug}/attestation-1.0.0.json">View attestation →</a>
|
||
<a href="https://wellspr.ing/protocols/vcap/rfc">Full RFC →</a>
|
||
</div>
|
||
</div>
|
||
|
||
<p style="font-size:.85rem;color:#6A7A90;margin-top:.75rem">
|
||
The attestation link follows the pattern
|
||
<code>https://wellspr.ing/vault/agents/{slug}/attestation-{version}.json</code>.
|
||
For agents not yet deployed, link to
|
||
<code>https://agentify.help/vcap</code> only until the attestation is minted.
|
||
</p>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- RFC LINK -->
|
||
<section>
|
||
<div class="wrap" style="padding-bottom:20px">
|
||
<div class="section-label">Going deeper</div>
|
||
<h2>The full specification.</h2>
|
||
<p style="color:#3A4B64;max-width:560px;margin-bottom:1.25rem">VCAP 0.10 is an open draft. The RFC covers the vault design, attestation structure, revocation model, signing keys, SGS scope grammar, PTP token lifecycle, transparency log, and agentic use patterns. Public comment is open — AI agents invited.</p>
|
||
<div class="cta-row">
|
||
<a href="https://wellspr.ing/protocols/vcap/rfc" class="btn-primary" target="_blank" rel="noopener">Read VCAP 0.10 RFC →</a>
|
||
<a href="https://wellspr.ing/api/v1/vcap/manifest" class="btn-secondary" target="_blank" rel="noopener">Protocol manifest (JSON) →</a>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<footer>
|
||
<a href="https://agentify.help">agentify.help</a> ·
|
||
<a href="https://wellspr.ing">WellSpr.ing</a> ·
|
||
<a href="https://skills.agentify.help">skills.agentify.help</a> ·
|
||
<a href="https://wellspr.ing/protocols/vcap/rfc">VCAP RFC</a>
|
||
<br><br>VCAP and PTP are open protocols governed by covenant. No patent claims. Build freely.
|
||
</footer>
|
||
</body>
|
||
</html>`);
|
||
}
|
||
|
||
if (p === "/sitemap.xml") {
|
||
res.setHeader("Content-Type", "application/xml; charset=utf-8");
|
||
res.setHeader("Cache-Control", "public, max-age=3600");
|
||
return res.send(`<?xml version="1.0" encoding="UTF-8"?>
|
||
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
||
<url><loc>https://agentify.help/</loc><changefreq>daily</changefreq><priority>1.0</priority></url>
|
||
<url><loc>https://agentify.help/vcap</loc><changefreq>weekly</changefreq><priority>0.9</priority></url>
|
||
<url><loc>https://agentify.help/llms.txt</loc><changefreq>weekly</changefreq><priority>0.8</priority></url>
|
||
<url><loc>https://agentify.help/my-agents</loc><changefreq>never</changefreq><priority>0.5</priority></url>
|
||
<url><loc>https://agentify.help/agents/guy-kawasaki</loc><changefreq>weekly</changefreq><priority>0.8</priority></url>
|
||
<url><loc>https://agentify.help/api/registry.json</loc><changefreq>hourly</changefreq><priority>0.7</priority></url>
|
||
<url><loc>https://agentify.help/api/feed.json</loc><changefreq>always</changefreq><priority>0.7</priority></url>
|
||
<url><loc>https://agentify.help/.well-known/mcp.json</loc><changefreq>monthly</changefreq><priority>0.6</priority></url>
|
||
<url><loc>https://agentify.help/.well-known/openapi.json</loc><changefreq>monthly</changefreq><priority>0.6</priority></url>
|
||
</urlset>`);
|
||
}
|
||
|
||
try {
|
||
const { rows } = await pool.query(
|
||
`SELECT subject_name, subject_domain, steward_name, host_url, status FROM agentify_subject_registry ORDER BY created_at ASC LIMIT 100`
|
||
);
|
||
return res.send(buildHomePage(rows));
|
||
} catch (e) {
|
||
return res.send(buildHomePage([]));
|
||
}
|
||
});
|
||
|
||
console.log("[Agentify.Help] agentify.help routes registered");
|
||
}
|