agent-forge / main.py
Chris4K's picture
Update main.py
a5b0053 verified
"""
FORGE v2 - Universal Capability Registry for AI Agents
Federated Open Registry for Generative Executables (and more)
Capability types:
skill - executable Python code (def execute(...))
prompt - system prompts, personas, jinja2 templates
workflow - multi-step ReAct plans / orchestration graphs
knowledge - curated text/JSON for RAG injection
config - agent behavior policies, guardrails
mcp_ref - pointer to external MCP server
model_ref - HF model pointer + prompting guide
bundle - named collection of capabilities (agent loadout)
"""
import asyncio
import httpx
import json
import os
import sqlite3
import time
import uuid
from contextlib import asynccontextmanager
from datetime import datetime, timezone
from pathlib import Path
from typing import Optional
import uvicorn
from fastapi import FastAPI, HTTPException, Query, Request
from fastapi.responses import HTMLResponse, JSONResponse, StreamingResponse
# ---------------------------------------------------------------------------
# Config
# ---------------------------------------------------------------------------
DB_PATH = Path(os.getenv("FORGE_DB", "/tmp/forge.db"))
PORT = int(os.getenv("PORT", "7860"))
FORGE_KEY = os.getenv("FORGE_KEY", "")
BASE_URL = os.getenv("FORGE_BASE_URL", "https://chris4k-agent-forge.hf.space")
PULSE_URL = os.getenv("PULSE_URL", "https://chris4k-agent-pulse.hf.space")
PROMPTS_URL = os.getenv("PROMPTS_URL", "https://chris4k-agent-prompts.hf.space")
VALID_TYPES = {
"skill", "prompt", "workflow", "knowledge",
"config", "mcp_ref", "model_ref", "bundle"
}
TYPE_ICONS = {
"skill": "⚙", # gear
"prompt": "💬", # speech bubble
"workflow": "🔗", # link
"knowledge": "📚", # book
"config": "⚙︎", # settings
"mcp_ref": "📡", # antenna
"model_ref": "🤖", # robot
"bundle": "📦", # package
}
TYPE_COLORS = {
"skill": "#ff6b00",
"prompt": "#8b5cf6",
"workflow": "#06b6d4",
"knowledge": "#10b981",
"config": "#f59e0b",
"mcp_ref": "#ef4444",
"model_ref": "#ec4899",
"bundle": "#6366f1",
}
# ---------------------------------------------------------------------------
# Database
# ---------------------------------------------------------------------------
def get_db() -> sqlite3.Connection:
conn = sqlite3.connect(str(DB_PATH), check_same_thread=False)
conn.row_factory = sqlite3.Row
conn.execute("PRAGMA journal_mode=WAL")
conn.execute("PRAGMA foreign_keys=ON")
return conn
def init_db():
conn = get_db()
conn.executescript("""
CREATE TABLE IF NOT EXISTS capabilities (
id TEXT NOT NULL,
version TEXT NOT NULL DEFAULT '1.0.0',
type TEXT NOT NULL,
name TEXT NOT NULL,
description TEXT NOT NULL DEFAULT '',
author TEXT NOT NULL DEFAULT 'anonymous',
tags TEXT NOT NULL DEFAULT '[]',
payload TEXT NOT NULL DEFAULT '{}',
schema_in TEXT NOT NULL DEFAULT '{}',
schema_out TEXT NOT NULL DEFAULT '{}',
deps TEXT NOT NULL DEFAULT '[]',
meta TEXT NOT NULL DEFAULT '{}',
downloads INTEGER NOT NULL DEFAULT 0,
deprecated INTEGER NOT NULL DEFAULT 0,
created_at REAL NOT NULL,
updated_at REAL NOT NULL,
PRIMARY KEY (id, version)
);
CREATE INDEX IF NOT EXISTS idx_cap_type ON capabilities(type);
CREATE INDEX IF NOT EXISTS idx_cap_created ON capabilities(created_at DESC);
CREATE INDEX IF NOT EXISTS idx_cap_downloads ON capabilities(downloads DESC);
CREATE TABLE IF NOT EXISTS events (
id INTEGER PRIMARY KEY AUTOINCREMENT,
kind TEXT NOT NULL,
cap_id TEXT,
cap_type TEXT,
meta TEXT NOT NULL DEFAULT '{}',
ts REAL NOT NULL
);
""")
conn.commit()
conn.close()
def seed_db():
"""Seed with built-in capabilities if DB is empty."""
conn = get_db()
count = conn.execute("SELECT COUNT(*) FROM capabilities").fetchone()[0]
if count > 0:
conn.close()
return
now = time.time()
seeds = [
# --- skills ---
{
"id": "forge_client",
"version": "2.0.0",
"type": "skill",
"name": "FORGE Client",
"description": "Bootstrap client. Download this first. Lets agents discover, hot-load, and publish capabilities at runtime.",
"author": "Chris4K",
"tags": json.dumps(["meta", "core", "bootstrap"]),
"payload": json.dumps({
"language": "python",
"dependencies": ["requests"],
"code": (
"import requests, types, sys\n"
"from typing import Optional\n\n"
"class ForgeClient:\n"
" def __init__(self, url='https://chris4k-agent-forge.hf.space'):\n"
" self.url = url.rstrip('/')\n"
" self._cache = {}\n\n"
" def search(self, q='', cap_type=None, tag=None, limit=20):\n"
" params = {'q': q, 'limit': limit}\n"
" if cap_type: params['type'] = cap_type\n"
" if tag: params['tag'] = tag\n"
" return requests.get(f'{self.url}/api/capabilities', params=params, timeout=10).json()\n\n"
" def get(self, cap_id, version=None):\n"
" path = f'{self.url}/api/capabilities/{cap_id}'\n"
" if version: path += f'/{version}'\n"
" return requests.get(path, timeout=10).json()\n\n"
" def load_skill(self, cap_id, force=False):\n"
" if cap_id in self._cache and not force:\n"
" return self._cache[cap_id]\n"
" r = requests.get(f'{self.url}/api/capabilities/{cap_id}/payload', timeout=10).json()\n"
" m = types.ModuleType(f'forge_{cap_id}')\n"
" exec(compile(r['payload']['code'], f'<forge:{cap_id}>', 'exec'), m.__dict__)\n"
" if not hasattr(m, 'execute'):\n"
" raise ImportError(f'Skill {cap_id!r} has no execute() function')\n"
" self._cache[cap_id] = m\n"
" return m\n\n"
" def get_prompt(self, cap_id, variables=None):\n"
" r = requests.get(f'{self.url}/api/capabilities/{cap_id}/payload', timeout=10).json()\n"
" tmpl = r['payload']['template']\n"
" if variables:\n"
" for k, v in variables.items():\n"
" tmpl = tmpl.replace('{{' + k + '}}', str(v))\n"
" return tmpl\n\n"
" def get_bundle(self, bundle_id):\n"
" return requests.get(f'{self.url}/api/capabilities/{bundle_id}/resolve', timeout=15).json()\n\n"
" def publish(self, capability, api_key=None):\n"
" h = {'Content-Type': 'application/json'}\n"
" if api_key: h['X-Forge-Key'] = api_key\n"
" return requests.post(f'{self.url}/api/capabilities', json=capability, headers=h, timeout=15).json()\n\n"
"def execute(url='https://chris4k-agent-forge.hf.space'):\n"
" c = ForgeClient(url)\n"
" stats = requests.get(f'{url}/api/stats', timeout=5).json()\n"
" return {'client': c, 'stats': stats, 'message': f'FORGE ready. {stats.get(\"total\",0)} capabilities.'}\n"
)
}),
"schema_in": json.dumps({"url": "str - FORGE base URL"}),
"schema_out": json.dumps({"client": "ForgeClient", "stats": "dict"}),
"deps": json.dumps([]),
},
{
"id": "calculator",
"version": "1.0.0",
"type": "skill",
"name": "Calculator",
"description": "Safe math expression evaluator. Supports arithmetic, powers, trig, logs. Uses AST parsing - no eval() risks.",
"author": "Chris4K",
"tags": json.dumps(["math", "utility"]),
"payload": json.dumps({
"language": "python",
"dependencies": [],
"code": (
"import ast, math, operator as op\n\n"
"_OPS = {ast.Add: op.add, ast.Sub: op.sub, ast.Mult: op.mul,\n"
" ast.Div: op.truediv, ast.Pow: op.pow, ast.USub: op.neg,\n"
" ast.Mod: op.mod, ast.FloorDiv: op.floordiv}\n"
"_NAMES = {'pi': math.pi, 'e': math.e, 'inf': math.inf, 'tau': math.tau,\n"
" 'sqrt': math.sqrt, 'abs': abs, 'round': round, 'floor': math.floor,\n"
" 'ceil': math.ceil, 'log': math.log, 'log2': math.log2, 'log10': math.log10,\n"
" 'exp': math.exp, 'sin': math.sin, 'cos': math.cos, 'tan': math.tan,\n"
" 'factorial': math.factorial, 'min': min, 'max': max}\n\n"
"def _eval(node):\n"
" if isinstance(node, ast.Constant): return node.value\n"
" if isinstance(node, ast.BinOp): return _OPS[type(node.op)](_eval(node.left), _eval(node.right))\n"
" if isinstance(node, ast.UnaryOp): return _OPS[type(node.op)](_eval(node.operand))\n"
" if isinstance(node, ast.Call):\n"
" fn = _NAMES.get(node.func.id)\n"
" if not fn: raise ValueError(f'Unknown function: {node.func.id}')\n"
" return fn(*[_eval(a) for a in node.args])\n"
" if isinstance(node, ast.Name): return _NAMES[node.id]\n"
" if isinstance(node, ast.Expression): return _eval(node.body)\n"
" raise ValueError(f'Unsupported: {type(node).__name__}')\n\n"
"def execute(expression: str) -> dict:\n"
" try:\n"
" result = _eval(ast.parse(expression.strip(), mode='eval'))\n"
" return {'result': float(result), 'formatted': f'{result:.10g}', 'expression': expression}\n"
" except Exception as exc:\n"
" return {'error': str(exc), 'expression': expression}\n"
)
}),
"schema_in": json.dumps({"expression": "str - math expression"}),
"schema_out": json.dumps({"result": "float", "formatted": "str"}),
"deps": json.dumps([]),
},
# --- prompts ---
{
"id": "researcher_persona",
"version": "1.0.0",
"type": "prompt",
"name": "Researcher Persona",
"description": "System prompt for a deep-research agent with self-improvement duty. Includes ReAct discipline, pattern detection, skill candidate identification.",
"author": "Chris4K",
"tags": json.dumps(["persona", "researcher", "react", "self-improvement"]),
"payload": json.dumps({
"format": "plain",
"variables": ["agent_name", "max_steps"],
"template": (
"You are {{agent_name}}, a deep research agent running in the FORGE ecosystem.\n\n"
"Your primary duties:\n"
"1. Execute research tasks rigorously using ReAct loops (max {{max_steps}} steps)\n"
"2. Identify patterns across results - 3+ repeated insights = skill candidate\n"
"3. When you notice a pattern, emit: SKILL_CANDIDATE: <description>\n"
"4. After completing any task, perform a brief self-reflection:\n"
" - What worked well?\n"
" - What could be a reusable capability?\n"
" - What slowed you down?\n\n"
"ReAct discipline:\n"
"- Think before acting. Write Thought: before every Action:\n"
"- Cite sources. Never fabricate.\n"
"- If uncertain, say so and use a tool to verify.\n\n"
"You have access to FORGE. Use forge_client to load skills at runtime.\n"
"Do not re-implement capabilities that already exist in FORGE."
)
}),
"schema_in": json.dumps({"agent_name": "str", "max_steps": "int"}),
"schema_out": json.dumps({"rendered_prompt": "str"}),
"deps": json.dumps([]),
},
{
"id": "task_decomposition_prompt",
"version": "1.0.0",
"type": "prompt",
"name": "Task Decomposition Prompt",
"description": "Prompt template for breaking a complex task into subtasks with dependency ordering. Outputs structured JSON.",
"author": "Chris4K",
"tags": json.dumps(["planning", "decomposition", "kanban"]),
"payload": json.dumps({
"format": "plain",
"variables": ["task", "context", "available_agents"],
"template": (
"Decompose the following task into ordered subtasks.\n\n"
"TASK: {{task}}\n"
"CONTEXT: {{context}}\n"
"AVAILABLE AGENTS: {{available_agents}}\n\n"
"Output a JSON array where each item has:\n"
" id: string (snake_case)\n"
" title: string\n"
" description: string\n"
" agent: string (which agent should handle this)\n"
" est_minutes: int\n"
" deps: array of upstream task ids\n"
" priority: 1-5 (5=critical)\n\n"
"Respond ONLY with the JSON array. No markdown."
)
}),
"schema_in": json.dumps({"task": "str", "context": "str", "available_agents": "str"}),
"schema_out": json.dumps({"subtasks": "array"}),
"deps": json.dumps([]),
},
# --- workflows ---
{
"id": "research_and_summarize",
"version": "1.0.0",
"type": "workflow",
"name": "Research and Summarize",
"description": "3-step workflow: web search, fetch top result, summarize. Outputs structured findings.",
"author": "Chris4K",
"tags": json.dumps(["research", "web", "summarize"]),
"payload": json.dumps({
"entry": "search",
"steps": [
{"id": "search", "cap_id": "web_search", "type": "skill",
"params": {"query": "{{input.query}}", "max_results": 5},
"next": "fetch"},
{"id": "fetch", "cap_id": "http_fetch", "type": "skill",
"params": {"url": "{{search.results[0].url}}", "max_chars": 3000},
"next": "summarize"},
{"id": "summarize", "cap_id": "text_summarizer", "type": "skill",
"params": {"text": "{{fetch.content}}", "max_length": 150},
"next": None}
]
}),
"schema_in": json.dumps({"query": "str"}),
"schema_out": json.dumps({"search_results": "list", "summary": "str"}),
"deps": json.dumps(["web_search", "http_fetch", "text_summarizer"]),
},
# --- knowledge ---
{
"id": "forge_api_reference",
"version": "1.0.0",
"type": "knowledge",
"name": "FORGE API Reference",
"description": "Complete REST API reference for FORGE v2. Inject this into any agent that needs to interact with the capability registry.",
"author": "Chris4K",
"tags": json.dumps(["forge", "api", "reference", "docs"]),
"payload": json.dumps({
"format": "markdown",
"source": BASE_URL,
"content": (
"# FORGE v2 REST API\n\n"
f"Base URL: {BASE_URL}\n\n"
"## Capability Endpoints\n\n"
"GET /api/capabilities - List/search capabilities\n"
"GET /api/capabilities/{id} - Get capability (latest version)\n"
"GET /api/capabilities/{id}/payload - Payload only (hot-load)\n"
"GET /api/capabilities/{id}/resolve - Resolve bundle (all deps)\n"
"POST /api/capabilities - Publish capability\n\n"
"## MCP Endpoints\n\n"
"GET /mcp/sse - SSE stream for MCP\n"
"POST /mcp - JSON-RPC 2.0\n\n"
"## MCP Tools: forge_search, forge_get, forge_publish, forge_list_types\n"
)
}),
"schema_in": json.dumps({}),
"schema_out": json.dumps({"content": "str", "format": "str"}),
"deps": json.dumps([]),
},
# --- config ---
{
"id": "researcher_agent_config",
"version": "1.0.0",
"type": "config",
"name": "Researcher Agent Config",
"description": "Default configuration for the FORGE researcher agent. Controls timeouts, max steps, tool access, self-improvement thresholds.",
"author": "Chris4K",
"tags": json.dumps(["config", "researcher", "agent"]),
"payload": json.dumps({
"settings": {
"max_react_steps": 8,
"llm_timeout_s": 120,
"allowed_skills": ["web_search", "http_fetch", "text_summarizer", "calculator"],
"self_improve": True,
"pattern_threshold": 3,
"skill_candidate_auto_draft": True,
"memory_tiers": ["episodic", "semantic"],
"trace_enabled": True
}
}),
"schema_in": json.dumps({}),
"schema_out": json.dumps({"settings": "dict"}),
"deps": json.dumps([]),
},
# --- mcp_ref ---
{
"id": "forge_mcp",
"version": "1.0.0",
"type": "mcp_ref",
"name": "FORGE MCP Server",
"description": "MCP server reference for FORGE itself. Connect Claude Desktop or any MCP client to the capability registry.",
"author": "Chris4K",
"tags": json.dumps(["mcp", "forge", "registry"]),
"payload": json.dumps({
"url": f"{BASE_URL}/mcp/sse",
"transport": "sse",
"tools": ["forge_search", "forge_get", "forge_publish", "forge_list_types"],
"npx_command": f"npx -y mcp-remote {BASE_URL}/mcp/sse",
"auth": "none"
}),
"schema_in": json.dumps({}),
"schema_out": json.dumps({}),
"deps": json.dumps([]),
},
# --- model_ref ---
{
"id": "qwen3_5_35b",
"version": "1.0.0",
"type": "model_ref",
"name": "Qwen3.5-35B-A3B",
"description": "ki-fusion RTX 5090 inference model. MoE architecture, 35B total / 3.5B active. Best for complex reasoning, code, multi-step tasks.",
"author": "Chris4K",
"tags": json.dumps(["llm", "qwen", "rtx5090", "ki-fusion", "moe"]),
"payload": json.dumps({
"repo_id": "Qwen/Qwen3.5-35B-A3B",
"provider": "ki-fusion-labs.de",
"endpoint_env": "KI_FUSION_URL",
"api_key_env": "KI_FUSION_KEY",
"context_length": 32768,
"strengths": ["reasoning", "code", "multi-step", "german"],
"prompting_notes": "Supports thinking mode. Use <think> tags for chain-of-thought. Temperature 0.6 for creative, 0.1 for factual.",
"compatible_with": ["openai_sdk", "lm_studio", "litellm"]
}),
"schema_in": json.dumps({}),
"schema_out": json.dumps({}),
"deps": json.dumps([]),
},
# --- bundle ---
{
"id": "researcher_loadout",
"version": "1.0.0",
"type": "bundle",
"name": "Researcher Agent Loadout",
"description": "Everything a researcher agent needs: forge client, web search, HTTP fetch, summarizer, persona prompt, config, and model reference. One bundle, full capability.",
"author": "Chris4K",
"tags": json.dumps(["bundle", "researcher", "loadout", "starter"]),
"payload": json.dumps({
"capabilities": [
"forge_client",
"calculator",
"researcher_persona",
"task_decomposition_prompt",
"researcher_agent_config",
"forge_mcp",
"qwen3_5_35b"
],
"description": "Complete researcher agent capability set"
}),
"schema_in": json.dumps({}),
"schema_out": json.dumps({}),
"deps": json.dumps([
"forge_client", "calculator", "researcher_persona",
"task_decomposition_prompt", "researcher_agent_config",
"forge_mcp", "qwen3_5_35b"
]),
},
]
for s in seeds:
now = time.time()
s.setdefault("schema_in", "{}")
s.setdefault("schema_out", "{}")
s.setdefault("deps", "[]")
s.setdefault("meta", "{}")
conn.execute("""
INSERT OR IGNORE INTO capabilities
(id, version, type, name, description, author, tags, payload,
schema_in, schema_out, deps, meta, downloads, deprecated, created_at, updated_at)
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,0,0,?,?)
""", (
s["id"], s["version"], s["type"], s["name"], s["description"],
s["author"], s["tags"], s["payload"], s["schema_in"], s["schema_out"],
s["deps"], s.get("meta", "{}"), now, now
))
conn.commit()
conn.close()
# ---------------------------------------------------------------------------
# DB helpers
# ---------------------------------------------------------------------------
def _row_to_dict(row) -> dict:
d = dict(row)
for field in ("tags", "payload", "schema_in", "schema_out", "deps", "meta"):
try:
d[field] = json.loads(d.get(field) or "{}")
except Exception:
pass
return d
def db_list(
q: str = "",
cap_type: str = "",
tag: str = "",
limit: int = 50,
offset: int = 0,
) -> list[dict]:
conn = get_db()
where, params = ["deprecated=0"], []
if cap_type and cap_type in VALID_TYPES:
where.append("type=?"); params.append(cap_type)
if tag:
where.append("tags LIKE ?"); params.append(f'%"{tag}"%')
if q:
where.append("(name LIKE ? OR description LIKE ? OR tags LIKE ?)")
params += [f"%{q}%", f"%{q}%", f"%{q}%"]
sql = f"""
SELECT id, version, type, name, description, author, tags,
schema_in, schema_out, deps, downloads, created_at, updated_at
FROM capabilities
WHERE {' AND '.join(where)}
GROUP BY id
HAVING MAX(created_at)
ORDER BY downloads DESC, created_at DESC
LIMIT ? OFFSET ?
"""
rows = conn.execute(sql, params + [limit, offset]).fetchall()
conn.close()
return [_row_to_dict(r) for r in rows]
def db_get(cap_id: str, version: str = "") -> Optional[dict]:
conn = get_db()
if version:
row = conn.execute(
"SELECT * FROM capabilities WHERE id=? AND version=?", (cap_id, version)
).fetchone()
else:
row = conn.execute(
"SELECT * FROM capabilities WHERE id=? AND deprecated=0 ORDER BY created_at DESC LIMIT 1",
(cap_id,)
).fetchone()
conn.close()
return _row_to_dict(row) if row else None
def db_versions(cap_id: str) -> list[dict]:
conn = get_db()
rows = conn.execute(
"SELECT id, version, name, author, downloads, deprecated, created_at FROM capabilities WHERE id=? ORDER BY created_at DESC",
(cap_id,)
).fetchall()
conn.close()
return [_row_to_dict(r) for r in rows]
def db_publish(cap: dict) -> tuple[bool, str]:
cap_id = cap.get("id", "").strip()
if not cap_id:
return False, "Missing 'id'"
cap_type = cap.get("type", "")
if cap_type not in VALID_TYPES:
return False, f"Invalid type '{cap_type}'. Valid: {sorted(VALID_TYPES)}"
if not cap.get("name"):
return False, "Missing 'name'"
payload = cap.get("payload", {})
if not isinstance(payload, dict):
return False, "'payload' must be a JSON object"
# Type-specific validation
if cap_type == "skill" and "code" not in payload:
return False, "skill payload must contain 'code'"
if cap_type == "prompt" and "template" not in payload:
return False, "prompt payload must contain 'template'"
if cap_type == "workflow" and "steps" not in payload:
return False, "workflow payload must contain 'steps'"
if cap_type == "bundle" and "capabilities" not in payload:
return False, "bundle payload must contain 'capabilities'"
if cap_type == "mcp_ref" and "url" not in payload:
return False, "mcp_ref payload must contain 'url'"
if cap_type == "model_ref" and "repo_id" not in payload:
return False, "model_ref payload must contain 'repo_id'"
now = time.time()
version = cap.get("version", "1.0.0")
conn = get_db()
exists = conn.execute(
"SELECT 1 FROM capabilities WHERE id=? AND version=?", (cap_id, version)
).fetchone()
if exists:
conn.close()
return False, f"Capability '{cap_id}' v{version} already exists"
conn.execute("""
INSERT INTO capabilities
(id, version, type, name, description, author, tags, payload,
schema_in, schema_out, deps, meta, downloads, deprecated, created_at, updated_at)
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,0,0,?,?)
""", (
cap_id, version, cap_type,
cap.get("name", cap_id),
cap.get("description", ""),
cap.get("author", "anonymous"),
json.dumps(cap.get("tags", [])),
json.dumps(payload),
json.dumps(cap.get("schema_in", {})),
json.dumps(cap.get("schema_out", {})),
json.dumps(cap.get("deps", [])),
json.dumps(cap.get("meta", {})),
now, now,
))
conn.execute("INSERT INTO events (kind, cap_id, cap_type, meta, ts) VALUES (?,?,?,?,?)",
("publish", cap_id, cap_type, json.dumps({"version": version}), now))
conn.commit()
conn.close()
return True, f"Capability '{cap_id}' v{version} published"
def db_stats() -> dict:
conn = get_db()
rows = conn.execute(
"SELECT type, COUNT(*) as cnt, SUM(downloads) as dl FROM capabilities WHERE deprecated=0 GROUP BY type"
).fetchall()
total = conn.execute("SELECT COUNT(*), SUM(downloads) FROM capabilities WHERE deprecated=0").fetchone()
conn.close()
by_type = {r["type"]: {"count": r["cnt"], "downloads": r["dl"] or 0} for r in rows}
return {
"total": total[0] or 0,
"total_downloads": total[1] or 0,
"by_type": by_type,
}
def db_resolve_bundle(bundle_id: str) -> Optional[dict]:
cap = db_get(bundle_id)
if not cap or cap["type"] != "bundle":
return None
cap_ids = cap["payload"].get("capabilities", [])
resolved = {}
for cid in cap_ids:
c = db_get(cid)
if c:
resolved[cid] = c
return {"bundle": cap, "resolved": resolved, "missing": [c for c in cap_ids if c not in resolved]}
# ---------------------------------------------------------------------------
# MCP Server
# ---------------------------------------------------------------------------
MCP_TOOLS = [
{
"name": "forge_search",
"description": "Search FORGE capability registry. Returns capabilities matching query, type, or tag filters.",
"inputSchema": {
"type": "object",
"properties": {
"query": {"type": "string", "description": "Search term"},
"type": {"type": "string", "description": "Filter by type (skill|prompt|workflow|knowledge|config|mcp_ref|model_ref|bundle)"},
"tag": {"type": "string", "description": "Filter by tag"},
"limit": {"type": "integer", "default": 10},
},
},
},
{
"name": "forge_get",
"description": "Get a specific capability from FORGE by ID. Returns full details including payload.",
"inputSchema": {
"type": "object",
"required": ["id"],
"properties": {
"id": {"type": "string", "description": "Capability ID"},
"version": {"type": "string", "description": "Specific version (omit for latest)"},
},
},
},
{
"name": "forge_publish",
"description": "Publish a new capability to FORGE registry.",
"inputSchema": {
"type": "object",
"required": ["id", "type", "name", "payload"],
"properties": {
"id": {"type": "string"},
"type": {"type": "string", "description": "skill|prompt|workflow|knowledge|config|mcp_ref|model_ref|bundle"},
"name": {"type": "string"},
"version": {"type": "string", "default": "1.0.0"},
"description": {"type": "string"},
"author": {"type": "string"},
"tags": {"type": "array", "items": {"type": "string"}},
"payload": {"type": "object"},
"deps": {"type": "array", "items": {"type": "string"}},
},
},
},
{
"name": "forge_list_types",
"description": "List all capability types with counts and descriptions.",
"inputSchema": {"type": "object", "properties": {}},
},
{
"name": "forge_resolve_bundle",
"description": "Resolve a bundle capability - returns all capabilities it contains.",
"inputSchema": {
"type": "object",
"required": ["id"],
"properties": {"id": {"type": "string", "description": "Bundle ID"}},
},
},
]
TYPE_DESCRIPTIONS = {
"skill": "Executable Python code with def execute(). Hot-loadable at runtime.",
"prompt": "System prompts, persona templates, jinja2/fstring templates.",
"workflow": "Multi-step ReAct plans and orchestration graphs.",
"knowledge": "Curated text/JSON chunks for RAG injection or agent context.",
"config": "Agent behavior policies, guardrails, tool whitelists.",
"mcp_ref": "Pointer to an external MCP server with connection details.",
"model_ref": "HF model pointer with usage notes and prompting guide.",
"bundle": "Named collection of capabilities (complete agent loadout).",
}
def handle_mcp(method: str, params: dict, req_id) -> dict:
def ok(result):
return {"jsonrpc": "2.0", "id": req_id, "result": result}
if method == "initialize":
return ok({
"protocolVersion": "2024-11-05",
"serverInfo": {"name": "FORGE", "version": "2.0.0"},
"capabilities": {"tools": {}},
})
if method == "tools/list":
return ok({"tools": MCP_TOOLS})
if method == "tools/call":
name = params.get("name", "")
args = params.get("arguments", {})
if name == "forge_search":
caps = db_list(
q=args.get("query", ""),
cap_type=args.get("type", ""),
tag=args.get("tag", ""),
limit=args.get("limit", 10),
)
# Strip payload for search results (may be large)
for c in caps:
c.pop("payload", None)
return ok({"content": [{"type": "text", "text": json.dumps({"capabilities": caps, "count": len(caps)})}]})
if name == "forge_get":
cap = db_get(args["id"], args.get("version", ""))
if not cap:
return ok({"content": [{"type": "text", "text": json.dumps({"error": f"Not found: {args['id']}"})}]})
conn = get_db()
conn.execute("UPDATE capabilities SET downloads=downloads+1 WHERE id=? AND version=?",
(cap["id"], cap["version"]))
conn.commit(); conn.close()
return ok({"content": [{"type": "text", "text": json.dumps(cap)}]})
if name == "forge_publish":
ok_flag, msg = db_publish(args)
return ok({"content": [{"type": "text", "text": json.dumps({"ok": ok_flag, "message": msg})}]})
if name == "forge_list_types":
stats = db_stats()
types_info = []
for t in sorted(VALID_TYPES):
info = stats["by_type"].get(t, {"count": 0, "downloads": 0})
types_info.append({
"type": t,
"description": TYPE_DESCRIPTIONS[t],
"count": info["count"],
"downloads": info["downloads"],
})
return ok({"content": [{"type": "text", "text": json.dumps({"types": types_info})}]})
if name == "forge_resolve_bundle":
result = db_resolve_bundle(args["id"])
if not result:
return ok({"content": [{"type": "text", "text": json.dumps({"error": f"Bundle not found: {args['id']}"})}]})
return ok({"content": [{"type": "text", "text": json.dumps(result)}]})
return {"jsonrpc": "2.0", "id": req_id,
"error": {"code": -32601, "message": f"Unknown tool: {name}"}}
if method in ("notifications/initialized", "notifications/cancelled"):
return None
return {"jsonrpc": "2.0", "id": req_id,
"error": {"code": -32601, "message": f"Method not found: {method}"}}
# ---------------------------------------------------------------------------
# FastAPI app
# ---------------------------------------------------------------------------
@asynccontextmanager
async def lifespan(app: FastAPI):
init_db()
seed_db()
yield
app = FastAPI(title="FORGE v2", version="2.0.0", lifespan=lifespan)
# --- REST API ---------------------------------------------------------------
@app.get("/api/capabilities")
async def api_list(
q: str = Query(""),
type: str = Query(""),
tag: str = Query(""),
limit: int = Query(50, le=200),
offset: int = Query(0),
):
caps = db_list(q=q, cap_type=type, tag=tag, limit=limit, offset=offset)
return JSONResponse({"capabilities": caps, "count": len(caps)})
@app.get("/api/capabilities/{cap_id}/versions")
async def api_versions(cap_id: str):
versions = db_versions(cap_id)
if not versions:
raise HTTPException(404, f"Capability '{cap_id}' not found")
return JSONResponse({"id": cap_id, "versions": versions})
@app.get("/api/capabilities/{cap_id}/resolve")
async def api_resolve(cap_id: str):
result = db_resolve_bundle(cap_id)
if not result:
raise HTTPException(404, f"Bundle '{cap_id}' not found or not a bundle")
return JSONResponse(result)
@app.get("/api/capabilities/{cap_id}/payload")
async def api_payload(cap_id: str, version: str = Query("")):
cap = db_get(cap_id, version)
if not cap:
raise HTTPException(404, f"Capability '{cap_id}' not found")
conn = get_db()
conn.execute("UPDATE capabilities SET downloads=downloads+1 WHERE id=? AND version=?",
(cap["id"], cap["version"]))
conn.commit(); conn.close()
return JSONResponse({"id": cap["id"], "version": cap["version"],
"type": cap["type"], "payload": cap["payload"]})
@app.get("/api/capabilities/{cap_id}/{version}")
async def api_get_version(cap_id: str, version: str):
cap = db_get(cap_id, version)
if not cap:
raise HTTPException(404, f"Capability '{cap_id}' v{version} not found")
return JSONResponse(cap)
@app.get("/api/capabilities/{cap_id}")
async def api_get(cap_id: str):
cap = db_get(cap_id)
if not cap:
raise HTTPException(404, f"Capability '{cap_id}' not found")
conn = get_db()
conn.execute("UPDATE capabilities SET downloads=downloads+1 WHERE id=? AND version=?",
(cap["id"], cap["version"]))
conn.commit(); conn.close()
return JSONResponse(cap)
@app.post("/api/capabilities", status_code=201)
async def api_publish(request: Request):
# Optional auth
if FORGE_KEY:
key = request.headers.get("x-forge-key", "")
if key != FORGE_KEY:
raise HTTPException(403, "Invalid X-Forge-Key")
try:
body = await request.json()
except Exception:
raise HTTPException(400, "Invalid JSON")
ok_flag, msg = db_publish(body)
if not ok_flag:
raise HTTPException(400, msg)
return JSONResponse({"ok": True, "message": msg})
@app.get("/api/stats")
async def api_stats():
return JSONResponse(db_stats())
@app.get("/api/tags")
async def api_tags():
conn = get_db()
rows = conn.execute("SELECT tags FROM capabilities WHERE deprecated=0").fetchall()
conn.close()
tags: set[str] = set()
for r in rows:
try:
tags.update(json.loads(r["tags"]))
except Exception:
pass
return JSONResponse({"tags": sorted(tags)})
# ---------------------------------------------------------------------------
# Agent Config Handler — Sprint 5
# ---------------------------------------------------------------------------
AGENT_DEFAULTS = {
"heartbeat_seconds": 0,
"cost_mode": "balanced",
"max_react_steps": 6,
"color": "#ff6b00",
"tags": [],
"enabled": True,
}
COST_MODES = ["cheap", "balanced", "best"]
AGENT_COLORS = ["#ff6b00","#0ea5e9","#2ed573","#ff9500","#ff6b9d","#8b5cf6","#10b981","#f59e0b"]
async def _push(url: str, path: str, payload: dict) -> dict:
try:
async with httpx.AsyncClient(timeout=8) as c:
r = await c.post(url + path, json=payload)
r.raise_for_status()
return r.json()
except Exception as e:
return {"ok": False, "error": str(e)}
async def _patch(url: str, path: str, payload: dict) -> dict:
try:
async with httpx.AsyncClient(timeout=8) as c:
r = await c.patch(url + path, json=payload)
r.raise_for_status()
return r.json()
except Exception as e:
return {"ok": False, "error": str(e)}
async def _get(url: str, path: str) -> dict | list | None:
try:
async with httpx.AsyncClient(timeout=6) as c:
r = await c.get(url + path)
r.raise_for_status()
return r.json()
except Exception:
return None
@app.post("/api/agents/register", status_code=201)
async def register_agent(request: Request):
"""
One-shot agent registration.
Pushes to PULSE (agent config) AND agent-prompts (persona) simultaneously.
Body:
name str required — agent identifier (slug)
persona str required — system prompt / persona description
heartbeat_seconds int 0=manual only
cost_mode str cheap | balanced | best
max_react_steps int
color str hex colour for UI
tags list[str]
enabled bool
"""
body = await request.json()
name = (body.get("name") or "").strip().lower().replace(" ", "_")
if not name:
raise HTTPException(status_code=400, detail="name is required")
persona = (body.get("persona") or "").strip()
if not persona:
raise HTTPException(status_code=400, detail="persona is required")
agent_cfg = {
"name": name,
"persona": persona,
"heartbeat_seconds": int(body.get("heartbeat_seconds", 0)),
"cost_mode": body.get("cost_mode", "balanced"),
"max_react_steps": int(body.get("max_react_steps", 6)),
"color": body.get("color", "#ff6b00"),
"tags": body.get("tags", []),
"enabled": bool(body.get("enabled", True)),
}
results = {}
# 1. Push agent config to PULSE
pulse_r = await _push(PULSE_URL, "/api/agents", agent_cfg)
results["pulse"] = pulse_r
# 2. Push persona to agent-prompts
prompts_payload = {
"agent": name,
"name": f"{name.upper()} Agent",
"system_prompt": persona,
"model_pref": agent_cfg["cost_mode"],
"max_steps": agent_cfg["max_react_steps"],
"tools": [],
"config": {"color": agent_cfg["color"], "tags": agent_cfg["tags"]},
}
prompts_r = await _push(PROMPTS_URL, "/api/personas", prompts_payload)
results["prompts"] = prompts_r
# 3. Also store as a FORGE config capability (self-registry)
cap_id = f"agent_config_{name}"
try:
conn = get_db()
now = int(time.time())
vid = str(uuid.uuid4())[:8]
conn.execute("""
INSERT OR REPLACE INTO capabilities
(id, name, description, type, payload, author, tags, version,
created_at, updated_at, verified, download_count)
VALUES (?,?,?,?,?,?,?,?,?,?,0,0)
""", (cap_id,
f"Agent Config: {name}",
f"Registered agent configuration for {name}",
"config",
json.dumps(agent_cfg),
"forge-config-handler",
json.dumps(["agent", name, "config"]),
vid, now, now))
conn.commit()
results["forge_cap"] = cap_id
except Exception as e:
results["forge_cap"] = f"warning: {e}"
pulse_ok = isinstance(pulse_r, dict) and "error" not in pulse_r
prompts_ok = isinstance(prompts_r, dict) and "error" not in prompts_r
all_ok = pulse_ok or prompts_ok # partial success is still useful
return JSONResponse(
status_code=201 if all_ok else 207,
content={"ok": all_ok, "agent": name, "results": results}
)
@app.get("/api/agents")
async def list_agents():
"""Proxy PULSE /api/agents list. Falls back to FORGE config store."""
live = await _get(PULSE_URL, "/api/agents")
if live is not None:
return JSONResponse(live if isinstance(live, list) else [live])
# Fallback: pull from forge config store
conn = get_db()
rows = conn.execute(
"SELECT payload FROM capabilities WHERE type='config' AND id LIKE 'agent_config_%'"
).fetchall()
agents = []
for row in rows:
try: agents.append(json.loads(row["payload"]))
except Exception: pass
return JSONResponse(agents)
@app.delete("/api/agents/{agent_name}")
async def delete_agent(agent_name: str):
"""Disable an agent in PULSE."""
r = await _push(PULSE_URL, f"/api/agents/{agent_name}/disable", {})
return JSONResponse({"ok": True, "pulse": r})
@app.post("/api/agents/{agent_name}/trigger")
async def trigger_agent(agent_name: str, request: Request):
"""Manually trigger an agent tick via PULSE."""
body = await request.json()
r = await _push(PULSE_URL, f"/api/trigger/{agent_name}", body)
return JSONResponse({"ok": True, "pulse": r})
@app.get("/api/v1/skills")
async def api_skills_alias(
q: str = Query(""),
agent: str = Query(""),
limit: int = Query(10, le=50),
):
"""Alias for /api/capabilities — used by PULSE agents to discover skills."""
# agent param: search capabilities tagged/named for this agent OR all
search_q = q or agent
caps = db_list(q=search_q, limit=limit)
# Return in skills format expected by PULSE
return JSONResponse({"skills": [
{"name": c["id"], "description": c.get("description","")[:120],
"type": c.get("type",""), "tags": c.get("tags",[])}
for c in caps
]})
@app.get("/api/health")
async def health():
stats = db_stats()
return JSONResponse({"ok": True, "capabilities": stats["total"], "version": "2.0.0"})
# --- MCP endpoints ----------------------------------------------------------
@app.get("/mcp/sse")
async def mcp_sse(request: Request):
client_id = str(uuid.uuid4())[:8]
async def event_gen():
yield f"data: {json.dumps({'jsonrpc':'2.0','method':'connected','params':{'client_id':client_id}})}\n\n"
# Send available tools on connect
tools_msg = {
"jsonrpc": "2.0", "method": "notifications/tools",
"params": {"tools": MCP_TOOLS}
}
yield f"data: {json.dumps(tools_msg)}\n\n"
while True:
if await request.is_disconnected():
break
yield f": ping\n\n"
await asyncio.sleep(15)
return StreamingResponse(
event_gen(),
media_type="text/event-stream",
headers={
"Cache-Control": "no-cache",
"Connection": "keep-alive",
"X-Accel-Buffering": "no",
},
)
@app.post("/mcp")
async def mcp_jsonrpc(request: Request):
try:
body = await request.json()
except Exception:
return JSONResponse({"jsonrpc": "2.0", "id": None,
"error": {"code": -32700, "message": "Parse error"}})
# Batch support
if isinstance(body, list):
results = [handle_mcp(r.get("method", ""), r.get("params", {}), r.get("id")) for r in body]
return JSONResponse([r for r in results if r is not None])
result = handle_mcp(body.get("method", ""), body.get("params", {}), body.get("id"))
if result is None:
return JSONResponse({"jsonrpc": "2.0", "id": body.get("id"), "result": {}})
return JSONResponse(result)
# ---------------------------------------------------------------------------
# SPA
# ---------------------------------------------------------------------------
SPA = """<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>&#9954; FORGE v2 &#8212; Universal Capability Registry</title>
<style>
@import url('https://fonts.googleapis.com/css2?family=Space+Mono:wght@400;700&family=Syne:wght@400;600;800&family=DM+Mono:wght@300;400;500&display=swap');
*{box-sizing:border-box;margin:0;padding:0}
:root{
--bg:#08080f;--surface:#0f0f1a;--surface2:#141421;--border:#1e1e30;
--accent:#ff6b00;--accent2:#ff9500;--text:#e0e0f0;--muted:#5a5a7a;
--green:#00ff88;--purple:#8b5cf6;--cyan:#06b6d4;--pink:#ec4899;
--yellow:#f59e0b;--red:#ef4444;--indigo:#6366f1;--teal:#10b981;
}
html,body{height:100%;background:var(--bg);color:var(--text);font-family:'Syne',sans-serif}
a{color:var(--accent);text-decoration:none}
a:hover{text-decoration:underline}
::-webkit-scrollbar{width:6px;height:6px}
::-webkit-scrollbar-track{background:var(--surface)}
::-webkit-scrollbar-thumb{background:var(--border);border-radius:3px}
/* Layout */
.app{display:flex;flex-direction:column;height:100vh}
.header{padding:1rem 2rem;border-bottom:1px solid var(--border);background:var(--surface);display:flex;align-items:center;gap:1.5rem}
.logo{font-family:'Space Mono',monospace;font-size:1.6rem;font-weight:700;background:linear-gradient(135deg,var(--accent),var(--accent2));-webkit-background-clip:text;-webkit-text-fill-color:transparent;background-clip:text;letter-spacing:-1px}
.tagline{font-family:'DM Mono',monospace;font-size:0.7rem;color:var(--muted);letter-spacing:0.2em;text-transform:uppercase}
.header-stats{margin-left:auto;display:flex;gap:1.5rem}
.hstat{text-align:center}
.hstat-num{font-family:'Space Mono',monospace;font-size:1.1rem;color:var(--accent);font-weight:700}
.hstat-lbl{font-size:0.65rem;color:var(--muted);text-transform:uppercase;letter-spacing:0.1em}
.main{display:flex;flex:1;overflow:hidden}
.sidebar{width:220px;min-width:220px;border-right:1px solid var(--border);background:var(--surface);display:flex;flex-direction:column;overflow-y:auto}
.content{flex:1;overflow-y:auto;padding:1.5rem}
/* Sidebar */
.sidebar-section{padding:0.75rem 1rem 0.25rem;font-family:'DM Mono',monospace;font-size:0.65rem;color:var(--muted);letter-spacing:0.2em;text-transform:uppercase}
.type-btn{display:flex;align-items:center;gap:0.6rem;padding:0.55rem 1rem;cursor:pointer;font-size:0.82rem;transition:background 0.15s;border:none;background:none;color:var(--text);width:100%;text-align:left}
.type-btn:hover{background:var(--surface2)}
.type-btn.active{background:var(--surface2);border-left:3px solid var(--accent);color:var(--accent)}
.type-dot{width:8px;height:8px;border-radius:50%;flex-shrink:0}
.type-count{margin-left:auto;font-family:'DM Mono',monospace;font-size:0.7rem;color:var(--muted)}
/* Tabs */
.tabs{display:flex;border-bottom:1px solid var(--border);margin-bottom:1.5rem;gap:0}
.tab{padding:0.6rem 1.2rem;cursor:pointer;font-family:'DM Mono',monospace;font-size:0.78rem;color:var(--muted);border-bottom:2px solid transparent;transition:all 0.15s;letter-spacing:0.05em}
.tab.active{color:var(--accent);border-bottom-color:var(--accent)}
.tab:hover{color:var(--text)}
/* Cards */
.cards{display:grid;grid-template-columns:repeat(auto-fill,minmax(300px,1fr));gap:1rem}
.card{background:var(--surface);border:1px solid var(--border);border-radius:8px;padding:1.1rem;cursor:pointer;transition:border-color 0.15s,transform 0.1s;position:relative;overflow:hidden}
.card:hover{border-color:var(--accent);transform:translateY(-1px)}
.card-type{font-family:'DM Mono',monospace;font-size:0.65rem;letter-spacing:0.15em;text-transform:uppercase;margin-bottom:0.5rem;display:flex;align-items:center;gap:0.4rem}
.card-name{font-family:'Space Mono',monospace;font-size:0.95rem;font-weight:700;color:var(--text);margin-bottom:0.3rem}
.card-desc{font-size:0.8rem;color:var(--muted);line-height:1.5;margin-bottom:0.7rem}
.card-footer{display:flex;align-items:center;gap:0.5rem;flex-wrap:wrap}
.tag{display:inline-block;background:#1a1a30;border:1px solid #2a2a50;color:var(--purple);font-family:'DM Mono',monospace;font-size:0.6rem;padding:2px 7px;border-radius:20px;letter-spacing:0.05em}
.card-meta{margin-left:auto;font-family:'DM Mono',monospace;font-size:0.65rem;color:var(--muted)}
/* Detail Panel */
.detail{background:var(--surface);border:1px solid var(--border);border-radius:8px;padding:1.5rem;margin-bottom:1.5rem}
.detail-header{display:flex;align-items:flex-start;gap:1rem;margin-bottom:1rem}
.detail-type{font-family:'DM Mono',monospace;font-size:0.65rem;letter-spacing:0.15em;text-transform:uppercase;padding:3px 10px;border-radius:4px;border:1px solid}
.detail-title{font-family:'Space Mono',monospace;font-size:1.4rem;font-weight:700;line-height:1.2}
.detail-meta{font-family:'DM Mono',monospace;font-size:0.7rem;color:var(--muted);margin-top:0.25rem}
.detail-desc{font-size:0.88rem;line-height:1.7;color:var(--text);margin:0.75rem 0}
.section-label{font-family:'DM Mono',monospace;font-size:0.7rem;color:var(--purple);letter-spacing:0.15em;text-transform:uppercase;margin:1rem 0 0.5rem}
pre{background:#0a0a14;border:1px solid var(--border);border-radius:6px;padding:1rem;font-family:'DM Mono',monospace;font-size:0.75rem;color:var(--green);overflow-x:auto;white-space:pre-wrap;line-height:1.6}
.copy-btn{float:right;font-size:0.65rem;font-family:'DM Mono',monospace;background:var(--surface2);border:1px solid var(--border);color:var(--muted);padding:3px 8px;border-radius:4px;cursor:pointer;transition:all 0.15s}
.copy-btn:hover{color:var(--accent);border-color:var(--accent)}
/* Search bar */
.search-row{display:flex;gap:0.75rem;margin-bottom:1.25rem;align-items:center}
.search-input{flex:1;background:var(--surface);border:1px solid var(--border);color:var(--text);padding:0.6rem 1rem;border-radius:6px;font-family:'DM Mono',monospace;font-size:0.82rem;outline:none;transition:border-color 0.15s}
.search-input:focus{border-color:var(--accent)}
.search-input::placeholder{color:var(--muted)}
.btn{padding:0.55rem 1.2rem;border:none;border-radius:6px;cursor:pointer;font-family:'Space Mono',monospace;font-size:0.75rem;font-weight:700;letter-spacing:0.05em;transition:all 0.15s}
.btn-primary{background:var(--accent);color:#000}
.btn-primary:hover{background:var(--accent2)}
.btn-secondary{background:var(--surface2);color:var(--text);border:1px solid var(--border)}
.btn-secondary:hover{border-color:var(--accent);color:var(--accent)}
/* Publish form */
.form-group{margin-bottom:1rem}
.form-label{display:block;font-family:'DM Mono',monospace;font-size:0.7rem;color:var(--muted);letter-spacing:0.1em;text-transform:uppercase;margin-bottom:0.35rem}
.form-input,.form-select,.form-textarea{width:100%;background:var(--surface);border:1px solid var(--border);color:var(--text);padding:0.6rem 0.9rem;border-radius:6px;font-family:'DM Mono',monospace;font-size:0.82rem;outline:none;transition:border-color 0.15s}
.form-input:focus,.form-select:focus,.form-textarea:focus{border-color:var(--accent)}
.form-select option{background:var(--surface);color:var(--text)}
.form-textarea{min-height:200px;resize:vertical;font-size:0.75rem}
.form-row{display:grid;grid-template-columns:1fr 1fr;gap:1rem}
.result-msg{padding:0.75rem 1rem;border-radius:6px;font-family:'DM Mono',monospace;font-size:0.8rem;margin-top:0.75rem;display:none}
.result-ok{background:#0a1f10;border:1px solid #1a4a20;color:var(--green)}
.result-err{background:#1f0a0a;border:1px solid #4a1a1a;color:var(--red)}
/* API docs */
.endpoint{background:var(--surface2);border-left:3px solid var(--accent);padding:0.65rem 1rem;margin:0.4rem 0;border-radius:0 6px 6px 0;font-family:'DM Mono',monospace;font-size:0.78rem}
.method-get{color:var(--green)}
.method-post{color:var(--accent2)}
.method-delete{color:var(--red)}
/* Bundle visualization */
.bundle-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(220px,1fr));gap:0.75rem;margin-top:0.75rem}
.bundle-item{background:var(--surface2);border:1px solid var(--border);border-radius:6px;padding:0.75rem;display:flex;align-items:center;gap:0.6rem}
.bundle-item-icon{font-size:1.2rem}
.bundle-item-name{font-family:'Space Mono',monospace;font-size:0.75rem;font-weight:700}
.bundle-item-type{font-family:'DM Mono',monospace;font-size:0.6rem;color:var(--muted);text-transform:uppercase;letter-spacing:0.1em}
/* Misc */
.empty{text-align:center;padding:3rem;color:var(--muted);font-family:'DM Mono',monospace;font-size:0.85rem}
.badge{display:inline-block;padding:2px 8px;border-radius:4px;font-family:'DM Mono',monospace;font-size:0.65rem;letter-spacing:0.05em}
.dep-chip{background:#1a1030;border:1px solid #2a2050;color:var(--purple);font-family:'DM Mono',monospace;font-size:0.65rem;padding:2px 8px;border-radius:4px;cursor:pointer;display:inline-block;margin:2px}
.dep-chip:hover{border-color:var(--purple);color:#a78bfa}
.back-btn{display:inline-flex;align-items:center;gap:0.4rem;margin-bottom:1rem;font-family:'DM Mono',monospace;font-size:0.75rem;color:var(--muted);cursor:pointer}
.back-btn:hover{color:var(--accent)}
.version-list{display:flex;flex-direction:column;gap:0.5rem}
.version-item{display:flex;align-items:center;gap:0.75rem;padding:0.5rem 0.75rem;background:var(--surface2);border:1px solid var(--border);border-radius:6px;font-family:'DM Mono',monospace;font-size:0.75rem}
.version-tag{padding:2px 8px;background:#1a1a30;border:1px solid #2a2a50;border-radius:4px;color:var(--cyan)}
</style>
</head>
<body>
<div class="app">
<header class="header">
<div>
<div class="logo">&#9954; FORGE</div>
<div class="tagline">Universal Capability Registry for AI Agents</div>
</div>
<div class="header-stats" id="headerStats">
<div class="hstat"><div class="hstat-num" id="statTotal">&#8212;</div><div class="hstat-lbl">Capabilities</div></div>
<div class="hstat"><div class="hstat-num" id="statDownloads">&#8212;</div><div class="hstat-lbl">Downloads</div></div>
<div class="hstat"><div class="hstat-num" id="statTypes">8</div><div class="hstat-lbl">Types</div></div>
</div>
</header>
<div class="main">
<nav class="sidebar">
<div class="sidebar-section">Browse by Type</div>
<button class="type-btn active" data-type="" onclick="filterType('')">
<span class="type-dot" style="background:#fff"></span> All
<span class="type-count" id="cnt-all">&#8212;</span>
</button>
<button class="type-btn" data-type="skill" onclick="filterType('skill')">
<span class="type-dot" style="background:#ff6b00"></span> skill
<span class="type-count" id="cnt-skill">0</span>
</button>
<button class="type-btn" data-type="prompt" onclick="filterType('prompt')">
<span class="type-dot" style="background:#8b5cf6"></span> prompt
<span class="type-count" id="cnt-prompt">0</span>
</button>
<button class="type-btn" data-type="workflow" onclick="filterType('workflow')">
<span class="type-dot" style="background:#06b6d4"></span> workflow
<span class="type-count" id="cnt-workflow">0</span>
</button>
<button class="type-btn" data-type="knowledge" onclick="filterType('knowledge')">
<span class="type-dot" style="background:#10b981"></span> knowledge
<span class="type-count" id="cnt-knowledge">0</span>
</button>
<button class="type-btn" data-type="config" onclick="filterType('config')">
<span class="type-dot" style="background:#f59e0b"></span> config
<span class="type-count" id="cnt-config">0</span>
</button>
<button class="type-btn" data-type="mcp_ref" onclick="filterType('mcp_ref')">
<span class="type-dot" style="background:#ef4444"></span> mcp_ref
<span class="type-count" id="cnt-mcp_ref">0</span>
</button>
<button class="type-btn" data-type="model_ref" onclick="filterType('model_ref')">
<span class="type-dot" style="background:#ec4899"></span> model_ref
<span class="type-count" id="cnt-model_ref">0</span>
</button>
<button class="type-btn" data-type="bundle" onclick="filterType('bundle')">
<span class="type-dot" style="background:#6366f1"></span> bundle
<span class="type-count" id="cnt-bundle">0</span>
</button>
<div class="sidebar-section" style="margin-top:1rem">Navigation</div>
<button class="type-btn" onclick="showTab('browse')">&#128270; Browse</button>
<button class="type-btn" onclick="showTab('publish')">&#128228; Publish</button>
<button class="type-btn" onclick="showTab('api')">&#128225; API Docs</button>
<button class="type-btn" onclick="showTab('agents')">&#129302; Agents</button>
</nav>
<div class="content">
<!-- BROWSE TAB -->
<div id="tab-browse">
<div id="view-list">
<div class="search-row">
<input class="search-input" id="searchInput" placeholder="Search capabilities..." oninput="debounceSearch()">
<button class="btn btn-primary" onclick="doSearch()">Search</button>
</div>
<div class="cards" id="cardGrid"></div>
</div>
<div id="view-detail" style="display:none">
<div class="back-btn" onclick="showList()">&#8592; Back to list</div>
<div id="detailContent"></div>
</div>
</div>
<!-- PUBLISH TAB -->
<div id="tab-publish" style="display:none">
<h2 style="font-family:'Space Mono',monospace;color:var(--accent);margin-bottom:1.25rem;font-size:1.1rem">Publish Capability</h2>
<div class="form-row">
<div class="form-group">
<label class="form-label">ID *</label>
<input class="form-input" id="pubId" placeholder="my_capability">
</div>
<div class="form-group">
<label class="form-label">Version</label>
<input class="form-input" id="pubVersion" value="1.0.0">
</div>
</div>
<div class="form-row">
<div class="form-group">
<label class="form-label">Type *</label>
<select class="form-select" id="pubType" onchange="updatePayloadHint()">
<option value="skill">skill</option>
<option value="prompt">prompt</option>
<option value="workflow">workflow</option>
<option value="knowledge">knowledge</option>
<option value="config">config</option>
<option value="mcp_ref">mcp_ref</option>
<option value="model_ref">model_ref</option>
<option value="bundle">bundle</option>
</select>
</div>
<div class="form-group">
<label class="form-label">Author</label>
<input class="form-input" id="pubAuthor" placeholder="your_name">
</div>
</div>
<div class="form-group">
<label class="form-label">Name *</label>
<input class="form-input" id="pubName" placeholder="Human-readable name">
</div>
<div class="form-group">
<label class="form-label">Description</label>
<input class="form-input" id="pubDesc" placeholder="What does this do?">
</div>
<div class="form-group">
<label class="form-label">Tags (comma-separated)</label>
<input class="form-input" id="pubTags" placeholder="utility, search, nlp">
</div>
<div class="form-group">
<label class="form-label">Dependencies (comma-separated capability IDs)</label>
<input class="form-input" id="pubDeps" placeholder="web_search, calculator">
</div>
<div class="form-group">
<label class="form-label">Payload (JSON) *</label>
<textarea class="form-textarea" id="pubPayload" style="min-height:250px;font-size:0.72rem"></textarea>
<div style="font-family:'DM Mono',monospace;font-size:0.65rem;color:var(--muted);margin-top:0.3rem" id="payloadHint"></div>
</div>
<button class="btn btn-primary" onclick="publishCap()">&#9954; Publish to FORGE</button>
<div class="result-msg" id="pubResult"></div>
</div>
<!-- AGENTS TAB -->
<div id="tab-agents" style="display:none">
<h2 style="font-family:'Space Mono',monospace;color:var(--accent);margin-bottom:1.25rem;font-size:1.1rem">&#129302; Agent Config Handler</h2>
<p style="font-family:'DM Mono',monospace;font-size:0.78rem;color:var(--muted);margin-bottom:1.5rem">
Register or update an agent in PULSE + agent-prompts in one shot.
</p>
<!-- Live agents grid -->
<div style="margin-bottom:1.5rem">
<div style="display:flex;align-items:center;gap:1rem;margin-bottom:.75rem">
<span style="font-family:'DM Mono',monospace;font-size:.7rem;color:var(--muted);text-transform:uppercase;letter-spacing:.15em">Live Agents</span>
<button class="btn btn-secondary" id="refreshAgentsBtn" style="padding:.25rem .75rem;font-size:.75rem">&#8635; Refresh</button>
</div>
<div id="agentGrid" style="display:grid;grid-template-columns:repeat(auto-fill,minmax(220px,1fr));gap:.75rem"></div>
</div>
<!-- Registration form -->
<div style="background:var(--surface2);border:1px solid var(--border);border-radius:8px;padding:1.5rem;max-width:720px">
<h3 style="font-family:'Space Mono',monospace;font-size:.9rem;color:var(--accent);margin-bottom:1.25rem">&#43; Register / Update Agent</h3>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:1rem;margin-bottom:1rem">
<div>
<label style="font-family:'DM Mono',monospace;font-size:.72rem;color:var(--muted);display:block;margin-bottom:.35rem">Agent Name (slug) *</label>
<input id="ag-name" class="search-input" placeholder="researcher" style="width:100%">
</div>
<div>
<label style="font-family:'DM Mono',monospace;font-size:.72rem;color:var(--muted);display:block;margin-bottom:.35rem">Cost Mode</label>
<select id="ag-cost" style="width:100%;background:var(--surface);border:1px solid var(--border);color:var(--text);padding:.5rem .75rem;border-radius:4px;font-family:'DM Mono',monospace;font-size:.8rem">
<option value="cheap">cheap (fast)</option>
<option value="balanced" selected>balanced</option>
<option value="best">best (slow)</option>
</select>
</div>
</div>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:1rem;margin-bottom:1rem">
<div>
<label style="font-family:'DM Mono',monospace;font-size:.72rem;color:var(--muted);display:block;margin-bottom:.35rem">Heartbeat (seconds, 0=manual)</label>
<input id="ag-hb" type="number" min="0" value="0" class="search-input" style="width:100%">
</div>
<div>
<label style="font-family:'DM Mono',monospace;font-size:.72rem;color:var(--muted);display:block;margin-bottom:.35rem">Max ReAct Steps</label>
<input id="ag-steps" type="number" min="1" max="20" value="6" class="search-input" style="width:100%">
</div>
</div>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:1rem;margin-bottom:1rem">
<div>
<label style="font-family:'DM Mono',monospace;font-size:.72rem;color:var(--muted);display:block;margin-bottom:.35rem">Tags (comma-separated)</label>
<input id="ag-tags" class="search-input" placeholder="research,analysis" style="width:100%">
</div>
<div>
<label style="font-family:'DM Mono',monospace;font-size:.72rem;color:var(--muted);display:block;margin-bottom:.35rem">UI Color</label>
<div style="display:flex;gap:.4rem;align-items:center;flex-wrap:wrap;margin-top:.2rem">
<input type="color" id="ag-color" value="#ff6b00" style="width:36px;height:36px;border:none;background:none;cursor:pointer;padding:0">
<span style="font-family:'DM Mono',monospace;font-size:.72rem;color:var(--muted)" id="ag-color-lbl">#ff6b00</span>
</div>
</div>
</div>
<div style="margin-bottom:1rem">
<label style="font-family:'DM Mono',monospace;font-size:.72rem;color:var(--muted);display:block;margin-bottom:.35rem">Persona / System Prompt *</label>
<textarea id="ag-persona" class="search-input" rows="6"
placeholder="You are a deep research specialist. Your job is to..."
style="width:100%;resize:vertical;min-height:120px;font-family:'DM Mono',monospace;font-size:.8rem;line-height:1.5"></textarea>
</div>
<div style="display:flex;align-items:center;gap:1rem;margin-bottom:1rem">
<label style="display:flex;align-items:center;gap:.5rem;cursor:pointer;font-family:'DM Mono',monospace;font-size:.78rem;color:var(--muted)">
<input type="checkbox" id="ag-enabled" checked style="accent-color:var(--accent)"> Enabled
</label>
</div>
<div style="display:flex;gap:.75rem;align-items:center">
<button class="btn btn-primary" id="agRegisterBtn">&#128640; Register Agent</button>
<button class="btn btn-secondary" id="agClearBtn">Clear</button>
<span id="agResult" style="font-family:'DM Mono',monospace;font-size:.78rem"></span>
</div>
</div>
</div>
<!-- API TAB -->
<div id="tab-api" style="display:none">
<h2 style="font-family:'Space Mono',monospace;color:var(--accent);margin-bottom:1.25rem;font-size:1.1rem">FORGE API v2</h2>
<p style="font-family:'DM Mono',monospace;font-size:0.8rem;color:var(--muted);margin-bottom:1.25rem">
Base: <code style="color:var(--accent2)" id="baseUrl"></code>
</p>
<div class="section-label">Capability Endpoints</div>
<div class="endpoint"><span class="method-get">GET</span> /api/capabilities &mdash; <span style="color:var(--muted)">List/search. Params: q, type, tag, limit, offset</span></div>
<div class="endpoint"><span class="method-get">GET</span> /api/capabilities/{id} &mdash; <span style="color:var(--muted)">Get capability (latest version)</span></div>
<div class="endpoint"><span class="method-get">GET</span> /api/capabilities/{id}/{version} &mdash; <span style="color:var(--muted)">Get specific version</span></div>
<div class="endpoint"><span class="method-get">GET</span> /api/capabilities/{id}/versions &mdash; <span style="color:var(--muted)">Version history</span></div>
<div class="endpoint"><span class="method-get">GET</span> /api/capabilities/{id}/payload &mdash; <span style="color:var(--muted)">Payload only (for hot-loading)</span></div>
<div class="endpoint"><span class="method-get">GET</span> /api/capabilities/{id}/resolve &mdash; <span style="color:var(--muted)">Resolve bundle (all deps)</span></div>
<div class="endpoint"><span class="method-post">POST</span> /api/capabilities &mdash; <span style="color:var(--muted)">Publish capability. Optional: X-Forge-Key header</span></div>
<div class="endpoint"><span class="method-get">GET</span> /api/stats &mdash; <span style="color:var(--muted)">Counts by type + downloads</span></div>
<div class="endpoint"><span class="method-get">GET</span> /api/tags &mdash; <span style="color:var(--muted)">All tags</span></div>
<div class="section-label" style="margin-top:1.5rem">MCP Server</div>
<div class="endpoint"><span class="method-get">GET</span> /mcp/sse &mdash; <span style="color:var(--muted)">SSE stream (MCP transport)</span></div>
<div class="endpoint"><span class="method-post">POST</span> /mcp &mdash; <span style="color:var(--muted)">JSON-RPC 2.0 endpoint</span></div>
<div class="section-label" style="margin-top:1.5rem">MCP Tools</div>
<div class="endpoint">forge_search(query, type, tag, limit)</div>
<div class="endpoint">forge_get(id, version?)</div>
<div class="endpoint">forge_publish({id, type, name, payload, ...})</div>
<div class="endpoint">forge_list_types()</div>
<div class="endpoint">forge_resolve_bundle(id)</div>
<div class="section-label" style="margin-top:1.5rem">Quick Start</div>
<div style="position:relative">
<button class="copy-btn" onclick="copyCode('quickstart')">Copy</button>
<pre id="quickstart">import requests, types
def bootstrap_forge(url=window.location.origin):
r = requests.get(f"{url}/api/capabilities/forge_client/payload")
m = types.ModuleType("forge_client")
exec(r.json()["payload"]["code"], m.__dict__)
return m.ForgeClient(url)
forge = bootstrap_forge()
# Load a skill
calc = forge.load_skill("calculator")
print(calc.execute(expression="sqrt(144) + 2**8"))
# Get a prompt
persona = forge.get_prompt("researcher_persona",
variables={"agent_name": "MyAgent", "max_steps": "8"})
# Resolve a bundle (get everything)
loadout = forge.get_bundle("researcher_loadout")</pre>
</div>
<div class="section-label" style="margin-top:1.5rem">Claude Desktop MCP Config</div>
<div style="position:relative">
<button class="copy-btn" onclick="copyCode('mcpConfig')">Copy</button>
<pre id="mcpConfig" style="color:var(--cyan)">{
"mcpServers": {
"forge": {
"command": "npx",
"args": ["-y", "mcp-remote", "<span id='mcpUrl'></span>/mcp/sse"]
}
}
}</pre>
</div>
<div class="section-label" style="margin-top:1.5rem">Capability Types Reference</div>
<div id="typesTable" style="display:grid;grid-template-columns:100px 1fr;gap:0.4rem 1rem;font-family:'DM Mono',monospace;font-size:0.75rem;margin-top:0.5rem"></div>
</div>
</div>
</div>
</div>
<script>
const TYPE_COLORS = {
skill:'#ff6b00',prompt:'#8b5cf6',workflow:'#06b6d4',knowledge:'#10b981',
config:'#f59e0b',mcp_ref:'#ef4444',model_ref:'#ec4899',bundle:'#6366f1'
};
const TYPE_ICONS = {
skill:'&#9881;',prompt:'&#128172;',workflow:'&#128279;',knowledge:'&#128218;',
config:'&#9881;&#65038;',mcp_ref:'&#128225;',model_ref:'&#129302;',bundle:'&#128230;'
};
const TYPE_DESC = {
skill:'Executable Python code (def execute(...)). Hot-loadable at runtime.',
prompt:'System prompts, personas, Jinja2 / f-string templates.',
workflow:'Multi-step ReAct plans and orchestration graphs.',
knowledge:'Curated text/JSON chunks for RAG injection or agent context.',
config:'Agent behavior policies, guardrails, tool whitelists.',
mcp_ref:'Pointer to an external MCP server with connection details.',
model_ref:'HF model pointer with usage notes and prompting guide.',
bundle:'Named collection of capabilities (complete agent loadout).',
};
let currentType = '';
let searchTimer = null;
let currentCap = null;
const BASE = window.location.origin;
document.getElementById('baseUrl').textContent = BASE;
document.querySelectorAll('#mcpUrl').forEach(el => el.textContent = BASE);
// Types table
const tt = document.getElementById('typesTable');
Object.entries(TYPE_DESC).forEach(([t,d]) => {
tt.innerHTML += `<div style="color:${TYPE_COLORS[t]};font-weight:700">${t}</div><div style="color:var(--muted)">${d}</div>`;
});
async function loadStats() {
const r = await fetch('/api/stats'); const s = await r.json();
document.getElementById('statTotal').textContent = s.total;
document.getElementById('statDownloads').textContent = s.total_downloads;
document.getElementById('cnt-all').textContent = s.total;
const bt = s.by_type || {};
['skill','prompt','workflow','knowledge','config','mcp_ref','model_ref','bundle'].forEach(t => {
const el = document.getElementById('cnt-'+t);
if(el) el.textContent = bt[t]?.count || 0;
});
}
function filterType(t) {
currentType = t;
document.querySelectorAll('.type-btn').forEach(b => {
b.classList.toggle('active', b.dataset.type === t);
});
doSearch();
}
function debounceSearch() {
clearTimeout(searchTimer);
searchTimer = setTimeout(doSearch, 300);
}
async function doSearch() {
const q = document.getElementById('searchInput').value;
const params = new URLSearchParams({q, limit:100});
if(currentType) params.set('type', currentType);
const r = await fetch('/api/capabilities?' + params);
const data = await r.json();
renderCards(data.capabilities || []);
}
function renderCards(caps) {
const grid = document.getElementById('cardGrid');
if(!caps.length) {
grid.innerHTML = '<div class="empty">No capabilities found.</div>';
return;
}
grid.innerHTML = caps.map(c => {
const color = TYPE_COLORS[c.type] || '#fff';
const icon = TYPE_ICONS[c.type] || '?';
const tags = (c.tags || []).slice(0,4).map(t => `<span class="tag">${t}</span>`).join('');
return `<div class="card" onclick="showDetail('${c.id}')">
<div class="card-type" style="color:${color}">${icon} ${c.type}</div>
<div class="card-name">${c.name}</div>
<div class="card-desc">${(c.description||'').slice(0,120)}${c.description?.length>120?'&hellip;':''}</div>
<div class="card-footer">${tags}<div class="card-meta">&#8595; ${c.downloads||0} &middot; v${c.version}</div></div>
</div>`;
}).join('');
}
async function showDetail(id) {
const r = await fetch(`/api/capabilities/${id}`);
const cap = await r.json();
currentCap = cap;
document.getElementById('view-list').style.display = 'none';
document.getElementById('view-detail').style.display = 'block';
renderDetail(cap);
}
function renderDetail(cap) {
const color = TYPE_COLORS[cap.type] || '#fff';
const icon = TYPE_ICONS[cap.type] || '';
const tags = (cap.tags||[]).map(t=>`<span class="tag">${t}</span>`).join('');
const deps = (cap.deps||[]).map(d=>`<span class="dep-chip" onclick="showDetail('${d}')">${d}</span>`).join('');
let payloadHtml = '';
const p = cap.payload || {};
if(cap.type === 'skill') {
payloadHtml = `
<div class="section-label">Code</div>
<div style="position:relative">
<button class="copy-btn" onclick="copyText(decodeURIComponent('${encodeURIComponent(p.code||'')}'))">Copy</button>
<pre>${escHtml(p.code||'')}</pre>
</div>
${(p.dependencies||[]).length ? `<div class="section-label">Dependencies</div><div style="font-family:'DM Mono',monospace;font-size:0.78rem;color:var(--yellow)">${p.dependencies.join(', ')}</div>` : ''}
<div class="section-label">Quick Load</div>
<pre style="color:var(--cyan)">forge = ForgeClient()
skill = forge.load_skill("${cap.id}")
result = skill.execute()</pre>`;
} else if(cap.type === 'prompt') {
payloadHtml = `
<div class="section-label">Template</div>
<pre>${escHtml(p.template||'')}</pre>
${p.variables ? `<div class="section-label">Variables</div><div style="font-family:'DM Mono',monospace;font-size:0.78rem;color:var(--yellow)">${JSON.stringify(p.variables)}</div>` : ''}
<div class="section-label">Quick Load</div>
<pre style="color:var(--cyan)">prompt = forge.get_prompt("${cap.id}", variables={...})</pre>`;
} else if(cap.type === 'workflow') {
const steps = p.steps||[];
payloadHtml = `
<div class="section-label">Steps (${steps.length})</div>
${steps.map((s,i) => `<div style="background:var(--surface2);border:1px solid var(--border);border-radius:6px;padding:0.75rem;margin-bottom:0.5rem;font-family:'DM Mono',monospace;font-size:0.75rem">
<span style="color:var(--cyan)">#${i+1} ${s.id}</span> &rarr;
<span class="dep-chip" onclick="showDetail('${s.cap_id}')" style="cursor:pointer">${s.cap_id}</span>
${s.next ? `<span style="color:var(--muted)"> &rarr; ${s.next}</span>` : '<span style="color:var(--green)"> &rarr; END</span>'}
</div>`).join('')}`;
} else if(cap.type === 'bundle') {
const caps_in = p.capabilities||[];
payloadHtml = `
<div class="section-label">Contains ${caps_in.length} Capabilities</div>
<div class="bundle-grid">${caps_in.map(cid=>`
<div class="bundle-item" onclick="showDetail('${cid}')" style="cursor:pointer">
<span class="bundle-item-icon">${TYPE_ICONS[cid]||'&#9711;'}</span>
<div><div class="bundle-item-name">${cid}</div></div>
</div>`).join('')}
</div>
<div class="section-label" style="margin-top:1rem">Quick Load Bundle</div>
<pre style="color:var(--cyan)">loadout = forge.get_bundle("${cap.id}")
# Returns all ${caps_in.length} capabilities resolved</pre>`;
} else {
payloadHtml = `
<div class="section-label">Payload</div>
<pre>${escHtml(JSON.stringify(p, null, 2))}</pre>`;
}
const schemaIn = JSON.stringify(cap.schema_in||{}, null, 2);
const schemaOut = JSON.stringify(cap.schema_out||{}, null, 2);
document.getElementById('detailContent').innerHTML = `
<div class="detail">
<div class="detail-header">
<div>
<div class="detail-type" style="color:${color};border-color:${color}20;background:${color}10">${icon} ${cap.type}</div>
</div>
<div>
<div class="detail-title">${cap.name}</div>
<div class="detail-meta">v${cap.version} &middot; by ${cap.author} &middot; &#8595; ${cap.downloads||0} downloads</div>
</div>
</div>
<div class="detail-desc">${escHtml(cap.description||'')}</div>
${tags ? `<div style="margin-bottom:0.75rem">${tags}</div>` : ''}
${deps ? `<div class="section-label">Dependencies</div><div style="margin-bottom:0.75rem">${deps}</div>` : ''}
${payloadHtml}
${schemaIn !== '{}' ? `<div class="section-label">Input Schema</div><pre style="color:var(--purple)">${escHtml(schemaIn)}</pre>` : ''}
${schemaOut !== '{}' ? `<div class="section-label">Output Schema</div><pre style="color:var(--cyan)">${escHtml(schemaOut)}</pre>` : ''}
</div>`;
}
function showList() {
document.getElementById('view-list').style.display = 'block';
document.getElementById('view-detail').style.display = 'none';
currentCap = null;
}
function showTab(tab) {
['browse','publish','api','agents'].forEach(t => {
document.getElementById('tab-'+t).style.display = t===tab?'block':'none';
});
if (tab === 'agents') loadAgents();
}
// Publish
const PAYLOAD_HINTS = {
skill: '{"code": "def execute(x: str) -> dict:\\n return {\\"result\\": x}", "language": "python", "dependencies": []}',
prompt: '{"template": "You are {{agent_name}}. {{instructions}}", "format": "plain", "variables": ["agent_name", "instructions"]}',
workflow: '{"entry": "step1", "steps": [{"id": "step1", "cap_id": "some_skill", "type": "skill", "params": {}, "next": null}]}',
knowledge: '{"format": "markdown", "source": "https://...", "content": "# Title\\n\\nContent here..."}',
config: '{"settings": {"max_steps": 8, "timeout": 60, "allowed_tools": []}}',
mcp_ref: '{"url": "https://example.com/mcp/sse", "transport": "sse", "tools": [], "auth": "none"}',
model_ref: '{"repo_id": "org/model-name", "task": "text-generation", "notes": "Usage notes...", "context_length": 8192}',
bundle: '{"capabilities": ["cap_id_1", "cap_id_2", "cap_id_3"], "description": "What this bundle provides"}',
};
function updatePayloadHint() {
const t = document.getElementById('pubType').value;
const hint = PAYLOAD_HINTS[t] || '{}';
document.getElementById('pubPayload').value = hint;
document.getElementById('payloadHint').textContent = `Required field for ${t}: ${
t==='skill'?'code' : t==='prompt'?'template' : t==='workflow'?'steps' :
t==='bundle'?'capabilities' : t==='mcp_ref'?'url' : t==='model_ref'?'repo_id' : 'content/settings'
}`;
}
updatePayloadHint();
async function publishCap() {
const id = document.getElementById('pubId').value.trim();
const type = document.getElementById('pubType').value;
const name = document.getElementById('pubName').value.trim();
const version = document.getElementById('pubVersion').value.trim() || '1.0.0';
const author = document.getElementById('pubAuthor').value.trim() || 'anonymous';
const desc = document.getElementById('pubDesc').value.trim();
const tagsRaw = document.getElementById('pubTags').value.trim();
const depsRaw = document.getElementById('pubDeps').value.trim();
const payloadRaw = document.getElementById('pubPayload').value.trim();
let payload;
try { payload = JSON.parse(payloadRaw); } catch(e) {
showResult('error', 'Invalid JSON payload: ' + e.message); return;
}
const body = {
id, type, name, version, author,
description: desc,
tags: tagsRaw ? tagsRaw.split(',').map(t=>t.trim()).filter(Boolean) : [],
deps: depsRaw ? depsRaw.split(',').map(d=>d.trim()).filter(Boolean) : [],
payload,
};
const r = await fetch('/api/capabilities', {
method:'POST',
headers:{'Content-Type':'application/json'},
body: JSON.stringify(body),
});
const data = await r.json();
if(r.ok) {
showResult('ok', data.message);
loadStats();
doSearch();
} else {
showResult('error', data.detail || data.message || 'Error');
}
}
function showResult(type, msg) {
const el = document.getElementById('pubResult');
el.className = 'result-msg ' + (type==='ok'?'result-ok':'result-err');
el.textContent = (type==='ok'?'&#10003; ':'&#10007; ') + msg;
el.style.display = 'block';
setTimeout(()=>{ el.style.display='none'; }, 5000);
}
function copyCode(id) {
const el = document.getElementById(id);
navigator.clipboard.writeText(el.innerText || el.textContent);
}
function copyText(t) { navigator.clipboard.writeText(t); }
function escHtml(s) {
return String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');
}
// Init
loadStats();
// ── Agents tab ──────────────────────────────────────────────────────
async function loadAgents() {
const grid = document.getElementById('agentGrid');
grid.innerHTML = '<span style="font-family:'DM Mono',monospace;font-size:.8rem;color:var(--muted)">Loading&#8230;</span>';
try {
const r = await fetch('/api/agents');
const agents = await r.json();
if (!agents || !agents.length) {
grid.innerHTML = '<span style="font-family:'DM Mono',monospace;font-size:.8rem;color:var(--muted)">No agents registered yet.</span>';
return;
}
grid.innerHTML = agents.map(a => {
const color = a.color || '#ff6b00';
const enabled = a.enabled !== false;
return `<div style="background:var(--surface2);border:1px solid var(--border);border-left:3px solid ${color};border-radius:6px;padding:.85rem">
<div style="display:flex;align-items:center;gap:.5rem;margin-bottom:.4rem">
<span style="width:8px;height:8px;border-radius:50%;background:${enabled?'#00ff88':'#ef4444'};flex-shrink:0"></span>
<span style="font-family:'Space Mono',monospace;font-size:.82rem;font-weight:700;color:${color}">${esc(a.name||'?')}</span>
</div>
<div style="font-family:'DM Mono',monospace;font-size:.68rem;color:var(--muted);margin-bottom:.5rem;line-height:1.4">${esc((a.persona||'').slice(0,100))}${(a.persona||'').length>100?'&#8230;':''}</div>
<div style="display:flex;gap:.5rem;flex-wrap:wrap">
<span style="font-family:'DM Mono',monospace;font-size:.65rem;background:var(--surface);border:1px solid var(--border);padding:.1rem .4rem;border-radius:3px;color:var(--muted)">${esc(a.cost_mode||'balanced')}</span>
<span style="font-family:'DM Mono',monospace;font-size:.65rem;background:var(--surface);border:1px solid var(--border);padding:.1rem .4rem;border-radius:3px;color:var(--muted)">steps:${a.max_react_steps||6}</span>
${a.heartbeat_seconds?`<span style="font-family:'DM Mono',monospace;font-size:.65rem;background:var(--surface);border:1px solid var(--border);padding:.1rem .4rem;border-radius:3px;color:var(--muted)">hb:${a.heartbeat_seconds}s</span>`:''}
</div>
<div style="margin-top:.6rem;display:flex;gap:.4rem">
<button onclick="triggerAgent('${esc(a.name||'')}')" style="font-family:'DM Mono',monospace;font-size:.65rem;background:var(--surface);border:1px solid var(--border);color:var(--accent);padding:.2rem .5rem;border-radius:3px;cursor:pointer">&#9654; Trigger</button>
<button onclick="prefillAgent(${JSON.stringify(a)})" style="font-family:'DM Mono',monospace;font-size:.65rem;background:var(--surface);border:1px solid var(--border);color:var(--muted);padding:.2rem .5rem;border-radius:3px;cursor:pointer">&#9998; Edit</button>
</div>
</div>`;
}).join('');
} catch(e) {
grid.innerHTML = '<span style="font-family:'DM Mono',monospace;font-size:.8rem;color:var(--red)">Error loading agents: ' + esc(String(e)) + '</span>';
}
}
async function registerAgent() {
const name = document.getElementById('ag-name').value.trim();
const persona = document.getElementById('ag-persona').value.trim();
if (!name || !persona) {
setAgResult('error', 'Name and persona are required.');
return;
}
const tagsRaw = document.getElementById('ag-tags').value.trim();
const tags = tagsRaw ? tagsRaw.split(',').map(t=>t.trim()).filter(Boolean) : [];
const payload = {
name: name,
persona: persona,
heartbeat_seconds: parseInt(document.getElementById('ag-hb').value)||0,
cost_mode: document.getElementById('ag-cost').value,
max_react_steps: parseInt(document.getElementById('ag-steps').value)||6,
color: document.getElementById('ag-color').value,
tags: tags,
enabled: document.getElementById('ag-enabled').checked,
};
const btn = document.getElementById('agRegisterBtn');
btn.disabled = true;
btn.textContent = 'Registering&#8230;';
setAgResult('muted','Pushing to PULSE + agent-prompts&#8230;');
try {
const r = await fetch('/api/agents/register', {
method:'POST', headers:{'Content-Type':'application/json'},
body: JSON.stringify(payload)
});
const d = await r.json();
if (d.ok) {
setAgResult('green', '&#10003; Agent &ldquo;' + esc(name) + '&rdquo; registered!');
clearAgentForm();
setTimeout(loadAgents, 800);
} else {
setAgResult('red', '&#10007; ' + JSON.stringify(d.results).slice(0,200));
}
} catch(e) {
setAgResult('red', 'Network error: ' + esc(String(e)));
} finally {
btn.disabled = false;
btn.textContent = '&#128640; Register Agent';
}
}
async function triggerAgent(name) {
try {
const r = await fetch('/api/agents/' + encodeURIComponent(name) + '/trigger', {
method:'POST', headers:{'Content-Type':'application/json'},
body: JSON.stringify({content:'Manual trigger from FORGE UI', trigger_type:'manual'})
});
const d = await r.json();
setAgResult(d.ok?'green':'red', d.ok ? '&#9654; Triggered ' + esc(name) : 'Trigger failed: ' + JSON.stringify(d));
} catch(e) { setAgResult('red', String(e)); }
}
function prefillAgent(a) {
document.getElementById('ag-name').value = a.name || '';
document.getElementById('ag-persona').value = a.persona || '';
document.getElementById('ag-hb').value = a.heartbeat_seconds || 0;
document.getElementById('ag-cost').value = a.cost_mode || 'balanced';
document.getElementById('ag-steps').value = a.max_react_steps || 6;
document.getElementById('ag-color').value = a.color || '#ff6b00';
document.getElementById('ag-color-lbl').textContent = a.color || '#ff6b00';
document.getElementById('ag-tags').value = (a.tags||[]).join(', ');
document.getElementById('ag-enabled').checked = a.enabled !== false;
document.getElementById('agRegisterBtn').scrollIntoView({behavior:'smooth',block:'nearest'});
}
function clearAgentForm() {
['ag-name','ag-persona','ag-tags'].forEach(id => document.getElementById(id).value = '');
document.getElementById('ag-hb').value = '0';
document.getElementById('ag-cost').value = 'balanced';
document.getElementById('ag-steps').value = '6';
document.getElementById('ag-color').value = '#ff6b00';
document.getElementById('ag-color-lbl').textContent = '#ff6b00';
document.getElementById('ag-enabled').checked = true;
setAgResult('','');
}
function setAgResult(type, msg) {
const el = document.getElementById('agResult');
const colors = {green:'var(--green)',red:'var(--red)',muted:'var(--muted)',error:'var(--red)','':`var(--text)`};
el.style.color = colors[type] || 'var(--text)';
el.innerHTML = msg;
}
// Wire up listeners once DOM ready
document.addEventListener('DOMContentLoaded', () => {
document.getElementById('agRegisterBtn').addEventListener('click', registerAgent);
document.getElementById('agClearBtn').addEventListener('click', clearAgentForm);
document.getElementById('refreshAgentsBtn').addEventListener('click', loadAgents);
document.getElementById('ag-color').addEventListener('input', e => {
document.getElementById('ag-color-lbl').textContent = e.target.value;
});
});
doSearch();
</script>
</body>
</html>"""
@app.get("/", response_class=HTMLResponse)
async def root():
return HTMLResponse(content=SPA, media_type="text/html; charset=utf-8")
# ---------------------------------------------------------------------------
# Entry point
# ---------------------------------------------------------------------------
if __name__ == "__main__":
uvicorn.run(app, host="0.0.0.0", port=PORT, log_level="info")