Spaces:
Paused
Paused
Upload 7 files
Browse files- HOWTO.md +448 -0
- app.py +333 -0
- claude_chat.json +28 -0
- claude_extractor.json +25 -0
- mcp_server.py +253 -0
- requirements.txt +5 -0
- skill_registry.py +231 -0
HOWTO.md
ADDED
|
@@ -0,0 +1,448 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# ⚒️ FORGE — Complete How-To Guide
|
| 2 |
+
|
| 3 |
+
## Table of Contents
|
| 4 |
+
1. [What is FORGE?](#what-is-forge)
|
| 5 |
+
2. [vs ClawHub](#vs-clawhub)
|
| 6 |
+
3. [Deploy to HuggingFace](#deploy)
|
| 7 |
+
4. [Skill Formats](#skill-formats)
|
| 8 |
+
5. [CRUD — Create, Read, Update, Delete](#crud)
|
| 9 |
+
6. [MCP Server — Claude native integration](#mcp)
|
| 10 |
+
7. [Bins & Node Tools](#bins-and-node-tools)
|
| 11 |
+
8. [Claude Skills (Anthropic API)](#claude-skills)
|
| 12 |
+
9. [Agent Quick Start](#agent-quick-start)
|
| 13 |
+
10. [REST API Reference](#rest-api)
|
| 14 |
+
|
| 15 |
+
---
|
| 16 |
+
|
| 17 |
+
## What is FORGE?
|
| 18 |
+
|
| 19 |
+
FORGE is a **skill artifactory for AI agents** — like npm, pip, or JFrog Artifactory but for executable agent capabilities.
|
| 20 |
+
|
| 21 |
+
Every skill is a self-contained, versioned unit that any agent can:
|
| 22 |
+
- **Discover** via REST API or MCP protocol
|
| 23 |
+
- **Download** as code (Python / Node.js / shell) or as a SKILL.md description
|
| 24 |
+
- **Hot-load** at runtime without retraining or redeployment
|
| 25 |
+
- **Execute** through a standard `execute()` interface
|
| 26 |
+
|
| 27 |
+
---
|
| 28 |
+
|
| 29 |
+
## vs ClawHub
|
| 30 |
+
|
| 31 |
+
| Feature | ClawHub | FORGE |
|
| 32 |
+
|---------|---------|-------|
|
| 33 |
+
| Primary format | SKILL.md (markdown) | JSON + embedded code |
|
| 34 |
+
| Also supports | — | SKILL.md (ClawHub compatible) ✅ |
|
| 35 |
+
| Code execution | Instructions only | Live executable Python ✅ |
|
| 36 |
+
| MCP Server | ❌ | ✅ |
|
| 37 |
+
| Bins / Node tools | Via Nix | Via `runtime` metadata |
|
| 38 |
+
| Claude skills | ❌ | ✅ (Anthropic API) |
|
| 39 |
+
| HF Space deploy | ❌ | ✅ one-click |
|
| 40 |
+
| Vector search | ✅ (OpenAI embeddings) | tag + text search |
|
| 41 |
+
| 3000+ community skills | ✅ | growing |
|
| 42 |
+
|
| 43 |
+
**Key difference:** ClawHub skills are *instructions* (markdown that goes into an LLM prompt). FORGE skills are *executable code* that agents run directly. You can publish both.
|
| 44 |
+
|
| 45 |
+
---
|
| 46 |
+
|
| 47 |
+
## Deploy
|
| 48 |
+
|
| 49 |
+
### HuggingFace Space (recommended)
|
| 50 |
+
|
| 51 |
+
```bash
|
| 52 |
+
# 1. Create Space at huggingface.co → SDK: Gradio
|
| 53 |
+
# 2. Clone your space
|
| 54 |
+
git clone https://huggingface.co/spaces/YOUR_NAME/agent-forge
|
| 55 |
+
cd agent-forge
|
| 56 |
+
|
| 57 |
+
# 3. Copy FORGE files into it
|
| 58 |
+
cp -r forge/* .
|
| 59 |
+
|
| 60 |
+
# 4. Push
|
| 61 |
+
git add . && git commit -m "Deploy FORGE" && git push
|
| 62 |
+
```
|
| 63 |
+
|
| 64 |
+
The `README.md` frontmatter configures the Space:
|
| 65 |
+
```yaml
|
| 66 |
+
---
|
| 67 |
+
title: FORGE Agent Skill Artifactory
|
| 68 |
+
emoji: ⚒️
|
| 69 |
+
colorFrom: orange
|
| 70 |
+
colorTo: red
|
| 71 |
+
sdk: gradio
|
| 72 |
+
sdk_version: 4.44.0
|
| 73 |
+
app_file: app.py
|
| 74 |
+
pinned: true
|
| 75 |
+
---
|
| 76 |
+
```
|
| 77 |
+
|
| 78 |
+
### Local dev
|
| 79 |
+
|
| 80 |
+
```bash
|
| 81 |
+
pip install -r requirements.txt
|
| 82 |
+
python app.py
|
| 83 |
+
# → http://localhost:7860
|
| 84 |
+
```
|
| 85 |
+
|
| 86 |
+
---
|
| 87 |
+
|
| 88 |
+
## Skill Formats
|
| 89 |
+
|
| 90 |
+
FORGE supports **three skill formats**:
|
| 91 |
+
|
| 92 |
+
### Format 1: JSON (executable Python)
|
| 93 |
+
Best for: skills that agents hot-load and execute directly.
|
| 94 |
+
|
| 95 |
+
```json
|
| 96 |
+
{
|
| 97 |
+
"id": "my_skill",
|
| 98 |
+
"name": "My Skill",
|
| 99 |
+
"version": "1.0.0",
|
| 100 |
+
"description": "What this skill does",
|
| 101 |
+
"author": "yourname",
|
| 102 |
+
"tags": ["utility"],
|
| 103 |
+
"dependencies": ["requests"],
|
| 104 |
+
"runtime": "python",
|
| 105 |
+
"schema": {
|
| 106 |
+
"input": { "text": "str" },
|
| 107 |
+
"output": { "result": "str" }
|
| 108 |
+
},
|
| 109 |
+
"code": "def execute(text: str) -> dict:\n return {'result': text.upper()}"
|
| 110 |
+
}
|
| 111 |
+
```
|
| 112 |
+
|
| 113 |
+
### Format 2: SKILL.md (ClawHub compatible)
|
| 114 |
+
Best for: LLM-readable instructions that go into agent prompts.
|
| 115 |
+
|
| 116 |
+
```markdown
|
| 117 |
+
---
|
| 118 |
+
name: my-skill
|
| 119 |
+
version: 1.0.0
|
| 120 |
+
description: Does something useful
|
| 121 |
+
author: yourname
|
| 122 |
+
tags: [utility, text]
|
| 123 |
+
runtime: instructions
|
| 124 |
+
---
|
| 125 |
+
|
| 126 |
+
# My Skill
|
| 127 |
+
|
| 128 |
+
## Purpose
|
| 129 |
+
Explain what this skill teaches an agent to do.
|
| 130 |
+
|
| 131 |
+
## Usage
|
| 132 |
+
When the user asks to process text, do the following:
|
| 133 |
+
1. Step one
|
| 134 |
+
2. Step two
|
| 135 |
+
|
| 136 |
+
## Examples
|
| 137 |
+
Input: "hello world"
|
| 138 |
+
Output: "HELLO WORLD"
|
| 139 |
+
```
|
| 140 |
+
|
| 141 |
+
### Format 3: Node.js / Shell tool
|
| 142 |
+
Best for: wrapping CLI tools, Node scripts, or system binaries.
|
| 143 |
+
|
| 144 |
+
```json
|
| 145 |
+
{
|
| 146 |
+
"id": "node_parser",
|
| 147 |
+
"name": "JSON Parser",
|
| 148 |
+
"version": "1.0.0",
|
| 149 |
+
"runtime": "node",
|
| 150 |
+
"dependencies_node": ["lodash"],
|
| 151 |
+
"bins": ["jq"],
|
| 152 |
+
"code": "const _ = require('lodash');\nfunction execute({data}) {\n return { keys: _.keys(JSON.parse(data)) };\n}\nmodule.exports = { execute };"
|
| 153 |
+
}
|
| 154 |
+
```
|
| 155 |
+
|
| 156 |
+
---
|
| 157 |
+
|
| 158 |
+
## CRUD
|
| 159 |
+
|
| 160 |
+
### CREATE — Publish a new skill
|
| 161 |
+
|
| 162 |
+
**Via UI:** Go to the "📦 Publish" tab → paste your skill JSON → click Publish.
|
| 163 |
+
|
| 164 |
+
**Via API:**
|
| 165 |
+
```bash
|
| 166 |
+
curl -X POST https://YOUR_SPACE.hf.space/api/v1/skills \
|
| 167 |
+
-H "Content-Type: application/json" \
|
| 168 |
+
-d @my_skill.json
|
| 169 |
+
```
|
| 170 |
+
|
| 171 |
+
**Via Python agent:**
|
| 172 |
+
```python
|
| 173 |
+
forge = bootstrap_forge()
|
| 174 |
+
result = forge.publish({
|
| 175 |
+
"id": "my_skill",
|
| 176 |
+
"name": "My Skill",
|
| 177 |
+
"version": "1.0.0",
|
| 178 |
+
"description": "...",
|
| 179 |
+
"author": "me",
|
| 180 |
+
"tags": ["utility"],
|
| 181 |
+
"code": "def execute(**kwargs): return {'ok': True}"
|
| 182 |
+
})
|
| 183 |
+
```
|
| 184 |
+
|
| 185 |
+
---
|
| 186 |
+
|
| 187 |
+
### READ — Discover and download skills
|
| 188 |
+
|
| 189 |
+
**Browse UI:** Go to "🔍 Browse" tab — search by keyword or filter by tag.
|
| 190 |
+
|
| 191 |
+
**Read one skill (metadata only):**
|
| 192 |
+
```bash
|
| 193 |
+
GET /api/v1/skills/calculator
|
| 194 |
+
```
|
| 195 |
+
|
| 196 |
+
**Read skill code (for hot-loading):**
|
| 197 |
+
```bash
|
| 198 |
+
GET /api/v1/skills/calculator/code
|
| 199 |
+
```
|
| 200 |
+
|
| 201 |
+
**Download as .py file:**
|
| 202 |
+
```bash
|
| 203 |
+
GET /api/v1/skills/calculator/download
|
| 204 |
+
```
|
| 205 |
+
|
| 206 |
+
**Full manifest (all skills + code for offline cache):**
|
| 207 |
+
```bash
|
| 208 |
+
GET /api/v1/manifest
|
| 209 |
+
```
|
| 210 |
+
|
| 211 |
+
**Search:**
|
| 212 |
+
```bash
|
| 213 |
+
GET /api/v1/search?q=math
|
| 214 |
+
GET /api/v1/skills?tag=web
|
| 215 |
+
```
|
| 216 |
+
|
| 217 |
+
---
|
| 218 |
+
|
| 219 |
+
### UPDATE — Publish a new version
|
| 220 |
+
|
| 221 |
+
FORGE uses **semver versioning**. To update a skill, publish it again with a bumped version:
|
| 222 |
+
|
| 223 |
+
```json
|
| 224 |
+
{
|
| 225 |
+
"id": "calculator",
|
| 226 |
+
"version": "1.1.0", ← bump this
|
| 227 |
+
...
|
| 228 |
+
}
|
| 229 |
+
```
|
| 230 |
+
|
| 231 |
+
Previous versions are kept on disk as `calculator.v1.0.0.json`.
|
| 232 |
+
|
| 233 |
+
**Via UI:** Skill Detail tab → "Edit" → modify → save as new version.
|
| 234 |
+
|
| 235 |
+
---
|
| 236 |
+
|
| 237 |
+
### DELETE — Remove a skill
|
| 238 |
+
|
| 239 |
+
**Via UI:** Skill Detail tab → "🗑 Delete" button (with confirmation).
|
| 240 |
+
|
| 241 |
+
**Via API:**
|
| 242 |
+
```bash
|
| 243 |
+
DELETE /api/v1/skills/my_skill
|
| 244 |
+
```
|
| 245 |
+
|
| 246 |
+
---
|
| 247 |
+
|
| 248 |
+
## MCP
|
| 249 |
+
|
| 250 |
+
FORGE exposes an **MCP (Model Context Protocol) server** so Claude Desktop, Claude API, and any MCP client can discover and use skills natively.
|
| 251 |
+
|
| 252 |
+
### Connect Claude to FORGE
|
| 253 |
+
|
| 254 |
+
Add to your `claude_desktop_config.json`:
|
| 255 |
+
```json
|
| 256 |
+
{
|
| 257 |
+
"mcpServers": {
|
| 258 |
+
"forge": {
|
| 259 |
+
"command": "npx",
|
| 260 |
+
"args": ["-y", "mcp-remote", "https://YOUR_SPACE.hf.space/mcp/sse"]
|
| 261 |
+
}
|
| 262 |
+
}
|
| 263 |
+
}
|
| 264 |
+
```
|
| 265 |
+
|
| 266 |
+
Or via the API with SSE:
|
| 267 |
+
```python
|
| 268 |
+
import anthropic
|
| 269 |
+
|
| 270 |
+
client = anthropic.Anthropic()
|
| 271 |
+
response = client.beta.messages.create(
|
| 272 |
+
model="claude-opus-4-6",
|
| 273 |
+
max_tokens=1024,
|
| 274 |
+
tools=[],
|
| 275 |
+
mcp_servers=[{
|
| 276 |
+
"type": "url",
|
| 277 |
+
"url": "https://YOUR_SPACE.hf.space/mcp/sse",
|
| 278 |
+
"name": "forge"
|
| 279 |
+
}],
|
| 280 |
+
messages=[{"role": "user", "content": "List all math skills in FORGE"}]
|
| 281 |
+
)
|
| 282 |
+
```
|
| 283 |
+
|
| 284 |
+
### MCP Tools exposed by FORGE
|
| 285 |
+
|
| 286 |
+
| Tool | Description |
|
| 287 |
+
|------|-------------|
|
| 288 |
+
| `forge_list_skills` | List all skills, optionally filtered |
|
| 289 |
+
| `forge_search` | Semantic search across skills |
|
| 290 |
+
| `forge_get_skill` | Get full skill including code |
|
| 291 |
+
| `forge_get_code` | Get executable code for a skill |
|
| 292 |
+
| `forge_publish_skill` | Publish a new skill |
|
| 293 |
+
| `forge_get_stats` | Registry statistics |
|
| 294 |
+
|
| 295 |
+
---
|
| 296 |
+
|
| 297 |
+
## Bins and Node Tools
|
| 298 |
+
|
| 299 |
+
Skills can declare required system binaries and Node packages:
|
| 300 |
+
|
| 301 |
+
```json
|
| 302 |
+
{
|
| 303 |
+
"id": "pdf_extractor",
|
| 304 |
+
"runtime": "python",
|
| 305 |
+
"bins": ["pdftotext", "gs"],
|
| 306 |
+
"dependencies": ["pypdf2"],
|
| 307 |
+
"install_notes": "apt-get install poppler-utils ghostscript",
|
| 308 |
+
"code": "..."
|
| 309 |
+
}
|
| 310 |
+
```
|
| 311 |
+
|
| 312 |
+
```json
|
| 313 |
+
{
|
| 314 |
+
"id": "image_optimizer",
|
| 315 |
+
"runtime": "node",
|
| 316 |
+
"bins": ["sharp", "imagemagick"],
|
| 317 |
+
"dependencies_node": ["sharp", "glob"],
|
| 318 |
+
"code": "const sharp = require('sharp'); ..."
|
| 319 |
+
}
|
| 320 |
+
```
|
| 321 |
+
|
| 322 |
+
Agents check `skill.bins` before executing and can skip skills whose binaries aren't available on the current host.
|
| 323 |
+
|
| 324 |
+
### Shell/Bin skill example
|
| 325 |
+
|
| 326 |
+
```json
|
| 327 |
+
{
|
| 328 |
+
"id": "git_status",
|
| 329 |
+
"runtime": "shell",
|
| 330 |
+
"bins": ["git"],
|
| 331 |
+
"code": "#!/bin/bash\ngit -C \"$1\" status --porcelain"
|
| 332 |
+
}
|
| 333 |
+
```
|
| 334 |
+
|
| 335 |
+
---
|
| 336 |
+
|
| 337 |
+
## Claude Skills
|
| 338 |
+
|
| 339 |
+
Skills that call the **Anthropic API** — these give your agent Claude-powered capabilities it can invoke as tools.
|
| 340 |
+
|
| 341 |
+
### Example: Claude Skill structure
|
| 342 |
+
|
| 343 |
+
```json
|
| 344 |
+
{
|
| 345 |
+
"id": "claude_summarizer",
|
| 346 |
+
"runtime": "python",
|
| 347 |
+
"dependencies": ["anthropic"],
|
| 348 |
+
"env_required": ["ANTHROPIC_API_KEY"],
|
| 349 |
+
"code": "import anthropic\n\ndef execute(text: str, style: str = 'bullet') -> dict:\n client = anthropic.Anthropic()\n msg = client.messages.create(\n model='claude-haiku-4-5-20251001',\n max_tokens=512,\n messages=[{'role': 'user', 'content': f'Summarize as {style} points:\\n{text}'}]\n )\n return {'summary': msg.content[0].text}\n"
|
| 350 |
+
}
|
| 351 |
+
```
|
| 352 |
+
|
| 353 |
+
### How agents use Claude skills
|
| 354 |
+
|
| 355 |
+
```python
|
| 356 |
+
forge = bootstrap_forge()
|
| 357 |
+
summarizer = forge.load("claude_summarizer")
|
| 358 |
+
|
| 359 |
+
# Agents set ANTHROPIC_API_KEY in their environment
|
| 360 |
+
result = summarizer.execute(
|
| 361 |
+
text="Long article text...",
|
| 362 |
+
style="executive"
|
| 363 |
+
)
|
| 364 |
+
# → {"summary": "• Key point 1\n• Key point 2..."}
|
| 365 |
+
```
|
| 366 |
+
|
| 367 |
+
### Available Claude skills in FORGE
|
| 368 |
+
|
| 369 |
+
| Skill ID | What it does |
|
| 370 |
+
|----------|-------------|
|
| 371 |
+
| `claude_chat` | Single-turn Q&A with Claude |
|
| 372 |
+
| `claude_summarizer` | Summarize text in various styles |
|
| 373 |
+
| `claude_extractor` | Extract structured data from text |
|
| 374 |
+
| `claude_judge` | Evaluate/score outputs (LLM-as-judge) |
|
| 375 |
+
| `claude_coder` | Generate code from a spec |
|
| 376 |
+
|
| 377 |
+
---
|
| 378 |
+
|
| 379 |
+
## Agent Quick Start
|
| 380 |
+
|
| 381 |
+
```python
|
| 382 |
+
import requests, types
|
| 383 |
+
|
| 384 |
+
# ── Bootstrap (one HTTP call) ──────────────────────────
|
| 385 |
+
def bootstrap_forge(url="https://chris4k-agent-forge.hf.space"):
|
| 386 |
+
r = requests.get(f"{url}/api/v1/skills/forge_client/code")
|
| 387 |
+
m = types.ModuleType("forge_client")
|
| 388 |
+
exec(r.json()["code"], m.__dict__)
|
| 389 |
+
return m.ForgeClient(url)
|
| 390 |
+
|
| 391 |
+
forge = bootstrap_forge()
|
| 392 |
+
|
| 393 |
+
# ── Discover ───────────────────────────────────────────
|
| 394 |
+
all_skills = forge.list() # all skills
|
| 395 |
+
math_skills = forge.list(tag="math") # by tag
|
| 396 |
+
results = forge.search("web scraping") # full-text search
|
| 397 |
+
|
| 398 |
+
# ── Load & Execute ─────────────────────────────────────
|
| 399 |
+
calc = forge.load("calculator")
|
| 400 |
+
search = forge.load("web_search")
|
| 401 |
+
memory = forge.load("memory_store")
|
| 402 |
+
fetcher = forge.load("http_fetch")
|
| 403 |
+
claude = forge.load("claude_chat") # Claude skill
|
| 404 |
+
|
| 405 |
+
# Run them
|
| 406 |
+
print(calc.execute(expression="2**32 / 1024"))
|
| 407 |
+
print(search.execute(query="AI news today", max_results=3))
|
| 408 |
+
memory.execute(action="set", key="goal", value="research AI", ttl=3600)
|
| 409 |
+
page = fetcher.execute(url="https://example.com", max_chars=2000)
|
| 410 |
+
reply = claude.execute(prompt="What is MCP?")
|
| 411 |
+
|
| 412 |
+
# ── Publish your own ───────────────────────────────────
|
| 413 |
+
forge.publish({
|
| 414 |
+
"id": "my_skill",
|
| 415 |
+
"name": "My Skill",
|
| 416 |
+
"version": "1.0.0",
|
| 417 |
+
"description": "Does something useful",
|
| 418 |
+
"author": "yourname",
|
| 419 |
+
"tags": ["utility"],
|
| 420 |
+
"code": "def execute(**kwargs): return {'ok': True, 'data': kwargs}"
|
| 421 |
+
})
|
| 422 |
+
```
|
| 423 |
+
|
| 424 |
+
---
|
| 425 |
+
|
| 426 |
+
## REST API Reference
|
| 427 |
+
|
| 428 |
+
Base URL: `https://YOUR_SPACE.hf.space`
|
| 429 |
+
|
| 430 |
+
| Method | Path | Description |
|
| 431 |
+
|--------|------|-------------|
|
| 432 |
+
| `GET` | `/api/v1/manifest` | Full manifest (all skills + code) |
|
| 433 |
+
| `GET` | `/api/v1/skills` | List skills. Params: `?tag=` `?q=` |
|
| 434 |
+
| `GET` | `/api/v1/skills/{id}` | Get skill metadata + code |
|
| 435 |
+
| `GET` | `/api/v1/skills/{id}/code` | Minimal code payload for hot-loading |
|
| 436 |
+
| `GET` | `/api/v1/skills/{id}/download` | Download as `.py` file |
|
| 437 |
+
| `POST` | `/api/v1/skills` | Publish new skill (JSON body) |
|
| 438 |
+
| `PUT` | `/api/v1/skills/{id}` | Update skill (new version) |
|
| 439 |
+
| `DELETE` | `/api/v1/skills/{id}` | Delete skill |
|
| 440 |
+
| `GET` | `/api/v1/search?q=` | Full-text search |
|
| 441 |
+
| `GET` | `/api/v1/tags` | All tags |
|
| 442 |
+
| `GET` | `/api/v1/stats` | Registry statistics |
|
| 443 |
+
| `GET` | `/mcp/sse` | MCP Server-Sent Events endpoint |
|
| 444 |
+
| `POST` | `/mcp` | MCP JSON-RPC endpoint |
|
| 445 |
+
|
| 446 |
+
---
|
| 447 |
+
|
| 448 |
+
*Built by Chris4K · ki-fusion-labs.de · MIT License*
|
app.py
ADDED
|
@@ -0,0 +1,333 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
⚒️ FORGE — Federated Open Registry for Generative Executables
|
| 3 |
+
v2.0 — CRUD UI · MCP Server · SKILL.md support · Claude skills
|
| 4 |
+
"""
|
| 5 |
+
import json
|
| 6 |
+
import time
|
| 7 |
+
from pathlib import Path
|
| 8 |
+
|
| 9 |
+
import gradio as gr
|
| 10 |
+
from fastapi import FastAPI, HTTPException, Request
|
| 11 |
+
from fastapi.responses import JSONResponse, Response
|
| 12 |
+
|
| 13 |
+
import skill_registry as registry
|
| 14 |
+
from mcp_server import register_mcp_routes
|
| 15 |
+
|
| 16 |
+
api = FastAPI(title="FORGE API", version="2.0.0")
|
| 17 |
+
register_mcp_routes(api)
|
| 18 |
+
|
| 19 |
+
def jresp(data, status=200):
|
| 20 |
+
return JSONResponse(content=data, status_code=status)
|
| 21 |
+
|
| 22 |
+
@api.get("/api/v1/manifest")
|
| 23 |
+
async def api_manifest():
|
| 24 |
+
return jresp(registry.get_manifest())
|
| 25 |
+
|
| 26 |
+
@api.get("/api/v1/skills")
|
| 27 |
+
async def api_list(tag: str | None = None, q: str | None = None):
|
| 28 |
+
skills = registry.list_skills(tag=tag, query=q)
|
| 29 |
+
return jresp({"skills": skills, "count": len(skills)})
|
| 30 |
+
|
| 31 |
+
@api.get("/api/v1/skills/{skill_id}")
|
| 32 |
+
async def api_get(skill_id: str):
|
| 33 |
+
s = registry.get_skill(skill_id)
|
| 34 |
+
if not s: raise HTTPException(404, f"'{skill_id}' not found")
|
| 35 |
+
return jresp(s)
|
| 36 |
+
|
| 37 |
+
@api.get("/api/v1/skills/{skill_id}/code")
|
| 38 |
+
async def api_code(skill_id: str):
|
| 39 |
+
d = registry.get_skill_code(skill_id)
|
| 40 |
+
if not d: raise HTTPException(404)
|
| 41 |
+
return jresp(d)
|
| 42 |
+
|
| 43 |
+
@api.get("/api/v1/skills/{skill_id}/download")
|
| 44 |
+
async def api_download(skill_id: str):
|
| 45 |
+
d = registry.get_skill_code(skill_id)
|
| 46 |
+
if not d: raise HTTPException(404)
|
| 47 |
+
code = f'"""\nFORGE Skill: {skill_id} v{d["version"]}\nhttps://chris4k-agent-forge.hf.space\n"""\n\n{d["code"]}\n'
|
| 48 |
+
return Response(code, media_type="text/x-python",
|
| 49 |
+
headers={"Content-Disposition": f'attachment; filename="{skill_id}.py"'})
|
| 50 |
+
|
| 51 |
+
@api.post("/api/v1/skills")
|
| 52 |
+
async def api_publish(request: Request):
|
| 53 |
+
try: skill = await request.json()
|
| 54 |
+
except Exception: raise HTTPException(400, "Invalid JSON")
|
| 55 |
+
ok, msg = registry.publish_skill(skill)
|
| 56 |
+
return jresp({"ok": ok, "message": msg}, 201 if ok else 400)
|
| 57 |
+
|
| 58 |
+
@api.put("/api/v1/skills/{skill_id}")
|
| 59 |
+
async def api_update(skill_id: str, request: Request):
|
| 60 |
+
try: updates = await request.json()
|
| 61 |
+
except Exception: raise HTTPException(400, "Invalid JSON")
|
| 62 |
+
ok, msg = registry.update_skill(skill_id, updates)
|
| 63 |
+
return jresp({"ok": ok, "message": msg}, 200 if ok else 404)
|
| 64 |
+
|
| 65 |
+
@api.delete("/api/v1/skills/{skill_id}")
|
| 66 |
+
async def api_delete(skill_id: str):
|
| 67 |
+
ok, msg = registry.delete_skill(skill_id)
|
| 68 |
+
return jresp({"ok": ok, "message": msg}, 200 if ok else 404)
|
| 69 |
+
|
| 70 |
+
@api.get("/api/v1/search")
|
| 71 |
+
async def api_search(q: str):
|
| 72 |
+
skills = registry.list_skills(query=q)
|
| 73 |
+
return jresp({"query": q, "skills": skills, "count": len(skills)})
|
| 74 |
+
|
| 75 |
+
@api.get("/api/v1/tags")
|
| 76 |
+
async def api_tags():
|
| 77 |
+
return jresp({"tags": registry.get_all_tags()})
|
| 78 |
+
|
| 79 |
+
@api.get("/api/v1/stats")
|
| 80 |
+
async def api_stats():
|
| 81 |
+
return jresp(registry.get_stats())
|
| 82 |
+
|
| 83 |
+
# ─── CSS ──────────────────────────────────────────────────────────
|
| 84 |
+
CSS = """
|
| 85 |
+
@import url('https://fonts.googleapis.com/css2?family=Space+Mono:wght@400;700&family=Syne:wght@400;600;800&display=swap');
|
| 86 |
+
:root{--fg:#0a0a0f;--surf:#111118;--bord:#1e1e2e;--acc:#ff6b00;--acc2:#ff9500;--txt:#e8e8f0;--mute:#6b6b8a;--grn:#00ff88;--pur:#8b5cf6;--red:#ff4444;}
|
| 87 |
+
body,.gradio-container{background:var(--fg)!important;font-family:'Syne',sans-serif!important;color:var(--txt)!important;}
|
| 88 |
+
.forge-header{text-align:center;padding:2.5rem 1rem 1rem;border-bottom:1px solid var(--bord);margin-bottom:1.5rem;background:linear-gradient(180deg,#0f0f1a 0%,var(--fg) 100%);}
|
| 89 |
+
.forge-logo{font-family:'Space Mono',monospace;font-size:3.2rem;font-weight:700;background:linear-gradient(135deg,var(--acc),var(--acc2));-webkit-background-clip:text;-webkit-text-fill-color:transparent;background-clip:text;letter-spacing:-2px;line-height:1;}
|
| 90 |
+
.forge-sub{font-family:'Space Mono',monospace;font-size:.65rem;color:var(--mute);letter-spacing:.35em;text-transform:uppercase;margin-top:.4rem;}
|
| 91 |
+
.stat-bar{display:flex;justify-content:center;gap:3rem;padding:.8rem;margin:1rem 0 1.5rem;border:1px solid var(--bord);border-radius:8px;background:var(--surf);}
|
| 92 |
+
.stat-num{font-family:'Space Mono',monospace;font-size:1.8rem;color:var(--acc);font-weight:700;}
|
| 93 |
+
.stat-lbl{font-size:.65rem;color:var(--mute);text-transform:uppercase;letter-spacing:.15em;}
|
| 94 |
+
.skill-card{background:var(--surf);border:1px solid var(--bord);border-radius:8px;padding:1.2rem;margin-bottom:.6rem;transition:border-color .2s,transform .1s;}
|
| 95 |
+
.skill-card:hover{border-color:var(--acc);transform:translateX(3px);}
|
| 96 |
+
.skill-name{font-family:'Space Mono',monospace;font-size:.95rem;font-weight:700;color:var(--acc);}
|
| 97 |
+
.skill-desc{font-size:.83rem;color:var(--mute);line-height:1.5;margin-top:.3rem;}
|
| 98 |
+
.tag-pill{display:inline-block;background:#1a1a30;border:1px solid #2a2a50;color:var(--pur);font-family:'Space Mono',monospace;font-size:.6rem;padding:2px 8px;border-radius:20px;margin:3px 2px 0;}
|
| 99 |
+
.api-row{background:var(--surf);border-left:3px solid var(--acc);padding:.6rem 1rem;margin:.4rem 0;border-radius:0 6px 6px 0;font-family:'Space Mono',monospace;font-size:.75rem;}
|
| 100 |
+
.m-get{color:var(--grn);}.m-post{color:var(--acc2);}.m-put{color:#60a5fa;}.m-del{color:var(--red);}
|
| 101 |
+
.mcp-badge{background:#1a0a30;border:1px solid #5b21b650;border-radius:6px;padding:.75rem 1rem;margin:.5rem 0;font-family:'Space Mono',monospace;font-size:.75rem;color:#a78bfa;}
|
| 102 |
+
.gr-button{background:var(--acc)!important;color:#000!important;font-family:'Space Mono',monospace!important;font-weight:700!important;border:none!important;}
|
| 103 |
+
.gr-button:hover{background:var(--acc2)!important;}
|
| 104 |
+
textarea,input{background:var(--surf)!important;border-color:var(--bord)!important;color:var(--txt)!important;}
|
| 105 |
+
label span{color:var(--mute)!important;font-family:'Space Mono',monospace!important;font-size:.72rem!important;letter-spacing:.1em!important;}
|
| 106 |
+
.tab-nav button{background:var(--surf)!important;color:var(--mute)!important;border:1px solid var(--bord)!important;font-family:'Space Mono',monospace!important;font-size:.75rem!important;}
|
| 107 |
+
.tab-nav button.selected{color:var(--acc)!important;border-bottom-color:var(--acc)!important;}
|
| 108 |
+
"""
|
| 109 |
+
|
| 110 |
+
PUBLISH_TEMPLATE = """{
|
| 111 |
+
"id": "my_skill",
|
| 112 |
+
"name": "My Skill",
|
| 113 |
+
"version": "1.0.0",
|
| 114 |
+
"description": "What this skill does for an agent.",
|
| 115 |
+
"author": "Chris4K",
|
| 116 |
+
"tags": ["utility"],
|
| 117 |
+
"runtime": "python",
|
| 118 |
+
"dependencies": [],
|
| 119 |
+
"env_required": [],
|
| 120 |
+
"bins": [],
|
| 121 |
+
"schema": {
|
| 122 |
+
"input": { "text": "str" },
|
| 123 |
+
"output": { "result": "str" }
|
| 124 |
+
},
|
| 125 |
+
"code": "def execute(text: str) -> dict:\\n return {'result': text.upper()}\\n"
|
| 126 |
+
}"""
|
| 127 |
+
|
| 128 |
+
SKILL_MD_TEMPLATE = """---
|
| 129 |
+
name: my-instructions-skill
|
| 130 |
+
version: 1.0.0
|
| 131 |
+
description: Teach an agent how to do something step-by-step.
|
| 132 |
+
author: Chris4K
|
| 133 |
+
tags: [instructions, utility]
|
| 134 |
+
runtime: instructions
|
| 135 |
+
---
|
| 136 |
+
|
| 137 |
+
# My Instructions Skill
|
| 138 |
+
|
| 139 |
+
## Purpose
|
| 140 |
+
Describe what capability this adds to an agent.
|
| 141 |
+
|
| 142 |
+
## When to use
|
| 143 |
+
Use this skill when the user asks you to...
|
| 144 |
+
|
| 145 |
+
## Steps
|
| 146 |
+
1. First, do this
|
| 147 |
+
2. Then do that
|
| 148 |
+
3. Finally return the result
|
| 149 |
+
|
| 150 |
+
## Examples
|
| 151 |
+
Input: "some example"
|
| 152 |
+
Output: "expected output"
|
| 153 |
+
"""
|
| 154 |
+
|
| 155 |
+
def fmt_card(s):
|
| 156 |
+
tags = "".join(f'<span class="tag-pill">{t}</span>' for t in s.get("tags",[]))
|
| 157 |
+
rt = s.get("runtime","python")
|
| 158 |
+
return f"""<div class="skill-card">
|
| 159 |
+
<div class="skill-name">⚙ {s['name']} <span style="font-size:.65rem;color:#555570;margin-left:6px">[{rt}]</span>
|
| 160 |
+
<span style="float:right;font-size:.65rem;color:#4a4a6a">↓{s.get('downloads',0)} · v{s['version']}</span></div>
|
| 161 |
+
<div style="font-family:'Space Mono',monospace;font-size:.62rem;color:#4a4a6a;margin:2px 0 6px">by {s['author']} · <span style="color:#ff6b0060">{s['id']}</span></div>
|
| 162 |
+
<div class="skill-desc">{s['description'][:150]}{'…' if len(s['description'])>150 else ''}</div>
|
| 163 |
+
<div style="margin-top:6px">{tags}</div></div>"""
|
| 164 |
+
|
| 165 |
+
def render_list(skills):
|
| 166 |
+
if not skills: return '<div style="color:#4a4a6a;text-align:center;padding:2rem;font-family:Space Mono">No skills found.</div>'
|
| 167 |
+
return "".join(fmt_card(s) for s in skills)
|
| 168 |
+
|
| 169 |
+
def render_stats():
|
| 170 |
+
st = registry.get_stats()
|
| 171 |
+
return f"""<div class="stat-bar">
|
| 172 |
+
<div><div class="stat-num">{st['total_skills']}</div><div class="stat-lbl">Skills</div></div>
|
| 173 |
+
<div><div class="stat-num">{st['total_downloads']}</div><div class="stat-lbl">Downloads</div></div>
|
| 174 |
+
<div><div class="stat-num">{st['total_tags']}</div><div class="stat-lbl">Tags</div></div>
|
| 175 |
+
<div><div class="stat-num">✓</div><div class="stat-lbl">MCP Live</div></div></div>"""
|
| 176 |
+
|
| 177 |
+
def do_browse(q, tag):
|
| 178 |
+
return render_list(registry.list_skills(tag=None if tag=="all" else tag, query=q.strip() or None))
|
| 179 |
+
|
| 180 |
+
def load_detail(skill_id):
|
| 181 |
+
if not skill_id.strip(): return ("Enter a skill ID.", "", "", "", "")
|
| 182 |
+
s = registry.get_skill(skill_id.strip())
|
| 183 |
+
if not s: return (f"Skill '{skill_id}' not found.", "", "", "", "")
|
| 184 |
+
tags_html = " ".join(f'<span class="tag-pill">{t}</span>' for t in s.get("tags",[]))
|
| 185 |
+
meta = f"""<div class="skill-card">
|
| 186 |
+
<div class="skill-name">⚙ {s['name']} <span style="font-size:.65rem;color:#555570">[{s.get('runtime','python')}]</span></div>
|
| 187 |
+
<div style="font-family:'Space Mono',monospace;font-size:.68rem;color:#6b6b8a">v{s['version']} · by {s['author']} · ↓{s.get('downloads',0)}</div>
|
| 188 |
+
<div style="margin:.6rem 0;line-height:1.6">{s['description']}</div><div>{tags_html}</div>
|
| 189 |
+
{"<div style='margin-top:.4rem;font-size:.65rem;color:#ff4444;font-family:Space Mono,monospace'>⚠ env: "+', '.join(s['env_required'])+"</div>" if s.get('env_required') else ""}
|
| 190 |
+
{"<div style='margin-top:.4rem;font-size:.65rem;color:#60a5fa;font-family:Space Mono,monospace'>🔧 bins: "+', '.join(s.get('bins',[]))+"</div>" if s.get('bins') else ""}
|
| 191 |
+
<div style="margin-top:.75rem;border-top:1px solid #1e1e2e;padding-top:.5rem">
|
| 192 |
+
<strong style="color:#8b5cf6;font-size:.65rem;font-family:Space Mono,monospace">SCHEMA</strong>
|
| 193 |
+
<pre style="font-size:.65rem;color:#8080a0;margin-top:.3rem">{json.dumps(s.get('schema',{}),indent=2)}</pre></div></div>"""
|
| 194 |
+
code = s.get("code","")
|
| 195 |
+
instructions = s.get("instructions","(no markdown instructions)")
|
| 196 |
+
usage = f"""# ⚒️ Quick Start: {s['id']}
|
| 197 |
+
import requests, types
|
| 198 |
+
def bootstrap_forge(url="https://chris4k-agent-forge.hf.space"):
|
| 199 |
+
r = requests.get(f"{{url}}/api/v1/skills/forge_client/code")
|
| 200 |
+
m = types.ModuleType("forge_client")
|
| 201 |
+
exec(r.json()["code"], m.__dict__)
|
| 202 |
+
return m.ForgeClient(url)
|
| 203 |
+
|
| 204 |
+
forge = bootstrap_forge()
|
| 205 |
+
{s['id']} = forge.load("{s['id']}")
|
| 206 |
+
result = {s['id']}.execute() # see schema above
|
| 207 |
+
print(result)
|
| 208 |
+
"""
|
| 209 |
+
edit_json = json.dumps({k:v for k,v in s.items() if k not in ("created_at","updated_at","downloads")}, indent=2)
|
| 210 |
+
return meta, code, instructions, usage, edit_json
|
| 211 |
+
|
| 212 |
+
def do_update(skill_id, edit_json):
|
| 213 |
+
if not skill_id.strip(): return "⚠ No skill loaded"
|
| 214 |
+
try: updates = json.loads(edit_json)
|
| 215 |
+
except json.JSONDecodeError as e: return f"❌ JSON error: {e}"
|
| 216 |
+
ok, msg = registry.update_skill(skill_id.strip(), updates)
|
| 217 |
+
return ("✅ " if ok else "❌ ") + msg
|
| 218 |
+
|
| 219 |
+
def do_delete(skill_id):
|
| 220 |
+
if not skill_id.strip(): return "⚠ No skill loaded"
|
| 221 |
+
ok, msg = registry.delete_skill(skill_id.strip())
|
| 222 |
+
return ("✅ " if ok else "❌ ") + msg
|
| 223 |
+
|
| 224 |
+
def publish_json(skill_json):
|
| 225 |
+
try: skill = json.loads(skill_json)
|
| 226 |
+
except json.JSONDecodeError as e: return f"❌ Invalid JSON: {e}"
|
| 227 |
+
ok, msg = registry.publish_skill(skill)
|
| 228 |
+
return ("✅ " if ok else "❌ ") + msg
|
| 229 |
+
|
| 230 |
+
def publish_md(skill_md):
|
| 231 |
+
skill, err = registry.parse_skill_md(skill_md)
|
| 232 |
+
if err: return f"❌ {err}"
|
| 233 |
+
ok, msg = registry.publish_skill(skill)
|
| 234 |
+
return ("✅ Published as '"+skill["id"]+"' — " if ok else "❌ ") + msg
|
| 235 |
+
|
| 236 |
+
def build_app():
|
| 237 |
+
with gr.Blocks(css=CSS, title="⚒️ FORGE") as demo:
|
| 238 |
+
gr.HTML("""<div class="forge-header">
|
| 239 |
+
<div class="forge-logo">⚒ FORGE</div>
|
| 240 |
+
<div class="forge-sub">Federated Open Registry for Generative Executables</div>
|
| 241 |
+
<div style="color:#4a4a6a;font-size:.78rem;margin-top:.6rem">Python · Node · SKILL.md · MCP · Claude Skills</div>
|
| 242 |
+
</div>""")
|
| 243 |
+
stats_html = gr.HTML(render_stats())
|
| 244 |
+
|
| 245 |
+
with gr.Tabs():
|
| 246 |
+
with gr.Tab("🔍 Browse"):
|
| 247 |
+
with gr.Row():
|
| 248 |
+
search_in = gr.Textbox(placeholder="search…", label="Search", scale=3)
|
| 249 |
+
tag_dd = gr.Dropdown(choices=["all"]+registry.get_all_tags(), value="all", label="Tag", scale=1)
|
| 250 |
+
srch_btn = gr.Button("Search", variant="primary", size="sm")
|
| 251 |
+
skills_out = gr.HTML(render_list(registry.list_skills()))
|
| 252 |
+
srch_btn.click(do_browse, [search_in, tag_dd], skills_out)
|
| 253 |
+
search_in.submit(do_browse, [search_in, tag_dd], skills_out)
|
| 254 |
+
tag_dd.change(do_browse, [search_in, tag_dd], skills_out)
|
| 255 |
+
|
| 256 |
+
with gr.Tab("⚙ Skill CRUD"):
|
| 257 |
+
skill_id_in = gr.Textbox(placeholder="e.g. calculator, claude_chat…", label="Skill ID")
|
| 258 |
+
with gr.Row():
|
| 259 |
+
load_btn = gr.Button("Load", variant="primary", size="sm")
|
| 260 |
+
update_btn = gr.Button("💾 Save Changes", size="sm")
|
| 261 |
+
delete_btn = gr.Button("🗑 Delete", size="sm")
|
| 262 |
+
crud_msg = gr.Textbox(label="Status", interactive=False, lines=1)
|
| 263 |
+
detail_meta = gr.HTML()
|
| 264 |
+
with gr.Tabs():
|
| 265 |
+
with gr.Tab("📄 Code"):
|
| 266 |
+
detail_code = gr.Code(language="python", label="Skill code")
|
| 267 |
+
with gr.Tab("📝 SKILL.md"):
|
| 268 |
+
detail_md = gr.Markdown()
|
| 269 |
+
with gr.Tab("🤖 Agent Usage"):
|
| 270 |
+
detail_use = gr.Code(language="python", label="Quick start")
|
| 271 |
+
with gr.Tab("✏️ Edit JSON"):
|
| 272 |
+
edit_json_in = gr.Code(language="json", label="Edit → Save Changes")
|
| 273 |
+
load_btn.click(load_detail, skill_id_in, [detail_meta, detail_code, detail_md, detail_use, edit_json_in])
|
| 274 |
+
skill_id_in.submit(load_detail, skill_id_in, [detail_meta, detail_code, detail_md, detail_use, edit_json_in])
|
| 275 |
+
update_btn.click(do_update, [skill_id_in, edit_json_in], crud_msg)
|
| 276 |
+
delete_btn.click(do_delete, skill_id_in, crud_msg)
|
| 277 |
+
|
| 278 |
+
with gr.Tab("📦 Publish"):
|
| 279 |
+
with gr.Tabs():
|
| 280 |
+
with gr.Tab("JSON (executable)"):
|
| 281 |
+
pub_json_in = gr.Code(value=PUBLISH_TEMPLATE, language="json", label="Skill JSON", lines=22)
|
| 282 |
+
gr.Button("⚒ Publish JSON Skill", variant="primary").click(publish_json, pub_json_in, gr.Textbox(label="Result", interactive=False))
|
| 283 |
+
with gr.Tab("SKILL.md (ClawHub compat)"):
|
| 284 |
+
pub_md_in = gr.Code(value=SKILL_MD_TEMPLATE, language="markdown", label="SKILL.md", lines=22)
|
| 285 |
+
gr.Button("⚒ Publish SKILL.md", variant="primary").click(publish_md, pub_md_in, gr.Textbox(label="Result", interactive=False))
|
| 286 |
+
|
| 287 |
+
with gr.Tab("🔌 MCP"):
|
| 288 |
+
gr.HTML("""<div style="padding:1rem">
|
| 289 |
+
<h2 style="color:#ff6b00;font-family:'Space Mono',monospace">⚒️ MCP Server</h2>
|
| 290 |
+
<p style="color:#6b6b8a">Connect Claude Desktop, Claude API, or any MCP client.</p>
|
| 291 |
+
<div class="mcp-badge"><strong>SSE (Claude Desktop)</strong><br>
|
| 292 |
+
<code>https://chris4k-agent-forge.hf.space/mcp/sse</code></div>
|
| 293 |
+
<div class="mcp-badge"><strong>JSON-RPC (API)</strong><br>
|
| 294 |
+
<code>https://chris4k-agent-forge.hf.space/mcp</code></div>
|
| 295 |
+
<h3 style="color:#8b5cf6;margin:1rem 0 .5rem;font-family:'Space Mono',monospace">Claude Desktop Config</h3>
|
| 296 |
+
<pre style="background:#0d0d1a;border:1px solid #1e1e2e;border-radius:6px;padding:1rem;font-size:.72rem;color:#00ff88">{"mcpServers":{"forge":{"command":"npx","args":["-y","mcp-remote","https://chris4k-agent-forge.hf.space/mcp/sse"]}}}</pre>
|
| 297 |
+
<h3 style="color:#8b5cf6;margin:1rem 0 .5rem;font-family:'Space Mono',monospace">MCP Tools</h3>
|
| 298 |
+
<div class="api-row">forge_list_skills · forge_get_skill · forge_get_code · forge_search · forge_publish_skill · forge_get_stats</div>
|
| 299 |
+
</div>""")
|
| 300 |
+
|
| 301 |
+
with gr.Tab("📡 API"):
|
| 302 |
+
gr.HTML("""<div style="font-family:'Space Mono',monospace;padding:1rem">
|
| 303 |
+
<h2 style="color:#ff6b00">REST API v2</h2>
|
| 304 |
+
<div class="api-row"><span class="m-get">GET</span> /api/v1/skills — list</div>
|
| 305 |
+
<div class="api-row"><span class="m-get">GET</span> /api/v1/skills/{id}/code — hot-load code</div>
|
| 306 |
+
<div class="api-row"><span class="m-get">GET</span> /api/v1/skills/{id}/download — .py file</div>
|
| 307 |
+
<div class="api-row"><span class="m-post">POST</span> /api/v1/skills — publish</div>
|
| 308 |
+
<div class="api-row"><span class="m-put">PUT</span> /api/v1/skills/{id} — update</div>
|
| 309 |
+
<div class="api-row"><span class="m-del">DEL</span> /api/v1/skills/{id} — delete</div>
|
| 310 |
+
<div class="api-row"><span class="m-get">GET</span> /api/v1/search?q= — search</div>
|
| 311 |
+
<div class="api-row"><span class="m-get">GET</span> /api/v1/manifest — full manifest</div>
|
| 312 |
+
<div class="api-row"><span class="m-get">GET</span> /mcp/sse — MCP SSE stream</div>
|
| 313 |
+
<div class="api-row"><span class="m-post">POST</span> /mcp — MCP JSON-RPC</div>
|
| 314 |
+
<pre style="background:#0d0d1a;border:1px solid #1e1e2e;border-radius:6px;padding:1rem;margin-top:1rem;font-size:.72rem;color:#00ff88">forge = bootstrap_forge()
|
| 315 |
+
calc = forge.load("calculator")
|
| 316 |
+
print(calc.execute(expression="2**32"))</pre></div>""")
|
| 317 |
+
|
| 318 |
+
with gr.Tab("📖 How-To"):
|
| 319 |
+
howto_path = Path(__file__).parent / "HOWTO.md"
|
| 320 |
+
howto_text = howto_path.read_text(encoding="utf-8") if howto_path.exists() else "HOWTO.md not found"
|
| 321 |
+
gr.Markdown(howto_text)
|
| 322 |
+
|
| 323 |
+
gr.HTML("""<div style="text-align:center;padding:1.5rem;border-top:1px solid #1e1e2e;margin-top:2rem;
|
| 324 |
+
font-family:'Space Mono',monospace;font-size:.65rem;color:#3a3a5a">
|
| 325 |
+
⚒️ FORGE v2.0 · ki-fusion-labs.de · <a href="https://huggingface.co/Chris4K" style="color:#ff6b00">Chris4K</a> · MIT
|
| 326 |
+
</div>""")
|
| 327 |
+
return demo
|
| 328 |
+
|
| 329 |
+
demo = build_app()
|
| 330 |
+
app = gr.mount_gradio_app(api, demo, path="/")
|
| 331 |
+
|
| 332 |
+
if __name__ == "__main__":
|
| 333 |
+
demo.launch(server_name="0.0.0.0", server_port=7860)
|
claude_chat.json
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"id": "claude_chat",
|
| 3 |
+
"name": "Claude Chat",
|
| 4 |
+
"version": "1.0.0",
|
| 5 |
+
"description": "Single-turn Q&A powered by Claude (Anthropic API). Agents can delegate reasoning, analysis, code generation, or any LLM task to Claude as a sub-skill. Requires ANTHROPIC_API_KEY env var.",
|
| 6 |
+
"author": "Chris4K",
|
| 7 |
+
"tags": ["claude", "anthropic", "llm", "reasoning", "ai"],
|
| 8 |
+
"runtime": "python",
|
| 9 |
+
"dependencies": ["anthropic"],
|
| 10 |
+
"env_required": ["ANTHROPIC_API_KEY"],
|
| 11 |
+
"schema": {
|
| 12 |
+
"input": {
|
| 13 |
+
"prompt": "str — the question or instruction for Claude",
|
| 14 |
+
"system": "str — optional system prompt (default: helpful assistant)",
|
| 15 |
+
"model": "str — model ID (default: claude-haiku-4-5-20251001)",
|
| 16 |
+
"max_tokens": "int — max response tokens (default: 1024)"
|
| 17 |
+
},
|
| 18 |
+
"output": {
|
| 19 |
+
"response": "str — Claude's response text",
|
| 20 |
+
"model": "str — model used",
|
| 21 |
+
"input_tokens": "int",
|
| 22 |
+
"output_tokens": "int"
|
| 23 |
+
}
|
| 24 |
+
},
|
| 25 |
+
"code": "import os\nfrom typing import Optional\n\n\ndef execute(\n prompt: str,\n system: str = \"You are a helpful AI assistant embedded in an agent skill.\",\n model: str = \"claude-haiku-4-5-20251001\",\n max_tokens: int = 1024,\n api_key: Optional[str] = None,\n) -> dict:\n \"\"\"\n Send a single-turn message to Claude and return the response.\n Used by agents to delegate LLM reasoning tasks.\n \"\"\"\n try:\n import anthropic\n except ImportError:\n return {\"error\": \"anthropic package not installed. Run: pip install anthropic\"}\n\n key = api_key or os.environ.get(\"ANTHROPIC_API_KEY\")\n if not key:\n return {\"error\": \"ANTHROPIC_API_KEY not set. Pass api_key= or set the env var.\"}\n\n if not prompt or not prompt.strip():\n return {\"error\": \"prompt cannot be empty\"}\n\n client = anthropic.Anthropic(api_key=key)\n\n try:\n msg = client.messages.create(\n model=model,\n max_tokens=max_tokens,\n system=system,\n messages=[{\"role\": \"user\", \"content\": prompt}],\n )\n response_text = msg.content[0].text if msg.content else \"\"\n return {\n \"response\": response_text,\n \"model\": model,\n \"input_tokens\": msg.usage.input_tokens,\n \"output_tokens\": msg.usage.output_tokens,\n \"stop_reason\": msg.stop_reason,\n }\n except anthropic.AuthenticationError:\n return {\"error\": \"Invalid ANTHROPIC_API_KEY\"}\n except anthropic.RateLimitError:\n return {\"error\": \"Rate limit hit — retry after a moment\"}\n except Exception as e:\n return {\"error\": str(e)}\n",
|
| 26 |
+
"downloads": 0,
|
| 27 |
+
"created_at": 1710000010
|
| 28 |
+
}
|
claude_extractor.json
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"id": "claude_extractor",
|
| 3 |
+
"name": "Claude Extractor",
|
| 4 |
+
"version": "1.0.0",
|
| 5 |
+
"description": "Extract structured data from unstructured text using Claude. Provide a schema and raw text; Claude returns a JSON object matching the schema. Perfect for parsing documents, emails, web pages, or any free-form input.",
|
| 6 |
+
"author": "Chris4K",
|
| 7 |
+
"tags": ["claude", "anthropic", "extraction", "nlp", "structured-output", "json"],
|
| 8 |
+
"runtime": "python",
|
| 9 |
+
"dependencies": ["anthropic"],
|
| 10 |
+
"env_required": ["ANTHROPIC_API_KEY"],
|
| 11 |
+
"schema": {
|
| 12 |
+
"input": {
|
| 13 |
+
"text": "str — raw text to extract from",
|
| 14 |
+
"schema": "dict — JSON schema describing what to extract, e.g. {name: str, date: str, amount: float}",
|
| 15 |
+
"instructions": "str — optional extra extraction instructions"
|
| 16 |
+
},
|
| 17 |
+
"output": {
|
| 18 |
+
"extracted": "dict — extracted fields matching the schema",
|
| 19 |
+
"raw_response": "str — Claude's full response (for debugging)"
|
| 20 |
+
}
|
| 21 |
+
},
|
| 22 |
+
"code": "import os\nimport json\nimport re\nfrom typing import Optional\n\n\ndef execute(\n text: str,\n schema: dict,\n instructions: str = \"\",\n api_key: Optional[str] = None,\n model: str = \"claude-haiku-4-5-20251001\",\n) -> dict:\n \"\"\"\n Extract structured data from text using Claude.\n Returns a dict matching the provided schema.\n \"\"\"\n try:\n import anthropic\n except ImportError:\n return {\"error\": \"anthropic package not installed\"}\n\n key = api_key or os.environ.get(\"ANTHROPIC_API_KEY\")\n if not key:\n return {\"error\": \"ANTHROPIC_API_KEY not set\"}\n\n schema_str = json.dumps(schema, indent=2)\n extra = f\"\\n\\nAdditional instructions: {instructions}\" if instructions else \"\"\n\n system = (\n \"You are a precise data extraction assistant. \"\n \"Extract information from text and return ONLY valid JSON — no explanation, no markdown, no backticks. \"\n \"If a field cannot be found in the text, use null.\"\n )\n\n prompt = f\"\"\"Extract data from the following text and return a JSON object matching this schema:\n\n{schema_str}{extra}\n\nText to extract from:\n---\n{text[:4000]}\n---\n\nReturn ONLY the JSON object, nothing else.\"\"\"\n\n client = anthropic.Anthropic(api_key=key)\n try:\n msg = client.messages.create(\n model=model,\n max_tokens=1024,\n system=system,\n messages=[{\"role\": \"user\", \"content\": prompt}],\n )\n raw = msg.content[0].text.strip() if msg.content else \"{}\"\n\n # Strip markdown fences if present\n raw_clean = re.sub(r\"^```[a-z]*\\n?\", \"\", raw).rstrip(\"```\").strip()\n\n try:\n extracted = json.loads(raw_clean)\n except json.JSONDecodeError:\n extracted = {\"_parse_error\": raw_clean}\n\n return {\n \"extracted\": extracted,\n \"raw_response\": raw,\n \"model\": model,\n }\n except Exception as e:\n return {\"error\": str(e)}\n",
|
| 23 |
+
"downloads": 0,
|
| 24 |
+
"created_at": 1710000011
|
| 25 |
+
}
|
mcp_server.py
ADDED
|
@@ -0,0 +1,253 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
FORGE MCP Server
|
| 3 |
+
Exposes FORGE skills via Model Context Protocol (MCP)
|
| 4 |
+
so Claude Desktop, Claude API, and any MCP client can use them natively.
|
| 5 |
+
|
| 6 |
+
Endpoints:
|
| 7 |
+
GET /mcp/sse — SSE stream (for Claude Desktop / mcp-remote)
|
| 8 |
+
POST /mcp — JSON-RPC 2.0 (for direct API calls)
|
| 9 |
+
"""
|
| 10 |
+
import json
|
| 11 |
+
import asyncio
|
| 12 |
+
import time
|
| 13 |
+
from typing import Any
|
| 14 |
+
|
| 15 |
+
from fastapi import FastAPI, Request
|
| 16 |
+
from fastapi.responses import StreamingResponse, JSONResponse
|
| 17 |
+
|
| 18 |
+
import skill_registry as registry
|
| 19 |
+
|
| 20 |
+
# ── MCP Tool Definitions ──────────────────────────────────────────
|
| 21 |
+
|
| 22 |
+
MCP_TOOLS = [
|
| 23 |
+
{
|
| 24 |
+
"name": "forge_list_skills",
|
| 25 |
+
"description": "List all skills available in the FORGE registry. Optionally filter by tag or search query.",
|
| 26 |
+
"inputSchema": {
|
| 27 |
+
"type": "object",
|
| 28 |
+
"properties": {
|
| 29 |
+
"tag": {"type": "string", "description": "Filter by tag (e.g. 'math', 'web', 'claude')"},
|
| 30 |
+
"query": {"type": "string", "description": "Text search across name, description, tags"},
|
| 31 |
+
},
|
| 32 |
+
},
|
| 33 |
+
},
|
| 34 |
+
{
|
| 35 |
+
"name": "forge_get_skill",
|
| 36 |
+
"description": "Get full metadata and executable code for a specific skill by ID.",
|
| 37 |
+
"inputSchema": {
|
| 38 |
+
"type": "object",
|
| 39 |
+
"properties": {
|
| 40 |
+
"skill_id": {"type": "string", "description": "The skill ID, e.g. 'calculator'"},
|
| 41 |
+
},
|
| 42 |
+
"required": ["skill_id"],
|
| 43 |
+
},
|
| 44 |
+
},
|
| 45 |
+
{
|
| 46 |
+
"name": "forge_get_code",
|
| 47 |
+
"description": "Get only the executable Python code for a skill. Returns minimal payload for agent hot-loading.",
|
| 48 |
+
"inputSchema": {
|
| 49 |
+
"type": "object",
|
| 50 |
+
"properties": {
|
| 51 |
+
"skill_id": {"type": "string", "description": "The skill ID"},
|
| 52 |
+
},
|
| 53 |
+
"required": ["skill_id"],
|
| 54 |
+
},
|
| 55 |
+
},
|
| 56 |
+
{
|
| 57 |
+
"name": "forge_search",
|
| 58 |
+
"description": "Search FORGE skills by keyword. Returns matching skills with descriptions and tags.",
|
| 59 |
+
"inputSchema": {
|
| 60 |
+
"type": "object",
|
| 61 |
+
"properties": {
|
| 62 |
+
"query": {"type": "string", "description": "Search query"},
|
| 63 |
+
},
|
| 64 |
+
"required": ["query"],
|
| 65 |
+
},
|
| 66 |
+
},
|
| 67 |
+
{
|
| 68 |
+
"name": "forge_get_stats",
|
| 69 |
+
"description": "Get FORGE registry statistics: total skills, downloads, top skills.",
|
| 70 |
+
"inputSchema": {
|
| 71 |
+
"type": "object",
|
| 72 |
+
"properties": {},
|
| 73 |
+
},
|
| 74 |
+
},
|
| 75 |
+
{
|
| 76 |
+
"name": "forge_publish_skill",
|
| 77 |
+
"description": "Publish a new skill to the FORGE registry. The skill must include id, name, version, description, author, tags, and code with a def execute() function.",
|
| 78 |
+
"inputSchema": {
|
| 79 |
+
"type": "object",
|
| 80 |
+
"properties": {
|
| 81 |
+
"skill_json": {
|
| 82 |
+
"type": "string",
|
| 83 |
+
"description": "JSON string of the skill document to publish",
|
| 84 |
+
},
|
| 85 |
+
},
|
| 86 |
+
"required": ["skill_json"],
|
| 87 |
+
},
|
| 88 |
+
},
|
| 89 |
+
]
|
| 90 |
+
|
| 91 |
+
# ── Tool Dispatcher ───────────────────────────────────────────────
|
| 92 |
+
|
| 93 |
+
def dispatch_tool(name: str, args: dict) -> Any:
|
| 94 |
+
if name == "forge_list_skills":
|
| 95 |
+
tag = args.get("tag")
|
| 96 |
+
query = args.get("query")
|
| 97 |
+
skills = registry.list_skills(tag=tag, query=query)
|
| 98 |
+
return {
|
| 99 |
+
"count": len(skills),
|
| 100 |
+
"skills": [
|
| 101 |
+
{
|
| 102 |
+
"id": s["id"],
|
| 103 |
+
"name": s["name"],
|
| 104 |
+
"version": s["version"],
|
| 105 |
+
"description": s["description"],
|
| 106 |
+
"tags": s.get("tags", []),
|
| 107 |
+
"downloads": s.get("downloads", 0),
|
| 108 |
+
}
|
| 109 |
+
for s in skills
|
| 110 |
+
],
|
| 111 |
+
}
|
| 112 |
+
|
| 113 |
+
elif name == "forge_get_skill":
|
| 114 |
+
skill_id = args.get("skill_id", "")
|
| 115 |
+
skill = registry.get_skill(skill_id)
|
| 116 |
+
if not skill:
|
| 117 |
+
return {"error": f"Skill '{skill_id}' not found"}
|
| 118 |
+
return skill
|
| 119 |
+
|
| 120 |
+
elif name == "forge_get_code":
|
| 121 |
+
skill_id = args.get("skill_id", "")
|
| 122 |
+
data = registry.get_skill_code(skill_id)
|
| 123 |
+
if not data:
|
| 124 |
+
return {"error": f"Skill '{skill_id}' not found"}
|
| 125 |
+
return data
|
| 126 |
+
|
| 127 |
+
elif name == "forge_search":
|
| 128 |
+
query = args.get("query", "")
|
| 129 |
+
skills = registry.list_skills(query=query)
|
| 130 |
+
return {
|
| 131 |
+
"query": query,
|
| 132 |
+
"count": len(skills),
|
| 133 |
+
"skills": [{"id": s["id"], "name": s["name"], "description": s["description"], "tags": s.get("tags", [])} for s in skills],
|
| 134 |
+
}
|
| 135 |
+
|
| 136 |
+
elif name == "forge_get_stats":
|
| 137 |
+
return registry.get_stats()
|
| 138 |
+
|
| 139 |
+
elif name == "forge_publish_skill":
|
| 140 |
+
try:
|
| 141 |
+
skill = json.loads(args.get("skill_json", "{}"))
|
| 142 |
+
except json.JSONDecodeError as e:
|
| 143 |
+
return {"error": f"Invalid JSON: {e}"}
|
| 144 |
+
ok, msg = registry.publish_skill(skill)
|
| 145 |
+
return {"ok": ok, "message": msg}
|
| 146 |
+
|
| 147 |
+
else:
|
| 148 |
+
return {"error": f"Unknown tool: {name}"}
|
| 149 |
+
|
| 150 |
+
|
| 151 |
+
# ── JSON-RPC 2.0 Handler ─────────────────────────────────────────
|
| 152 |
+
|
| 153 |
+
def handle_jsonrpc(request_body: dict) -> dict:
|
| 154 |
+
method = request_body.get("method", "")
|
| 155 |
+
params = request_body.get("params", {})
|
| 156 |
+
req_id = request_body.get("id", 1)
|
| 157 |
+
|
| 158 |
+
def ok(result):
|
| 159 |
+
return {"jsonrpc": "2.0", "id": req_id, "result": result}
|
| 160 |
+
|
| 161 |
+
def err(code, message):
|
| 162 |
+
return {"jsonrpc": "2.0", "id": req_id, "error": {"code": code, "message": message}}
|
| 163 |
+
|
| 164 |
+
# MCP lifecycle
|
| 165 |
+
if method == "initialize":
|
| 166 |
+
return ok({
|
| 167 |
+
"protocolVersion": "2024-11-05",
|
| 168 |
+
"capabilities": {"tools": {"listChanged": False}},
|
| 169 |
+
"serverInfo": {
|
| 170 |
+
"name": "forge",
|
| 171 |
+
"version": "1.0.0",
|
| 172 |
+
"description": "FORGE — Federated Open Registry for Generative Executables. Skill artifactory for AI agents.",
|
| 173 |
+
},
|
| 174 |
+
})
|
| 175 |
+
|
| 176 |
+
elif method == "tools/list":
|
| 177 |
+
return ok({"tools": MCP_TOOLS})
|
| 178 |
+
|
| 179 |
+
elif method == "tools/call":
|
| 180 |
+
tool_name = params.get("name", "")
|
| 181 |
+
tool_args = params.get("arguments", {})
|
| 182 |
+
result = dispatch_tool(tool_name, tool_args)
|
| 183 |
+
return ok({
|
| 184 |
+
"content": [{"type": "text", "text": json.dumps(result, indent=2, ensure_ascii=False)}]
|
| 185 |
+
})
|
| 186 |
+
|
| 187 |
+
elif method == "ping":
|
| 188 |
+
return ok({})
|
| 189 |
+
|
| 190 |
+
else:
|
| 191 |
+
return err(-32601, f"Method not found: {method}")
|
| 192 |
+
|
| 193 |
+
|
| 194 |
+
# ── FastAPI Routes ────────────────────────────────────────────────
|
| 195 |
+
|
| 196 |
+
def register_mcp_routes(app: FastAPI):
|
| 197 |
+
|
| 198 |
+
@app.post("/mcp")
|
| 199 |
+
async def mcp_jsonrpc(request: Request):
|
| 200 |
+
try:
|
| 201 |
+
body = await request.json()
|
| 202 |
+
except Exception:
|
| 203 |
+
return JSONResponse(
|
| 204 |
+
{"jsonrpc": "2.0", "id": None, "error": {"code": -32700, "message": "Parse error"}},
|
| 205 |
+
status_code=400,
|
| 206 |
+
)
|
| 207 |
+
response = handle_jsonrpc(body)
|
| 208 |
+
return JSONResponse(response)
|
| 209 |
+
|
| 210 |
+
@app.get("/mcp/sse")
|
| 211 |
+
async def mcp_sse(request: Request):
|
| 212 |
+
"""
|
| 213 |
+
SSE endpoint for Claude Desktop / mcp-remote clients.
|
| 214 |
+
Streams MCP messages over Server-Sent Events.
|
| 215 |
+
"""
|
| 216 |
+
async def event_stream():
|
| 217 |
+
# Send server info on connect
|
| 218 |
+
server_info = json.dumps({
|
| 219 |
+
"jsonrpc": "2.0",
|
| 220 |
+
"method": "notifications/initialized",
|
| 221 |
+
"params": {
|
| 222 |
+
"serverInfo": {
|
| 223 |
+
"name": "forge",
|
| 224 |
+
"version": "1.0.0",
|
| 225 |
+
},
|
| 226 |
+
"tools": MCP_TOOLS,
|
| 227 |
+
},
|
| 228 |
+
})
|
| 229 |
+
yield f"event: message\ndata: {server_info}\n\n"
|
| 230 |
+
|
| 231 |
+
# Keep alive
|
| 232 |
+
try:
|
| 233 |
+
while True:
|
| 234 |
+
if await request.is_disconnected():
|
| 235 |
+
break
|
| 236 |
+
yield f"event: ping\ndata: {json.dumps({'ts': int(time.time())})}\n\n"
|
| 237 |
+
await asyncio.sleep(15)
|
| 238 |
+
except asyncio.CancelledError:
|
| 239 |
+
pass
|
| 240 |
+
|
| 241 |
+
return StreamingResponse(
|
| 242 |
+
event_stream(),
|
| 243 |
+
media_type="text/event-stream",
|
| 244 |
+
headers={
|
| 245 |
+
"Cache-Control": "no-cache",
|
| 246 |
+
"X-Accel-Buffering": "no",
|
| 247 |
+
},
|
| 248 |
+
)
|
| 249 |
+
|
| 250 |
+
@app.get("/mcp/tools")
|
| 251 |
+
async def mcp_tools_list():
|
| 252 |
+
"""Quick HTTP endpoint listing all MCP tools (non-SSE)."""
|
| 253 |
+
return JSONResponse({"tools": MCP_TOOLS, "count": len(MCP_TOOLS)})
|
requirements.txt
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
gradio>=4.44.0
|
| 2 |
+
fastapi>=0.111.0
|
| 3 |
+
uvicorn>=0.30.0
|
| 4 |
+
requests>=2.31.0
|
| 5 |
+
anthropic>=0.40.0
|
skill_registry.py
ADDED
|
@@ -0,0 +1,231 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
FORGE — Skill Registry Core
|
| 3 |
+
Handles loading, validation, search, and serving of skills.
|
| 4 |
+
"""
|
| 5 |
+
import json
|
| 6 |
+
import os
|
| 7 |
+
import re
|
| 8 |
+
import time
|
| 9 |
+
from pathlib import Path
|
| 10 |
+
from typing import Optional
|
| 11 |
+
|
| 12 |
+
SKILLS_DIR = Path(__file__).parent / "skills"
|
| 13 |
+
|
| 14 |
+
REQUIRED_FIELDS = {"id", "name", "version", "description", "author", "tags", "code"}
|
| 15 |
+
|
| 16 |
+
# ─────────────────────────────────────────────
|
| 17 |
+
# Internal helpers
|
| 18 |
+
# ─────────────────────────────────────────────
|
| 19 |
+
|
| 20 |
+
def _load_skill_file(path: Path) -> dict | None:
|
| 21 |
+
try:
|
| 22 |
+
data = json.loads(path.read_text(encoding="utf-8"))
|
| 23 |
+
if REQUIRED_FIELDS.issubset(data.keys()):
|
| 24 |
+
data.setdefault("downloads", 0)
|
| 25 |
+
data.setdefault("created_at", int(path.stat().st_mtime))
|
| 26 |
+
return data
|
| 27 |
+
except Exception:
|
| 28 |
+
pass
|
| 29 |
+
return None
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
def _save_skill_file(skill: dict):
|
| 33 |
+
path = SKILLS_DIR / f"{skill['id']}.json"
|
| 34 |
+
path.write_text(json.dumps(skill, indent=2, ensure_ascii=False), encoding="utf-8")
|
| 35 |
+
|
| 36 |
+
|
| 37 |
+
# ─────────────────────────────────────────────
|
| 38 |
+
# Public API
|
| 39 |
+
# ─────────────────────────────────────────────
|
| 40 |
+
|
| 41 |
+
def list_skills(tag: Optional[str] = None, query: Optional[str] = None) -> list[dict]:
|
| 42 |
+
"""Return all skills (stripped of code), optionally filtered."""
|
| 43 |
+
skills = []
|
| 44 |
+
for path in sorted(SKILLS_DIR.glob("*.json")):
|
| 45 |
+
skill = _load_skill_file(path)
|
| 46 |
+
if skill is None:
|
| 47 |
+
continue
|
| 48 |
+
if tag and tag.lower() not in [t.lower() for t in skill.get("tags", [])]:
|
| 49 |
+
continue
|
| 50 |
+
if query:
|
| 51 |
+
q = query.lower()
|
| 52 |
+
searchable = f"{skill['name']} {skill['description']} {' '.join(skill.get('tags', []))}"
|
| 53 |
+
if q not in searchable.lower():
|
| 54 |
+
continue
|
| 55 |
+
skills.append({k: v for k, v in skill.items() if k != "code"})
|
| 56 |
+
return skills
|
| 57 |
+
|
| 58 |
+
|
| 59 |
+
def get_skill(skill_id: str) -> dict | None:
|
| 60 |
+
"""Return full skill including code."""
|
| 61 |
+
path = SKILLS_DIR / f"{skill_id}.json"
|
| 62 |
+
if not path.exists():
|
| 63 |
+
return None
|
| 64 |
+
skill = _load_skill_file(path)
|
| 65 |
+
if skill:
|
| 66 |
+
# bump download count
|
| 67 |
+
skill["downloads"] = skill.get("downloads", 0) + 1
|
| 68 |
+
_save_skill_file(skill)
|
| 69 |
+
return skill
|
| 70 |
+
|
| 71 |
+
|
| 72 |
+
def get_skill_code(skill_id: str) -> dict | None:
|
| 73 |
+
"""Return just the code + minimal meta for agent consumption."""
|
| 74 |
+
skill = get_skill(skill_id)
|
| 75 |
+
if not skill:
|
| 76 |
+
return None
|
| 77 |
+
return {
|
| 78 |
+
"id": skill["id"],
|
| 79 |
+
"version": skill["version"],
|
| 80 |
+
"dependencies": skill.get("dependencies", []),
|
| 81 |
+
"schema": skill.get("schema", {}),
|
| 82 |
+
"code": skill["code"],
|
| 83 |
+
}
|
| 84 |
+
|
| 85 |
+
|
| 86 |
+
def get_manifest() -> dict:
|
| 87 |
+
"""Full manifest for agent bootstrap — includes code for all skills."""
|
| 88 |
+
skills = []
|
| 89 |
+
for path in sorted(SKILLS_DIR.glob("*.json")):
|
| 90 |
+
skill = _load_skill_file(path)
|
| 91 |
+
if skill:
|
| 92 |
+
skills.append(skill)
|
| 93 |
+
return {
|
| 94 |
+
"forge_version": "1.0.0",
|
| 95 |
+
"generated_at": int(time.time()),
|
| 96 |
+
"skill_count": len(skills),
|
| 97 |
+
"skills": skills,
|
| 98 |
+
}
|
| 99 |
+
|
| 100 |
+
|
| 101 |
+
def get_all_tags() -> list[str]:
|
| 102 |
+
tags: set[str] = set()
|
| 103 |
+
for path in SKILLS_DIR.glob("*.json"):
|
| 104 |
+
skill = _load_skill_file(path)
|
| 105 |
+
if skill:
|
| 106 |
+
tags.update(skill.get("tags", []))
|
| 107 |
+
return sorted(tags)
|
| 108 |
+
|
| 109 |
+
|
| 110 |
+
def publish_skill(skill: dict) -> tuple[bool, str]:
|
| 111 |
+
"""Validate and save a new skill. Returns (ok, message)."""
|
| 112 |
+
missing = REQUIRED_FIELDS - set(skill.keys())
|
| 113 |
+
if missing:
|
| 114 |
+
return False, f"Missing required fields: {missing}"
|
| 115 |
+
|
| 116 |
+
# Validate ID format
|
| 117 |
+
if not re.match(r"^[a-z][a-z0-9_]{1,48}[a-z0-9]$", skill["id"]):
|
| 118 |
+
return False, "ID must be lowercase alphanumeric + underscores, 3-50 chars"
|
| 119 |
+
|
| 120 |
+
# Check execute() is present
|
| 121 |
+
if "def execute(" not in skill["code"]:
|
| 122 |
+
return False, "Skill code must contain a def execute(...) function"
|
| 123 |
+
|
| 124 |
+
# Don't overwrite existing (use versioning)
|
| 125 |
+
path = SKILLS_DIR / f"{skill['id']}.json"
|
| 126 |
+
if path.exists():
|
| 127 |
+
existing = _load_skill_file(path)
|
| 128 |
+
if existing and existing.get("version") == skill.get("version"):
|
| 129 |
+
return False, f"Version {skill['version']} of '{skill['id']}' already exists"
|
| 130 |
+
|
| 131 |
+
skill["created_at"] = int(time.time())
|
| 132 |
+
skill["downloads"] = 0
|
| 133 |
+
_save_skill_file(skill)
|
| 134 |
+
return True, f"Skill '{skill['id']}' v{skill['version']} published successfully"
|
| 135 |
+
|
| 136 |
+
|
| 137 |
+
def update_skill(skill_id: str, updates: dict) -> tuple[bool, str]:
|
| 138 |
+
"""Update fields of an existing skill. Bumps version if code changes."""
|
| 139 |
+
path = SKILLS_DIR / f"{skill_id}.json"
|
| 140 |
+
if not path.exists():
|
| 141 |
+
return False, f"Skill '{skill_id}' not found"
|
| 142 |
+
existing = _load_skill_file(path)
|
| 143 |
+
if not existing:
|
| 144 |
+
return False, f"Failed to load skill '{skill_id}'"
|
| 145 |
+
|
| 146 |
+
# Prevent ID change via update
|
| 147 |
+
updates.pop("id", None)
|
| 148 |
+
updates.pop("created_at", None)
|
| 149 |
+
updates.pop("downloads", None)
|
| 150 |
+
|
| 151 |
+
existing.update(updates)
|
| 152 |
+
existing["updated_at"] = int(time.time())
|
| 153 |
+
_save_skill_file(existing)
|
| 154 |
+
return True, f"Skill '{skill_id}' updated"
|
| 155 |
+
|
| 156 |
+
|
| 157 |
+
def delete_skill(skill_id: str) -> tuple[bool, str]:
|
| 158 |
+
"""Delete a skill from the registry."""
|
| 159 |
+
path = SKILLS_DIR / f"{skill_id}.json"
|
| 160 |
+
if not path.exists():
|
| 161 |
+
return False, f"Skill '{skill_id}' not found"
|
| 162 |
+
# Archive before deletion
|
| 163 |
+
archive = SKILLS_DIR / f"{skill_id}.deleted.json"
|
| 164 |
+
path.rename(archive)
|
| 165 |
+
return True, f"Skill '{skill_id}' deleted"
|
| 166 |
+
|
| 167 |
+
|
| 168 |
+
def parse_skill_md(content: str) -> tuple[dict | None, str]:
|
| 169 |
+
"""
|
| 170 |
+
Parse a SKILL.md file (ClawHub-compatible format) into a FORGE skill dict.
|
| 171 |
+
Returns (skill_dict, error_message).
|
| 172 |
+
SKILL.md frontmatter (YAML between --- delimiters) + markdown body.
|
| 173 |
+
"""
|
| 174 |
+
import re
|
| 175 |
+
|
| 176 |
+
# Extract YAML frontmatter
|
| 177 |
+
fm_match = re.match(r"^---\s*\n(.*?)\n---\s*\n?(.*)", content, re.DOTALL)
|
| 178 |
+
if not fm_match:
|
| 179 |
+
return None, "No YAML frontmatter found. SKILL.md must start with ---"
|
| 180 |
+
|
| 181 |
+
yaml_str, body = fm_match.group(1), fm_match.group(2)
|
| 182 |
+
|
| 183 |
+
# Simple YAML parser (avoid pyyaml dependency)
|
| 184 |
+
meta = {}
|
| 185 |
+
for line in yaml_str.splitlines():
|
| 186 |
+
if ":" in line:
|
| 187 |
+
k, _, v = line.partition(":")
|
| 188 |
+
k = k.strip()
|
| 189 |
+
v = v.strip().strip('"').strip("'")
|
| 190 |
+
# Handle list values: [a, b, c]
|
| 191 |
+
if v.startswith("[") and v.endswith("]"):
|
| 192 |
+
v = [x.strip().strip('"').strip("'") for x in v[1:-1].split(",") if x.strip()]
|
| 193 |
+
meta[k] = v
|
| 194 |
+
|
| 195 |
+
required = {"name", "version", "description"}
|
| 196 |
+
missing = required - set(meta.keys())
|
| 197 |
+
if missing:
|
| 198 |
+
return None, f"Missing required frontmatter fields: {missing}"
|
| 199 |
+
|
| 200 |
+
# Build skill ID from name
|
| 201 |
+
import re as _re
|
| 202 |
+
skill_id = _re.sub(r"[^a-z0-9_]", "_", meta["name"].lower()).strip("_")
|
| 203 |
+
|
| 204 |
+
skill = {
|
| 205 |
+
"id": meta.get("id", skill_id),
|
| 206 |
+
"name": meta["name"],
|
| 207 |
+
"version": meta.get("version", "1.0.0"),
|
| 208 |
+
"description": meta["description"],
|
| 209 |
+
"author": meta.get("author", "unknown"),
|
| 210 |
+
"tags": meta.get("tags", []) if isinstance(meta.get("tags"), list) else [meta.get("tags", "instructions")],
|
| 211 |
+
"runtime": meta.get("runtime", "instructions"),
|
| 212 |
+
"dependencies": [],
|
| 213 |
+
"schema": {},
|
| 214 |
+
# SKILL.md skills store markdown body as "instructions" (not executable code)
|
| 215 |
+
"instructions": body.strip(),
|
| 216 |
+
# Minimal execute() wrapper that returns the instructions
|
| 217 |
+
"code": f'def execute(**kwargs) -> dict:\n """SKILL.md instruction skill — returns the instructions for the agent."""\n return {{"instructions": """{body.strip()[:500]}""", "runtime": "instructions"}}',
|
| 218 |
+
}
|
| 219 |
+
return skill, ""
|
| 220 |
+
|
| 221 |
+
|
| 222 |
+
def get_stats() -> dict:
|
| 223 |
+
skills = list_skills()
|
| 224 |
+
total_downloads = sum(s.get("downloads", 0) for s in skills)
|
| 225 |
+
all_tags = get_all_tags()
|
| 226 |
+
return {
|
| 227 |
+
"total_skills": len(skills),
|
| 228 |
+
"total_downloads": total_downloads,
|
| 229 |
+
"total_tags": len(all_tags),
|
| 230 |
+
"top_skills": sorted(skills, key=lambda x: x.get("downloads", 0), reverse=True)[:5],
|
| 231 |
+
}
|