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
+669
-17
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
|
- name: Validate registry
|
||||||
run: python scripts/validate.py
|
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
|
- [ ] `context_window` and `max_output_tokens` are positive integers
|
||||||
- [ ] Boolean capability fields are correct
|
- [ ] Boolean capability fields are correct
|
||||||
- [ ] Pricing verified from official source
|
- [ ] 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
|
## 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
|
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)
|
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)
|
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_tools = true # Optional: tool/function calling support (default: false)
|
||||||
supports_vision = true # Optional: vision/image input support (default: false)
|
supports_vision = true # Optional: vision/image input support (default: false)
|
||||||
supports_streaming = true # Optional: streaming response support (default: true)
|
supports_streaming = true # Optional: streaming response support (default: true)
|
||||||
|
|||||||
+94
-17
@@ -3,6 +3,9 @@
|
|||||||
|
|
||||||
Usage:
|
Usage:
|
||||||
python scripts/validate.py
|
python scripts/validate.py
|
||||||
|
python scripts/validate.py --strict
|
||||||
|
python scripts/validate.py --type providers
|
||||||
|
python scripts/validate.py --strict --type agents
|
||||||
|
|
||||||
Validates:
|
Validates:
|
||||||
- Provider TOML files (required fields, valid tiers, non-negative costs, no duplicates)
|
- 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])
|
- Integration TOML files (required fields: id, name, [transport])
|
||||||
- Skill TOML files (required fields: [skill].name, [runtime].type)
|
- Skill TOML files (required fields: [skill].name, [runtime].type)
|
||||||
- aliases.toml parsing
|
- 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.
|
Exit code 0 on success, 1 on any validation error.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import os
|
import argparse
|
||||||
import sys
|
import sys
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
@@ -42,6 +47,8 @@ REQUIRED_MODEL_FIELDS = {
|
|||||||
"input_cost_per_m", "output_cost_per_m",
|
"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]:
|
def load_toml(filepath: Path) -> tuple[dict | None, str | None]:
|
||||||
"""Load a TOML file, return (data, error)."""
|
"""Load a TOML file, return (data, error)."""
|
||||||
@@ -126,8 +133,14 @@ def validate_agent_file(filepath: Path) -> list[str]:
|
|||||||
return errors
|
return errors
|
||||||
|
|
||||||
|
|
||||||
def validate_hand_file(filepath: Path) -> list[str]:
|
def validate_hand_file(filepath: Path, integration_ids: set[str]) -> list[str]:
|
||||||
"""Validate a HAND.toml file."""
|
"""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)
|
data, err = load_toml(filepath)
|
||||||
if err:
|
if err:
|
||||||
return [err]
|
return [err]
|
||||||
@@ -152,6 +165,19 @@ def validate_hand_file(filepath: Path) -> list[str]:
|
|||||||
if "agent" not in data:
|
if "agent" not in data:
|
||||||
errors.append(f"{rel}: Missing [agent] section")
|
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
|
return errors
|
||||||
|
|
||||||
|
|
||||||
@@ -233,16 +259,52 @@ def validate_aliases_file(filepath: Path) -> list[str]:
|
|||||||
return errors
|
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():
|
def main():
|
||||||
|
args = parse_args()
|
||||||
|
|
||||||
script_dir = Path(__file__).resolve().parent
|
script_dir = Path(__file__).resolve().parent
|
||||||
root = script_dir.parent
|
root = script_dir.parent
|
||||||
|
|
||||||
all_errors = []
|
all_errors = []
|
||||||
stats = {}
|
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 ---
|
||||||
providers_dir = root / "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"))
|
toml_files = sorted(providers_dir.glob("*.toml"))
|
||||||
total_models = 0
|
total_models = 0
|
||||||
global_model_ids = {}
|
global_model_ids = {}
|
||||||
@@ -273,7 +335,7 @@ def main():
|
|||||||
|
|
||||||
# --- Agents ---
|
# --- Agents ---
|
||||||
agents_dir = root / "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(".")])
|
agent_dirs = sorted([d for d in agents_dir.iterdir() if d.is_dir() and not d.name.startswith(".")])
|
||||||
for d in agent_dirs:
|
for d in agent_dirs:
|
||||||
agent_toml = d / "agent.toml"
|
agent_toml = d / "agent.toml"
|
||||||
@@ -285,19 +347,18 @@ def main():
|
|||||||
|
|
||||||
# --- Hands ---
|
# --- Hands ---
|
||||||
hands_dir = root / "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(".")])
|
hand_dirs = sorted([d for d in hands_dir.iterdir() if d.is_dir() and not d.name.startswith(".")])
|
||||||
for d in hand_dirs:
|
for d in hand_dirs:
|
||||||
hand_toml = d / "HAND.toml"
|
hand_toml = d / "HAND.toml"
|
||||||
if hand_toml.exists():
|
if hand_toml.exists():
|
||||||
all_errors.extend(validate_hand_file(hand_toml))
|
all_errors.extend(validate_hand_file(hand_toml, integration_ids))
|
||||||
else:
|
else:
|
||||||
all_errors.append(f"hands/{d.name}: Missing HAND.toml")
|
all_errors.append(f"hands/{d.name}: Missing HAND.toml")
|
||||||
stats["hands"] = len(hand_dirs)
|
stats["hands"] = len(hand_dirs)
|
||||||
|
|
||||||
# --- Integrations ---
|
# --- Integrations ---
|
||||||
integrations_dir = root / "integrations"
|
if integrations_dir.is_dir() and should_validate("integrations", args.content_type):
|
||||||
if integrations_dir.is_dir():
|
|
||||||
int_files = sorted(integrations_dir.glob("*.toml"))
|
int_files = sorted(integrations_dir.glob("*.toml"))
|
||||||
for fp in int_files:
|
for fp in int_files:
|
||||||
all_errors.extend(validate_integration_file(fp))
|
all_errors.extend(validate_integration_file(fp))
|
||||||
@@ -305,7 +366,7 @@ def main():
|
|||||||
|
|
||||||
# --- Skills ---
|
# --- Skills ---
|
||||||
skills_dir = root / "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(".")])
|
skill_dirs = sorted([d for d in skills_dir.iterdir() if d.is_dir() and not d.name.startswith(".")])
|
||||||
for d in skill_dirs:
|
for d in skill_dirs:
|
||||||
skill_toml = d / "skill.toml"
|
skill_toml = d / "skill.toml"
|
||||||
@@ -317,7 +378,7 @@ def main():
|
|||||||
|
|
||||||
# --- Plugins ---
|
# --- Plugins ---
|
||||||
plugins_dir = root / "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(".")])
|
plugin_dirs = sorted([d for d in plugins_dir.iterdir() if d.is_dir() and not d.name.startswith(".")])
|
||||||
for d in plugin_dirs:
|
for d in plugin_dirs:
|
||||||
plugin_toml = d / "plugin.toml"
|
plugin_toml = d / "plugin.toml"
|
||||||
@@ -342,14 +403,18 @@ def main():
|
|||||||
stats["plugins"] = len(plugin_dirs)
|
stats["plugins"] = len(plugin_dirs)
|
||||||
|
|
||||||
# --- Aliases ---
|
# --- Aliases ---
|
||||||
|
if should_validate("aliases", args.content_type):
|
||||||
all_errors.extend(validate_aliases_file(root / "aliases.toml"))
|
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)
|
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
|
# 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(".")]):
|
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")
|
data, _ = load_toml(d / "agent.toml")
|
||||||
if data:
|
if data:
|
||||||
@@ -358,12 +423,12 @@ def main():
|
|||||||
key = alias.lower()
|
key = alias.lower()
|
||||||
if key in all_aliases:
|
if key in all_aliases:
|
||||||
prev_type, prev_name = all_aliases[key]
|
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:
|
else:
|
||||||
all_aliases[key] = ("agent", d.name)
|
all_aliases[key] = ("agent", d.name)
|
||||||
|
|
||||||
# Collect hand routing aliases
|
# 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(".")]):
|
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")
|
data, _ = load_toml(d / "HAND.toml")
|
||||||
if data:
|
if data:
|
||||||
@@ -372,13 +437,25 @@ def main():
|
|||||||
key = alias.lower()
|
key = alias.lower()
|
||||||
if key in all_aliases:
|
if key in all_aliases:
|
||||||
prev_type, prev_name = all_aliases[key]
|
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:
|
else:
|
||||||
all_aliases[key] = ("hand", d.name)
|
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 ---
|
# --- Report ---
|
||||||
print("=" * 60)
|
print("=" * 60)
|
||||||
print("LibreFang Registry Validation")
|
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("=" * 60)
|
||||||
print()
|
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