agent-forge-bak / app.py
Chris4K's picture
Update app.py
d2d3df9 verified
"""
⚒️ FORGE — Federated Open Registry for Generative Executables
v2.2 — Gradio 6.9 native theme · CRUD · MCP · SKILL.md · Claude Skills
"""
import json
from pathlib import Path
import gradio as gr
from fastapi import FastAPI, HTTPException, Request
from fastapi.responses import JSONResponse, Response
import skill_registry as registry
from mcp_server import register_mcp_routes
# ─── FastAPI + MCP ────────────────────────────────────────────────
api = FastAPI(title="FORGE API", version="2.2.0")
register_mcp_routes(api)
def jresp(data, status=200):
return JSONResponse(content=data, status_code=status)
@api.get("/api/v1/manifest")
async def api_manifest():
return jresp(registry.get_manifest())
@api.get("/api/v1/skills")
async def api_list(tag: str | None = None, q: str | None = None):
return jresp({"skills": registry.list_skills(tag=tag, query=q), "count": len(registry.list_skills(tag=tag, query=q))})
@api.get("/api/v1/skills/{skill_id}")
async def api_get(skill_id: str):
s = registry.get_skill(skill_id)
if not s: raise HTTPException(404, f"'{skill_id}' not found")
return jresp(s)
@api.get("/api/v1/skills/{skill_id}/code")
async def api_code(skill_id: str):
d = registry.get_skill_code(skill_id)
if not d: raise HTTPException(404)
return jresp(d)
@api.get("/api/v1/skills/{skill_id}/download")
async def api_download(skill_id: str):
d = registry.get_skill_code(skill_id)
if not d: raise HTTPException(404)
code = f'"""\nFORGE Skill: {skill_id} v{d["version"]}\nhttps://huggingface.co/spaces/Chris4K/agent-forge\n"""\n\n{d["code"]}\n'
return Response(code, media_type="text/x-python",
headers={"Content-Disposition": f'attachment; filename="{skill_id}.py"'})
@api.post("/api/v1/skills")
async def api_publish(request: Request):
try: skill = await request.json()
except Exception: raise HTTPException(400, "Invalid JSON")
ok, msg = registry.publish_skill(skill)
return jresp({"ok": ok, "message": msg}, 201 if ok else 400)
@api.put("/api/v1/skills/{skill_id}")
async def api_update(skill_id: str, request: Request):
try: updates = await request.json()
except Exception: raise HTTPException(400, "Invalid JSON")
ok, msg = registry.update_skill(skill_id, updates)
return jresp({"ok": ok, "message": msg}, 200 if ok else 404)
@api.delete("/api/v1/skills/{skill_id}")
async def api_delete(skill_id: str):
ok, msg = registry.delete_skill(skill_id)
return jresp({"ok": ok, "message": msg}, 200 if ok else 404)
@api.get("/api/v1/search")
async def api_search(q: str):
skills = registry.list_skills(query=q)
return jresp({"query": q, "skills": skills, "count": len(skills)})
@api.get("/api/v1/tags")
async def api_tags():
return jresp({"tags": registry.get_all_tags()})
@api.get("/api/v1/stats")
async def api_stats():
return jresp(registry.get_stats())
# ─── Gradio 6 Theme ───────────────────────────────────────────────
# All dark palette values set via the theme system — no CSS class hacks
class ForgeTheme(gr.themes.Base):
def __init__(self):
super().__init__(
primary_hue=gr.themes.colors.Color(
c50="#fff3e0", c100="#ffe0b2", c200="#ffcc80",
c300="#ffb74d", c400="#ffa726", c500="#ff9500",
c600="#ff6b00", c700="#e65100", c800="#bf360c",
c900="#8b1a00", c950="#5a0e00",
),
neutral_hue=gr.themes.colors.Color(
c50="#f0f0f8", c100="#d8d8e8", c200="#b0b0c8",
c300="#8888a8", c400="#606080", c500="#404060",
c600="#2a2a45", c700="#1e1e35", c800="#141425",
c900="#0e0e1a", c950="#0a0a0f",
),
font=gr.themes.GoogleFont("Space Mono"),
font_mono=gr.themes.GoogleFont("Space Mono"),
)
# Body
self.body_background_fill = "#0a0a0f"
self.body_background_fill_dark = "#0a0a0f"
self.body_text_color = "#e8e8f0"
self.body_text_color_dark = "#e8e8f0"
self.body_text_color_subdued = "#6b6b8a"
self.body_text_color_subdued_dark = "#6b6b8a"
# Blocks/panels
self.block_background_fill = "#111118"
self.block_background_fill_dark = "#111118"
self.block_border_color = "#1e1e2e"
self.block_border_color_dark = "#1e1e2e"
self.block_border_width = "1px"
self.block_title_text_color = "#ff6b00"
self.block_title_text_color_dark = "#ff6b00"
self.block_label_text_color = "#6b6b8a"
self.block_label_text_color_dark = "#6b6b8a"
self.block_label_background_fill = "#111118"
self.block_label_background_fill_dark = "#111118"
# Background fills (secondary panels)
self.background_fill_primary = "#0a0a0f"
self.background_fill_primary_dark = "#0a0a0f"
self.background_fill_secondary = "#111118"
self.background_fill_secondary_dark = "#111118"
# Inputs
self.input_background_fill = "#111118"
self.input_background_fill_dark = "#111118"
self.input_background_fill_focus = "#16162a"
self.input_background_fill_focus_dark = "#16162a"
self.input_border_color = "#2a2a45"
self.input_border_color_dark = "#2a2a45"
self.input_border_color_focus = "#ff6b00"
self.input_border_color_focus_dark = "#ff6b00"
self.input_placeholder_color = "#4a4a6a"
self.input_placeholder_color_dark = "#4a4a6a"
# Borders
self.border_color_primary = "#1e1e2e"
self.border_color_primary_dark = "#1e1e2e"
self.border_color_accent = "#ff6b00"
self.border_color_accent_dark = "#ff6b00"
# Buttons
self.button_primary_background_fill = "#ff6b00"
self.button_primary_background_fill_dark = "#ff6b00"
self.button_primary_background_fill_hover = "#ff9500"
self.button_primary_background_fill_hover_dark = "#ff9500"
self.button_primary_text_color = "#000000"
self.button_primary_text_color_dark = "#000000"
self.button_secondary_background_fill = "#1e1e2e"
self.button_secondary_background_fill_dark = "#1e1e2e"
self.button_secondary_background_fill_hover = "#2a2a45"
self.button_secondary_background_fill_hover_dark = "#2a2a45"
self.button_secondary_text_color = "#e8e8f0"
self.button_secondary_text_color_dark = "#e8e8f0"
self.button_secondary_border_color = "#2a2a45"
self.button_secondary_border_color_dark = "#2a2a45"
# Code blocks
self.code_background_fill = "#0d0d1a"
self.code_background_fill_dark = "#0d0d1a"
# Shadow
self.block_shadow = "none"
self.block_shadow_dark = "none"
# ─── Custom HTML-only CSS (skill cards, stat bar, forge branding) ─
# Only targets our own HTML components, not Gradio internals.
CUSTOM_CSS = """
/* Forge brand */
.forge-header {
text-align: center;
padding: 2.2rem 1rem 1.2rem;
border-bottom: 1px solid #1e1e2e;
background: linear-gradient(180deg, #0f0f1a 0%, #0a0a0f 100%);
margin-bottom: 0.5rem;
}
.forge-logo {
font-family: 'Space Mono', monospace;
font-size: 3rem;
font-weight: 700;
background: linear-gradient(135deg, #ff6b00, #ff9500);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
line-height: 1;
letter-spacing: -2px;
}
.forge-sub {
font-family: 'Space Mono', monospace;
font-size: 0.62rem;
color: #4a4a6a;
letter-spacing: 0.32em;
text-transform: uppercase;
margin-top: 0.4rem;
}
/* Stats row */
.stat-bar {
display: flex;
justify-content: center;
gap: 2.5rem;
padding: 0.8rem 1rem;
border: 1px solid #1e1e2e;
border-radius: 8px;
background: #111118;
margin: 0.8rem 0 1rem;
}
.stat-num {
font-family: 'Space Mono', monospace;
font-size: 1.7rem;
color: #ff6b00;
font-weight: 700;
line-height: 1;
}
.stat-lbl {
font-size: 0.58rem;
color: #4a4a6a;
text-transform: uppercase;
letter-spacing: 0.18em;
margin-top: 3px;
}
/* Skill cards */
.skill-card {
background: #111118;
border: 1px solid #1e1e2e;
border-radius: 8px;
padding: 1rem 1.2rem;
margin-bottom: 0.55rem;
transition: border-color 0.15s, transform 0.1s;
cursor: default;
}
.skill-card:hover {
border-color: #ff6b00;
transform: translateX(3px);
}
.sn {
font-family: 'Space Mono', monospace;
font-size: 0.88rem;
font-weight: 700;
color: #ff6b00;
}
.sd {
font-size: 0.8rem;
color: #6b6b8a;
line-height: 1.5;
margin-top: 0.3rem;
}
/* Tags + runtime badges */
.tag {
display: inline-block;
background: #16162a;
border: 1px solid #2a2a50;
color: #8b5cf6;
font-family: 'Space Mono', monospace;
font-size: 0.58rem;
padding: 1px 7px;
border-radius: 20px;
margin: 2px 2px 0;
}
.rt { display:inline-block; font-family:'Space Mono',monospace; font-size:0.58rem; padding:1px 6px; border-radius:3px; margin-left:6px; background:#0f2018; color:#00ff88; border:1px solid #00ff8825; }
.rt.node { background:#121a0a; color:#84cc16; border-color:#84cc1625; }
.rt.ins { background:#12122a; color:#8b5cf6; border-color:#8b5cf625; }
.rt.cld { background:#1e1208; color:#ff9500; border-color:#ff950025; }
/* API / MCP boxes */
.api-row {
background: #111118;
border-left: 3px solid #ff6b00;
padding: 0.52rem 1rem;
margin: 0.32rem 0;
border-radius: 0 6px 6px 0;
font-family: 'Space Mono', monospace;
font-size: 0.7rem;
color: #c0c0d8;
}
.m-get { color: #00ff88; font-weight: 700; }
.m-post { color: #ff9500; font-weight: 700; }
.m-put { color: #60a5fa; font-weight: 700; }
.m-del { color: #ff4444; font-weight: 700; }
.mcp-box {
background: #100a20;
border: 1px solid #5b21b640;
border-radius: 8px;
padding: 0.75rem 1rem;
margin: 0.45rem 0;
font-family: 'Space Mono', monospace;
font-size: 0.7rem;
color: #c4b5fd;
}
.mcp-box code { color: #a78bfa; }
.codebox {
background: #0d0d1a;
border: 1px solid #1e1e2e;
border-radius: 6px;
padding: 0.9rem 1rem;
font-family: 'Space Mono', monospace;
font-size: 0.68rem;
color: #00ff88;
white-space: pre;
overflow-x: auto;
line-height: 1.55;
}
.sec {
font-family: 'Space Mono', monospace;
color: #8b5cf6;
font-size: 0.68rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.15em;
margin: 1.1rem 0 0.45rem;
}
.info-note {
font-family: 'Space Mono', monospace;
font-size: 0.68rem;
color: #4a4a6a;
padding: 0.4rem 0;
}
"""
PUBLISH_TEMPLATE = """{
"id": "my_skill",
"name": "My Skill",
"version": "1.0.0",
"description": "What this skill does for an agent.",
"author": "Chris4K",
"tags": ["utility"],
"runtime": "python",
"dependencies": [],
"env_required": [],
"bins": [],
"schema": {
"input": { "text": "str" },
"output": { "result": "str" }
},
"code": "def execute(text: str) -> dict:\\n return {'result': text.upper()}\\n"
}"""
SKILL_MD_TEMPLATE = """---
name: my-instructions-skill
version: 1.0.0
description: Teach an agent how to do something step-by-step.
author: Chris4K
tags: [instructions, utility]
runtime: instructions
---
# My Instructions Skill
## Purpose
Describe what capability this adds to an agent.
## When to use
Use this skill when the user asks you to...
## Steps
1. First, do this
2. Then do that
3. Return the result
## Examples
Input: "example"
Output: "expected"
"""
# ─── HTML helpers ─────────────────────────────────────────────────
def _rt(rt):
rt = (rt or "python").lower()
c = "node" if rt in ("node","nodejs","shell") else "ins" if rt=="instructions" else "cld" if "claude" in rt else ""
return f'<span class="rt {c}">{rt}</span>'
def fmt_card(s):
tags = "".join(f'<span class="tag">{t}</span>' for t in s.get("tags", []))
return f"""<div class="skill-card">
<div class="sn">⚙ {s['name']}{_rt(s.get('runtime','python'))}
<span style="float:right;font-size:.6rem;color:#3a3a5a">↓{s.get('downloads',0)} · v{s['version']}</span>
</div>
<div style="font-family:'Space Mono',monospace;font-size:.6rem;color:#3a3a5a;margin:2px 0 5px">
by {s['author']} · <code style="color:#ff6b0050">{s['id']}</code>
</div>
<div class="sd">{s['description'][:170]}{'…' if len(s['description'])>170 else ''}</div>
<div style="margin-top:5px">{tags}</div>
</div>"""
def render_list(skills):
if not skills:
return '<div style="color:#3a3a5a;text-align:center;padding:2rem;font-family:Space Mono,monospace">No skills found.</div>'
return "".join(fmt_card(s) for s in skills)
def make_header():
st = registry.get_stats()
return f"""<div class="forge-header">
<div class="forge-logo">⚒ FORGE</div>
<div class="forge-sub">Federated Open Registry for Generative Executables</div>
<div style="color:#3a3a5a;font-size:.72rem;margin-top:.5rem;font-family:'Space Mono',monospace">
Python · Node · SKILL.md · MCP · Claude Skills
</div>
</div>
<div class="stat-bar">
<div><div class="stat-num">{st['total_skills']}</div><div class="stat-lbl">Skills</div></div>
<div><div class="stat-num">{st['total_downloads']}</div><div class="stat-lbl">Downloads</div></div>
<div><div class="stat-num">{st['total_tags']}</div><div class="stat-lbl">Tags</div></div>
<div><div class="stat-num" style="color:#00ff88">✓</div><div class="stat-lbl">MCP Live</div></div>
</div>"""
# ─── Tab handlers ─────────────────────────────────────────────────
def do_browse(q, tag):
return render_list(registry.list_skills(
tag=None if tag == "all" else tag,
query=q.strip() or None
))
def load_detail(skill_id):
empty4 = ("", "", "", "")
blank = '<div class="info-note" style="padding:.5rem">Enter a skill ID above, then click 📂 Load.</div>'
if not skill_id.strip():
return blank, *empty4
s = registry.get_skill(skill_id.strip())
if not s:
return f'<div style="color:#ff4444;font-family:Space Mono,monospace;padding:.5rem">Skill \'{skill_id}\' not found.</div>', *empty4
tags = " ".join(f'<span class="tag">{t}</span>' for t in s.get("tags", []))
env_w = f'<div style="color:#ff4444;font-family:Space Mono,monospace;font-size:.62rem;margin-top:.35rem">⚠ env vars needed: {", ".join(s["env_required"])}</div>' if s.get("env_required") else ""
bins_i = f'<div style="color:#60a5fa;font-family:Space Mono,monospace;font-size:.62rem;margin-top:.2rem">🔧 system bins: {", ".join(s["bins"])}</div>' if s.get("bins") else ""
deps = ", ".join(s.get("dependencies", [])) or "none"
meta = f"""<div class="skill-card">
<div class="sn">⚙ {s['name']}{_rt(s.get('runtime','python'))}</div>
<div style="font-family:'Space Mono',monospace;font-size:.62rem;color:#6b6b8a">
v{s['version']} · by {s['author']} · ↓{s.get('downloads',0)} downloads
</div>
<div style="margin:.6rem 0;line-height:1.6;font-size:.85rem">{s['description']}</div>
<div>{tags}</div>
{env_w}{bins_i}
<div style="margin-top:.75rem;border-top:1px solid #1e1e2e;padding-top:.55rem">
<div class="sec">Schema</div>
<pre style="font-size:.62rem;color:#7070a0;margin-top:.3rem;font-family:'Space Mono',monospace;line-height:1.5">{json.dumps(s.get("schema", {}), indent=2)}</pre>
<div class="sec">Dependencies</div>
<div style="font-family:'Space Mono',monospace;font-size:.65rem;color:#ff9500;margin-top:.2rem">{deps}</div>
</div>
</div>"""
code = s.get("code", "# no code")
instr = s.get("instructions", "_No SKILL.md instructions for this skill._")
usage = f"""# ⚒️ Quick Start: {s['id']}
# ─────────────────────────────────────────────────
import requests, types
def bootstrap_forge(url="https://huggingface.co/spaces/Chris4K/agent-forge"):
r = requests.get(f"{{url}}/api/v1/skills/forge_client/code")
m = types.ModuleType("forge_client")
exec(r.json()["code"], m.__dict__)
return m.ForgeClient(url)
forge = bootstrap_forge()
skill = forge.load("{s['id']}")
result = skill.execute() # ← fill in params from schema above
print(result)
"""
edit_j = json.dumps(
{k: v for k, v in s.items() if k not in ("created_at","updated_at","downloads")},
indent=2, ensure_ascii=False
)
return meta, code, instr, usage, edit_j
def do_update(sid, ej):
if not sid.strip(): return "⚠ Load a skill first"
try: u = json.loads(ej)
except json.JSONDecodeError as e: return f"❌ JSON error: {e}"
ok, msg = registry.update_skill(sid.strip(), u)
return ("✅ " if ok else "❌ ") + msg
def do_delete(sid):
if not sid.strip(): return "⚠ Load a skill first"
ok, msg = registry.delete_skill(sid.strip())
return ("✅ " if ok else "❌ ") + msg
def publish_json(jstr):
try: s = json.loads(jstr)
except json.JSONDecodeError as e: return f"❌ Invalid JSON: {e}"
ok, msg = registry.publish_skill(s)
return ("✅ " if ok else "❌ ") + msg
def publish_md(mstr):
s, err = registry.parse_skill_md(mstr)
if err: return f"❌ {err}"
ok, msg = registry.publish_skill(s)
return ("✅ Published '" + s["id"] + "' — " if ok else "❌ ") + msg
# ─── Build UI ─────────────────────────────────────────────────────
def build_app():
theme = ForgeTheme()
with gr.Blocks(title="⚒️ FORGE — Skill Artifactory", theme=theme) as demo:
# Inject only our custom component CSS via <style>
gr.HTML(f"<style>{CUSTOM_CSS}</style>")
# Header + stats bar
gr.HTML(make_header())
with gr.Tabs():
# ── BROWSE ────────────────────────────────────────────
with gr.Tab("🔍 Browse"):
with gr.Row():
search_in = gr.Textbox(
placeholder="search name, tags, description…",
label="Search", scale=3, container=True
)
tag_dd = gr.Dropdown(
choices=["all"] + registry.get_all_tags(),
value="all", label="Tag", scale=1
)
srch_btn = gr.Button("🔍 Search", variant="primary")
skills_out = gr.HTML(render_list(registry.list_skills()))
srch_btn.click(do_browse, [search_in, tag_dd], skills_out)
search_in.submit(do_browse, [search_in, tag_dd], skills_out)
tag_dd.change(do_browse, [search_in, tag_dd], skills_out)
# ── CRUD ─────────────────────────────────────────────
with gr.Tab("⚙ Skill CRUD"):
gr.HTML('<div class="info-note">Load any skill by ID to view, edit, or delete it.</div>')
sid_in = gr.Textbox(
placeholder="e.g. calculator web_search claude_chat",
label="Skill ID"
)
with gr.Row():
load_btn = gr.Button("📂 Load", variant="primary")
update_btn = gr.Button("💾 Save Changes", variant="secondary")
delete_btn = gr.Button("🗑 Delete", variant="secondary")
crud_msg = gr.Textbox(label="Status", interactive=False, max_lines=1)
detail_meta = gr.HTML('<div class="info-note">No skill loaded.</div>')
with gr.Tabs():
with gr.Tab("📄 Code"):
d_code = gr.Code(language="python", label="Executable code")
with gr.Tab("📝 SKILL.md"):
d_md = gr.Markdown()
with gr.Tab("🤖 Usage"):
d_use = gr.Code(language="python", label="Agent bootstrap")
with gr.Tab("✏️ Edit JSON"):
d_edit = gr.Code(language="json", label="Edit → Save Changes")
load_btn.click(load_detail, sid_in, [detail_meta, d_code, d_md, d_use, d_edit])
sid_in.submit(load_detail, sid_in, [detail_meta, d_code, d_md, d_use, d_edit])
update_btn.click(do_update, [sid_in, d_edit], crud_msg)
delete_btn.click(do_delete, sid_in, crud_msg)
# ── PUBLISH ───────────────────────────────────────────
with gr.Tab("📦 Publish"):
with gr.Tabs():
with gr.Tab("🐍 Python / JSON"):
gr.HTML('<div class="info-note">Must include <code>def execute(...) → dict</code></div>')
pj_in = gr.Code(value=PUBLISH_TEMPLATE, language="json", label="Skill JSON", lines=20)
pj_btn = gr.Button("⚒ Publish JSON Skill", variant="primary")
pj_out = gr.Textbox(label="Result", interactive=False)
pj_btn.click(publish_json, pj_in, pj_out)
with gr.Tab("📝 SKILL.md"):
gr.HTML('<div class="info-note">YAML frontmatter + markdown body. Compatible with ClawHub / OpenClaw.</div>')
pm_in = gr.Code(value=SKILL_MD_TEMPLATE, language="markdown", label="SKILL.md", lines=20)
pm_btn = gr.Button("⚒ Publish SKILL.md", variant="primary")
pm_out = gr.Textbox(label="Result", interactive=False)
pm_btn.click(publish_md, pm_in, pm_out)
# ── MCP ───────────────────────────────────────────────
with gr.Tab("🔌 MCP"):
gr.HTML(f"""<div style="padding:.75rem 0">
<div class="sec" style="font-size:.9rem;color:#ff6b00;margin-top:0">⚒️ FORGE MCP Server</div>
<p style="color:#6b6b8a;font-size:.82rem;margin-bottom:.75rem">Connect Claude Desktop, Claude API, or any MCP client directly to FORGE.</p>
<div class="sec">SSE Endpoint — Claude Desktop / mcp-remote</div>
<div class="mcp-box"><code>https://huggingface.co/spaces/Chris4K/agent-forge/mcp/sse</code></div>
<div class="sec">JSON-RPC Endpoint — direct API</div>
<div class="mcp-box"><code>https://huggingface.co/spaces/Chris4K/agent-forge/mcp</code></div>
<div class="sec">Claude Desktop Config</div>
<div class="codebox">{{
"mcpServers": {{
"forge": {{
"command": "npx",
"args": ["-y", "mcp-remote",
"https://huggingface.co/spaces/Chris4K/agent-forge/mcp/sse"]
}}
}}
}}</div>
<div class="sec">Anthropic API with MCP</div>
<div class="codebox">import anthropic
client = anthropic.Anthropic()
response = client.beta.messages.create(
model="claude-opus-4-6",
max_tokens=1024,
mcp_servers=[{{
"type": "url",
"url": "https://huggingface.co/spaces/Chris4K/agent-forge/mcp/sse",
"name": "forge"
}}],
messages=[{{"role": "user",
"content": "List all skills in FORGE, load the calculator, compute 2**32"}}]
)</div>
<div class="sec">Available MCP Tools</div>
<div class="api-row">forge_list_skills · forge_get_skill · forge_get_code · forge_search · forge_publish_skill · forge_get_stats</div>
</div>""")
# ── API ───────────────────────────────────────────────
with gr.Tab("📡 API"):
gr.HTML(f"""<div style="padding:.75rem 0">
<div class="sec" style="font-size:.9rem;color:#ff6b00;margin-top:0">REST API v2</div>
<p style="font-family:'Space Mono',monospace;font-size:.68rem;color:#6b6b8a;margin-bottom:.75rem">
Base: <code style="color:#ff9500">https://huggingface.co/spaces/Chris4K/agent-forge</code>
</p>
<div class="api-row"><span class="m-get">GET</span> /api/v1/skills &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;List skills &nbsp; ?tag= ?q=</div>
<div class="api-row"><span class="m-get">GET</span> /api/v1/skills/{{id}} &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Get skill + code</div>
<div class="api-row"><span class="m-get">GET</span> /api/v1/skills/{{id}}/code &nbsp;&nbsp;&nbsp;Minimal hot-load payload</div>
<div class="api-row"><span class="m-get">GET</span> /api/v1/skills/{{id}}/download &nbsp;Download as .py</div>
<div class="api-row"><span class="m-post">POST</span> /api/v1/skills &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Publish new skill</div>
<div class="api-row"><span class="m-put">PUT</span> /api/v1/skills/{{id}} &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Update skill</div>
<div class="api-row"><span class="m-del">DEL</span> /api/v1/skills/{{id}} &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Delete skill</div>
<div class="api-row"><span class="m-get">GET</span> /api/v1/search?q= &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Full-text search</div>
<div class="api-row"><span class="m-get">GET</span> /api/v1/manifest &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;All skills + code (offline)</div>
<div class="api-row"><span class="m-get">GET</span> /mcp/sse &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;MCP SSE stream</div>
<div class="api-row"><span class="m-post">POST</span> /mcp &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;MCP JSON-RPC</div>
<div class="sec">Agent Bootstrap</div>
<div class="codebox">import requests, types
def bootstrap_forge(url="https://huggingface.co/spaces/Chris4K/agent-forge"):
r = requests.get(f"{{url}}/api/v1/skills/forge_client/code")
m = types.ModuleType("forge_client")
exec(r.json()["code"], m.__dict__)
return m.ForgeClient(url)
forge = bootstrap_forge()
calc = forge.load("calculator") # pure Python math
search = forge.load("web_search") # DuckDuckGo, no key needed
memory = forge.load("memory_store") # KV store with TTL
fetch = forge.load("http_fetch") # web page scraper
claude = forge.load("claude_chat") # needs ANTHROPIC_API_KEY
print(calc.execute(expression="2**32"))
print(search.execute(query="AI news today", max_results=3))
memory.execute(action="set", key="goal", value="build JARVIS", ttl=3600)
reply = claude.execute(prompt="Explain MCP in one paragraph")</div>
</div>""")
# ── HOWTO ─────────────────────────────────────────────
with gr.Tab("📖 How-To"):
p = Path(__file__).parent / "HOWTO.md"
gr.Markdown(p.read_text(encoding="utf-8") if p.exists() else "HOWTO.md not found")
gr.HTML('<div style="text-align:center;padding:1.2rem;border-top:1px solid #1e1e2e;margin-top:1rem;font-family:Space Mono,monospace;font-size:.6rem;color:#2a2a45">⚒️ FORGE v2.2 · ki-fusion-labs.de · <a href="https://huggingface.co/Chris4K" style="color:#ff6b00;text-decoration:none">Chris4K</a> · MIT</div>')
return demo
# ─── Mount ────────────────────────────────────────────────────────
demo = build_app()
app = gr.mount_gradio_app(api, demo, path="/")
if __name__ == "__main__":
demo.launch(server_name="0.0.0.0", server_port=7860, ssr_mode=False)