feat: comprehensive registry improvements
Community docs: - CODE_OF_CONDUCT.md (Contributor Covenant v2.1) - SECURITY.md (vulnerability reporting policy) - CHANGELOG.md (initial release notes) - CODEOWNERS (per-type review ownership) GitHub config: - Issue templates: bug-report, pricing-correction, documentation - FUNDING.yml (GitHub Sponsors) Validation enhancements: - Cross-reference check: hand [[requires]] → integration existence - Routing alias collisions as warnings (errors with --strict) - --strict flag to promote warnings to errors - --type filter to validate single content type - CI: add taplo format check and lychee link check jobs Developer experience: - Makefile with validate, fmt, and scaffold targets - Scaffold templates for all 5 content types - .pre-commit-config.yaml (trailing whitespace, TOML check, validate) - docs/content-guide.md (naming, descriptions, prompts, decision guide) Content quality: - schema.toml: add last_verified field for model pricing - CONTRIBUTING.md: add pricing verification guide with source links
This commit is contained in:
1 parent
6cc5c745be
commit
a8b7c9d08b
20 files changed
+670
-18
No files matched your search
@@ -0,0 +1 @@
|
||||
github: [librefang]
|
||||
@@ -0,0 +1,49 @@
|
||||
name: Bug Report
|
||||
description: Report an issue with existing registry content (wrong data, broken config, etc.)
|
||||
labels: ["bug"]
|
||||
body:
|
||||
- type: dropdown
|
||||
id: content_type
|
||||
attributes:
|
||||
label: Content Type
|
||||
description: What type of registry content has the issue?
|
||||
options:
|
||||
- Provider
|
||||
- Agent
|
||||
- Hand
|
||||
- Integration (MCP server)
|
||||
- Skill
|
||||
- Plugin
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
id: content_name
|
||||
attributes:
|
||||
label: Content Name
|
||||
description: The name or ID of the affected content.
|
||||
placeholder: e.g. openai, code-review-agent, github
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: whats_wrong
|
||||
attributes:
|
||||
label: What's Wrong
|
||||
description: Describe the issue you found.
|
||||
placeholder: |
|
||||
e.g. The integration command listed in the registry fails to install,
|
||||
or the agent config references a model that doesn't exist.
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: expected_vs_actual
|
||||
attributes:
|
||||
label: Expected vs Actual
|
||||
description: What did you expect, and what actually happened?
|
||||
placeholder: |
|
||||
Expected: The provider config should list gpt-4o as a supported model.
|
||||
Actual: gpt-4o is missing from the models array.
|
||||
validations:
|
||||
required: true
|
||||
@@ -0,0 +1,23 @@
|
||||
name: Documentation Issue
|
||||
description: Report incorrect, incomplete, or missing documentation
|
||||
labels: ["documentation"]
|
||||
body:
|
||||
- type: input
|
||||
id: which_document
|
||||
attributes:
|
||||
label: Which Document
|
||||
description: The file path or page where the issue exists.
|
||||
placeholder: e.g. README.md, docs/contributing.md, providers/openai.toml
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: whats_wrong
|
||||
attributes:
|
||||
label: What's Wrong or Missing
|
||||
description: Describe the documentation issue.
|
||||
placeholder: |
|
||||
e.g. The contributing guide doesn't explain how to add a new provider,
|
||||
or the README setup instructions are outdated.
|
||||
validations:
|
||||
required: true
|
||||
@@ -0,0 +1,44 @@
|
||||
name: Pricing Correction
|
||||
description: Report outdated or incorrect model pricing
|
||||
labels: ["pricing"]
|
||||
body:
|
||||
- type: input
|
||||
id: provider
|
||||
attributes:
|
||||
label: Provider Name
|
||||
description: Which provider's pricing needs correction?
|
||||
placeholder: e.g. openai, anthropic, groq
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
id: model_id
|
||||
attributes:
|
||||
label: Model ID
|
||||
description: The canonical model identifier.
|
||||
placeholder: e.g. gpt-4o, claude-sonnet-4-20250514
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
id: current_pricing
|
||||
attributes:
|
||||
label: Current Pricing in Registry
|
||||
description: What does the registry currently show?
|
||||
placeholder: "e.g. Input: $5.00 / 1M tokens, Output: $15.00 / 1M tokens"
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: correct_pricing
|
||||
attributes:
|
||||
label: Correct Pricing (with source)
|
||||
description: |
|
||||
Provide the correct pricing and a link to the official source
|
||||
so we can verify the update.
|
||||
placeholder: |
|
||||
Input: $2.50 / 1M tokens
|
||||
Output: $10.00 / 1M tokens
|
||||
Source: https://openai.com/pricing
|
||||
validations:
|
||||
required: true
|
||||
@@ -18,3 +18,30 @@ jobs:
|
||||
|
||||
- name: Validate registry
|
||||
run: python scripts/validate.py
|
||||
|
||||
toml-format-check:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
|
||||
- name: Install taplo
|
||||
run: |
|
||||
curl -fsSL https://github.com/tamasfe/taplo/releases/latest/download/taplo-full-linux-x86_64.gz \
|
||||
| gunzip > /usr/local/bin/taplo
|
||||
chmod +x /usr/local/bin/taplo
|
||||
|
||||
- name: Check TOML formatting
|
||||
continue-on-error: true
|
||||
run: taplo fmt --check **/*.toml
|
||||
|
||||
markdown-link-check:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
|
||||
- name: Check markdown links
|
||||
uses: lycheeverse/lychee-action@v2
|
||||
with:
|
||||
args: --verbose --no-progress '**/*.md'
|
||||
fail: false
|
||||
continue-on-error: true
|
||||
@@ -0,0 +1,16 @@
|
||||
repos:
|
||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||
rev: v5.0.0
|
||||
hooks:
|
||||
- id: trailing-whitespace
|
||||
- id: end-of-file-fixer
|
||||
- id: check-toml
|
||||
|
||||
- repo: local
|
||||
hooks:
|
||||
- id: validate-registry
|
||||
name: Validate registry TOML files
|
||||
entry: python3 scripts/validate.py
|
||||
language: system
|
||||
pass_filenames: false
|
||||
always_run: true
|
||||
@@ -0,0 +1,23 @@
|
||||
# Changelog
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/).
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [1.0.0] - 2026-03-21
|
||||
|
||||
### Added
|
||||
|
||||
- 46 LLM providers with 220+ model definitions
|
||||
- 33 agent definitions across diverse domains
|
||||
- 14 hand definitions (communication, content, data, development, productivity)
|
||||
- 25 MCP integration templates
|
||||
- 2 skill examples (prompt-only and Python)
|
||||
- 1 plugin example (echo-memory)
|
||||
- Validation script with per-type and cross-type checks
|
||||
- CI workflow for automated validation
|
||||
- README documentation for every directory and subdirectory
|
||||
- CONTRIBUTING guide with per-type examples and checklists
|
||||
- Schema reference (schema.toml)
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
# LibreFang Registry — Code Owners
|
||||
# Each line maps a path pattern to the team responsible for review.
|
||||
|
||||
providers/ @librefang/models
|
||||
agents/ @librefang/agents
|
||||
hands/ @librefang/agents
|
||||
integrations/ @librefang/integrations
|
||||
skills/ @librefang/agents
|
||||
plugins/ @librefang/agents
|
||||
scripts/ @librefang/core
|
||||
.github/ @librefang/core
|
||||
*.md @librefang/docs
|
||||
@@ -0,0 +1,29 @@
|
||||
# Code of Conduct
|
||||
|
||||
## Our Pledge
|
||||
|
||||
We pledge to make participation in our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation.
|
||||
|
||||
## Our Standards
|
||||
|
||||
Examples of behavior that contributes to a positive environment:
|
||||
|
||||
- Using welcoming and inclusive language
|
||||
- Being respectful of differing viewpoints and experiences
|
||||
- Gracefully accepting constructive criticism
|
||||
- Focusing on what is best for the community
|
||||
|
||||
Examples of unacceptable behavior:
|
||||
|
||||
- Trolling, insulting/derogatory comments, and personal or political attacks
|
||||
- Public or private harassment
|
||||
- Publishing others' private information without explicit permission
|
||||
- Other conduct which could reasonably be considered inappropriate
|
||||
|
||||
## Enforcement
|
||||
|
||||
Instances of unacceptable behavior may be reported to **team@librefang.dev**. All complaints will be reviewed and investigated promptly and fairly.
|
||||
|
||||
## Attribution
|
||||
|
||||
This Code of Conduct is adapted from the [Contributor Covenant](https://www.contributor-covenant.org/), version 2.1.
|
||||
@@ -222,6 +222,31 @@ aliases = ["short-name"]
|
||||
- [ ] `context_window` and `max_output_tokens` are positive integers
|
||||
- [ ] Boolean capability fields are correct
|
||||
- [ ] Pricing verified from official source
|
||||
- [ ] `last_verified` date included (ISO format, e.g. `2025-03-15`)
|
||||
|
||||
### Pricing Verification
|
||||
|
||||
**Always verify pricing from official sources before submitting.** Model pricing changes frequently and stale data leads to incorrect cost tracking for users.
|
||||
|
||||
When adding or updating model pricing:
|
||||
|
||||
1. Check the provider's official pricing page (see links below)
|
||||
2. Record the exact `input_cost_per_m` and `output_cost_per_m` values in USD per million tokens
|
||||
3. Include the `last_verified` field with today's date in ISO format (`YYYY-MM-DD`)
|
||||
4. If a model is subscription-based (e.g. GitHub Copilot) or has no public per-token pricing, note this in your PR description
|
||||
|
||||
Common official pricing pages:
|
||||
|
||||
- **OpenAI**: https://openai.com/pricing
|
||||
- **Anthropic**: https://docs.anthropic.com/en/docs/about-claude/models
|
||||
- **Google Gemini**: https://ai.google.dev/pricing
|
||||
- **DeepSeek**: https://platform.deepseek.com/api-docs/pricing
|
||||
- **Mistral**: https://mistral.ai/technology/#pricing
|
||||
- **Groq**: https://wow.groq.com/
|
||||
- **xAI**: https://docs.x.ai/docs
|
||||
- **Together**: https://www.together.ai/pricing
|
||||
- **Fireworks**: https://fireworks.ai/pricing
|
||||
- **OpenRouter**: https://openrouter.ai/models (per-model pricing listed)
|
||||
|
||||
## Where to Find Model Information
|
||||
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
# LibreFang Registry — Development Tooling
|
||||
# Usage: make help
|
||||
|
||||
.DEFAULT_GOAL := help
|
||||
SHELL := /bin/bash
|
||||
|
||||
TEMPLATES_DIR := templates
|
||||
AGENTS_DIR := agents
|
||||
HANDS_DIR := hands
|
||||
INTEGRATIONS_DIR := integrations
|
||||
SKILLS_DIR := skills
|
||||
PROVIDERS_DIR := providers
|
||||
|
||||
# ── Validation ────────────────────────────────────────────────────────────────
|
||||
|
||||
.PHONY: validate
|
||||
validate: ## Run registry validation
|
||||
python3 scripts/validate.py
|
||||
|
||||
.PHONY: validate-strict
|
||||
validate-strict: ## Run validation treating warnings as errors
|
||||
python3 scripts/validate.py --strict
|
||||
|
||||
# ── Formatting ────────────────────────────────────────────────────────────────
|
||||
|
||||
.PHONY: fmt
|
||||
fmt: ## Format all TOML files with taplo (skips if not installed)
|
||||
@if command -v taplo >/dev/null 2>&1; then \
|
||||
echo "Formatting TOML files..."; \
|
||||
find . -name '*.toml' -not -path './.git/*' -exec taplo fmt {} +; \
|
||||
echo "Done."; \
|
||||
else \
|
||||
echo "taplo not found — skipping TOML formatting."; \
|
||||
echo "Install: cargo install taplo-cli or brew install taplo"; \
|
||||
fi
|
||||
|
||||
.PHONY: fmt-check
|
||||
fmt-check: ## Check TOML formatting without modifying
|
||||
@if command -v taplo >/dev/null 2>&1; then \
|
||||
taplo fmt --check $$(find . -name '*.toml' -not -path './.git/*'); \
|
||||
else \
|
||||
echo "taplo not found — skipping format check."; \
|
||||
echo "Install: cargo install taplo-cli or brew install taplo"; \
|
||||
fi
|
||||
|
||||
# ── Scaffolding ───────────────────────────────────────────────────────────────
|
||||
|
||||
.PHONY: new-agent
|
||||
new-agent: ## Scaffold a new agent (usage: make new-agent NAME=my-agent)
|
||||
@if [ -z "$(NAME)" ]; then echo "ERROR: NAME is required. Usage: make new-agent NAME=my-agent"; exit 1; fi
|
||||
@if [ -d "$(AGENTS_DIR)/$(NAME)" ]; then echo "ERROR: Agent '$(NAME)' already exists."; exit 1; fi
|
||||
@mkdir -p "$(AGENTS_DIR)/$(NAME)"
|
||||
@sed 's/{{NAME}}/$(NAME)/g' "$(TEMPLATES_DIR)/agent.toml" > "$(AGENTS_DIR)/$(NAME)/agent.toml"
|
||||
@echo "Created $(AGENTS_DIR)/$(NAME)/agent.toml"
|
||||
@echo "Next: edit the agent.toml to fill in TODO placeholders."
|
||||
|
||||
.PHONY: new-hand
|
||||
new-hand: ## Scaffold a new hand (usage: make new-hand NAME=my-hand)
|
||||
@if [ -z "$(NAME)" ]; then echo "ERROR: NAME is required. Usage: make new-hand NAME=my-hand"; exit 1; fi
|
||||
@if [ -d "$(HANDS_DIR)/$(NAME)" ]; then echo "ERROR: Hand '$(NAME)' already exists."; exit 1; fi
|
||||
@mkdir -p "$(HANDS_DIR)/$(NAME)"
|
||||
@sed 's/{{NAME}}/$(NAME)/g' "$(TEMPLATES_DIR)/HAND.toml" > "$(HANDS_DIR)/$(NAME)/HAND.toml"
|
||||
@echo "Created $(HANDS_DIR)/$(NAME)/HAND.toml"
|
||||
@echo "Next: edit the HAND.toml to fill in TODO placeholders."
|
||||
|
||||
.PHONY: new-integration
|
||||
new-integration: ## Scaffold a new integration (usage: make new-integration NAME=my-service)
|
||||
@if [ -z "$(NAME)" ]; then echo "ERROR: NAME is required. Usage: make new-integration NAME=my-service"; exit 1; fi
|
||||
@if [ -f "$(INTEGRATIONS_DIR)/$(NAME).toml" ]; then echo "ERROR: Integration '$(NAME)' already exists."; exit 1; fi
|
||||
@sed 's/{{NAME}}/$(NAME)/g' "$(TEMPLATES_DIR)/integration.toml" > "$(INTEGRATIONS_DIR)/$(NAME).toml"
|
||||
@echo "Created $(INTEGRATIONS_DIR)/$(NAME).toml"
|
||||
@echo "Next: edit the file to fill in TODO placeholders."
|
||||
|
||||
.PHONY: new-skill
|
||||
new-skill: ## Scaffold a new skill (usage: make new-skill NAME=my-skill)
|
||||
@if [ -z "$(NAME)" ]; then echo "ERROR: NAME is required. Usage: make new-skill NAME=my-skill"; exit 1; fi
|
||||
@if [ -d "$(SKILLS_DIR)/$(NAME)" ]; then echo "ERROR: Skill '$(NAME)' already exists."; exit 1; fi
|
||||
@mkdir -p "$(SKILLS_DIR)/$(NAME)"
|
||||
@sed 's/{{NAME}}/$(NAME)/g' "$(TEMPLATES_DIR)/skill.toml" > "$(SKILLS_DIR)/$(NAME)/skill.toml"
|
||||
@echo "Created $(SKILLS_DIR)/$(NAME)/skill.toml"
|
||||
@echo "Next: edit the skill.toml to fill in TODO placeholders."
|
||||
|
||||
.PHONY: new-provider
|
||||
new-provider: ## Scaffold a new provider (usage: make new-provider NAME=my-provider)
|
||||
@if [ -z "$(NAME)" ]; then echo "ERROR: NAME is required. Usage: make new-provider NAME=my-provider"; exit 1; fi
|
||||
@if [ -f "$(PROVIDERS_DIR)/$(NAME).toml" ]; then echo "ERROR: Provider '$(NAME)' already exists."; exit 1; fi
|
||||
@sed 's/{{NAME}}/$(NAME)/g' "$(TEMPLATES_DIR)/provider.toml" > "$(PROVIDERS_DIR)/$(NAME).toml"
|
||||
@echo "Created $(PROVIDERS_DIR)/$(NAME).toml"
|
||||
@echo "Next: edit the file to fill in TODO placeholders."
|
||||
|
||||
# ── Help ──────────────────────────────────────────────────────────────────────
|
||||
|
||||
.PHONY: help
|
||||
help: ## Show this help
|
||||
@echo "LibreFang Registry — Available targets:"
|
||||
@echo ""
|
||||
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | \
|
||||
awk 'BEGIN {FS = ":.*?## "}; {printf " \033[36m%-20s\033[0m %s\n", $$1, $$2}'
|
||||
@echo ""
|
||||
@echo "Scaffold examples:"
|
||||
@echo " make new-agent NAME=my-agent"
|
||||
@echo " make new-hand NAME=my-hand"
|
||||
@echo " make new-integration NAME=my-service"
|
||||
@echo " make new-skill NAME=my-skill"
|
||||
@echo " make new-provider NAME=my-provider"
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
# Security Policy
|
||||
|
||||
## Scope
|
||||
|
||||
This repository contains TOML content definitions (agents, hands, integrations, skills, plugins, providers). Security concerns include:
|
||||
|
||||
- Malicious content in system prompts or descriptions
|
||||
- Command injection in integration transport commands
|
||||
- Integration URLs pointing to phishing or malicious sites
|
||||
- Credential exposure in TOML files
|
||||
|
||||
## Reporting a Vulnerability
|
||||
|
||||
**Do NOT open a public issue for security vulnerabilities.**
|
||||
|
||||
Email **security@librefang.dev** with:
|
||||
|
||||
1. Description of the vulnerability
|
||||
2. Affected file(s) and content type
|
||||
3. Steps to reproduce or exploit
|
||||
4. Suggested fix (if any)
|
||||
|
||||
## Response Timeline
|
||||
|
||||
- **Acknowledgment**: within 48 hours
|
||||
- **Assessment**: within 5 business days
|
||||
- **Fix**: dependent on severity, typically within 2 weeks
|
||||
|
||||
## Supported Versions
|
||||
|
||||
| Version | Supported |
|
||||
|---------|-----------|
|
||||
| main branch | Yes |
|
||||
| Other branches | No |
|
||||
@@ -0,0 +1,50 @@
|
||||
# Content Guide
|
||||
|
||||
Guidelines for contributing content to the LibreFang Registry.
|
||||
|
||||
## Naming Conventions
|
||||
|
||||
- Use **lowercase, hyphenated** names: `my-agent`, `web-scraper`, `code-reviewer`.
|
||||
- Agent/hand/skill directory names must match their `name`/`id` field in the TOML.
|
||||
- Provider and integration filenames must match their `id` field.
|
||||
|
||||
## Writing Descriptions
|
||||
|
||||
- Keep descriptions to **1-2 sentences**. Lead with a verb.
|
||||
- Good: "Analyzes pull requests and suggests improvements."
|
||||
- Bad: "This is an agent that can be used to analyze pull requests."
|
||||
|
||||
## System Prompts (Agents & Hands)
|
||||
|
||||
- Start with a clear role statement: "You are X, responsible for Y."
|
||||
- Include specific instructions on behavior, not vague aspirations.
|
||||
- Define what the agent should **not** do (scope boundaries).
|
||||
- List tools it should use and when.
|
||||
- Aim for **100-500 words** for agents, up to 1000 for complex hands.
|
||||
- Avoid repeating information already in the TOML metadata.
|
||||
|
||||
## Agent vs Hand vs Skill
|
||||
|
||||
| Type | Use When |
|
||||
|------|----------|
|
||||
| **Agent** | Conversational, general-purpose, stateless Q&A or analysis. |
|
||||
| **Hand** | Multi-step workflow requiring tools (shell, files, APIs). Has settings, dashboard, requirements. |
|
||||
| **Skill** | Single focused task with defined inputs/outputs. Prompt-only or a short script. |
|
||||
|
||||
- If it needs `shell_exec` or external tools, it is probably a **hand**.
|
||||
- If it is a reusable prompt template with parameters, it is a **skill**.
|
||||
- If it is a conversational assistant for a domain, it is an **agent**.
|
||||
|
||||
## Provider Entries
|
||||
|
||||
- Include all models the provider offers that support chat completions.
|
||||
- Use accurate `input_cost_per_m` / `output_cost_per_m` (USD per million tokens).
|
||||
- Set `tier` honestly: `frontier` is reserved for the most capable models.
|
||||
- Always include `context_window` and `max_output_tokens` from official docs.
|
||||
|
||||
## General Tips
|
||||
|
||||
- Run `make validate` before submitting.
|
||||
- Run `make fmt` if you have taplo installed to keep TOML formatting consistent.
|
||||
- Check `schema.toml` for the full field reference.
|
||||
- Test scaffold output: `make new-agent NAME=test-agent`, verify, then delete.
|
||||
@@ -27,6 +27,7 @@ context_window = 128000 # Required: maximum input tokens
|
||||
max_output_tokens = 16384 # Required: maximum output tokens
|
||||
input_cost_per_m = 2.50 # Required: USD per million input tokens (0.0 for free/local)
|
||||
output_cost_per_m = 10.0 # Required: USD per million output tokens (0.0 for free/local)
|
||||
last_verified = "2025-03-15" # Optional: ISO date when pricing was last verified against official sources
|
||||
supports_tools = true # Optional: tool/function calling support (default: false)
|
||||
supports_vision = true # Optional: vision/image input support (default: false)
|
||||
supports_streaming = true # Optional: streaming response support (default: true)
|
||||
|
||||
+95
-18
@@ -3,6 +3,9 @@
|
||||
|
||||
Usage:
|
||||
python scripts/validate.py
|
||||
python scripts/validate.py --strict
|
||||
python scripts/validate.py --type providers
|
||||
python scripts/validate.py --strict --type agents
|
||||
|
||||
Validates:
|
||||
- Provider TOML files (required fields, valid tiers, non-negative costs, no duplicates)
|
||||
@@ -11,11 +14,13 @@ Validates:
|
||||
- Integration TOML files (required fields: id, name, [transport])
|
||||
- Skill TOML files (required fields: [skill].name, [runtime].type)
|
||||
- aliases.toml parsing
|
||||
- Cross-reference: hand [[requires]] integration references
|
||||
- Routing alias collision detection (WARNING, ERROR with --strict)
|
||||
|
||||
Exit code 0 on success, 1 on any validation error.
|
||||
"""
|
||||
|
||||
import os
|
||||
import argparse
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
@@ -42,6 +47,8 @@ REQUIRED_MODEL_FIELDS = {
|
||||
"input_cost_per_m", "output_cost_per_m",
|
||||
}
|
||||
|
||||
VALID_CONTENT_TYPES = {"providers", "agents", "hands", "integrations", "skills", "plugins", "aliases"}
|
||||
|
||||
|
||||
def load_toml(filepath: Path) -> tuple[dict | None, str | None]:
|
||||
"""Load a TOML file, return (data, error)."""
|
||||
@@ -126,8 +133,14 @@ def validate_agent_file(filepath: Path) -> list[str]:
|
||||
return errors
|
||||
|
||||
|
||||
def validate_hand_file(filepath: Path) -> list[str]:
|
||||
"""Validate a HAND.toml file."""
|
||||
def validate_hand_file(filepath: Path, integration_ids: set[str]) -> list[str]:
|
||||
"""Validate a HAND.toml file.
|
||||
|
||||
Args:
|
||||
filepath: Path to the HAND.toml file.
|
||||
integration_ids: Set of known integration IDs (filenames without .toml
|
||||
from the integrations/ directory) for cross-reference validation.
|
||||
"""
|
||||
data, err = load_toml(filepath)
|
||||
if err:
|
||||
return [err]
|
||||
@@ -152,6 +165,19 @@ def validate_hand_file(filepath: Path) -> list[str]:
|
||||
if "agent" not in data:
|
||||
errors.append(f"{rel}: Missing [agent] section")
|
||||
|
||||
# Cross-reference: check that [[requires]] with requirement_type = "integration"
|
||||
# reference existing integration TOML files
|
||||
requires_list = data.get("requires", [])
|
||||
if isinstance(requires_list, list):
|
||||
for req in requires_list:
|
||||
if isinstance(req, dict) and req.get("requirement_type") == "integration":
|
||||
req_key = req.get("key", "")
|
||||
if req_key and req_key not in integration_ids:
|
||||
errors.append(
|
||||
f"{rel}: [[requires]] references integration '{req_key}' "
|
||||
f"but no integrations/{req_key}.toml exists"
|
||||
)
|
||||
|
||||
return errors
|
||||
|
||||
|
||||
@@ -233,16 +259,52 @@ def validate_aliases_file(filepath: Path) -> list[str]:
|
||||
return errors
|
||||
|
||||
|
||||
def parse_args() -> argparse.Namespace:
|
||||
"""Parse command-line arguments."""
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Validate all TOML files in the LibreFang registry."
|
||||
)
|
||||
parser.add_argument(
|
||||
"--strict",
|
||||
action="store_true",
|
||||
help="Treat all warnings as errors.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--type",
|
||||
choices=sorted(VALID_CONTENT_TYPES),
|
||||
dest="content_type",
|
||||
default=None,
|
||||
help="Validate only the specified content type (e.g. providers, agents, hands).",
|
||||
)
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def should_validate(content_type: str, filter_type: str | None) -> bool:
|
||||
"""Check whether a content type should be validated given the --type filter."""
|
||||
if filter_type is None:
|
||||
return True
|
||||
return content_type == filter_type
|
||||
|
||||
|
||||
def main():
|
||||
args = parse_args()
|
||||
|
||||
script_dir = Path(__file__).resolve().parent
|
||||
root = script_dir.parent
|
||||
|
||||
all_errors = []
|
||||
stats = {}
|
||||
|
||||
# --- Collect integration IDs for cross-reference validation ---
|
||||
integrations_dir = root / "integrations"
|
||||
integration_ids: set[str] = set()
|
||||
if integrations_dir.is_dir():
|
||||
for fp in integrations_dir.glob("*.toml"):
|
||||
integration_ids.add(fp.stem)
|
||||
|
||||
# --- Providers ---
|
||||
providers_dir = root / "providers"
|
||||
if providers_dir.is_dir():
|
||||
if providers_dir.is_dir() and should_validate("providers", args.content_type):
|
||||
toml_files = sorted(providers_dir.glob("*.toml"))
|
||||
total_models = 0
|
||||
global_model_ids = {}
|
||||
@@ -273,7 +335,7 @@ def main():
|
||||
|
||||
# --- Agents ---
|
||||
agents_dir = root / "agents"
|
||||
if agents_dir.is_dir():
|
||||
if agents_dir.is_dir() and should_validate("agents", args.content_type):
|
||||
agent_dirs = sorted([d for d in agents_dir.iterdir() if d.is_dir() and not d.name.startswith(".")])
|
||||
for d in agent_dirs:
|
||||
agent_toml = d / "agent.toml"
|
||||
@@ -285,19 +347,18 @@ def main():
|
||||
|
||||
# --- Hands ---
|
||||
hands_dir = root / "hands"
|
||||
if hands_dir.is_dir():
|
||||
if hands_dir.is_dir() and should_validate("hands", args.content_type):
|
||||
hand_dirs = sorted([d for d in hands_dir.iterdir() if d.is_dir() and not d.name.startswith(".")])
|
||||
for d in hand_dirs:
|
||||
hand_toml = d / "HAND.toml"
|
||||
if hand_toml.exists():
|
||||
all_errors.extend(validate_hand_file(hand_toml))
|
||||
all_errors.extend(validate_hand_file(hand_toml, integration_ids))
|
||||
else:
|
||||
all_errors.append(f"hands/{d.name}: Missing HAND.toml")
|
||||
stats["hands"] = len(hand_dirs)
|
||||
|
||||
# --- Integrations ---
|
||||
integrations_dir = root / "integrations"
|
||||
if integrations_dir.is_dir():
|
||||
if integrations_dir.is_dir() and should_validate("integrations", args.content_type):
|
||||
int_files = sorted(integrations_dir.glob("*.toml"))
|
||||
for fp in int_files:
|
||||
all_errors.extend(validate_integration_file(fp))
|
||||
@@ -305,7 +366,7 @@ def main():
|
||||
|
||||
# --- Skills ---
|
||||
skills_dir = root / "skills"
|
||||
if skills_dir.is_dir():
|
||||
if skills_dir.is_dir() and should_validate("skills", args.content_type):
|
||||
skill_dirs = sorted([d for d in skills_dir.iterdir() if d.is_dir() and not d.name.startswith(".")])
|
||||
for d in skill_dirs:
|
||||
skill_toml = d / "skill.toml"
|
||||
@@ -317,7 +378,7 @@ def main():
|
||||
|
||||
# --- Plugins ---
|
||||
plugins_dir = root / "plugins"
|
||||
if plugins_dir.is_dir():
|
||||
if plugins_dir.is_dir() and should_validate("plugins", args.content_type):
|
||||
plugin_dirs = sorted([d for d in plugins_dir.iterdir() if d.is_dir() and not d.name.startswith(".")])
|
||||
for d in plugin_dirs:
|
||||
plugin_toml = d / "plugin.toml"
|
||||
@@ -342,14 +403,18 @@ def main():
|
||||
stats["plugins"] = len(plugin_dirs)
|
||||
|
||||
# --- Aliases ---
|
||||
all_errors.extend(validate_aliases_file(root / "aliases.toml"))
|
||||
if should_validate("aliases", args.content_type):
|
||||
all_errors.extend(validate_aliases_file(root / "aliases.toml"))
|
||||
|
||||
# --- Cross-type: duplicate routing aliases ---
|
||||
# --- Cross-type: duplicate routing aliases (now treated as ERRORS) ---
|
||||
# Only run this cross-type check when no --type filter is active,
|
||||
# or when filtering to agents or hands specifically.
|
||||
all_aliases = {} # alias -> (type, name)
|
||||
warnings = []
|
||||
alias_collisions = []
|
||||
run_alias_check = args.content_type is None or args.content_type in ("agents", "hands")
|
||||
|
||||
# Collect agent routing aliases
|
||||
if agents_dir.is_dir():
|
||||
if run_alias_check and agents_dir.is_dir():
|
||||
for d in sorted([d for d in agents_dir.iterdir() if d.is_dir() and not d.name.startswith(".")]):
|
||||
data, _ = load_toml(d / "agent.toml")
|
||||
if data:
|
||||
@@ -358,12 +423,12 @@ def main():
|
||||
key = alias.lower()
|
||||
if key in all_aliases:
|
||||
prev_type, prev_name = all_aliases[key]
|
||||
warnings.append(f"Routing alias '{alias}' used by both {prev_type}/{prev_name} and agent/{d.name}")
|
||||
alias_collisions.append(f"Routing alias '{alias}' used by both {prev_type}/{prev_name} and agent/{d.name}")
|
||||
else:
|
||||
all_aliases[key] = ("agent", d.name)
|
||||
|
||||
# Collect hand routing aliases
|
||||
if hands_dir.is_dir():
|
||||
if run_alias_check and hands_dir.is_dir():
|
||||
for d in sorted([d for d in hands_dir.iterdir() if d.is_dir() and not d.name.startswith(".")]):
|
||||
data, _ = load_toml(d / "HAND.toml")
|
||||
if data:
|
||||
@@ -372,13 +437,25 @@ def main():
|
||||
key = alias.lower()
|
||||
if key in all_aliases:
|
||||
prev_type, prev_name = all_aliases[key]
|
||||
warnings.append(f"Routing alias '{alias}' used by both {prev_type}/{prev_name} and hand/{d.name}")
|
||||
alias_collisions.append(f"Routing alias '{alias}' used by both {prev_type}/{prev_name} and hand/{d.name}")
|
||||
else:
|
||||
all_aliases[key] = ("hand", d.name)
|
||||
|
||||
# --- Collect warnings ---
|
||||
warnings = list(alias_collisions)
|
||||
|
||||
# --- In --strict mode, promote warnings to errors ---
|
||||
if args.strict and warnings:
|
||||
all_errors.extend(warnings)
|
||||
warnings = []
|
||||
|
||||
# --- Report ---
|
||||
print("=" * 60)
|
||||
print("LibreFang Registry Validation")
|
||||
if args.content_type:
|
||||
print(f" Filter: --type {args.content_type}")
|
||||
if args.strict:
|
||||
print(" Mode: --strict (warnings are errors)")
|
||||
print("=" * 60)
|
||||
print()
|
||||
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
id = "{{NAME}}"
|
||||
name = "TODO: Display Name"
|
||||
description = "TODO: One-sentence description of what this hand does."
|
||||
category = "productivity" # communication | content | data | development | devops | finance | productivity | research | social
|
||||
icon = "TODO"
|
||||
tools = ["shell_exec", "file_read", "file_write", "file_list", "memory_store", "memory_recall"]
|
||||
|
||||
[routing]
|
||||
aliases = ["TODO: activation phrases"]
|
||||
weak_aliases = ["TODO: keyword hints"]
|
||||
|
||||
[agent]
|
||||
name = "{{NAME}}-hand"
|
||||
description = "TODO: Agent description"
|
||||
module = "builtin:chat"
|
||||
provider = "default"
|
||||
model = "default"
|
||||
max_tokens = 8192
|
||||
temperature = 0.4
|
||||
max_iterations = 30
|
||||
system_prompt = """TODO: Write a detailed system prompt for the hand's agent.
|
||||
|
||||
Define:
|
||||
- The hand's purpose and capabilities
|
||||
- Step-by-step workflow it follows
|
||||
- Tools it should use and how
|
||||
- Output format and reporting
|
||||
"""
|
||||
|
||||
[metadata]
|
||||
frequency = "on-demand" # continuous | on-demand | scheduled
|
||||
token_consumption = "medium" # low | medium | high
|
||||
default_active = true
|
||||
@@ -0,0 +1,32 @@
|
||||
name = "{{NAME}}"
|
||||
version = "0.1.0"
|
||||
description = "TODO: One-sentence description of what this agent does."
|
||||
author = "TODO"
|
||||
module = "builtin:chat"
|
||||
|
||||
[metadata.routing]
|
||||
aliases = ["TODO: exact match phrases"]
|
||||
weak_aliases = ["TODO: partial match keywords"]
|
||||
|
||||
[model]
|
||||
provider = "default"
|
||||
model = "default"
|
||||
max_tokens = 4096
|
||||
temperature = 0.7
|
||||
system_prompt = """TODO: Write a clear system prompt that defines:
|
||||
- Who the agent is and its role
|
||||
- What it should and should not do
|
||||
- How it should format responses
|
||||
- Any domain-specific instructions
|
||||
|
||||
Keep it focused and under 500 words."""
|
||||
|
||||
[resources]
|
||||
max_llm_tokens_per_hour = 100000
|
||||
|
||||
[capabilities]
|
||||
tools = ["web_search", "web_fetch", "memory_store", "memory_recall"]
|
||||
network = ["*"]
|
||||
memory_read = ["*"]
|
||||
memory_write = ["self.*"]
|
||||
agent_spawn = false
|
||||
@@ -0,0 +1,29 @@
|
||||
id = "{{NAME}}"
|
||||
name = "TODO: Service Display Name"
|
||||
description = "TODO: One-sentence description of this integration."
|
||||
category = "devtools" # devtools | communication | storage | monitoring | data | productivity
|
||||
icon = "TODO"
|
||||
tags = ["TODO"]
|
||||
|
||||
[transport]
|
||||
type = "stdio"
|
||||
command = "npx"
|
||||
args = ["-y", "TODO: @scope/mcp-server-package"]
|
||||
|
||||
[[required_env]]
|
||||
name = "TODO_API_KEY"
|
||||
label = "TODO: API Key"
|
||||
help = "TODO: How to obtain this key"
|
||||
is_secret = true
|
||||
get_url = "https://example.com/settings"
|
||||
|
||||
[health_check]
|
||||
interval_secs = 60
|
||||
unhealthy_threshold = 3
|
||||
|
||||
setup_instructions = """
|
||||
TODO: Step-by-step setup instructions.
|
||||
1. Go to ...
|
||||
2. Create an API key with ...
|
||||
3. Paste the key above.
|
||||
"""
|
||||
@@ -0,0 +1,22 @@
|
||||
# TODO: Provider Name — https://example.com
|
||||
# Models: 1
|
||||
|
||||
[provider]
|
||||
id = "{{NAME}}"
|
||||
display_name = "TODO: Provider Display Name"
|
||||
api_key_env = "TODO_API_KEY"
|
||||
base_url = "https://api.example.com/v1"
|
||||
key_required = true
|
||||
|
||||
[[models]]
|
||||
id = "TODO-model-id"
|
||||
display_name = "TODO: Model Display Name"
|
||||
tier = "balanced" # frontier | smart | balanced | fast | local
|
||||
context_window = 128000
|
||||
max_output_tokens = 4096
|
||||
input_cost_per_m = 0.0
|
||||
output_cost_per_m = 0.0
|
||||
supports_tools = true
|
||||
supports_vision = false
|
||||
supports_streaming = true
|
||||
aliases = []
|
||||
@@ -0,0 +1,20 @@
|
||||
[skill]
|
||||
name = "{{NAME}}"
|
||||
version = "0.1.0"
|
||||
description = "TODO: One-sentence description of what this skill does."
|
||||
author = "TODO"
|
||||
tags = ["TODO"]
|
||||
|
||||
[runtime]
|
||||
type = "promptonly"
|
||||
|
||||
[input]
|
||||
topic = { type = "string", description = "TODO: Describe this input parameter", required = true }
|
||||
|
||||
[prompt]
|
||||
template = """TODO: Write the prompt template for this skill.
|
||||
|
||||
Use {{topic}} to reference input parameters.
|
||||
|
||||
Be specific about the desired output format and constraints.
|
||||
"""
|
||||
Reference in new issue
Block a user