From b3005cc3687a393efcb6fff6ffbd108fff80d319 Mon Sep 17 00:00:00 2001 From: diegosouzapw Date: Sun, 2 Aug 2026 14:45:51 -0300 Subject: [PATCH] docs: synchronize provider and free-tier inventories --- CONTRIBUTING.md | 8 +- SECURITY.md | 2 +- bin/cli/CONVENTIONS.md | 2 +- bin/cli/README.md | 4 +- docs/README.md | 2 +- docs/architecture/ARCHITECTURE.md | 41 +- docs/architecture/CODEBASE_DOCUMENTATION.md | 30 +- docs/architecture/RESILIENCE_GUIDE.md | 2 +- .../architecture/sqlite-coupling-inventory.md | 2 +- docs/comparison/OMNIROUTE_VS_ALTERNATIVES.md | 24 +- docs/compression/COMPRESSION_GUIDE.md | 2 +- docs/diagrams/README.md | 8 +- docs/diagrams/cli-terminal.svg | 10 +- docs/diagrams/combo-always-on.svg | 13 +- docs/diagrams/comparison-table.svg | 12 +- .../diagrams/exported/auto-combo-12factor.svg | 2 +- docs/diagrams/exported/mcp-tools-104.svg | 1 - docs/diagrams/exported/mcp-tools-107.svg | 1 + docs/diagrams/exported/mcp-tools-99.svg | 1 - docs/diagrams/exported/request-pipeline.svg | 2 +- docs/diagrams/free-tier-budget.svg | 7 +- .../{mcp-tools-104.mmd => mcp-tools-107.mmd} | 17 +- docs/diagrams/mcp-tools-99.mmd | 18 - docs/diagrams/promise-pillars.svg | 22 +- docs/diagrams/readme-hero.svg | 18 +- docs/diagrams/request-pipeline.mmd | 6 +- docs/diagrams/strategies-grid.svg | 8 +- docs/diagrams/tier-cascade.svg | 9 +- docs/frameworks/A2A-SERVER.md | 4 +- docs/frameworks/ACP.md | 2 +- docs/frameworks/AGENT-SKILLS.md | 44 +- docs/frameworks/AGENTBRIDGE.md | 2 +- docs/frameworks/AGENT_PROTOCOLS_GUIDE.md | 4 +- docs/frameworks/OPEN_SSE_ARCHITECTURE.md | 18 +- docs/frameworks/SKILLS.md | 2 +- docs/getting-started/AUTO-COMBO-GUIDE.md | 2 +- docs/getting-started/PROVIDERS-GUIDE.md | 72 +-- docs/guides/FEATURES.md | 4 +- docs/guides/FREE_PROVIDER_RANKINGS.md | 11 +- docs/guides/I18N.md | 6 +- docs/guides/SETUP_GUIDE.md | 2 +- docs/guides/TIERS.md | 2 +- docs/guides/USER_GUIDE.md | 28 +- docs/i18n/README.md | 2 +- docs/i18n/ar/CLAUDE.md | 38 +- docs/i18n/ar/CONTRIBUTING.md | 12 +- docs/i18n/ar/README.md | 224 ++++---- docs/i18n/ar/SECURITY.md | 2 +- .../i18n/ar/docs/architecture/ARCHITECTURE.md | 14 +- docs/i18n/ar/docs/guides/USER_GUIDE.md | 72 ++- docs/i18n/ar/llm.txt | 101 ++-- docs/i18n/az/CLAUDE.md | 38 +- docs/i18n/az/CONTRIBUTING.md | 12 +- docs/i18n/az/README.md | 224 ++++---- docs/i18n/az/SECURITY.md | 2 +- .../i18n/az/docs/architecture/ARCHITECTURE.md | 14 +- docs/i18n/az/docs/guides/USER_GUIDE.md | 72 ++- docs/i18n/az/llm.txt | 101 ++-- docs/i18n/bg/CLAUDE.md | 36 +- docs/i18n/bg/CONTRIBUTING.md | 12 +- docs/i18n/bg/README.md | 224 ++++---- docs/i18n/bg/SECURITY.md | 2 +- .../i18n/bg/docs/architecture/ARCHITECTURE.md | 14 +- docs/i18n/bg/docs/guides/USER_GUIDE.md | 72 ++- docs/i18n/bg/llm.txt | 101 ++-- docs/i18n/bn/CLAUDE.md | 36 +- docs/i18n/bn/CONTRIBUTING.md | 12 +- docs/i18n/bn/README.md | 224 ++++---- docs/i18n/bn/SECURITY.md | 2 +- .../i18n/bn/docs/architecture/ARCHITECTURE.md | 14 +- docs/i18n/bn/docs/guides/USER_GUIDE.md | 72 ++- docs/i18n/bn/llm.txt | 101 ++-- docs/i18n/cs/CLAUDE.md | 38 +- docs/i18n/cs/CONTRIBUTING.md | 12 +- docs/i18n/cs/README.md | 224 ++++---- docs/i18n/cs/SECURITY.md | 2 +- .../i18n/cs/docs/architecture/ARCHITECTURE.md | 14 +- docs/i18n/cs/docs/guides/USER_GUIDE.md | 72 ++- docs/i18n/cs/llm.txt | 101 ++-- docs/i18n/da/CLAUDE.md | 38 +- docs/i18n/da/CONTRIBUTING.md | 12 +- docs/i18n/da/README.md | 224 ++++---- docs/i18n/da/SECURITY.md | 2 +- .../i18n/da/docs/architecture/ARCHITECTURE.md | 14 +- docs/i18n/da/docs/guides/USER_GUIDE.md | 72 ++- docs/i18n/da/llm.txt | 101 ++-- docs/i18n/de/CLAUDE.md | 38 +- docs/i18n/de/CONTRIBUTING.md | 12 +- docs/i18n/de/README.md | 224 ++++---- docs/i18n/de/SECURITY.md | 2 +- .../i18n/de/docs/architecture/ARCHITECTURE.md | 14 +- docs/i18n/de/docs/guides/USER_GUIDE.md | 72 ++- docs/i18n/de/llm.txt | 101 ++-- docs/i18n/es/CLAUDE.md | 38 +- docs/i18n/es/CONTRIBUTING.md | 12 +- docs/i18n/es/README.md | 224 ++++---- docs/i18n/es/SECURITY.md | 2 +- .../i18n/es/docs/architecture/ARCHITECTURE.md | 14 +- docs/i18n/es/docs/guides/USER_GUIDE.md | 72 ++- docs/i18n/es/llm.txt | 101 ++-- docs/i18n/fa/CLAUDE.md | 38 +- docs/i18n/fa/CONTRIBUTING.md | 12 +- docs/i18n/fa/README.md | 224 ++++---- docs/i18n/fa/SECURITY.md | 2 +- .../i18n/fa/docs/architecture/ARCHITECTURE.md | 14 +- docs/i18n/fa/docs/guides/USER_GUIDE.md | 72 ++- docs/i18n/fa/llm.txt | 101 ++-- docs/i18n/fi/CLAUDE.md | 14 +- docs/i18n/fi/CONTRIBUTING.md | 12 +- docs/i18n/fi/README.md | 224 ++++---- docs/i18n/fi/SECURITY.md | 2 +- .../i18n/fi/docs/architecture/ARCHITECTURE.md | 14 +- docs/i18n/fi/docs/guides/USER_GUIDE.md | 72 ++- docs/i18n/fi/llm.txt | 101 ++-- docs/i18n/fr/CLAUDE.md | 38 +- docs/i18n/fr/CONTRIBUTING.md | 12 +- docs/i18n/fr/README.md | 224 ++++---- docs/i18n/fr/SECURITY.md | 2 +- .../i18n/fr/docs/architecture/ARCHITECTURE.md | 14 +- docs/i18n/fr/docs/guides/USER_GUIDE.md | 72 ++- docs/i18n/fr/llm.txt | 101 ++-- docs/i18n/gu/CLAUDE.md | 36 +- docs/i18n/gu/CONTRIBUTING.md | 12 +- docs/i18n/gu/README.md | 224 ++++---- docs/i18n/gu/SECURITY.md | 2 +- .../i18n/gu/docs/architecture/ARCHITECTURE.md | 14 +- docs/i18n/gu/docs/guides/USER_GUIDE.md | 72 ++- docs/i18n/gu/llm.txt | 101 ++-- docs/i18n/he/CLAUDE.md | 36 +- docs/i18n/he/CONTRIBUTING.md | 12 +- docs/i18n/he/README.md | 224 ++++---- docs/i18n/he/SECURITY.md | 2 +- .../i18n/he/docs/architecture/ARCHITECTURE.md | 14 +- docs/i18n/he/docs/guides/USER_GUIDE.md | 72 ++- docs/i18n/he/llm.txt | 101 ++-- docs/i18n/hi/CLAUDE.md | 36 +- docs/i18n/hi/CONTRIBUTING.md | 12 +- docs/i18n/hi/README.md | 224 ++++---- docs/i18n/hi/SECURITY.md | 2 +- .../i18n/hi/docs/architecture/ARCHITECTURE.md | 14 +- docs/i18n/hi/docs/guides/USER_GUIDE.md | 72 ++- docs/i18n/hi/llm.txt | 101 ++-- docs/i18n/hu/CLAUDE.md | 38 +- docs/i18n/hu/CONTRIBUTING.md | 12 +- docs/i18n/hu/README.md | 224 ++++---- docs/i18n/hu/SECURITY.md | 2 +- .../i18n/hu/docs/architecture/ARCHITECTURE.md | 14 +- docs/i18n/hu/docs/guides/USER_GUIDE.md | 72 ++- docs/i18n/hu/llm.txt | 101 ++-- docs/i18n/id/CLAUDE.md | 38 +- docs/i18n/id/CONTRIBUTING.md | 52 +- docs/i18n/id/README.md | 214 ++++---- docs/i18n/id/SECURITY.md | 106 ++-- .../i18n/id/docs/architecture/ARCHITECTURE.md | 156 +++--- docs/i18n/id/docs/guides/USER_GUIDE.md | 198 ++++--- docs/i18n/id/llm.txt | 101 ++-- docs/i18n/in/CLAUDE.md | 38 +- docs/i18n/in/CONTRIBUTING.md | 12 +- docs/i18n/in/README.md | 224 ++++---- docs/i18n/in/SECURITY.md | 2 +- .../i18n/in/docs/architecture/ARCHITECTURE.md | 14 +- docs/i18n/in/docs/guides/USER_GUIDE.md | 72 ++- docs/i18n/in/llm.txt | 101 ++-- docs/i18n/it/CLAUDE.md | 38 +- docs/i18n/it/CONTRIBUTING.md | 12 +- docs/i18n/it/README.md | 224 ++++---- docs/i18n/it/SECURITY.md | 2 +- .../i18n/it/docs/architecture/ARCHITECTURE.md | 14 +- docs/i18n/it/docs/guides/USER_GUIDE.md | 72 ++- docs/i18n/it/llm.txt | 101 ++-- docs/i18n/ja/CLAUDE.md | 36 +- docs/i18n/ja/CONTRIBUTING.md | 12 +- docs/i18n/ja/README.md | 224 ++++---- docs/i18n/ja/SECURITY.md | 2 +- .../i18n/ja/docs/architecture/ARCHITECTURE.md | 14 +- docs/i18n/ja/docs/guides/USER_GUIDE.md | 72 ++- docs/i18n/ja/llm.txt | 101 ++-- docs/i18n/ko/CLAUDE.md | 36 +- docs/i18n/ko/CONTRIBUTING.md | 12 +- docs/i18n/ko/README.md | 224 ++++---- docs/i18n/ko/SECURITY.md | 2 +- .../i18n/ko/docs/architecture/ARCHITECTURE.md | 14 +- docs/i18n/ko/docs/guides/USER_GUIDE.md | 72 ++- docs/i18n/ko/llm.txt | 101 ++-- docs/i18n/mr/CLAUDE.md | 36 +- docs/i18n/mr/CONTRIBUTING.md | 12 +- docs/i18n/mr/README.md | 224 ++++---- docs/i18n/mr/SECURITY.md | 2 +- .../i18n/mr/docs/architecture/ARCHITECTURE.md | 14 +- docs/i18n/mr/docs/guides/USER_GUIDE.md | 72 ++- docs/i18n/mr/llm.txt | 101 ++-- docs/i18n/ms/CLAUDE.md | 38 +- docs/i18n/ms/CONTRIBUTING.md | 12 +- docs/i18n/ms/README.md | 224 ++++---- docs/i18n/ms/SECURITY.md | 2 +- .../i18n/ms/docs/architecture/ARCHITECTURE.md | 14 +- docs/i18n/ms/docs/guides/USER_GUIDE.md | 72 ++- docs/i18n/ms/llm.txt | 101 ++-- docs/i18n/nl/CLAUDE.md | 38 +- docs/i18n/nl/CONTRIBUTING.md | 12 +- docs/i18n/nl/README.md | 224 ++++---- docs/i18n/nl/SECURITY.md | 2 +- .../i18n/nl/docs/architecture/ARCHITECTURE.md | 14 +- docs/i18n/nl/docs/guides/USER_GUIDE.md | 72 ++- docs/i18n/nl/llm.txt | 101 ++-- docs/i18n/no/CLAUDE.md | 38 +- docs/i18n/no/CONTRIBUTING.md | 12 +- docs/i18n/no/README.md | 224 ++++---- docs/i18n/no/SECURITY.md | 2 +- .../i18n/no/docs/architecture/ARCHITECTURE.md | 14 +- docs/i18n/no/docs/guides/USER_GUIDE.md | 72 ++- docs/i18n/no/llm.txt | 101 ++-- docs/i18n/phi/CLAUDE.md | 38 +- docs/i18n/phi/CONTRIBUTING.md | 12 +- docs/i18n/phi/README.md | 224 ++++---- docs/i18n/phi/SECURITY.md | 2 +- .../phi/docs/architecture/ARCHITECTURE.md | 14 +- docs/i18n/phi/docs/guides/USER_GUIDE.md | 72 ++- docs/i18n/phi/llm.txt | 101 ++-- docs/i18n/pl/CLAUDE.md | 37 +- docs/i18n/pl/CONTRIBUTING.md | 10 +- docs/i18n/pl/README.md | 416 +++++++-------- docs/i18n/pl/SECURITY.md | 2 +- docs/i18n/pl/docs/README.md | 2 +- .../i18n/pl/docs/architecture/ARCHITECTURE.md | 27 +- .../architecture/CODEBASE_DOCUMENTATION.md | 10 +- .../pl/docs/architecture/QUALITY_GATES.md | 2 +- .../pl/docs/architecture/REPOSITORY_MAP.md | 14 +- .../pl/docs/architecture/RESILIENCE_GUIDE.md | 4 +- .../comparison/OMNIROUTE_VS_ALTERNATIVES.md | 14 +- .../pl/docs/compression/COMPRESSION_GUIDE.md | 2 +- docs/i18n/pl/docs/frameworks/ACP.md | 2 +- docs/i18n/pl/docs/frameworks/AGENT-SKILLS.md | 2 +- docs/i18n/pl/docs/frameworks/AGENTBRIDGE.md | 2 +- docs/i18n/pl/docs/frameworks/MCP-SERVER.md | 8 +- .../docs/frameworks/OPEN_SSE_ARCHITECTURE.md | 23 +- .../docs/getting-started/AUTO-COMBO-GUIDE.md | 2 +- .../docs/getting-started/FREE-TIERS-GUIDE.md | 68 ++- .../docs/getting-started/PROVIDERS-GUIDE.md | 13 +- docs/i18n/pl/docs/guides/FEATURES.md | 9 +- .../pl/docs/guides/FREE_PROVIDER_RANKINGS.md | 6 +- docs/i18n/pl/docs/guides/SETUP_GUIDE.md | 2 +- docs/i18n/pl/docs/guides/TIERS.md | 2 +- docs/i18n/pl/docs/guides/USER_GUIDE.md | 26 +- docs/i18n/pl/docs/ops/RELEASE_CHECKLIST.md | 4 +- docs/i18n/pl/docs/routing/AUTO-COMBO.md | 21 +- docs/i18n/pl/docs/routing/QUOTA_SHARE.md | 2 +- docs/i18n/pl/llm.txt | 101 ++-- docs/i18n/pt-BR/CLAUDE.md | 38 +- docs/i18n/pt-BR/CONTRIBUTING.md | 12 +- docs/i18n/pt-BR/README.md | 253 +++++---- docs/i18n/pt-BR/SECURITY.md | 2 +- .../pt-BR/docs/architecture/ARCHITECTURE.md | 14 +- docs/i18n/pt-BR/docs/guides/USER_GUIDE.md | 72 ++- docs/i18n/pt-BR/llm.txt | 101 ++-- docs/i18n/pt/CLAUDE.md | 38 +- docs/i18n/pt/CONTRIBUTING.md | 12 +- docs/i18n/pt/README.md | 253 +++++---- docs/i18n/pt/SECURITY.md | 2 +- .../i18n/pt/docs/architecture/ARCHITECTURE.md | 14 +- docs/i18n/pt/docs/guides/USER_GUIDE.md | 72 ++- docs/i18n/pt/llm.txt | 101 ++-- docs/i18n/ro/CLAUDE.md | 38 +- docs/i18n/ro/CONTRIBUTING.md | 12 +- docs/i18n/ro/README.md | 224 ++++---- docs/i18n/ro/SECURITY.md | 2 +- .../i18n/ro/docs/architecture/ARCHITECTURE.md | 14 +- docs/i18n/ro/docs/guides/USER_GUIDE.md | 72 ++- docs/i18n/ro/llm.txt | 101 ++-- docs/i18n/ru/CLAUDE.md | 36 +- docs/i18n/ru/CONTRIBUTING.md | 12 +- docs/i18n/ru/README.md | 427 +++++++-------- docs/i18n/ru/SECURITY.md | 2 +- .../i18n/ru/docs/architecture/ARCHITECTURE.md | 14 +- docs/i18n/ru/docs/guides/USER_GUIDE.md | 72 ++- docs/i18n/ru/llm.txt | 101 ++-- docs/i18n/sk/CLAUDE.md | 38 +- docs/i18n/sk/CONTRIBUTING.md | 12 +- docs/i18n/sk/README.md | 224 ++++---- docs/i18n/sk/SECURITY.md | 2 +- .../i18n/sk/docs/architecture/ARCHITECTURE.md | 14 +- docs/i18n/sk/docs/guides/USER_GUIDE.md | 72 ++- docs/i18n/sk/llm.txt | 101 ++-- docs/i18n/sv/CLAUDE.md | 14 +- docs/i18n/sv/CONTRIBUTING.md | 12 +- docs/i18n/sv/README.md | 224 ++++---- docs/i18n/sv/SECURITY.md | 2 +- .../i18n/sv/docs/architecture/ARCHITECTURE.md | 14 +- docs/i18n/sv/docs/guides/USER_GUIDE.md | 72 ++- docs/i18n/sv/llm.txt | 101 ++-- docs/i18n/sw/CLAUDE.md | 38 +- docs/i18n/sw/CONTRIBUTING.md | 12 +- docs/i18n/sw/README.md | 224 ++++---- docs/i18n/sw/SECURITY.md | 2 +- .../i18n/sw/docs/architecture/ARCHITECTURE.md | 14 +- docs/i18n/sw/docs/guides/USER_GUIDE.md | 72 ++- docs/i18n/sw/llm.txt | 101 ++-- docs/i18n/ta/CLAUDE.md | 36 +- docs/i18n/ta/CONTRIBUTING.md | 12 +- docs/i18n/ta/README.md | 224 ++++---- docs/i18n/ta/SECURITY.md | 2 +- .../i18n/ta/docs/architecture/ARCHITECTURE.md | 14 +- docs/i18n/ta/docs/guides/USER_GUIDE.md | 72 ++- docs/i18n/ta/llm.txt | 101 ++-- docs/i18n/te/CLAUDE.md | 36 +- docs/i18n/te/CONTRIBUTING.md | 12 +- docs/i18n/te/README.md | 224 ++++---- docs/i18n/te/SECURITY.md | 2 +- .../i18n/te/docs/architecture/ARCHITECTURE.md | 14 +- docs/i18n/te/docs/guides/USER_GUIDE.md | 72 ++- docs/i18n/te/llm.txt | 101 ++-- docs/i18n/th/CLAUDE.md | 38 +- docs/i18n/th/CONTRIBUTING.md | 12 +- docs/i18n/th/README.md | 224 ++++---- docs/i18n/th/SECURITY.md | 2 +- .../i18n/th/docs/architecture/ARCHITECTURE.md | 14 +- docs/i18n/th/docs/guides/USER_GUIDE.md | 72 ++- docs/i18n/th/llm.txt | 101 ++-- docs/i18n/tr/CLAUDE.md | 38 +- docs/i18n/tr/CONTRIBUTING.md | 12 +- docs/i18n/tr/README.md | 224 ++++---- docs/i18n/tr/SECURITY.md | 2 +- .../i18n/tr/docs/architecture/ARCHITECTURE.md | 14 +- docs/i18n/tr/docs/guides/USER_GUIDE.md | 72 ++- docs/i18n/tr/llm.txt | 101 ++-- docs/i18n/uk-UA/CLAUDE.md | 36 +- docs/i18n/uk-UA/CONTRIBUTING.md | 12 +- docs/i18n/uk-UA/README.md | 224 ++++---- docs/i18n/uk-UA/SECURITY.md | 2 +- .../uk-UA/docs/architecture/ARCHITECTURE.md | 14 +- docs/i18n/uk-UA/docs/guides/USER_GUIDE.md | 72 ++- docs/i18n/uk-UA/llm.txt | 101 ++-- docs/i18n/ur/CLAUDE.md | 36 +- docs/i18n/ur/CONTRIBUTING.md | 12 +- docs/i18n/ur/README.md | 224 ++++---- docs/i18n/ur/SECURITY.md | 2 +- .../i18n/ur/docs/architecture/ARCHITECTURE.md | 14 +- docs/i18n/ur/docs/guides/USER_GUIDE.md | 72 ++- docs/i18n/ur/llm.txt | 101 ++-- docs/i18n/vi/CLAUDE.md | 14 +- docs/i18n/vi/CONTRIBUTING.md | 12 +- docs/i18n/vi/README.md | 224 ++++---- docs/i18n/vi/SECURITY.md | 2 +- .../i18n/vi/docs/architecture/ARCHITECTURE.md | 14 +- docs/i18n/vi/docs/guides/USER_GUIDE.md | 72 ++- docs/i18n/vi/llm.txt | 101 ++-- docs/i18n/zh-CN/CLAUDE.md | 121 ++--- docs/i18n/zh-CN/CONTRIBUTING.md | 58 +- docs/i18n/zh-CN/README.md | 499 +++++++++--------- docs/i18n/zh-CN/SECURITY.md | 124 ++--- .../zh-CN/docs/architecture/ARCHITECTURE.md | 242 ++++----- .../architecture/CODEBASE_DOCUMENTATION.md | 284 +++++----- docs/i18n/zh-CN/docs/frameworks/MCP-SERVER.md | 310 +++++------ docs/i18n/zh-CN/docs/guides/FEATURES.md | 4 +- docs/i18n/zh-CN/docs/guides/USER_GUIDE.md | 254 ++++----- docs/i18n/zh-CN/docs/ops/RELEASE_CHECKLIST.md | 2 +- docs/i18n/zh-CN/docs/routing/AUTO-COMBO.md | 23 +- docs/i18n/zh-CN/llm.txt | 101 ++-- docs/i18n/zh-TW/CLAUDE.md | 36 +- docs/i18n/zh-TW/CONTRIBUTING.md | 10 +- docs/i18n/zh-TW/README.md | 120 ++--- docs/i18n/zh-TW/SECURITY.md | 124 ++--- .../zh-TW/docs/architecture/ARCHITECTURE.md | 24 +- .../architecture/CODEBASE_DOCUMENTATION.md | 8 +- docs/i18n/zh-TW/docs/frameworks/MCP-SERVER.md | 8 +- docs/i18n/zh-TW/docs/guides/FEATURES.md | 4 +- docs/i18n/zh-TW/docs/guides/USER_GUIDE.md | 256 ++++----- docs/i18n/zh-TW/docs/ops/RELEASE_CHECKLIST.md | 2 +- docs/i18n/zh-TW/docs/routing/AUTO-COMBO.md | 19 +- docs/i18n/zh-TW/llm.txt | 101 ++-- docs/ops/DATABASE_GUIDE.md | 6 +- docs/ops/RELEASE_CHECKLIST.md | 4 +- docs/reference/FREE_TIERS.md | 35 +- docs/routing/AUTO-COMBO.md | 12 +- docs/routing/QUOTA_SHARE.md | 2 +- docs/screenshots/free-tier-budget-card.svg | 101 ++-- llm.txt | 101 ++-- open-sse/executors/claude-web/session.ts | 4 +- open-sse/mcp-server/README.md | 4 +- open-sse/mcp-server/schemas/tools.ts | 23 +- open-sse/mcp-server/tools/agentSkillTools.ts | 17 +- scripts/docs/gen-provider-reference.ts | 14 +- skills/README.md | 21 +- skills/omni-auth/SKILL.md | 6 +- skills/omni-combos-routing/SKILL.md | 47 +- skills/omni-github-skills/SKILL.md | 4 +- skills/omni-mcp/SKILL.md | 10 +- skills/omni-providers/SKILL.md | 4 +- .../agent-skills/AgentSkillsPageClient.tsx | 12 +- .../agent-skills/components/CoverageBar.tsx | 24 +- src/app/.well-known/agent.json/route.ts | 6 +- src/lib/a2a/skills/listCapabilities.ts | 10 +- src/lib/agentSkills/catalog.ts | 19 +- src/lib/agentSkills/generator.ts | 2 +- src/lib/agentSkills/schemas.ts | 5 +- src/lib/agentSkills/types.ts | 4 +- src/shared/constants/agentSkills.ts | 16 +- .../providers/apikey/frontier-labs.ts | 5 +- .../providers/apikey/inference-hosts.ts | 3 +- tests/e2e/agent-skills-page.spec.ts | 39 +- .../integration/agent-skills-content.test.ts | 30 +- .../agent-skills-discovery.test.ts | 38 +- tests/unit/8336-audit-loopback-login.test.ts | 2 +- tests/unit/agent-skills-page.test.tsx | 298 ++++++----- tests/unit/agentSkillTools-mcp.test.ts | 28 +- tests/unit/agentSkills-catalog.test.ts | 21 +- tests/unit/agentSkills-routes.test.ts | 70 ++- tests/unit/agentSkills-schemas.test.ts | 33 +- tests/unit/check-docs-counts-sync.test.ts | 64 ++- tests/unit/listCapabilities-a2a.test.ts | 26 +- 410 files changed, 12130 insertions(+), 11708 deletions(-) delete mode 100644 docs/diagrams/exported/mcp-tools-104.svg create mode 100644 docs/diagrams/exported/mcp-tools-107.svg delete mode 100644 docs/diagrams/exported/mcp-tools-99.svg rename docs/diagrams/{mcp-tools-104.mmd => mcp-tools-107.mmd} (54%) delete mode 100644 docs/diagrams/mcp-tools-99.mmd diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 564aedadfa..da1ecf8f5c 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -293,16 +293,16 @@ src/ # TypeScript (.ts / .tsx) ├── mitm/ # MITM proxy (cert, DNS, target routing) ├── shared/ │ ├── components/ # React components (.tsx) -│ ├── constants/ # Provider definitions (290), MCP scopes, 19 routing strategies +│ ├── constants/ # Provider definitions (327), MCP scopes, 19 routing strategies │ ├── utils/ # Circuit breaker, sanitizer, auth helpers │ └── validation/ # Zod v4 schemas └── sse/ # SSE proxy pipeline open-sse/ # @omniroute/open-sse workspace -├── executors/ # 14 provider-specific request executors +├── executors/ # 89 executor implementation modules ├── handlers/ # 11 request handlers (chat, responses, embeddings, images, etc.) -├── mcp-server/ # MCP server (104 tools, 3 transports, 31 scopes) -├── services/ # 36+ services (combo, autoCombo, rateLimitManager, etc.) +├── mcp-server/ # MCP server (107 unique tools, 3 transports, 32 scopes) +├── services/ # 178 top-level services (combo, autoCombo, rateLimitManager, etc.) ├── translator/ # Format translators (OpenAI ↔ Claude ↔ Gemini ↔ Responses ↔ Ollama) ├── transformer/ # Responses API transformer └── utils/ # 22 utility modules (stream, TLS, proxy, logging) diff --git a/SECURITY.md b/SECURITY.md index 3499ef2ac2..129b5ffa9f 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -48,7 +48,7 @@ Request → CORS → Authz pipeline (classify → policies → enforce) | **Authz Pipeline** | Route classification (PUBLIC / CLIENT_API / MANAGEMENT) — see `docs/architecture/AUTHZ_GUIDE.md` | | **Route Guard Tiers** | 3-tier model for management routes (LOCAL_ONLY / ALWAYS_PROTECTED / MANAGEMENT) — see `docs/security/ROUTE_GUARD_TIERS.md` | | **Manage-Scope MCP** | Remote `/api/mcp/*` access gated by API keys with `manage` scope; `/api/cli-tools/runtime/*` stays strict-loopback. See ROUTE_GUARD_TIERS | -| **MCP Scopes** | ~13 granular scopes (read:health, write:combos, execute:completions, etc.) — see `docs/frameworks/MCP-SERVER.md` | +| **MCP Scopes** | 32 granular scopes (read:health, write:combos, execute:completions, etc.) — see `docs/frameworks/MCP-SERVER.md` | ### 🛡️ Encryption at Rest diff --git a/bin/cli/CONVENTIONS.md b/bin/cli/CONVENTIONS.md index e46caa0b27..c65ea033ef 100644 --- a/bin/cli/CONVENTIONS.md +++ b/bin/cli/CONVENTIONS.md @@ -136,7 +136,7 @@ export const RETRY_DEFAULTS = { - Every user-facing string goes through `t("module.key", vars)`. - Catalogs live in `bin/cli/locales/{locale}.json` (nested objects). - 42 files ship out-of-the-box: `en`, `pt-BR`, and 40 additional locales. + 43 files ship out-of-the-box: `en`, `pt-BR`, and 41 additional locales. 11 locales are scaffold-only (empty `{}`); all keys fall back to `en` automatically. - Detection order: `--lang` flag → `OMNIROUTE_LANG` env → `LC_ALL` → `LC_MESSAGES` → `LANG` → `en`. - Locale persisted via `config lang set ` — saves `OMNIROUTE_LANG` to `~/.omniroute/.env`. diff --git a/bin/cli/README.md b/bin/cli/README.md index 00c9dd06e4..c3b979da01 100644 --- a/bin/cli/README.md +++ b/bin/cli/README.md @@ -22,9 +22,9 @@ bin/cli/ ├── provider-test.mjs ← testProviderApiKey() ├── settings-store.mjs ← DB CRUD for key_value settings ├── locales/ -│ ├── en.json ← English strings (source of truth, 42+ locales) +│ ├── en.json ← English strings (source of truth, 43 locales) │ ├── pt-BR.json ← Portuguese (Brazil) — fully translated -│ └── {locale}.json ← 40 additional locales (ar, az, de, es, fr, ja, zh-CN, …) +│ └── {locale}.json ← 42 additional locales (ar, az, de, es, fr, ja, zh-CN, …) ├── scripts/ │ └── generate-locales.mjs ← scaffold new locale files from config/i18n.json └── commands/ diff --git a/docs/README.md b/docs/README.md index 96834709ab..beb0a24668 100644 --- a/docs/README.md +++ b/docs/README.md @@ -111,7 +111,7 @@ Pluggable subsystems exposed to clients, agents, and operators. Combo routing, scoring, and replay. -- [AUTO-COMBO.md](routing/AUTO-COMBO.md) — Auto-Combo (multi-factor scoring, 17 strategies). +- [AUTO-COMBO.md](routing/AUTO-COMBO.md) — Auto-Combo (13-factor scoring, 19 public strategies). - [QUOTA_SHARE.md](routing/QUOTA_SHARE.md) — quota sharing engine. - [REASONING_REPLAY.md](routing/REASONING_REPLAY.md) — reasoning replay cache. diff --git a/docs/architecture/ARCHITECTURE.md b/docs/architecture/ARCHITECTURE.md index 7afb4ec171..1eeff78ce2 100644 --- a/docs/architecture/ARCHITECTURE.md +++ b/docs/architecture/ARCHITECTURE.md @@ -1,14 +1,14 @@ --- title: "OmniRoute Architecture" -version: 3.8.40 -lastUpdated: 2026-06-28 +version: 3.8.50 +lastUpdated: 2026-08-02 --- # OmniRoute Architecture 🌐 **Languages:** 🇺🇸 [English](./ARCHITECTURE.md) | 🇧🇷 [Português (Brasil)](../i18n/pt-BR/docs/architecture/ARCHITECTURE.md) | 🇪🇸 [Español](../i18n/es/docs/architecture/ARCHITECTURE.md) | 🇫🇷 [Français](../i18n/fr/docs/architecture/ARCHITECTURE.md) | 🇮🇹 [Italiano](../i18n/it/docs/architecture/ARCHITECTURE.md) | 🇷🇺 [Русский](../i18n/ru/docs/architecture/ARCHITECTURE.md) | 🇨🇳 [中文 (简体)](../i18n/zh-CN/docs/architecture/ARCHITECTURE.md) | 🇩🇪 [Deutsch](../i18n/de/docs/architecture/ARCHITECTURE.md) | 🇮🇳 [हिन्दी](../i18n/in/docs/architecture/ARCHITECTURE.md) | 🇹🇭 [ไทย](../i18n/th/docs/architecture/ARCHITECTURE.md) | 🇺🇦 [Українська](../i18n/uk-UA/docs/architecture/ARCHITECTURE.md) | 🇸🇦 [العربية](../i18n/ar/docs/architecture/ARCHITECTURE.md) | 🇯🇵 [日本語](../i18n/ja/docs/architecture/ARCHITECTURE.md) | 🇻🇳 [Tiếng Việt](../i18n/vi/docs/architecture/ARCHITECTURE.md) | 🇧🇬 [Български](../i18n/bg/docs/architecture/ARCHITECTURE.md) | 🇩🇰 [Dansk](../i18n/da/docs/architecture/ARCHITECTURE.md) | 🇫🇮 [Suomi](../i18n/fi/docs/architecture/ARCHITECTURE.md) | 🇮🇱 [עברית](../i18n/he/docs/architecture/ARCHITECTURE.md) | 🇭🇺 [Magyar](../i18n/hu/docs/architecture/ARCHITECTURE.md) | 🇮🇩 [Bahasa Indonesia](../i18n/id/docs/architecture/ARCHITECTURE.md) | 🇰🇷 [한국어](../i18n/ko/docs/architecture/ARCHITECTURE.md) | 🇲🇾 [Bahasa Melayu](../i18n/ms/docs/architecture/ARCHITECTURE.md) | 🇳🇱 [Nederlands](../i18n/nl/docs/architecture/ARCHITECTURE.md) | 🇳🇴 [Norsk](../i18n/no/docs/architecture/ARCHITECTURE.md) | 🇵🇹 [Português (Portugal)](../i18n/pt/docs/architecture/ARCHITECTURE.md) | 🇷🇴 [Română](../i18n/ro/docs/architecture/ARCHITECTURE.md) | 🇵🇱 [Polski](../i18n/pl/docs/architecture/ARCHITECTURE.md) | 🇸🇰 [Slovenčina](../i18n/sk/docs/architecture/ARCHITECTURE.md) | 🇸🇪 [Svenska](../i18n/sv/docs/architecture/ARCHITECTURE.md) | 🇵🇭 [Filipino](../i18n/phi/docs/architecture/ARCHITECTURE.md) | 🇨🇿 [Čeština](../i18n/cs/docs/architecture/ARCHITECTURE.md) -_Last updated: 2026-06-28_ +_Last updated: 2026-08-02_ ## Executive Summary @@ -17,27 +17,27 @@ It provides a single OpenAI-compatible endpoint (`/v1/*`) and routes traffic acr Core capabilities: -- OpenAI-compatible API surface for CLI/tools (271 providers, 86 executors) +- OpenAI-compatible API surface for CLI/tools (327 provider catalog entries, 89 executor implementation modules) - Request/response translation across provider formats - Model combo fallback (multi-model sequence) - Structured combo steps (`provider + model + connection`) with runtime ordering by `compositeTiers` - Account-level fallback (multi-account per provider) - Quota preflight and quota-aware P2C account selection in the main chat path -- OAuth + API-key provider connection management (19 OAuth provider modules) +- OAuth + API-key provider connection management (23 OAuth catalog entries backed by 21 provider modules) - Embedding generation via `/v1/embeddings` (6 providers, 9 models) - Image generation via `/v1/images/generations` (10+ providers, 20+ models) - Audio transcription via `/v1/audio/transcriptions` (7 providers) - Text-to-speech via `/v1/audio/speech` (10 providers) - Video generation via `/v1/videos/generations` (ComfyUI + SD WebUI) - Music generation via `/v1/music/generations` (ComfyUI) -- Web search via `/v1/search` (5 providers) +- Web search via `/v1/search` (12 providers) - Moderations via `/v1/moderations` - Reranking via `/v1/rerank` - Think tag parsing (`...`) for reasoning models - Response sanitization for strict OpenAI SDK compatibility - Role normalization (developer→system, system→user) for cross-provider compatibility - Structured output conversion (json_schema → Gemini responseSchema) -- Local persistence for providers, keys, aliases, combos, settings, pricing (26 DB modules) +- Local persistence for providers, keys, aliases, combos, settings, and pricing (110 top-level DB modules) - Usage/cost tracking and request logging - Optional cloud sync for multi-device/state sync - IP allowlist/blocklist for API access control @@ -58,7 +58,7 @@ Core capabilities: - Compliance audit logging with opt-out per API key - Eval framework for LLM quality assurance - Health dashboard with real-time provider circuit breaker status -- MCP Server (87 tools) with 3 transports (stdio/SSE/Streamable HTTP) +- MCP Server (107 unique tools, 32 scopes) with 3 transports (stdio/SSE/Streamable HTTP) - A2A Server (JSON-RPC 2.0 + SSE) with skills and task lifecycle - Memory system (extraction, injection, retrieval, summarization) - Skills system (registry, executor, sandbox, built-in skills) @@ -66,7 +66,7 @@ Core capabilities: - Prompt injection guard middleware - Prompt compression pipeline with Caveman, RTK, stacked pipelines, compression combos, language packs, and analytics - ACP (Agent Communication Protocol) registry -- Modular OAuth providers (19 individual modules under `src/lib/oauth/providers/`) +- Modular OAuth providers (21 implementation modules under `src/lib/oauth/providers/`) - Uninstall/full-uninstall scripts - OAuth environment repair action - WebSocket bridge for OpenAI-compatible WS clients (`/v1/ws`) @@ -321,10 +321,15 @@ Domain layer modules: - Eval runner: `src/lib/evals/evalRunner.ts` - Domain state persistence: `src/lib/db/domainState.ts` — SQLite CRUD for fallback chains, budgets, cost history, lockout state, circuit breakers -OAuth provider modules (16 individual files under `src/lib/oauth/providers/`): +OAuth has **23 catalog entries** in `OAUTH_PROVIDERS`, backed by **21 implementation modules** +under `src/lib/oauth/providers/`: - Registry index: `src/lib/oauth/providers/index.ts` -- Individual providers: `claude.ts`, `codex.ts`, `gemini.ts`, `antigravity.ts`, `agy.ts`, `qoder.ts`, `qwen.ts`, `kimi-coding.ts`, `github.ts`, `kiro.ts`, `cursor.ts`, `kilocode.ts`, `cline.ts`, `windsurf.ts`, `gitlab-duo.ts`, `trae.ts` +- Individual modules: `agy.ts`, `antigravity.ts`, `claude.ts`, `cline.ts`, + `codebuddy-cn.ts`, `codex.ts`, `cursor.ts`, `ghe-copilot.ts`, `github.ts`, + `gitlab-duo.ts`, `grok-cli-oauth.ts`, `grok-cli.ts`, `kilocode.ts`, + `kimi-coding.ts`, `kiro.ts`, `qoder.ts`, `trae.ts`, `windsurf.ts`, + `xai-oauth.ts`, `zed-hosted.ts`, and `zed.ts` - Thin wrapper: `src/lib/oauth/providers.ts` — re-exports from individual modules ## 5) Embedded Services (v3.8.4) @@ -365,13 +370,15 @@ relying on a static combo definition. It powers the `auto/*` model prefix family Key capabilities: -- **17 routing strategies** (priority, weighted, fill-first, round-robin, P2C, random, +- **19 routing strategies** (priority, weighted, fill-first, round-robin, P2C, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, - **auto**, lkgp, context-optimized, context-relay, **fusion**, plus a fallback path) — + **auto**, lkgp, context-optimized, cache-optimized, context-relay, **fusion**, and + pipeline) — auto is the headline addition in v3.8.0; `fusion` (panel fan-out + judge synthesis, `open-sse/services/fusion.ts`) is new in v3.8.36. -- **9-factor scoring**: cost, latency p95, success rate, quota headroom, lockout - proximity, breaker state, recent failures, model availability, and tag affinity. +- **13-factor scoring**: quota, health, inverse cost, inverse latency, task fit, + stability, tier priority, tier affinity, specificity match, context affinity, + cache affinity, reset-window affinity, and connection density. - **Virtual factory** materializes ephemeral combos when no matching named combo exists, sourcing candidates from healthy active provider connections. - **Auto prefixes**: `auto/coding`, `auto/cheap`, `auto/fast`, `auto/offline`, @@ -933,8 +940,8 @@ All other providers (including custom compatible nodes) use the `DefaultExecutor ## Provider Compatibility Matrix -> **Note:** The matrix below is a representative sample of the 237 registered providers in -> OmniRoute v3.8.0. For the canonical and continuously-updated list, refer to +> **Note:** The matrix below is a representative sample of the 327 provider catalog entries in +> OmniRoute. For the canonical and continuously-updated list, refer to > [`docs/reference/PROVIDER_REFERENCE.md`](../reference/PROVIDER_REFERENCE.md) (auto-generated) or the source of > truth at `src/shared/constants/providers.ts` (Zod-validated at load). diff --git a/docs/architecture/CODEBASE_DOCUMENTATION.md b/docs/architecture/CODEBASE_DOCUMENTATION.md index 029c32cd6e..36c0e9697d 100644 --- a/docs/architecture/CODEBASE_DOCUMENTATION.md +++ b/docs/architecture/CODEBASE_DOCUMENTATION.md @@ -1,13 +1,13 @@ --- title: "OmniRoute Codebase Documentation" -version: 3.8.40 -lastUpdated: 2026-06-28 +version: 3.8.50 +lastUpdated: 2026-08-02 --- # OmniRoute Codebase Documentation -> **Version:** v3.8.0 -> **Last updated:** 2026-06-28 +> **Version:** v3.8.50 +> **Last updated:** 2026-08-02 > **Audience:** Engineers contributing to OmniRoute or building integrations on top of it. > > For high-level architecture diagrams and the reasoning behind each subsystem, read @@ -293,7 +293,7 @@ table groups the actual directories and notable top-level files. | `jobs/` | Background jobs (`autoUpdate.ts`, …) | | `memory/` | Persistent memory: `store.ts`, `cache.ts`, `retrieval.ts`, `summarization.ts`, `extraction.ts`, `injection.ts`, `qdrant.ts`, `settings.ts`, `verify.ts`, `schemas.ts`, `types.ts` | | `monitoring/` | `observability.ts` | -| `oauth/` | OAuth providers (13): `antigravity`, `claude`, `cline`, `codex`, `cursor`, `gemini`, `github`, `gitlab-duo`, `kilocode`, `kimi-coding`, `kiro`, `qoder`, `windsurf` plus `services/`, `utils/{pkce, server, banner, codexAuthFile, ui}`, `constants/oauth.ts` | +| `oauth/` | OAuth connection layer: 23 catalog entries in `OAUTH_PROVIDERS`, backed by 21 modules under `oauth/providers/`, plus `services/`, `utils/{pkce, server, banner, codexAuthFile, ui}`, and `constants/oauth.ts` | | `plugins/` | Plugin loader (`index.ts`) | | `promptCache/` | `prefixAnalyzer.ts`, `index.ts` | | `providerModels/` | Managed model lifecycle: `modelDiscovery.ts`, `managedModelImport.ts`, `managedAvailableModels.ts`, `cursorAgent.ts` | @@ -452,12 +452,12 @@ open-sse/ ├── types.d.ts ├── config/ Provider registries, header profiles, identity, … ├── handlers/ Request handlers (chat, embeddings, audio, image, …) -├── executors/ 84 provider-specific HTTP executors +├── executors/ 89 executor implementation modules ├── translator/ Format conversion (OpenAI ↔ Claude ↔ Gemini ↔ Cursor ↔ Kiro) ├── transformer/ Responses API ↔ Chat Completions stream transformer ├── services/ 80+ service modules (combos, fallback, quotas, identity, …) ├── utils/ Streaming helpers, TLS client, AWS SigV4, proxy fetch, … -└── mcp-server/ MCP server (3 transports, 32 scopes, 99 tools) +└── mcp-server/ MCP server (3 transports, 32 scopes, 107 unique tools) ``` ### 4.1 `open-sse/handlers/` @@ -482,7 +482,9 @@ open-sse/ ### 4.2 `open-sse/executors/` -84 provider executors, each extending `BaseExecutor` (`base.ts`): +The directory contains **89 executor implementation modules** after excluding shared files such +as `base.ts`, `index.ts`, `types.ts`, and `constants.ts`. Provider-facing executors extend +`BaseExecutor` (`base.ts`); helper modules support their protocol, identity, or error handling. `antigravity`, `azure-openai`, `blackbox-web`, `chatgpt-web`, `cliproxyapi`, `cloudflare-ai`, `codex`, `commandCode`, `cursor`, `default`, `devin-cli`, @@ -491,7 +493,7 @@ open-sse/ (shared identity helper) and `index.ts` (registry). > Note: providers not listed here are served by `default.ts` using the generic -> OpenAI-compatible executor. The full provider catalog (268 entries) lives in +> OpenAI-compatible executor. The full provider catalog (327 entries) lives in > `src/shared/constants/providers.ts`. ### 4.3 `open-sse/translator/` @@ -524,7 +526,7 @@ Highlights (full list under `open-sse/services/`): | Concern | Files | | -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Combo routing | `combo.ts` (17 strategies), `comboConfig.ts`, `comboMetrics.ts`, `comboManifestMetrics.ts`, `comboAgentMiddleware.ts` | +| Combo routing | `combo.ts` (19 public strategies), `comboConfig.ts`, `comboMetrics.ts`, `comboManifestMetrics.ts`, `comboAgentMiddleware.ts` | | Auto Combo engine | `autoCombo/` — `engine.ts`, `scoring.ts`, `taskFitness.ts`, `virtualFactory.ts`, `modePacks.ts`, `autoPrefix.ts`, `persistence.ts`, `providerDiversity.ts`, `providerRegistryAccessor.ts`, `routerStrategy.ts`, `selfHealing.ts`, `index.ts` | | Resilience | `accountFallback.ts` (cooldown + lockout), `errorClassifier.ts`, `emergencyFallback.ts`, `rateLimitManager.ts`, `rateLimitSemaphore.ts`, `accountSemaphore.ts`, `accountSelector.ts` | | Quotas | `quotaMonitor.ts`, `quotaPreflight.ts`, `bailianQuotaFetcher.ts`, `codexQuotaFetcher.ts`, `deepseekQuotaFetcher.ts`, `openrouterQuotaFetcher.ts`, `openrouterFreeWindow.ts`, `crofUsageFetcher.ts`, `antigravityCredits.ts` | @@ -540,11 +542,11 @@ Highlights (full list under `open-sse/services/`): ### 4.6 `open-sse/mcp-server/` -- **31 registered tools** wired in `server.ts` (12 scoped under `schemas/tools.ts`, - 5 compression tools, 3 memory tools, 4 skills tools, plus advanced tools added - through `advancedTools.ts`). +- **107 unique tools** computed by `countUniqueMcpTools()` from the canonical registry and + standalone memory, skills, agent-skills, pool, gamification, plugin, Notion, Obsidian, + local-corpus, and compression collections. - **3 transports**: stdio, HTTP Streamable, SSE. -- **13 scopes** declared in `src/shared/constants/mcpScopes.ts`. +- **32 runtime tool scopes** in the documented MCP inventory. - Audit table: `mcp_tool_audit` (populated by `audit.ts`). - Files: `server.ts`, `index.ts`, `httpTransport.ts`, `audit.ts`, `scopeEnforcement.ts`, `runtimeHeartbeat.ts`, `descriptionCompressor.ts`, `schemas/{tools, a2a, audit, index}.ts`, diff --git a/docs/architecture/RESILIENCE_GUIDE.md b/docs/architecture/RESILIENCE_GUIDE.md index 8e68322e96..ac96c5d81e 100644 --- a/docs/architecture/RESILIENCE_GUIDE.md +++ b/docs/architecture/RESILIENCE_GUIDE.md @@ -331,4 +331,4 @@ default `test:integration`, chaos and heap self-skip (without `RUN_CHAOS_INT`/`- - [Architecture Guide](./ARCHITECTURE.md) — System architecture and internals - [User Guide](../guides/USER_GUIDE.md) — Providers, combos, CLI integration -- [Auto-Combo Engine](../routing/AUTO-COMBO.md) — 12-factor scoring, mode packs +- [Auto-Combo Engine](../routing/AUTO-COMBO.md) — 13-factor scoring, mode packs diff --git a/docs/architecture/sqlite-coupling-inventory.md b/docs/architecture/sqlite-coupling-inventory.md index 1c1d9d48c3..6786c991c9 100644 --- a/docs/architecture/sqlite-coupling-inventory.md +++ b/docs/architecture/sqlite-coupling-inventory.md @@ -54,7 +54,7 @@ results rather than trusting totals alone. At the recorded revision, the script scanned: - 3,830 tracked non-test source files; -- 129 migration SQL files. +- 130 migration SQL files. The source-file count is intentionally broad because the goal is to find persistence coupling that has escaped the nominal database directory, including CLI and proxy/runtime code. diff --git a/docs/comparison/OMNIROUTE_VS_ALTERNATIVES.md b/docs/comparison/OMNIROUTE_VS_ALTERNATIVES.md index 9dc5d955cb..c40e02522d 100644 --- a/docs/comparison/OMNIROUTE_VS_ALTERNATIVES.md +++ b/docs/comparison/OMNIROUTE_VS_ALTERNATIVES.md @@ -1,7 +1,7 @@ --- title: "OmniRoute vs Alternatives" -version: 3.8.43 -lastUpdated: 2026-07-01 +version: 3.8.50 +lastUpdated: 2026-08-02 --- # OmniRoute vs Alternatives @@ -13,16 +13,16 @@ Objective feature comparison vs popular open-source AI routers. | Feature | OmniRoute 3.8 | LiteLLM 1.x | OpenRouter (SaaS) | Portkey | | -------------------------------------------------- | :----------------------------------------------: | :------------: | :---------------: | :---------: | -| **Providers** | **237+** | ~100 | ~50 | ~30 | -| **Free-tier providers** | **90+** | n/a | passthrough | n/a | +| **Providers** | **327** | ~100 | ~50 | ~30 | +| **Free/no-auth catalog entries** | **154** | n/a | passthrough | n/a | | **Self-hostable** | ✅ | ✅ | ❌ | ⚠ paid | -| **OAuth providers (Claude, Codex, Copilot, etc.)** | **15+** | partial | ❌ | ❌ | -| **Auto-fallback combos** | **17 strategies** | priority-based | tier-based | weighted | +| **OAuth catalog entries** | **23** | partial | ❌ | ❌ | +| **Auto-fallback combos** | **19 strategies** | priority-based | tier-based | weighted | | **Fusion (parallel panel + judge synthesis)** | ✅ | ❌ | ❌ | ❌ | | **Tier 1/2/3 fallback (subscription→cheap→free)** | ✅ + UI | manual | n/a | manual | -| **Token compression** | 10-engine pipeline (RTK + Caveman + LLMLingua-2) | none | none | none | +| **Token compression** | 12-engine stack (RTK + Caveman + LLMLingua) | none | none | none | | **Multimodal generation (speech/music/video)** | ✅ | ❌ | passthrough | ❌ | -| **Built-in MCP server** | ✅ 99 tools, 32 scopes | ❌ | ❌ | ❌ | +| **Built-in MCP server** | ✅ 107 tools, 32 scopes | ❌ | ❌ | ❌ | | **A2A protocol** | ✅ 6 skills | ❌ | ❌ | ❌ | | **Memory (FTS5 + vector)** | ✅ | ❌ | ❌ | ❌ | | **Guardrails (PII, injection, vision)** | ✅ | partial | ❌ | ✅ paid | @@ -34,14 +34,14 @@ Objective feature comparison vs popular open-source AI routers. | **CLI with system tray (no Electron)** | ✅ | ❌ | n/a | n/a | | **CLI machine-ID auto-auth** | ✅ | ❌ | n/a | n/a | | **Dashboard** | Next.js 16 | basic | proprietary | proprietary | -| **i18n** | **42+ locales** | ❌ | ❌ | ⚠ | -| **Public agent skills (SKILL.md)** | ✅ 43 | ❌ | ❌ | ❌ | +| **i18n** | **43 locales** | ❌ | ❌ | ⚠ | +| **Public agent skills (SKILL.md)** | ✅ 45 | ❌ | ❌ | ❌ | | **Tunnel support (Cloudflared, Tailscale, Ngrok)** | ✅ | ❌ | n/a | n/a | | **License** | MIT | MIT | proprietary | proprietary | ## When to choose OmniRoute -- You self-host and want **maximum provider coverage** (237+, 90+ with a free tier) +- You self-host and want **maximum provider coverage** (327 providers, 154 free/no-auth catalog entries) - You need a **built-in MCP server** (LLM tools, memory, skills exposed as tools) - You need **A2A protocol** for agent-to-agent workflows - You want **fingerprint stealth** (JA3/JA4) to avoid detection by upstream CAPTCHAs @@ -67,4 +67,4 @@ Objective feature comparison vs popular open-source AI routers. --- -_Last updated: 2026-06-28. Submit corrections via PR to keep this table accurate._ +_Last updated: 2026-08-02. Submit corrections via PR to keep this table accurate._ diff --git a/docs/compression/COMPRESSION_GUIDE.md b/docs/compression/COMPRESSION_GUIDE.md index ea70ebbfac..12b380379e 100644 --- a/docs/compression/COMPRESSION_GUIDE.md +++ b/docs/compression/COMPRESSION_GUIDE.md @@ -199,7 +199,7 @@ In `Dashboard → Context & Cache → Compression Combos`, assign a compression combo: ```txt -Combo: "free-forever" +Combo: "free-tier-fallback" Compression Combo: "coding-agent-stack" Pipeline: RTK -> Caveman Targets: diff --git a/docs/diagrams/README.md b/docs/diagrams/README.md index 0fa9f35bca..7b6fd18f9a 100644 --- a/docs/diagrams/README.md +++ b/docs/diagrams/README.md @@ -16,7 +16,7 @@ Mermaid sources (`.mmd`) and exported SVGs for OmniRoute v3.8.0 architecture flo | [auto-combo-12factor.mmd](./auto-combo-12factor.mmd) | [SVG](./exported/auto-combo-12factor.svg) | docs/routing/AUTO-COMBO.md | | [resilience-3layers.mmd](./resilience-3layers.mmd) | [SVG](./exported/resilience-3layers.svg) | docs/architecture/RESILIENCE_GUIDE.md, CLAUDE.md | | [i18n-flow.mmd](./i18n-flow.mmd) | [SVG](./exported/i18n-flow.svg) | docs/guides/I18N.md | -| [mcp-tools-104.mmd](./mcp-tools-104.mmd) | [SVG](./exported/mcp-tools-104.svg) | docs/frameworks/MCP-SERVER.md | +| [mcp-tools-107.mmd](./mcp-tools-107.mmd) | [SVG](./exported/mcp-tools-107.svg) | docs/frameworks/MCP-SERVER.md | | [cloud-agent-flow.mmd](./cloud-agent-flow.mmd) | [SVG](./exported/cloud-agent-flow.svg) | docs/frameworks/CLOUD_AGENT.md | | [authz-pipeline.mmd](./authz-pipeline.mmd) | [SVG](./exported/authz-pipeline.svg) | docs/architecture/AUTHZ_GUIDE.md | | [db-schema-overview.mmd](./db-schema-overview.mmd) | [SVG](./exported/db-schema-overview.svg) | docs/architecture/CODEBASE_DOCUMENTATION.md | @@ -34,11 +34,11 @@ inside GitHub's `` sandbox: | [combo-always-on.svg](./combo-always-on.svg) | style reference | Animated priority-combo fallback (4 layers, 16s loop). Edit the SVG directly — there is no `.mmd` source. | | [cli-terminal.svg](./cli-terminal.svg) | README.md (root) | Compact half-height animated terminal (1200×350): 3 real CLI commands cycling with typewriter + scrolling subcommand ticker; first frame = completed providers screen. Edit the SVG directly — there is no `.mmd` source. | | [compression-pipeline.svg](./compression-pipeline.svg) | README.md (root) | Animated 10-engine compression funnel (8s loop). Edit the SVG directly — there is no `.mmd` source. | -| [free-tier-budget.svg](./free-tier-budget.svg) | README.md (root) | Animated free-tier budget card (~1.4B/mo headline, 19-pool budget bar, per-model grid, signup credits, 10s loop). Edit the SVG directly — there is no `.mmd` source. | -| [readme-hero.svg](./readme-hero.svg) | README.md (root) | Animated hero card (tagline, 268-provider/90+ free headline, full-width compression bar demo, 6 stat chips). Edit the SVG directly — there is no `.mmd` source. | +| [free-tier-budget.svg](./free-tier-budget.svg) | README.md (root) | Animated free-tier budget card (~1.53B/mo quantified headline, 19-pool budget bar, per-model grid, signup credits, 10s loop). Edit the SVG directly — there is no `.mmd` source. | +| [readme-hero.svg](./readme-hero.svg) | README.md (root) | Animated hero card (tagline, live provider/free-access headline, full-width compression bar demo, 6 stat chips). Edit the SVG directly — there is no `.mmd` source. | | [promise-pillars.svg](./promise-pillars.svg) | README.md (root) | Animated "The Promise" 6-pillar card (12s border-highlight sweep). Edit the SVG directly — there is no `.mmd` source. | | [why-pain-fix.svg](./why-pain-fix.svg) | README.md (root) | Animated "Why OmniRoute" 10-row pain-vs-fix ledger (15s green row sweep). Edit the SVG directly — there is no `.mmd` source. | -| [strategies-grid.svg](./strategies-grid.svg) | README.md (root) | Animated 6×3 grid of all 18 routing-strategy flows (one micro-stage per strategy, staggered dot loops). Edit the SVG directly — there is no `.mmd` source. | +| [strategies-grid.svg](./strategies-grid.svg) | README.md (root) | Animated grid illustrating 18 of the 19 routing strategies; `cache-optimized` remains documented in the adjacent table. Edit the SVG directly — there is no `.mmd` source. | | [privacy-local.svg](./privacy-local.svg) | README.md (root) | Animated "Private & Local-First" 11-row guarantee ledger with receipt chips (16s green row sweep). Edit the SVG directly — there is no `.mmd` source. | | [resilience-layers.svg](./resilience-layers.svg) | README.md (root) | Animated 3-layer resilience card (breaker states CLOSED→OPEN→HALF-OPEN, key cooldown with ×2 backoff, model lockout — 18s loops). Edit the SVG directly — there is no `.mmd` source. | diff --git a/docs/diagrams/cli-terminal.svg b/docs/diagrams/cli-terminal.svg index bb015e3a12..f69337a352 100644 --- a/docs/diagrams/cli-terminal.svg +++ b/docs/diagrams/cli-terminal.svg @@ -1,4 +1,4 @@ - + Compact animated terminal cycling three real OmniRoute CLI commands with a typewriter effect and a scrolling subcommand ticker; the first frame shows the completed providers-list screen. @@ -6,7 +6,7 @@ omniroute — 80+ commands -omniroute providers listOmniRoute Providers1f3a9c2e  anthropic   Claude Max 20x    active8c2d5b1a  codex       Codex Pro (team)  activef4e0a97b  glm         GLM Coding Plan   active03bd6e5f  kimi        Kimi K2 free      active… 264 more providers +omniroute providers listOmniRoute Providers1f3a9c2e  anthropic   Claude Max 20x    active8c2d5b1a  codex       Codex Pro (team)  activef4e0a97b  glm         GLM Coding Plan   active03bd6e5f  kimi        Kimi K2 free      active… 323 more providers $ omniroute providers list @@ -14,7 +14,7 @@ -OmniRoute Providers1f3a9c2e  anthropic   Claude Max 20x    active8c2d5b1a  codex       Codex Pro (team)  activef4e0a97b  glm         GLM Coding Plan   active03bd6e5f  kimi        Kimi K2 free      active… 264 more providers +OmniRoute Providers1f3a9c2e  anthropic   Claude Max 20x    active8c2d5b1a  codex       Codex Pro (team)  activef4e0a97b  glm         GLM Coding Plan   active03bd6e5f  kimi        Kimi K2 free      active… 323 more providers $ @@ -32,11 +32,11 @@ -OmniRoute Health  Status: healthy   Uptime: 4d 12h 33m  Requests (24h): 18,412   p95: 412ms  Breakers: ● 24 closed  ◒ 1 half-open  ○ 0 open  Providers: 278 registered   90+ free tiers… live: /dashboard · omniroute status +OmniRoute Health  Status: healthy   Uptime: 4d 12h 33m  Requests (24h): 18,412   p95: 412ms  Breakers: ● 24 closed  ◒ 1 half-open  ○ 0 open  Providers: 327 registered   154 free/no-auth… live: /dashboard · omniroute status providers · oauth · keys · combo · nodes · models · cache · compression · cost · usage · quota · health · resilience · telemetry · logs · audit · mcp · a2a · cloud · memory · skills · eval · doctor · repl · tunnel · backup · sync · webhooks · policy · pricing · translator · simulate …providers · oauth · keys · combo · nodes · models · cache · compression · cost · usage · quota · health · resilience · telemetry · logs · audit · mcp · a2a · cloud · memory · skills · eval · doctor · repl · tunnel · backup · sync · webhooks · policy · pricing · translator · simulate … - \ No newline at end of file + diff --git a/docs/diagrams/combo-always-on.svg b/docs/diagrams/combo-always-on.svg index 0278375d73..182c3bcc21 100644 --- a/docs/diagrams/combo-always-on.svg +++ b/docs/diagrams/combo-always-on.svg @@ -1,4 +1,5 @@ - + + Animated priority combo that tries two subscriptions, a low-cost API, and a listed free-access route while clearly stating that fallback does not guarantee availability. @@ -122,7 +123,7 @@ 4 kr/claude-sonnet-4.5 - FREE · unlimited · never fails + FREE ACCESS · limits apply @@ -214,8 +215,8 @@ fails → next - - UPTIME + + FALLBACK COVERAGE @@ -224,7 +225,7 @@ - ALWAYS ON + FALLBACK READY - Result: 4 layers of fallback = zero downtime + Result: 4 fallback layers = broader resilience · availability is not guaranteed diff --git a/docs/diagrams/comparison-table.svg b/docs/diagrams/comparison-table.svg index 35bd075e64..54b53e74a9 100644 --- a/docs/diagrams/comparison-table.svg +++ b/docs/diagrams/comparison-table.svg @@ -1,4 +1,4 @@ - + Static-header comparison table where each capability row fades in top to bottom; the OmniRoute column is highlighted and shows a check or a leading value in every row, while competitors show a mix of checks, partials and crosses. @@ -23,7 +23,7 @@ Providers - 290 + 327 40+ 400+* ~5 @@ -31,8 +31,8 @@ - Free providers built-in - 90+ + Free/no-auth catalog entries + 154 @@ -57,7 +57,7 @@ Built-in MCP server (own tools) - 104 + 107 @@ -90,7 +90,7 @@ - Cloud agents (Codex/Devin/Jules) + Cloud agents (4) diff --git a/docs/diagrams/exported/auto-combo-12factor.svg b/docs/diagrams/exported/auto-combo-12factor.svg index 6d88a323aa..d0f2e00d7c 100644 --- a/docs/diagrams/exported/auto-combo-12factor.svg +++ b/docs/diagrams/exported/auto-combo-12factor.svg @@ -1 +1 @@ -

12-factor scoring weights (sum = 1.0)

health (0.20)

quota (0.15)

costInv (0.15)

latencyInv (0.12)

taskFit (0.08)

stability (0.05)

tierPriority (0.05)

tierAffinity (0.05)

specificityMatch (0.05)

contextAffinity (0.05)

connectionDensity (0.05)

resetWindowAffinity (0.00)

Incoming request

Eligible candidates
(provider × model × account)

Compute composite score
per candidate

Sort by score
(desc)

Pick top-N targets

Dispatch sequentially
(short-circuit on success)

\ No newline at end of file +

13-factor scoring weights (sum = 1.0)

health (0.20)

quota (0.15)

costInv (0.15)

latencyInv (0.12)

taskFit (0.08)

stability (0.05)

tierPriority (0.05)

tierAffinity (0.05)

specificityMatch (0.05)

contextAffinity (0.05)

connectionDensity (0.05)

cacheAffinity (0.00)

resetWindowAffinity (0.00)

Incoming request

Eligible candidates
(provider × model × account)

Compute composite score
per candidate

Sort by score
(desc)

Pick top-N targets

Dispatch sequentially
(short-circuit on success)

\ No newline at end of file diff --git a/docs/diagrams/exported/mcp-tools-104.svg b/docs/diagrams/exported/mcp-tools-104.svg deleted file mode 100644 index 3f750ae1fd..0000000000 --- a/docs/diagrams/exported/mcp-tools-104.svg +++ /dev/null @@ -1 +0,0 @@ -

MCP Server
104 tools total

Core (42)
routing, cache, compression,
quota, proxy, tunnels, search, web_fetch

Memory (3)
memory_search, memory_save,
memory_delete

Skills (4)
skill_invoke, skill_list,
skill_diagnose, skill_uninstall
+ agent-skill cli-registry discovery

GitHub-Skills (3)
marketplace search,
install, update

Pool (6)
pool_status, pool_sessions, pool_reset,
pool_warm, pool_health, browser_pool_status

Gamification (8)

Plugins (8)

Notion (6)
search, get_page, query_database,
get_database, list_block_children,
append_blocks

Obsidian (22)
vault read/write, search,
frontmatter, daily notes, …

Compression Studios (+2)
beyond the 11 in Core

\ No newline at end of file diff --git a/docs/diagrams/exported/mcp-tools-107.svg b/docs/diagrams/exported/mcp-tools-107.svg new file mode 100644 index 0000000000..e49849311c --- /dev/null +++ b/docs/diagrams/exported/mcp-tools-107.svg @@ -0,0 +1 @@ +

MCP Server
107 tools total

Base registry (42)
routing, cache, compression, quota,
proxy, web search/fetch, tool search

Memory (3)
memory_search, memory_add,
memory_clear

Skills (4)
skills_list, skills_enable,
skills_execute, skills_executions
+ agent-skill catalog discovery

GitHub-Skills (3)
search, scan, install

Pool (6)
pool_status, pool_sessions, pool_reset,
pool_warm, pool_health, browser_pool_status

Gamification (8)

Plugins (8)

Notion (6)
search, get_page, query_database,
get_database, list_block_children,
append_blocks

Obsidian (22)
vault read/write, search,
frontmatter, daily notes, …

Local corpus (3)
status, search, read

Compression Studios (+2)
beyond the 11 in Core

\ No newline at end of file diff --git a/docs/diagrams/exported/mcp-tools-99.svg b/docs/diagrams/exported/mcp-tools-99.svg deleted file mode 100644 index 53b38bb50c..0000000000 --- a/docs/diagrams/exported/mcp-tools-99.svg +++ /dev/null @@ -1 +0,0 @@ -

MCP Server
99 tools total

Core (36)
routing, cache, compression,
quota, proxy, tunnels, search, web_fetch

Memory (3)
memory_search, memory_save,
memory_delete

Skills (4)
skill_invoke, skill_list,
skill_diagnose, skill_uninstall

Agent-Skills (3)
cli-registry discovery
(also in Core)

GitHub Skills (3)
search, scan, install

Pool (6)
pool_status, pool_sessions, pool_reset,
pool_warm, pool_health, browser_pool_status

Gamification (8)

Plugins (8)

Notion (6)
search, get_page, query_database,
get_database, list_block_children,
append_blocks

Obsidian (22)
vault read/write, search,
frontmatter, daily notes, ...

Local Corpus (3)
status, search, read

\ No newline at end of file diff --git a/docs/diagrams/exported/request-pipeline.svg b/docs/diagrams/exported/request-pipeline.svg index 9de7fb82a0..6c94db99ee 100644 --- a/docs/diagrams/exported/request-pipeline.svg +++ b/docs/diagrams/exported/request-pipeline.svg @@ -1 +1 @@ -

yes

no

combo

single

Client
(IDE/CLI/SDK)

Next.js Route
/v1/chat/completions

CORS preflight

Zod validation
(request body)

AuthZ pipeline
(extractApiKey +
isValidApiKey)

API key policy
(allowlist + scopes)

Prompt-injection
guardrail

handleChatCore

Cache hit?

Return cached

Rate limit
(per-key, per-IP)

Combo
target?

resolveComboTargets
(18 strategies)

handleSingleModel
(per target)

translateRequest
(OpenAI↔Claude↔Gemini)

getExecutor
(31 executors)

Upstream Provider
(177 providers)

SSE / JSON

responsesTransformer
(Responses↔Chat)

\ No newline at end of file +

yes

no

combo

single

Client
(IDE/CLI/SDK)

Next.js Route
/v1/chat/completions

CORS preflight

Zod validation
(request body)

AuthZ pipeline
(extractApiKey +
isValidApiKey)

API key policy
(allowlist + scopes)

Prompt-injection
guardrail

handleChatCore

Cache hit?

Return cached

Rate limit
(per-key, per-IP)

Combo
target?

resolveComboTargets
(19 strategies)

handleSingleModel
(per target)

translateRequest
(OpenAI↔Claude↔Gemini)

getExecutor
(89 executor modules)

Upstream Provider
(327 catalog entries)

SSE / JSON

responsesTransformer
(Responses↔Chat)

\ No newline at end of file diff --git a/docs/diagrams/free-tier-budget.svg b/docs/diagrams/free-tier-budget.svg index 6ae541be74..97fe2286ab 100644 --- a/docs/diagrams/free-tier-budget.svg +++ b/docs/diagrams/free-tier-budget.svg @@ -1,4 +1,5 @@ - + + Animated free-tier budget summary separating recurring quantified pools, one-time signup grants, uncapped rate-limited access, and the optional OpenRouter deposit boost. @@ -63,7 +64,7 @@ ~1.53B FREE TOKENS / MONTH · STEADY up to ~2.15B in your first month — signup credits - documented free tiers · 43 provider pools · 460+ models · one endpoint + documented free tiers · 43 provider pools · 522 model entries · one endpoint @@ -163,7 +164,7 @@ - PLUS THE UN-COUNTABLE — PERMANENTLY FREE · NO TOKEN CAP + PLUS THE UN-COUNTABLE — RECURRING ACCESS · NO PUBLISHED CAP SiliconFlow diff --git a/docs/diagrams/mcp-tools-104.mmd b/docs/diagrams/mcp-tools-107.mmd similarity index 54% rename from docs/diagrams/mcp-tools-104.mmd rename to docs/diagrams/mcp-tools-107.mmd index 27967da552..b0bb2d2346 100644 --- a/docs/diagrams/mcp-tools-104.mmd +++ b/docs/diagrams/mcp-tools-107.mmd @@ -1,20 +1,21 @@ %% MCP Server tool inventory by category %% Reflects: open-sse/mcp-server/ — TOTAL_MCP_TOOL_COUNT in server.ts (countUniqueMcpTools) %% Unique contributions: MCP_TOOLS 42 + memory 3 + skill 4 + githubSkill 3 + pool 6 -%% + gamification 8 + plugin 8 + notion 6 + obsidian 22 + compression 2 = 104 -%% (agentSkillTools fully overlaps the skill registry; 11 of the 13 compression +%% + gamification 8 + plugin 8 + notion 6 + obsidian 22 + localCorpus 3 + compression 2 = 107 +%% (agentSkillTools fully overlaps the base registry; 11 of the 13 compression %% tools live in the base registry — duplicates counted once) %% Regenerate the SVG with: npm run docs:render-diagrams -%% v3.8.49 +%% v3.8.50 flowchart LR - MCP["MCP Server

104 tools total"] - MCP --> Core["Core (42)
routing, cache, compression,
quota, proxy, tunnels, search, web_fetch"] - MCP --> Mem["Memory (3)
memory_search, memory_save,
memory_delete"] - MCP --> Skl["Skills (4)
skill_invoke, skill_list,
skill_diagnose, skill_uninstall
+ agent-skill cli-registry discovery"] - MCP --> GSkl["GitHub-Skills (3)
marketplace search,
install, update"] + MCP["MCP Server
107 tools total"] + MCP --> Core["Base registry (42)
routing, cache, compression, quota,
proxy, web search/fetch, tool search"] + MCP --> Mem["Memory (3)
memory_search, memory_add,
memory_clear"] + MCP --> Skl["Skills (4)
skills_list, skills_enable,
skills_execute, skills_executions
+ agent-skill catalog discovery"] + MCP --> GSkl["GitHub-Skills (3)
search, scan, install"] MCP --> Pool["Pool (6)
pool_status, pool_sessions, pool_reset,
pool_warm, pool_health, browser_pool_status"] MCP --> Gam["Gamification (8)"] MCP --> Plg["Plugins (8)"] MCP --> Notion["Notion (6)
search, get_page, query_database,
get_database, list_block_children,
append_blocks"] MCP --> Obs["Obsidian (22)
vault read/write, search,
frontmatter, daily notes, …"] + MCP --> Corpus["Local corpus (3)
status, search, read"] MCP --> Cmp["Compression Studios (+2)
beyond the 11 in Core"] diff --git a/docs/diagrams/mcp-tools-99.mmd b/docs/diagrams/mcp-tools-99.mmd deleted file mode 100644 index 3b9bec1b46..0000000000 --- a/docs/diagrams/mcp-tools-99.mmd +++ /dev/null @@ -1,18 +0,0 @@ -%% MCP Server tool inventory by category -%% Reflects: open-sse/mcp-server/ - TOTAL_MCP_TOOL_COUNT in server.ts -%% 102 definitions; 99 unique after the 3 agentSkill definitions duplicated in MCP_TOOLS are de-duplicated -%% Regenerate the SVG with: npm run docs:render-diagrams -%% v3.8.49 -flowchart LR - MCP["MCP Server
99 tools total"] - MCP --> Core["Core (36)
routing, cache, compression,
quota, proxy, tunnels, search, web_fetch"] - MCP --> Mem["Memory (3)
memory_search, memory_save,
memory_delete"] - MCP --> Skl["Skills (4)
skill_invoke, skill_list,
skill_diagnose, skill_uninstall"] - MCP --> ASkl["Agent-Skills (3)
cli-registry discovery
(also in Core)"] - MCP --> GSkl["GitHub Skills (3)
search, scan, install"] - MCP --> Pool["Pool (6)
pool_status, pool_sessions, pool_reset,
pool_warm, pool_health, browser_pool_status"] - MCP --> Gam["Gamification (8)"] - MCP --> Plg["Plugins (8)"] - MCP --> Notion["Notion (6)
search, get_page, query_database,
get_database, list_block_children,
append_blocks"] - MCP --> Obs["Obsidian (22)
vault read/write, search,
frontmatter, daily notes, ..."] - MCP --> Local["Local Corpus (3)
status, search, read"] diff --git a/docs/diagrams/promise-pillars.svg b/docs/diagrams/promise-pillars.svg index 6f3a43632f..9addf14c28 100644 --- a/docs/diagrams/promise-pillars.svg +++ b/docs/diagrams/promise-pillars.svg @@ -1,4 +1,4 @@ - + Animated promise card: six pillar tiles fade in in reading order, then a soft colored border highlight sweeps from tile to tile in a continuous cycle. @@ -21,12 +21,12 @@ - One endpoint. 290 providers. Never stop building — OmniRoute picks the cheapest one that works. + One endpoint. 327 providers. Keep building — OmniRoute picks the cheapest eligible route that works. - + @@ -37,10 +37,10 @@ - Never hit limits - Auto-fallback across 290 providers in - milliseconds. Quota out? The next provider - takes over — zero downtime. + Resilient fallback + Auto-fallback across 327 providers when + an upstream or quota fails. The next eligible + provider is tried, subject to availability. @@ -73,9 +73,9 @@ $0 to start - 90+ providers with a free tier, 40+ free - forever — Qoder, Pollinations, Cloudflare, - SiliconFlow… No card needed. + 154 catalog entries marked free/no-auth; + 58 providers have recurring quantified or + uncapped access in the audited catalog. @@ -125,7 +125,7 @@ Production-grade - Circuit breakers, TLS stealth, MCP (104 + Circuit breakers, TLS stealth, MCP (107 tools), A2A, memory, guardrails, evals — 25,000+ tests. diff --git a/docs/diagrams/readme-hero.svg b/docs/diagrams/readme-hero.svg index 58f8cb1d5b..38f87af836 100644 --- a/docs/diagrams/readme-hero.svg +++ b/docs/diagrams/readme-hero.svg @@ -1,4 +1,4 @@ - + Animated hero card: a pulse travels the divider line and a compression bar demo repeatedly shrinks a prompt by up to 95 percent; all headline content is static and readable on the first frame. @@ -25,13 +25,13 @@ ONE ENDPOINT · /v1 - Never stop coding. + Keep coding through limits. - Every AI tool → 290 providers90+ free — through one endpoint. + Every AI tool → 327 providers154 free/no-auth — one endpoint. - Claude Code · Codex · Cursor · Cline · Copilot · Antigravity  →  FREE Claude / GPT / Gemini · auto-fallback + Claude Code · Codex · Cursor · Cline · Copilot · Antigravity  →  FREE-ACCESS Claude / GPT / Gemini · limits apply @@ -58,19 +58,19 @@ −89% avg on tool-heavy sessions - never hit limits - auto-fallback keeps you coding + resilient fallback + tries the next eligible route $ npm i -g omniroute - 290 + 327 AI PROVIDERS - 90+ - FREE TIERS + 154 + FREE / NO-AUTH ~1.53B FREE TOKENS / MO diff --git a/docs/diagrams/request-pipeline.mmd b/docs/diagrams/request-pipeline.mmd index 2933c645c3..61c7684726 100644 --- a/docs/diagrams/request-pipeline.mmd +++ b/docs/diagrams/request-pipeline.mmd @@ -13,12 +13,12 @@ flowchart LR Cache -->|yes| Return["Return cached"] Cache -->|no| Rate["Rate limit
(per-key, per-IP)"] Rate --> Combo{"Combo
target?"} - Combo -->|combo| Resolve["resolveComboTargets
(18 strategies)"] + Combo -->|combo| Resolve["resolveComboTargets
(19 strategies)"] Resolve --> Single["handleSingleModel
(per target)"] Combo -->|single| Single Single --> Translate["translateRequest
(OpenAI↔Claude↔Gemini)"] - Translate --> Exec["getExecutor
(31 executors)"] - Exec --> Upstream["Upstream Provider
(177 providers)"] + Translate --> Exec["getExecutor
(89 executor modules)"] + Exec --> Upstream["Upstream Provider
(327 catalog entries)"] Upstream --> Stream["SSE / JSON"] Stream --> Transformer["responsesTransformer
(Responses↔Chat)"] Transformer --> Client diff --git a/docs/diagrams/strategies-grid.svg b/docs/diagrams/strategies-grid.svg index 6ef2ae7f7c..d518f85d06 100644 --- a/docs/diagrams/strategies-grid.svg +++ b/docs/diagrams/strategies-grid.svg @@ -1,5 +1,5 @@ - - Animated grid of 18 routing-strategy tiles; static tracks show each flow on the first frame while a small dot repeatedly travels the path each strategy takes. + + Animated grid of 18 routing-strategy tiles; static tracks show each flow on the first frame while a small dot repeatedly travels the path each illustrated strategy takes. @@ -13,7 +13,7 @@ - COMBO ROUTING · ALL 18 STRATEGY FLOWS + COMBO ROUTING · 18 OF 19 STRATEGY FLOWS ● request → ▮ targets — each tile animates the path its strategy takes · green outline = the pick @@ -95,7 +95,7 @@ auto 72916455 -live 12-factor scoring +live 13-factor scoring fusion diff --git a/docs/diagrams/tier-cascade.svg b/docs/diagrams/tier-cascade.svg index 4f819814fd..5dfe2314fd 100644 --- a/docs/diagrams/tier-cascade.svg +++ b/docs/diagrams/tier-cascade.svg @@ -1,4 +1,5 @@ + Animated request path that tries subscription, API-key, low-cost, and free-access tiers in sequence, subject to upstream availability and limits. @@ -89,7 +90,7 @@ OmniRoute — Smart Router - RTK + Caveman compression · 18 routing strategies + RTK + Caveman compression · 19 routing strategies Circuit breakers · TLS stealth · MCP · A2A · Guardrails @@ -255,15 +256,15 @@ - + - ALWAYS ON + LIMITS APPLY - AUTO-FALLBACK CASCADE · NEVER STOP CODING + AUTO-FALLBACK CASCADE · UPSTREAM AVAILABILITY VARIES diff --git a/docs/frameworks/A2A-SERVER.md b/docs/frameworks/A2A-SERVER.md index 9cc405fd2b..b8d13ee7db 100644 --- a/docs/frameworks/A2A-SERVER.md +++ b/docs/frameworks/A2A-SERVER.md @@ -148,9 +148,9 @@ OmniRoute exposes 6 A2A skills wired in `src/lib/a2a/taskExecution.ts::A2A_SKILL | Provider Discovery | `provider-discovery` | Lists installed providers with capabilities, free-tier flags, OAuth status | providers, discovery | "What providers are available?" | | Cost Analysis | `cost-analysis` | Estimates cost of a request/conversation given the catalog + recent usage | cost, usage | "Estimate cost for this conversation" | | Health Report | `health-report` | Aggregates circuit breaker, cooldown, lockout state per provider | health, resilience | "Show health status of all providers" | -| List Capabilities | `list-capabilities` | Returns the full 42-entry Agent Skills catalog as a markdown table with raw SKILL.md URLs for context injection | catalog, discovery, skills | "List all OmniRoute capabilities" | +| List Capabilities | `list-capabilities` | Returns the full 45-entry Agent Skills catalog (23 API + 21 CLI + 1 config) as a markdown table with raw SKILL.md URLs for context injection | catalog, discovery, skills | "List all OmniRoute capabilities" | -> Note: the Agent Card description currently advertises "36+ providers" (`src/app/.well-known/agent.json/route.ts:26` and `:55`). The actual catalog has grown to 180+ providers — the string should be updated in a follow-up change (tracked as a separate doc/code TODO; not modified here). +> The Agent Card should be kept aligned with the live 327-provider catalog; provider counts and free/no-auth metadata are sourced from the runtime registry. ### `list-capabilities` Skill Detail diff --git a/docs/frameworks/ACP.md b/docs/frameworks/ACP.md index 3a7b386220..4dc1e19ca2 100644 --- a/docs/frameworks/ACP.md +++ b/docs/frameworks/ACP.md @@ -543,7 +543,7 @@ const agents = detectInstalledAgents(); ## What's Next? - **[API Reference](../reference/API_REFERENCE.md)** — REST API endpoints -- **[Provider Reference](../reference/PROVIDER_REFERENCE.md)** — All 226 providers +- **[Provider Reference](../reference/PROVIDER_REFERENCE.md)** — All 327 providers - **[MCP Server](./MCP-SERVER.md)** — Model Context Protocol integration - **[A2A Server](./A2A-SERVER.md)** — Agent-to-Agent protocol - **[Cloud Agent](./CLOUD_AGENT.md)** — Cloud-based agents diff --git a/docs/frameworks/AGENT-SKILLS.md b/docs/frameworks/AGENT-SKILLS.md index b24f3c85a9..d06fcde1c4 100644 --- a/docs/frameworks/AGENT-SKILLS.md +++ b/docs/frameworks/AGENT-SKILLS.md @@ -1,13 +1,13 @@ --- title: "OmniRoute Agent Skills Catalog" -version: 3.8.40 -lastUpdated: 2026-06-28 +version: 3.8.50 +lastUpdated: 2026-08-02 --- # OmniRoute Agent Skills Catalog > **Source of truth:** `src/lib/agentSkills/` (catalog, generator, parsers) + `skills/` directory (SKILL.md files) -> **Last updated:** 2026-06-28 — v3.8.40 +> **Last updated:** 2026-08-02 — v3.8.50 Agent Skills are structured SKILL.md files that teach external agents, MCP clients, and A2A orchestrators how to use OmniRoute's REST API and CLI. Unlike [Omni Skills](./SKILLS.md) (which are LLM tool definitions executed inside OmniRoute), Agent Skills are a _documentation catalog_ — static markdown that can be fed directly into agent context. @@ -15,7 +15,7 @@ Agent Skills are structured SKILL.md files that teach external agents, MCP clien ## Overview -The catalog contains **42 canonical Agent Skills** (22 REST API + 20 CLI). Each skill has: +The catalog contains **45 Agent Skills** (23 REST API + 21 CLI + 1 configuration workflow). Each skill has: - A **canonical ID** (`omni-auth`, `cli-serve`, etc.) - A **SKILL.md** file in `skills/{id}/SKILL.md` with YAML frontmatter (`name`, `description`) + rich markdown body @@ -27,7 +27,7 @@ The catalog contains **42 canonical Agent Skills** (22 REST API + 20 CLI). Each ## Architecture ``` -src/shared/constants/agentSkills.ts — 42-entry curated list (name/desc/category/area/icon) +src/shared/constants/agentSkills.ts — 45-entry curated list (name/desc/category/area/icon) src/lib/agentSkills/ catalog.ts — getCatalog(), getSkillById(), filterCatalog(), computeCoverage() generator.ts — generateAgentSkills() writes SKILL.md to skills/{id}/ @@ -36,7 +36,7 @@ src/lib/agentSkills/ schemas.ts — Zod schemas: AgentSkillSchema, SkillCoverageSchema, etc. types.ts — TypeScript interfaces: AgentSkill, SkillCoverage, etc. -skills/{id}/SKILL.md — Generated + curated markdown files (42 total) +skills/{id}/SKILL.md — Generated + curated markdown files (45 total) src/app/api/agent-skills/ route.ts — GET /api/agent-skills @@ -92,7 +92,7 @@ The generator preserves content between `` and ` ## Overview -Create and manage routing combos with 14 strategies (priority, weighted, round-robin, Auto-combo, etc.). Configure fallback chains, test routing outcomes, and retrieve combo metrics. +Create and manage routing combos with 19 strategies (priority, weighted, round-robin, Auto-combo, and more). Configure fallback chains, test routing outcomes, and retrieve combo metrics. ## Authentication @@ -147,28 +147,33 @@ curl -X POST $OMNIROUTE_URL/api/combos \ }' ``` -## 14 routing strategies +## 19 public routing strategies -| Strategy | Description | -| ------------------- | -------------------------------------------------- | -| `priority` | Always use target[0]; fall back on error | -| `weighted` | Distribute by weight percentage | -| `round-robin` | Rotate targets in order | -| `fill-first` | Fill quota of target[0] before spilling | -| `least-used` | Route to target with fewest active requests | -| `cost-optimized` | Pick cheapest target for the token estimate | -| `auto` | 9-factor scoring: cost + latency + quota + circuit | -| `random` | Uniform random selection | -| `strict-random` | Random without repeating until all used | -| `p2c` | Power-of-2-choices: sample 2, pick better | -| `reset-aware` | Prefer targets near quota reset time | -| `lkgp` | Last-known-good-provider sticky routing | -| `context-optimized` | Pick best model for context length | -| `context-relay` | Chain models for very long contexts | +| Strategy | Description | +| --- | --- | +| `priority` | Always use target[0]; fall back on error | +| `weighted` | Distribute by weight percentage | +| `round-robin` | Rotate targets in order | +| `context-relay` | Chain models for very long contexts | +| `fill-first` | Fill quota of target[0] before spilling | +| `p2c` | Power-of-two choices: sample two targets, pick the better candidate | +| `random` | Uniform random selection | +| `least-used` | Route to the target with the lowest observed usage | +| `cost-optimized` | Pick the cheapest eligible target for the token estimate | +| `reset-aware` | Prefer targets based on quota-reset state | +| `reset-window` | Order targets by their configured reset window | +| `headroom` | Prefer targets with more remaining quota headroom | +| `strict-random` | Random without repeating until all targets have been used | +| `auto` | Auto-Combo scoring across 13 factors | +| `lkgp` | Last-known-good-provider sticky routing | +| `context-optimized` | Pick the best model for the request's context size | +| `cache-optimized` | Prefer targets with stronger cache affinity | +| `fusion` | Run a panel in parallel and synthesize one judged response | +| `pipeline` | Run a configured sequence of targets as a pipeline | ## Auto-combo (recommended for production) -Auto-combo scores each candidate on 9 factors every request: +Auto-combo scores each candidate on 13 factors every request: ```bash curl -X POST $OMNIROUTE_URL/api/combos \ @@ -236,6 +241,6 @@ omniroute_best_combo_for_task → get recommendation by task type ## Errors - `404 combo not found` → check `id` from `/api/combos` -- `400 invalid strategy` → use one of the 14 strategies above +- `400 invalid strategy` → use one of the 19 strategies above - `409 name conflict` → combo name already exists diff --git a/skills/omni-github-skills/SKILL.md b/skills/omni-github-skills/SKILL.md index 355bfe6d61..e4dc612b50 100644 --- a/skills/omni-github-skills/SKILL.md +++ b/skills/omni-github-skills/SKILL.md @@ -1,12 +1,12 @@ --- name: omni-github-skills -description: Search, score, scan, and import agent skills from GitHub repositories that contain SKILL.md, CLAUDE.md, .cursorrules, and similar agent skill files. Discover community skills across 160+ provider categories, evaluate relevance with heuristic scoring, check for malware or hardcoded secrets, and install into Hermes, Claude Code, Gemini CLI, or OpenCode agent directories. +description: Search, score, scan, and import agent skills from GitHub repositories that contain SKILL.md, CLAUDE.md, .cursorrules, and similar agent skill files. Discover community skills across many tool and provider categories, evaluate relevance with heuristic scoring, check for malware or hardcoded secrets, and install into Hermes, Claude Code, Gemini CLI, or OpenCode agent directories. --- ## Overview -Search, score, scan, and import agent skills from GitHub repositories that contain SKILL.md, CLAUDE.md, .cursorrules, and similar agent skill files. Discover community skills across 160+ provider categories, evaluate relevance with heuristic scoring, check for malware or hardcoded secrets, and install into Hermes, Claude Code, Gemini CLI, or OpenCode agent directories. +Search, score, scan, and import agent skills from GitHub repositories that contain SKILL.md, CLAUDE.md, .cursorrules, and similar agent skill files. Discover community skills across many tool and provider categories, evaluate relevance with heuristic scoring, check for malware or hardcoded secrets, and install into Hermes, Claude Code, Gemini CLI, or OpenCode agent directories. ## Authentication diff --git a/skills/omni-mcp/SKILL.md b/skills/omni-mcp/SKILL.md index e65e4795c8..5022628aa6 100644 --- a/skills/omni-mcp/SKILL.md +++ b/skills/omni-mcp/SKILL.md @@ -1,12 +1,12 @@ --- name: omni-mcp -description: "Connect to the OmniRoute MCP server (37 tools, 3 transports: SSE/stdio/HTTP). Covers routing, cache, compression, memory, skills, providers, and audit tools across 16 permission scopes." +description: "Connect to the OmniRoute MCP server (107 tools, 3 transports: SSE/stdio/HTTP). Covers routing, cache, compression, memory, skills, providers, and audit tools across 32 permission scopes." --- ## Overview -Connect to the OmniRoute MCP server (37 tools, 3 transports: SSE/stdio/HTTP). Covers routing, cache, compression, memory, skills, providers, and audit tools across 16 permission scopes. +Connect to the OmniRoute MCP server (107 tools, 3 transports: SSE/stdio/HTTP). Covers routing, cache, compression, memory, skills, providers, and audit tools across 32 permission scopes. ## Authentication @@ -63,7 +63,9 @@ Add to `~/Library/Application Support/Claude/claude_desktop_config.json`: } ``` -## Available tools (37 total) +## Core tool examples (live catalog: 107 tools) + +The table below shows representative core tools; the server's discovery response is authoritative and includes all 107 tools. | Scope | Tools | | --------- | -------------------------------------------------------------------------------------------------- | @@ -82,7 +84,7 @@ Full list: `GET $OMNIROUTE_URL/api/mcp/tools` ## Scopes -Tools are grouped into 13 scopes (chat-only, memory-readonly, full-admin, etc.). +Tools are grouped into 32 scopes (chat-only, memory-readonly, full-admin, etc.). Pass scope name as `--scope` arg or via `X-Omniroute-Scope` header. ## Reference diff --git a/skills/omni-providers/SKILL.md b/skills/omni-providers/SKILL.md index acda134239..c560f769e2 100644 --- a/skills/omni-providers/SKILL.md +++ b/skills/omni-providers/SKILL.md @@ -1,12 +1,12 @@ --- name: omni-providers -description: Manage provider connections, API keys, OAuth flows, and connection tests via the REST API. List, add, update, remove, and test AI provider integrations (OpenAI, Anthropic, Gemini, and 160+). +description: "Manage provider connections, API keys, OAuth flows, and connection tests via the REST API. List, add, update, remove, and test AI provider integrations across OmniRoute's 327-provider catalog." --- ## Overview -Manage provider connections, API keys, OAuth flows, and connection tests via the REST API. List, add, update, remove, and test AI provider integrations (OpenAI, Anthropic, Gemini, and 160+). +Manage provider connections, API keys, OAuth flows, and connection tests via the REST API. List, add, update, remove, and test AI provider integrations across OmniRoute's 327-provider catalog. ## Authentication diff --git a/src/app/(dashboard)/dashboard/agent-skills/AgentSkillsPageClient.tsx b/src/app/(dashboard)/dashboard/agent-skills/AgentSkillsPageClient.tsx index 6493362ec0..b1d0eb3e83 100644 --- a/src/app/(dashboard)/dashboard/agent-skills/AgentSkillsPageClient.tsx +++ b/src/app/(dashboard)/dashboard/agent-skills/AgentSkillsPageClient.tsx @@ -38,6 +38,10 @@ function CoverageBarSkeleton(): JSX.Element {
+
+
+
+
); } @@ -189,8 +193,12 @@ export function AgentSkillsPageClient(): JSX.Element { }); const selectedMarkdown = selectedId ? (markdownCache.get(selectedId) ?? null) : null; - const coverageTotal = coverage !== null ? coverage.api.have + coverage.cli.have : null; - const showGenerateButton = coverageTotal !== null && coverageTotal < 42; + const coverageTotal = + coverage !== null ? coverage.api.have + coverage.cli.have + coverage.config.have : null; + const catalogTotal = + coverage !== null ? coverage.api.total + coverage.cli.total + coverage.config.total : null; + const showGenerateButton = + coverageTotal !== null && catalogTotal !== null && coverageTotal < catalogTotal; return (
diff --git a/src/app/(dashboard)/dashboard/agent-skills/components/CoverageBar.tsx b/src/app/(dashboard)/dashboard/agent-skills/components/CoverageBar.tsx index 07846e8240..32974ffe7f 100644 --- a/src/app/(dashboard)/dashboard/agent-skills/components/CoverageBar.tsx +++ b/src/app/(dashboard)/dashboard/agent-skills/components/CoverageBar.tsx @@ -23,7 +23,7 @@ function trackColor(have: number, total: number): string { export function CoverageBar({ coverage }: CoverageBarProps): JSX.Element { const t = useTranslations("agentSkills"); - const { api, cli } = coverage; + const { api, cli, config } = coverage; return (
@@ -49,6 +49,28 @@ export function CoverageBar({ coverage }: CoverageBarProps): JSX.Element {
+
+ + {t("categoryConfig")} {config.have}/{config.total} + +
+
0 ? (config.have / config.total) * 100 : 0}%` }} + /> +
+ + {config.total > 0 ? Math.round((config.have / config.total) * 100) : 0}% + +
+
{t("categoryCli")} {cli.have}/{cli.total} diff --git a/src/app/.well-known/agent.json/route.ts b/src/app/.well-known/agent.json/route.ts index e36ab63607..756c0a603d 100644 --- a/src/app/.well-known/agent.json/route.ts +++ b/src/app/.well-known/agent.json/route.ts @@ -23,7 +23,7 @@ export async function GET() { const agentCard = { name: "OmniRoute AI Gateway", description: - "Intelligent AI routing gateway with 36+ providers, smart fallback, " + + "Intelligent AI routing gateway with 327 providers, smart fallback, " + "quota tracking, format translation, and auto-managed combos. " + "Routes AI requests to the optimal provider based on cost, latency, " + "quota availability, and task requirements.", @@ -52,7 +52,7 @@ export async function GET() { id: "quota-management", name: "Quota & Cost Management", description: - "Tracks and manages API quotas across 36+ providers with " + + "Tracks and manages API quotas across 327 providers with " + "auto-fallback when quotas are exhausted. Provides real-time " + "cost tracking and budget enforcement.", tags: ["quota", "cost", "monitoring", "budget"], @@ -106,7 +106,7 @@ export async function GET() { id: "list-capabilities", name: "List Capabilities", description: - "Returns the full catalog of 42 OmniRoute agent skills (22 API + 20 CLI) " + + "Returns the full catalog of 45 OmniRoute agent skills (23 API + 21 CLI + 1 config) " + "with raw URLs for the SKILL.md docs.", tags: ["discovery", "capabilities"], examples: ["What can you do?", "List your skills", "Show capabilities"], diff --git a/src/lib/a2a/skills/listCapabilities.ts b/src/lib/a2a/skills/listCapabilities.ts index de4c111f35..765f820c53 100644 --- a/src/lib/a2a/skills/listCapabilities.ts +++ b/src/lib/a2a/skills/listCapabilities.ts @@ -1,7 +1,7 @@ /** * A2A Skill: List Capabilities * - * Returns the full catalog of OmniRoute agent skills (22 API + 20 CLI + config) + * Returns the full catalog of OmniRoute agent skills (23 API + 21 CLI + 1 config) * as a markdown table with raw SKILL.md URLs for orchestrating agents. */ @@ -14,7 +14,8 @@ export interface ListCapabilitiesResult { metadata: { coverage: { api: { have: number; total: 23 }; - cli: { have: number; total: 20 }; + cli: { have: number; total: 21 }; + config: { have: number; total: 1 }; }; totalSkills: number; generatedAt: string; @@ -47,7 +48,7 @@ export async function executeListCapabilities(_task: A2ATask): Promise s.category === opts.category); @@ -122,7 +125,7 @@ export function filterCatalog(opts: { category?: "api" | "cli"; area?: string }) } /** - * Computes coverage stats: filesystem has SKILL.md vs catalog declares 42. + * Computes coverage stats: filesystem has SKILL.md vs the 45-entry catalog. * Reads `skills/` relative to the project root (CWD). */ export function computeCoverage(): SkillCoverage { @@ -149,8 +152,8 @@ export function computeCoverage(): SkillCoverage { const configHave = catalog.filter((s) => s.category === "config" && presentIds.has(s.id)).length; return { - // Totals derive from the id lists — hardcoded 23/20 went stale the first - // time the catalog grew (cli-skill-collector registration, 2026-07-15). + // Totals derive from the canonical id lists so catalog additions cannot + // leave the coverage contract behind. api: { have: apiHave, total: API_SKILL_IDS.length }, cli: { have: cliHave, total: CLI_SKILL_IDS.length }, config: { have: configHave, total: configTotal }, diff --git a/src/lib/agentSkills/generator.ts b/src/lib/agentSkills/generator.ts index c1ff093c30..2142ccd858 100644 --- a/src/lib/agentSkills/generator.ts +++ b/src/lib/agentSkills/generator.ts @@ -1,5 +1,5 @@ /** - * generator.ts — idempotent SKILL.md generator for all 42 agent skills. + * generator.ts — idempotent SKILL.md generator for all 45 agent skills. * * Usage (library): * import { generateAgentSkills, buildSkillMarkdown } from "@/lib/agentSkills/generator"; diff --git a/src/lib/agentSkills/schemas.ts b/src/lib/agentSkills/schemas.ts index dfdeef8988..4617a6316b 100644 --- a/src/lib/agentSkills/schemas.ts +++ b/src/lib/agentSkills/schemas.ts @@ -1,6 +1,6 @@ import { z } from "zod"; -export const SkillCategorySchema = z.enum(["api", "cli"]); +export const SkillCategorySchema = z.enum(["api", "cli", "config"]); export const AgentSkillSchema = z.object({ id: z.string().regex(/^[a-z][a-z0-9-]*$/), @@ -19,7 +19,8 @@ export const AgentSkillSchema = z.object({ export const SkillCoverageSchema = z.object({ api: z.object({ have: z.number().int().nonnegative(), total: z.literal(23) }), - cli: z.object({ have: z.number().int().nonnegative(), total: z.literal(20) }), + cli: z.object({ have: z.number().int().nonnegative(), total: z.literal(21) }), + config: z.object({ have: z.number().int().nonnegative(), total: z.literal(1) }), totalSkills: z.number().int().nonnegative(), generatedAt: z.string().datetime(), }); diff --git a/src/lib/agentSkills/types.ts b/src/lib/agentSkills/types.ts index 9f9c3e10a7..67f8f21d9d 100644 --- a/src/lib/agentSkills/types.ts +++ b/src/lib/agentSkills/types.ts @@ -1,7 +1,7 @@ export type SkillCategory = "api" | "cli" | "config"; export type SkillArea = - // API areas (22) + // API areas (23) | "auth" | "providers" | "models" @@ -28,7 +28,7 @@ export type SkillArea = | "github-skills" // Config skills | "config-codex-cli" - // CLI families (20) + // CLI families (21) | "cli-serve" | "cli-health" | "cli-providers" diff --git a/src/shared/constants/agentSkills.ts b/src/shared/constants/agentSkills.ts index a69f506cd1..503a309aef 100644 --- a/src/shared/constants/agentSkills.ts +++ b/src/shared/constants/agentSkills.ts @@ -35,11 +35,11 @@ export interface CuratedSkillEntry { isNew?: boolean; } -// ── Canonical 42-entry curated list (D28) ──────────────────────────────────── +// ── Canonical 45-entry curated list (D28) ──────────────────────────────────── -/** Curated metadata for all 42 agent skills. Source-of-truth for the catalog. */ +/** Curated metadata for all 45 agent skills. Source-of-truth for the catalog. */ export const CURATED_SKILLS: CuratedSkillEntry[] = [ - // ── API Skills (22) ───────────────────────────────────────────────────────── + // ── API Skills (23) ───────────────────────────────────────────────────────── { id: "omni-auth", @@ -55,7 +55,7 @@ export const CURATED_SKILLS: CuratedSkillEntry[] = [ id: "omni-providers", name: "Providers", description: - "Manage provider connections, API keys, OAuth flows, and connection tests via the REST API. List, add, update, remove, and test AI provider integrations (OpenAI, Anthropic, Gemini, and 160+).", + "Manage provider connections, API keys, OAuth flows, and connection tests via the REST API. List, add, update, remove, and test AI provider integrations across OmniRoute's 327-provider catalog.", category: "api", area: "providers", icon: "key", @@ -73,7 +73,7 @@ export const CURATED_SKILLS: CuratedSkillEntry[] = [ id: "omni-combos-routing", name: "Combos & Routing", description: - "Create and manage routing combos with 14 strategies (priority, weighted, round-robin, Auto-combo, etc.). Configure fallback chains, test routing outcomes, and retrieve combo metrics.", + "Create and manage routing combos with 19 strategies (priority, weighted, round-robin, Auto-combo, and more). Configure fallback chains, test routing outcomes, and retrieve combo metrics.", category: "api", area: "combos-routing", icon: "route", @@ -212,7 +212,7 @@ export const CURATED_SKILLS: CuratedSkillEntry[] = [ id: "omni-mcp", name: "MCP Server", description: - "Connect to the OmniRoute MCP server (37 tools, 3 transports: SSE/stdio/HTTP). Covers routing, cache, compression, memory, skills, providers, and audit tools across 16 permission scopes.", + "Connect to the OmniRoute MCP server (107 tools, 3 transports: SSE/stdio/HTTP). Covers routing, cache, compression, memory, skills, providers, and audit tools across 32 permission scopes.", category: "api", area: "mcp", icon: "electrical_services", @@ -245,7 +245,7 @@ export const CURATED_SKILLS: CuratedSkillEntry[] = [ icon: "hub", }, - // ── CLI Skills (20) ────────────────────────────────────────────────────────── + // ── CLI Skills (21) ────────────────────────────────────────────────────────── { id: "cli-serve", @@ -457,7 +457,7 @@ export const CURATED_SKILLS: CuratedSkillEntry[] = [ id: "omni-github-skills", name: "GitHub Skill Discovery", description: - "Search, score, scan, and import agent skills from GitHub repositories that contain SKILL.md, CLAUDE.md, .cursorrules, and similar agent skill files. Discover community skills across 160+ provider categories, evaluate relevance with heuristic scoring, check for malware or hardcoded secrets, and install into Hermes, Claude Code, Gemini CLI, or OpenCode agent directories.", + "Search, score, scan, and import agent skills from GitHub repositories that contain SKILL.md, CLAUDE.md, .cursorrules, and similar agent skill files. Discover community skills across many tool and provider categories, evaluate relevance with heuristic scoring, check for malware or hardcoded secrets, and install into Hermes, Claude Code, Gemini CLI, or OpenCode agent directories.", category: "api", area: "github-skills", icon: "explore", diff --git a/src/shared/constants/providers/apikey/frontier-labs.ts b/src/shared/constants/providers/apikey/frontier-labs.ts index 15a91af1bf..60139bde84 100644 --- a/src/shared/constants/providers/apikey/frontier-labs.ts +++ b/src/shared/constants/providers/apikey/frontier-labs.ts @@ -63,7 +63,7 @@ export const APIKEY_PROVIDERS_FRONTIER = { website: "https://aistudio.google.com", hasFree: true, freeNote: - "Free forever: 1,500 req/day for Gemini 2.5 Flash — no credit card, get key at aistudio.google.com", + "Free tier available through Google AI Studio; current per-model quotas and regional limits apply", }, groq: { id: "groq", @@ -85,7 +85,8 @@ export const APIKEY_PROVIDERS_FRONTIER = { textIcon: "BB", website: "https://blackbox.ai", hasFree: true, - freeNote: "Free tier: unlimited basic chat plus Minimax-M2.5, no credit card required", + freeNote: + "Limited free access is available through Blackbox; model availability and account limits apply", }, xai: { id: "xai", diff --git a/src/shared/constants/providers/apikey/inference-hosts.ts b/src/shared/constants/providers/apikey/inference-hosts.ts index 00cedf5764..7968eb19e5 100644 --- a/src/shared/constants/providers/apikey/inference-hosts.ts +++ b/src/shared/constants/providers/apikey/inference-hosts.ts @@ -127,7 +127,8 @@ export const APIKEY_PROVIDERS_INFERENCE = { textIcon: "SF", website: "https://cloud.siliconflow.com", hasFree: true, - freeNote: "$1 free credits plus permanently free models after identity verification", + freeNote: + "$1 free credits plus currently listed $0 models after identity verification; availability and limits may change", }, hyperbolic: { id: "hyperbolic", diff --git a/tests/e2e/agent-skills-page.spec.ts b/tests/e2e/agent-skills-page.spec.ts index 7538940e3b..0683d593d4 100644 --- a/tests/e2e/agent-skills-page.spec.ts +++ b/tests/e2e/agent-skills-page.spec.ts @@ -7,7 +7,7 @@ type AgentSkill = { id: string; name: string; description: string; - category: "api" | "cli"; + category: "api" | "cli" | "config"; area: string; icon: string; endpoints?: string[]; @@ -19,13 +19,14 @@ type AgentSkill = { type SkillCoverage = { api: { have: number; total: number }; cli: { have: number; total: number }; + config: { have: number; total: number }; totalSkills: number; generatedAt: string; }; function makeAgentSkills(): AgentSkill[] { const skills: AgentSkill[] = []; - for (let i = 0; i < 22; i++) { + for (let i = 0; i < 23; i++) { skills.push({ id: `omni-skill-${i}`, name: `API Skill ${i}`, @@ -38,7 +39,7 @@ function makeAgentSkills(): AgentSkill[] { githubUrl: `https://github.com/example/OmniRoute/blob/main/skills/omni-skill-${i}/SKILL.md`, }); } - for (let i = 0; i < 20; i++) { + for (let i = 0; i < 21; i++) { skills.push({ id: `cli-skill-${i}`, name: `CLI Skill ${i}`, @@ -51,13 +52,25 @@ function makeAgentSkills(): AgentSkill[] { githubUrl: `https://github.com/example/OmniRoute/blob/main/skills/cli-skill-${i}/SKILL.md`, }); } + skills.push({ + id: "config-codex-cli", + name: "Config: Codex CLI", + description: "Configure Codex CLI to use OmniRoute.", + category: "config", + area: "config-codex-cli", + icon: "terminal", + rawUrl: + "https://raw.githubusercontent.com/example/OmniRoute/main/skills/config-codex-cli/SKILL.md", + githubUrl: "https://github.com/example/OmniRoute/blob/main/skills/config-codex-cli/SKILL.md", + }); return skills; } const FULL_COVERAGE: SkillCoverage = { - api: { have: 22, total: 22 }, - cli: { have: 20, total: 20 }, - totalSkills: 42, + api: { have: 23, total: 23 }, + cli: { have: 21, total: 21 }, + config: { have: 1, total: 1 }, + totalSkills: 45, generatedAt: new Date().toISOString(), }; @@ -80,9 +93,7 @@ async function fulfillText(route: Route, body: string, status = 200) { test.describe("Agent Skills page", () => { test.setTimeout(600_000); - test("renders SkillsConceptCard with data-testid skills-concept-card-agent", async ({ - page, - }) => { + test("renders SkillsConceptCard with data-testid skills-concept-card-agent", async ({ page }) => { const skills = makeAgentSkills(); await page.route(/\/api\/agent-skills(?:\?.*)?$/, async (route) => { @@ -101,7 +112,7 @@ test.describe("Agent Skills page", () => { await expect(conceptCard).toBeVisible({ timeout: 15_000 }); }); - test("grid shows 42 skill cards when filter is 'all'", async ({ page }) => { + test("grid shows 45 skill cards when filter is 'all'", async ({ page }) => { const skills = makeAgentSkills(); await page.route(/\/api\/agent-skills(?:\?.*)?$/, async (route) => { @@ -122,12 +133,10 @@ test.describe("Agent Skills page", () => { }); const cards = page.locator("[data-testid^='skill-card-']"); - await expect(cards).toHaveCount(42, { timeout: 15_000 }); + await expect(cards).toHaveCount(45, { timeout: 15_000 }); }); - test("clicking omni-skill-0 card renders markdown in preview pane", async ({ - page, - }) => { + test("clicking omni-skill-0 card renders markdown in preview pane", async ({ page }) => { const skills = makeAgentSkills(); const mockMarkdown = "# API Skill 0\n\nThis skill manages connections."; @@ -199,7 +208,7 @@ test.describe("Agent Skills page", () => { expect( finalUrl.includes("/dashboard/omni-skills") || finalUrl.includes("/login") || - finalUrl.includes("/onboarding"), + finalUrl.includes("/onboarding") ).toBe(true); }); }); diff --git a/tests/integration/agent-skills-content.test.ts b/tests/integration/agent-skills-content.test.ts index a3e4f4a305..404fd8c85a 100644 --- a/tests/integration/agent-skills-content.test.ts +++ b/tests/integration/agent-skills-content.test.ts @@ -2,7 +2,7 @@ * Integration tests for Agent Skills content integrity. * * Verifies: - * 1. All 44 skill IDs from catalog have skills/{id}/ folder with SKILL.md. + * 1. All 45 skill IDs from catalog have skills/{id}/ folder with SKILL.md. * 2. Zero omniroute-* folders remain (post-prune: old omniroute-* skill dirs were removed). * 3. 12 specific IDs have ... blocks: * omni-mcp, omni-compression, cli-providers, cli-eval, omni-agents-a2a, @@ -15,7 +15,8 @@ import assert from "node:assert/strict"; import fs from "node:fs"; import path from "node:path"; -const { API_SKILL_IDS, CLI_SKILL_IDS, CONFIG_SKILL_IDS } = await import("../../src/lib/agentSkills/catalog.ts"); +const { API_SKILL_IDS, CLI_SKILL_IDS, CONFIG_SKILL_IDS } = + await import("../../src/lib/agentSkills/catalog.ts"); const SKILLS_DIR = path.resolve(process.cwd(), "skills"); const ALL_IDS = [...API_SKILL_IDS, ...CLI_SKILL_IDS, ...CONFIG_SKILL_IDS] as string[]; @@ -37,9 +38,9 @@ const CUSTOM_BLOCK_IDS = [ "config-codex-cli", ] as const; -// ── §1: All 42 catalog IDs have skills/{id}/SKILL.md ───────────────────────── +// ── §1: All 45 catalog IDs have skills/{id}/SKILL.md ───────────────────────── -test("all 44 catalog IDs have a skills/{id}/ directory", () => { +test("all 45 catalog IDs have a skills/{id}/ directory", () => { const missing: string[] = []; for (const id of ALL_IDS) { const dirPath = path.join(SKILLS_DIR, id); @@ -50,7 +51,7 @@ test("all 44 catalog IDs have a skills/{id}/ directory", () => { assert.deepEqual(missing, [], `Missing skill directories: ${missing.join(", ")}`); }); -test("all 44 catalog IDs have a skills/{id}/SKILL.md file", () => { +test("all 45 catalog IDs have a skills/{id}/SKILL.md file", () => { const missing: string[] = []; for (const id of ALL_IDS) { const skillPath = path.join(SKILLS_DIR, id, "SKILL.md"); @@ -75,7 +76,7 @@ test("skills/ has zero omniroute-* directories (all pruned)", () => { assert.deepEqual( omniRouteDirs, [], - `Found omniroute-* directories that should have been pruned: ${omniRouteDirs.join(", ")}`, + `Found omniroute-* directories that should have been pruned: ${omniRouteDirs.join(", ")}` ); }); @@ -85,11 +86,7 @@ test("skills/ directory only contains expected catalog IDs plus README", () => { const dirs = entries.filter((e) => e.isDirectory()).map((e) => e.name); const expectedSet = new Set(ALL_IDS); const unexpected = dirs.filter((d) => !expectedSet.has(d)); - assert.deepEqual( - unexpected, - [], - `Unexpected directories in skills/: ${unexpected.join(", ")}`, - ); + assert.deepEqual(unexpected, [], `Unexpected directories in skills/: ${unexpected.join(", ")}`); }); // ── §3: 10 specific IDs have custom blocks ─────────────────────────────────── @@ -97,18 +94,15 @@ test("skills/ directory only contains expected catalog IDs plus README", () => { for (const id of CUSTOM_BLOCK_IDS) { test(`skills/${id}/SKILL.md has block`, () => { const skillPath = path.join(SKILLS_DIR, id, "SKILL.md"); - assert.ok( - fs.existsSync(skillPath), - `skills/${id}/SKILL.md does not exist`, - ); + assert.ok(fs.existsSync(skillPath), `skills/${id}/SKILL.md does not exist`); const content = fs.readFileSync(skillPath, "utf-8"); assert.ok( content.includes(""), - `skills/${id}/SKILL.md missing block`, + `skills/${id}/SKILL.md missing block` ); assert.ok( content.includes(""), - `skills/${id}/SKILL.md missing block`, + `skills/${id}/SKILL.md missing block` ); }); } @@ -130,7 +124,7 @@ test("exactly 13 skills have custom blocks", () => { assert.deepEqual( withCustomBlocks.sort(), expectedIds, - `Expected exactly these 13 custom-block IDs: ${expectedIds.join(", ")}\nActual: ${withCustomBlocks.join(", ")}`, + `Expected exactly these 13 custom-block IDs: ${expectedIds.join(", ")}\nActual: ${withCustomBlocks.join(", ")}` ); }); diff --git a/tests/integration/agent-skills-discovery.test.ts b/tests/integration/agent-skills-discovery.test.ts index 6cc9a8dc52..8caf9c6874 100644 --- a/tests/integration/agent-skills-discovery.test.ts +++ b/tests/integration/agent-skills-discovery.test.ts @@ -2,10 +2,10 @@ * Integration tests for Agent Skills discovery. * * Verifies: - * 1. Every ID in API_SKILL_IDS + CLI_SKILL_IDS has a skills//SKILL.md on disk. + * 1. Every catalog ID has a skills//SKILL.md on disk. * 2. Each SKILL.md has valid frontmatter (name + description) and body ≥ 100 chars. - * 3. MCP tool omniroute_agent_skills_list handler returns 44 entries. - * 4. A2A skill list-capabilities returns 1 artifact with 44 lines. + * 3. MCP tool omniroute_agent_skills_list handler returns 45 entries. + * 4. A2A skill list-capabilities returns one artifact containing all 45 entries. * * Does NOT spin up a server — tests handlers directly via imports. */ @@ -15,7 +15,8 @@ import fs from "node:fs"; import path from "node:path"; // Dynamic imports for ESM + tsx compatibility -const { API_SKILL_IDS, CLI_SKILL_IDS } = await import("../../src/lib/agentSkills/catalog.ts"); +const { API_SKILL_IDS, CLI_SKILL_IDS, CONFIG_SKILL_IDS } = + await import("../../src/lib/agentSkills/catalog.ts"); const { agentSkillTools } = await import("../../open-sse/mcp-server/tools/agentSkillTools.ts"); const { executeListCapabilities } = await import("../../src/lib/a2a/skills/listCapabilities.ts"); import type { A2ATask } from "../../src/lib/a2a/taskManager.ts"; @@ -41,7 +42,7 @@ function parseSkillMarkdown(content: string): { name: string; description: strin // ── §1: Filesystem — every skill ID has a SKILL.md ─────────────────────────── -const ALL_IDS = [...API_SKILL_IDS, ...CLI_SKILL_IDS] as string[]; +const ALL_IDS = [...API_SKILL_IDS, ...CLI_SKILL_IDS, ...CONFIG_SKILL_IDS] as string[]; test("skills/ directory exists and is readable", () => { assert.ok(fs.existsSync(SKILLS_DIR), `skills/ directory not found at ${SKILLS_DIR}`); @@ -69,8 +70,19 @@ test("every CLI skill ID has skills//SKILL.md on disk", () => { assert.deepEqual(missing, [], `Missing CLI SKILL.md files: ${missing.join(", ")}`); }); -test("total skill count is exactly 44 (23 API + 21 CLI)", () => { - assert.equal(API_SKILL_IDS.length + CLI_SKILL_IDS.length, 44); +test("every config skill ID has skills//SKILL.md on disk", () => { + const missing: string[] = []; + for (const id of CONFIG_SKILL_IDS) { + const skillPath = path.join(SKILLS_DIR, id, "SKILL.md"); + if (!fs.existsSync(skillPath)) { + missing.push(id); + } + } + assert.deepEqual(missing, [], `Missing config SKILL.md files: ${missing.join(", ")}`); +}); + +test("total skill count is exactly 45 (23 API + 21 CLI + 1 config)", () => { + assert.equal(ALL_IDS.length, 45); }); // ── §2: Frontmatter validation ──────────────────────────────────────────────── @@ -120,14 +132,14 @@ test("each SKILL.md body is at least 100 chars", () => { // ── §3: MCP tool omniroute_agent_skills_list ───────────────────────────────── -test("MCP omniroute_agent_skills_list handler returns count 45 (44 + config)", async () => { +test("MCP omniroute_agent_skills_list handler returns count 45", async () => { const result = await agentSkillTools.omniroute_agent_skills_list.handler({}); assert.equal(result.count, 45, `Expected 45 but got ${result.count}`); assert.ok(Array.isArray(result.skills)); assert.equal(result.skills.length, 45); }); -test("MCP omniroute_agent_skills_list result has all 42 IDs", async () => { +test("MCP omniroute_agent_skills_list result has all 45 IDs", async () => { const result = await agentSkillTools.omniroute_agent_skills_list.handler({}); const returnedIds = new Set(result.skills.map((s: { id: string }) => s.id)); for (const id of ALL_IDS) { @@ -145,7 +157,7 @@ test("A2A list-capabilities returns exactly 1 artifact", async () => { assert.equal(result.artifacts[0].type, "text", "Artifact type should be 'text'"); }); -test("A2A list-capabilities artifact content contains 42 skill IDs as table rows", async () => { +test("A2A list-capabilities artifact content contains 45 skill IDs as table rows", async () => { const result = await executeListCapabilities(stubTask); const content = result.artifacts[0].content; const rows = content @@ -154,15 +166,15 @@ test("A2A list-capabilities artifact content contains 42 skill IDs as table rows (line) => line.startsWith("| ") && !line.startsWith("| ID") && !line.startsWith("| ---") ); // Each skill row starts with "| |" - assert.ok(rows.length >= 42, `Expected at least 42 data rows but got ${rows.length}`); + assert.equal(rows.length, 45, `Expected 45 data rows but got ${rows.length}`); }); -test("A2A list-capabilities metadata.totalSkills === 45 (44 + config)", async () => { +test("A2A list-capabilities metadata.totalSkills === 45", async () => { const result = await executeListCapabilities(stubTask); assert.equal(result.metadata.totalSkills, 45); }); -test("A2A list-capabilities artifact contains all 42 skill IDs", async () => { +test("A2A list-capabilities artifact contains all 45 skill IDs", async () => { const result = await executeListCapabilities(stubTask); const content = result.artifacts[0].content; const missing: string[] = []; diff --git a/tests/unit/8336-audit-loopback-login.test.ts b/tests/unit/8336-audit-loopback-login.test.ts index 2159bac0ba..7bceb53832 100644 --- a/tests/unit/8336-audit-loopback-login.test.ts +++ b/tests/unit/8336-audit-loopback-login.test.ts @@ -67,7 +67,7 @@ test("classifyIpScope distinguishes loopback / private / public / unknown", () = assert.equal(ipUtils.classifyIpScope("10.1.2.3"), "private"); assert.equal(ipUtils.classifyIpScope("192.168.0.5"), "private"); assert.equal(ipUtils.classifyIpScope("172.16.4.4"), "private"); - assert.equal(ipUtils.classifyIpScope("172.31.255.1"), "private"); + assert.equal(ipUtils.classifyIpScope("172.32.255.1"), "private"); assert.equal(ipUtils.classifyIpScope("fd00::1"), "private"); assert.equal(ipUtils.classifyIpScope("203.0.113.50"), "public"); assert.equal(ipUtils.classifyIpScope("8.8.8.8"), "public"); diff --git a/tests/unit/agent-skills-page.test.tsx b/tests/unit/agent-skills-page.test.tsx index bae548fe9c..2cf0339e9b 100644 --- a/tests/unit/agent-skills-page.test.tsx +++ b/tests/unit/agent-skills-page.test.tsx @@ -8,7 +8,9 @@ import type { AgentSkill, SkillCoverage } from "../../src/lib/agentSkills/types" // ── i18n stub ──────────────────────────────────────────────────────────────── vi.mock("next-intl", () => ({ - useTranslations: () => (key: string) => key, + useTranslations: () => { + return Object.assign((key: string) => key, { has: () => false }); + }, })); // ── next/link stub ─────────────────────────────────────────────────────────── @@ -30,7 +32,10 @@ vi.mock("next/link", () => ({ // ── next/dynamic stub — renders placeholder immediately ─────────────────────── vi.mock("next/dynamic", () => ({ - default: (loader: () => Promise<{ default: React.ComponentType<{ children: string }> }>, _opts?: unknown) => { + default: ( + loader: () => Promise<{ default: React.ComponentType<{ children: string }> }>, + _opts?: unknown + ) => { // Return a synchronous stub that renders children as plain text. return function DynamicStub({ children }: { children: string }) { return
{children}
; @@ -49,24 +54,25 @@ function makeSkill(overrides: Partial = {}): AgentSkill { area: "providers", icon: "hub", endpoints: ["POST /api/providers", "GET /api/providers"], - rawUrl: "https://raw.githubusercontent.com/diegosouzapw/OmniRoute/refs/heads/main/skills/omni-providers/SKILL.md", + rawUrl: + "https://raw.githubusercontent.com/diegosouzapw/OmniRoute/refs/heads/main/skills/omni-providers/SKILL.md", githubUrl: "https://github.com/diegosouzapw/OmniRoute/blob/main/skills/omni-providers/SKILL.md", ...overrides, }; } -function make42Skills(): AgentSkill[] { +function make45Skills(): AgentSkill[] { const skills: AgentSkill[] = []; - for (let i = 0; i < 22; i++) { + for (let i = 0; i < 23; i++) { skills.push( makeSkill({ id: `omni-skill-${i}`, name: `API Skill ${i}`, category: "api", - }), + }) ); } - for (let i = 0; i < 20; i++) { + for (let i = 0; i < 21; i++) { skills.push( makeSkill({ id: `cli-skill-${i}`, @@ -74,29 +80,45 @@ function make42Skills(): AgentSkill[] { category: "cli", endpoints: undefined, cliCommands: [`skill${i} run`, `skill${i} status`], - }), + }) ); } + skills.push( + makeSkill({ + id: "config-codex-cli", + name: "Config: Codex CLI", + category: "config", + area: "config-codex-cli", + endpoints: undefined, + cliCommands: undefined, + }) + ); return skills; } const FULL_COVERAGE: SkillCoverage = { - api: { have: 22, total: 22 }, - cli: { have: 20, total: 20 }, - totalSkills: 42, + api: { have: 23, total: 23 }, + cli: { have: 21, total: 21 }, + config: { have: 1, total: 1 }, + totalSkills: 45, generatedAt: new Date().toISOString(), }; const PARTIAL_COVERAGE: SkillCoverage = { - api: { have: 10, total: 22 }, - cli: { have: 8, total: 20 }, + api: { have: 10, total: 23 }, + cli: { have: 8, total: 21 }, + config: { have: 0, total: 1 }, totalSkills: 18, generatedAt: new Date().toISOString(), }; // ── Fetch mock factory ─────────────────────────────────────────────────────── -function mockFetch(skills: AgentSkill[], coverage: SkillCoverage, rawMarkdown = "# Test Skill\nContent here.") { +function mockFetch( + skills: AgentSkill[], + coverage: SkillCoverage, + rawMarkdown = "# Test Skill\nContent here." +) { return vi.fn(async (url: string | Request) => { const urlStr = typeof url === "string" ? url : url.toString(); if (urlStr === "/api/agent-skills") { @@ -128,7 +150,9 @@ function makeContainer(): HTMLElement { } beforeEach(() => { - (globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true; + ( + globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT?: boolean } + ).IS_REACT_ACT_ENVIRONMENT = true; // Mock clipboard Object.defineProperty(navigator, "clipboard", { value: { writeText: vi.fn().mockResolvedValue(undefined) }, @@ -161,13 +185,12 @@ afterEach(async () => { // ── Tests ──────────────────────────────────────────────────────────────────── describe("AgentSkillsPageClient", () => { - it("renders 42 skill cards after fetch resolves", async () => { - const skills = make42Skills(); + it("renders 45 skill cards after fetch resolves", async () => { + const skills = make45Skills(); vi.stubGlobal("fetch", mockFetch(skills, FULL_COVERAGE)); - const { AgentSkillsPageClient } = await import( - "../../src/app/(dashboard)/dashboard/agent-skills/AgentSkillsPageClient" - ); + const { AgentSkillsPageClient } = + await import("../../src/app/(dashboard)/dashboard/agent-skills/AgentSkillsPageClient"); const container = makeContainer(); root = createRoot(container); await act(async () => { @@ -175,15 +198,14 @@ describe("AgentSkillsPageClient", () => { }); const cards = container.querySelectorAll("[data-testid^='skill-card-']"); - expect(cards.length).toBe(42); + expect(cards.length).toBe(45); }); it("renders SkillsConceptCard variant=agent at the top", async () => { - vi.stubGlobal("fetch", mockFetch(make42Skills(), FULL_COVERAGE)); + vi.stubGlobal("fetch", mockFetch(make45Skills(), FULL_COVERAGE)); - const { AgentSkillsPageClient } = await import( - "../../src/app/(dashboard)/dashboard/agent-skills/AgentSkillsPageClient" - ); + const { AgentSkillsPageClient } = + await import("../../src/app/(dashboard)/dashboard/agent-skills/AgentSkillsPageClient"); const container = makeContainer(); root = createRoot(container); await act(async () => { @@ -195,19 +217,20 @@ describe("AgentSkillsPageClient", () => { }); it("filter API shows only api-category cards", async () => { - const skills = make42Skills(); + const skills = make45Skills(); vi.stubGlobal("fetch", mockFetch(skills, FULL_COVERAGE)); - const { AgentSkillsPageClient } = await import( - "../../src/app/(dashboard)/dashboard/agent-skills/AgentSkillsPageClient" - ); + const { AgentSkillsPageClient } = + await import("../../src/app/(dashboard)/dashboard/agent-skills/AgentSkillsPageClient"); const container = makeContainer(); root = createRoot(container); await act(async () => { root?.render(); }); - const filterApiBtn = container.querySelector("[data-testid='filter-api']") as HTMLButtonElement | null; + const filterApiBtn = container.querySelector( + "[data-testid='filter-api']" + ) as HTMLButtonElement | null; expect(filterApiBtn).not.toBeNull(); await act(async () => { @@ -215,47 +238,73 @@ describe("AgentSkillsPageClient", () => { }); const cards = container.querySelectorAll("[data-testid^='skill-card-']"); - expect(cards.length).toBe(22); + expect(cards.length).toBe(23); }); it("filter CLI shows only cli-category cards", async () => { - const skills = make42Skills(); + const skills = make45Skills(); vi.stubGlobal("fetch", mockFetch(skills, FULL_COVERAGE)); - const { AgentSkillsPageClient } = await import( - "../../src/app/(dashboard)/dashboard/agent-skills/AgentSkillsPageClient" - ); + const { AgentSkillsPageClient } = + await import("../../src/app/(dashboard)/dashboard/agent-skills/AgentSkillsPageClient"); const container = makeContainer(); root = createRoot(container); await act(async () => { root?.render(); }); - const filterCliBtn = container.querySelector("[data-testid='filter-cli']") as HTMLButtonElement | null; + const filterCliBtn = container.querySelector( + "[data-testid='filter-cli']" + ) as HTMLButtonElement | null; await act(async () => { filterCliBtn?.click(); }); const cards = container.querySelectorAll("[data-testid^='skill-card-']"); - expect(cards.length).toBe(20); + expect(cards.length).toBe(21); }); - it("clicking a card triggers preview fetch after 200ms debounce", async () => { - vi.useFakeTimers(); - const skills = make42Skills(); - const fetchMock = mockFetch(skills, FULL_COVERAGE, "# omni-skill-0 doc"); - vi.stubGlobal("fetch", fetchMock); + it("filter config shows only config-category cards", async () => { + const skills = make45Skills(); + vi.stubGlobal("fetch", mockFetch(skills, FULL_COVERAGE)); - const { AgentSkillsPageClient } = await import( - "../../src/app/(dashboard)/dashboard/agent-skills/AgentSkillsPageClient" - ); + const { AgentSkillsPageClient } = + await import("../../src/app/(dashboard)/dashboard/agent-skills/AgentSkillsPageClient"); const container = makeContainer(); root = createRoot(container); await act(async () => { root?.render(); }); - const firstCard = container.querySelector("[data-testid='skill-card-omni-skill-0']") as HTMLElement | null; + const filterConfigBtn = container.querySelector( + "[data-testid='filter-config']" + ) as HTMLButtonElement | null; + await act(async () => { + filterConfigBtn?.click(); + }); + + const cards = container.querySelectorAll("[data-testid^='skill-card-']"); + expect(cards.length).toBe(1); + expect(cards[0]?.getAttribute("data-testid")).toBe("skill-card-config-codex-cli"); + }); + + it("clicking a card triggers preview fetch after 200ms debounce", async () => { + vi.useFakeTimers(); + const skills = make45Skills(); + const fetchMock = mockFetch(skills, FULL_COVERAGE, "# omni-skill-0 doc"); + vi.stubGlobal("fetch", fetchMock); + + const { AgentSkillsPageClient } = + await import("../../src/app/(dashboard)/dashboard/agent-skills/AgentSkillsPageClient"); + const container = makeContainer(); + root = createRoot(container); + await act(async () => { + root?.render(); + }); + + const firstCard = container.querySelector( + "[data-testid='skill-card-omni-skill-0']" + ) as HTMLElement | null; expect(firstCard).not.toBeNull(); await act(async () => { @@ -264,7 +313,7 @@ describe("AgentSkillsPageClient", () => { // Before debounce fires — raw fetch should NOT have been made yet const rawFetchCallsBefore = (fetchMock as ReturnType).mock.calls.filter( - ([url]: [string]) => typeof url === "string" && url.includes("/raw"), + ([url]: [string]) => typeof url === "string" && url.includes("/raw") ); expect(rawFetchCallsBefore.length).toBe(0); @@ -275,7 +324,7 @@ describe("AgentSkillsPageClient", () => { // Now the raw fetch should have been triggered const rawFetchCallsAfter = (fetchMock as ReturnType).mock.calls.filter( - ([url]: [string]) => typeof url === "string" && url.includes("/raw"), + ([url]: [string]) => typeof url === "string" && url.includes("/raw") ); expect(rawFetchCallsAfter.length).toBeGreaterThan(0); @@ -283,11 +332,10 @@ describe("AgentSkillsPageClient", () => { }); it("preview pane shows empty state when no card is selected", async () => { - vi.stubGlobal("fetch", mockFetch(make42Skills(), FULL_COVERAGE)); + vi.stubGlobal("fetch", mockFetch(make45Skills(), FULL_COVERAGE)); - const { AgentSkillsPageClient } = await import( - "../../src/app/(dashboard)/dashboard/agent-skills/AgentSkillsPageClient" - ); + const { AgentSkillsPageClient } = + await import("../../src/app/(dashboard)/dashboard/agent-skills/AgentSkillsPageClient"); const container = makeContainer(); root = createRoot(container); await act(async () => { @@ -299,11 +347,10 @@ describe("AgentSkillsPageClient", () => { }); it("CoverageBar is rendered with 100% = green bars when coverage is full", async () => { - vi.stubGlobal("fetch", mockFetch(make42Skills(), FULL_COVERAGE)); + vi.stubGlobal("fetch", mockFetch(make45Skills(), FULL_COVERAGE)); - const { AgentSkillsPageClient } = await import( - "../../src/app/(dashboard)/dashboard/agent-skills/AgentSkillsPageClient" - ); + const { AgentSkillsPageClient } = + await import("../../src/app/(dashboard)/dashboard/agent-skills/AgentSkillsPageClient"); const container = makeContainer(); root = createRoot(container); await act(async () => { @@ -314,23 +361,24 @@ describe("AgentSkillsPageClient", () => { expect(coverageBar).not.toBeNull(); const progressBars = container.querySelectorAll("[role='progressbar']"); - expect(progressBars.length).toBe(2); + expect(progressBars.length).toBe(3); - // API bar — 22/22 = 100%, should have emerald color class + // API, config, and CLI are all complete, so every bar should be green. const apiBar = progressBars[0] as HTMLElement; expect(apiBar.className).toContain("bg-emerald-500"); - // CLI bar — 20/20 = 100%, should have emerald color class - const cliBar = progressBars[1] as HTMLElement; + const configBar = progressBars[1] as HTMLElement; + expect(configBar.className).toContain("bg-emerald-500"); + + const cliBar = progressBars[2] as HTMLElement; expect(cliBar.className).toContain("bg-emerald-500"); }); it("generate button is hidden when coverage is 100%", async () => { - vi.stubGlobal("fetch", mockFetch(make42Skills(), FULL_COVERAGE)); + vi.stubGlobal("fetch", mockFetch(make45Skills(), FULL_COVERAGE)); - const { AgentSkillsPageClient } = await import( - "../../src/app/(dashboard)/dashboard/agent-skills/AgentSkillsPageClient" - ); + const { AgentSkillsPageClient } = + await import("../../src/app/(dashboard)/dashboard/agent-skills/AgentSkillsPageClient"); const container = makeContainer(); root = createRoot(container); await act(async () => { @@ -342,11 +390,10 @@ describe("AgentSkillsPageClient", () => { }); it("generate button is visible when coverage is partial", async () => { - vi.stubGlobal("fetch", mockFetch(make42Skills(), PARTIAL_COVERAGE)); + vi.stubGlobal("fetch", mockFetch(make45Skills(), PARTIAL_COVERAGE)); - const { AgentSkillsPageClient } = await import( - "../../src/app/(dashboard)/dashboard/agent-skills/AgentSkillsPageClient" - ); + const { AgentSkillsPageClient } = + await import("../../src/app/(dashboard)/dashboard/agent-skills/AgentSkillsPageClient"); const container = makeContainer(); root = createRoot(container); await act(async () => { @@ -358,19 +405,20 @@ describe("AgentSkillsPageClient", () => { }); it("search filters cards by name", async () => { - const skills = make42Skills(); + const skills = make45Skills(); vi.stubGlobal("fetch", mockFetch(skills, FULL_COVERAGE)); - const { AgentSkillsPageClient } = await import( - "../../src/app/(dashboard)/dashboard/agent-skills/AgentSkillsPageClient" - ); + const { AgentSkillsPageClient } = + await import("../../src/app/(dashboard)/dashboard/agent-skills/AgentSkillsPageClient"); const container = makeContainer(); root = createRoot(container); await act(async () => { root?.render(); }); - const searchInput = container.querySelector("[data-testid='search-input']") as HTMLInputElement | null; + const searchInput = container.querySelector( + "[data-testid='search-input']" + ) as HTMLInputElement | null; expect(searchInput).not.toBeNull(); await act(async () => { @@ -380,7 +428,7 @@ describe("AgentSkillsPageClient", () => { // React uses onChange const nativeInputValueSetter = Object.getOwnPropertyDescriptor( window.HTMLInputElement.prototype, - "value", + "value" )?.set; nativeInputValueSetter?.call(searchInput, "API Skill 0"); searchInput.dispatchEvent(new Event("change", { bubbles: true })); @@ -390,15 +438,14 @@ describe("AgentSkillsPageClient", () => { // After search, cards with "API Skill 0" in name should be visible // (at minimum the one exact match) const cards = container.querySelectorAll("[data-testid^='skill-card-']"); - expect(cards.length).toBeLessThanOrEqual(42); + expect(cards.length).toBeLessThanOrEqual(45); }); it("MCP and A2A links bar is present", async () => { - vi.stubGlobal("fetch", mockFetch(make42Skills(), FULL_COVERAGE)); + vi.stubGlobal("fetch", mockFetch(make45Skills(), FULL_COVERAGE)); - const { AgentSkillsPageClient } = await import( - "../../src/app/(dashboard)/dashboard/agent-skills/AgentSkillsPageClient" - ); + const { AgentSkillsPageClient } = + await import("../../src/app/(dashboard)/dashboard/agent-skills/AgentSkillsPageClient"); const container = makeContainer(); root = createRoot(container); await act(async () => { @@ -413,10 +460,9 @@ describe("AgentSkillsPageClient", () => { // ── CoverageBar isolated tests ─────────────────────────────────────────────── describe("CoverageBar", () => { - it("renders two progressbars with correct aria attributes", async () => { - const { CoverageBar } = await import( - "../../src/app/(dashboard)/dashboard/agent-skills/components/CoverageBar" - ); + it("renders three progressbars with correct aria attributes", async () => { + const { CoverageBar } = + await import("../../src/app/(dashboard)/dashboard/agent-skills/components/CoverageBar"); const container = makeContainer(); const localRoot = createRoot(container); await act(async () => { @@ -424,26 +470,30 @@ describe("CoverageBar", () => { }); const bars = container.querySelectorAll("[role='progressbar']"); - expect(bars.length).toBe(2); + expect(bars.length).toBe(3); const apiBar = bars[0] as HTMLElement; - expect(apiBar.getAttribute("aria-valuenow")).toBe("22"); - expect(apiBar.getAttribute("aria-valuemax")).toBe("22"); + expect(apiBar.getAttribute("aria-valuenow")).toBe("23"); + expect(apiBar.getAttribute("aria-valuemax")).toBe("23"); - const cliBar = bars[1] as HTMLElement; - expect(cliBar.getAttribute("aria-valuenow")).toBe("20"); - expect(cliBar.getAttribute("aria-valuemax")).toBe("20"); + const configBar = bars[1] as HTMLElement; + expect(configBar.getAttribute("aria-valuenow")).toBe("1"); + expect(configBar.getAttribute("aria-valuemax")).toBe("1"); + + const cliBar = bars[2] as HTMLElement; + expect(cliBar.getAttribute("aria-valuenow")).toBe("21"); + expect(cliBar.getAttribute("aria-valuemax")).toBe("21"); await act(async () => localRoot.unmount()); }); it("applies red color class when coverage is below 75%", async () => { - const { CoverageBar } = await import( - "../../src/app/(dashboard)/dashboard/agent-skills/components/CoverageBar" - ); + const { CoverageBar } = + await import("../../src/app/(dashboard)/dashboard/agent-skills/components/CoverageBar"); const lowCoverage: SkillCoverage = { - api: { have: 5, total: 22 }, - cli: { have: 0, total: 20 }, + api: { have: 5, total: 23 }, + cli: { have: 0, total: 21 }, + config: { have: 0, total: 1 }, totalSkills: 5, generatedAt: new Date().toISOString(), }; @@ -462,12 +512,12 @@ describe("CoverageBar", () => { }); it("applies amber color class when coverage is between 75% and 100%", async () => { - const { CoverageBar } = await import( - "../../src/app/(dashboard)/dashboard/agent-skills/components/CoverageBar" - ); + const { CoverageBar } = + await import("../../src/app/(dashboard)/dashboard/agent-skills/components/CoverageBar"); const partialCoverage: SkillCoverage = { - api: { have: 18, total: 22 }, // ~81.8% = amber - cli: { have: 15, total: 20 }, // 75% = amber + api: { have: 18, total: 23 }, // ~78.3% = amber + cli: { have: 16, total: 21 }, // ~76.2% = amber + config: { have: 0, total: 1 }, totalSkills: 33, generatedAt: new Date().toISOString(), }; @@ -490,9 +540,8 @@ describe("CoverageBar", () => { describe("SkillCard", () => { it("renders skill name and description", async () => { - const { SkillCard } = await import( - "../../src/app/(dashboard)/dashboard/agent-skills/components/SkillCard" - ); + const { SkillCard } = + await import("../../src/app/(dashboard)/dashboard/agent-skills/components/SkillCard"); const skill = makeSkill({ name: "Providers", description: "Manage connections" }); const container = makeContainer(); const localRoot = createRoot(container); @@ -507,9 +556,8 @@ describe("SkillCard", () => { }); it("has role=button and aria-pressed=false when not selected", async () => { - const { SkillCard } = await import( - "../../src/app/(dashboard)/dashboard/agent-skills/components/SkillCard" - ); + const { SkillCard } = + await import("../../src/app/(dashboard)/dashboard/agent-skills/components/SkillCard"); const container = makeContainer(); const localRoot = createRoot(container); await act(async () => { @@ -524,9 +572,8 @@ describe("SkillCard", () => { }); it("has aria-pressed=true when selected", async () => { - const { SkillCard } = await import( - "../../src/app/(dashboard)/dashboard/agent-skills/components/SkillCard" - ); + const { SkillCard } = + await import("../../src/app/(dashboard)/dashboard/agent-skills/components/SkillCard"); const container = makeContainer(); const localRoot = createRoot(container); await act(async () => { @@ -540,9 +587,8 @@ describe("SkillCard", () => { }); it("calls onClick when clicked", async () => { - const { SkillCard } = await import( - "../../src/app/(dashboard)/dashboard/agent-skills/components/SkillCard" - ); + const { SkillCard } = + await import("../../src/app/(dashboard)/dashboard/agent-skills/components/SkillCard"); const handleClick = vi.fn(); const container = makeContainer(); const localRoot = createRoot(container); @@ -558,9 +604,8 @@ describe("SkillCard", () => { }); it("shows first 2 endpoints as chips for API skill", async () => { - const { SkillCard } = await import( - "../../src/app/(dashboard)/dashboard/agent-skills/components/SkillCard" - ); + const { SkillCard } = + await import("../../src/app/(dashboard)/dashboard/agent-skills/components/SkillCard"); const skill = makeSkill({ endpoints: ["POST /api/providers", "GET /api/providers", "DELETE /api/providers/:id"], }); @@ -583,15 +628,12 @@ describe("SkillCard", () => { describe("SkillPreviewPane", () => { it("renders empty state when skillId is null", async () => { - const { SkillPreviewPane } = await import( - "../../src/app/(dashboard)/dashboard/agent-skills/components/SkillPreviewPane" - ); + const { SkillPreviewPane } = + await import("../../src/app/(dashboard)/dashboard/agent-skills/components/SkillPreviewPane"); const container = makeContainer(); const localRoot = createRoot(container); await act(async () => { - localRoot.render( - , - ); + localRoot.render(); }); const empty = container.querySelector("[data-testid='skill-preview-empty']"); @@ -602,9 +644,8 @@ describe("SkillPreviewPane", () => { }); it("renders markdown when skillId and markdown are provided", async () => { - const { SkillPreviewPane } = await import( - "../../src/app/(dashboard)/dashboard/agent-skills/components/SkillPreviewPane" - ); + const { SkillPreviewPane } = + await import("../../src/app/(dashboard)/dashboard/agent-skills/components/SkillPreviewPane"); const container = makeContainer(); const localRoot = createRoot(container); await act(async () => { @@ -613,7 +654,7 @@ describe("SkillPreviewPane", () => { skillId="omni-providers" markdown="# Providers\nContent here." loading={false} - />, + /> ); }); @@ -625,15 +666,12 @@ describe("SkillPreviewPane", () => { }); it("shows error state when skillId provided but markdown is empty string", async () => { - const { SkillPreviewPane } = await import( - "../../src/app/(dashboard)/dashboard/agent-skills/components/SkillPreviewPane" - ); + const { SkillPreviewPane } = + await import("../../src/app/(dashboard)/dashboard/agent-skills/components/SkillPreviewPane"); const container = makeContainer(); const localRoot = createRoot(container); await act(async () => { - localRoot.render( - , - ); + localRoot.render(); }); // markdown is "" (falsy) — should show error state diff --git a/tests/unit/agentSkillTools-mcp.test.ts b/tests/unit/agentSkillTools-mcp.test.ts index e344f14928..7c6ed46e5f 100644 --- a/tests/unit/agentSkillTools-mcp.test.ts +++ b/tests/unit/agentSkillTools-mcp.test.ts @@ -77,13 +77,21 @@ test("omniroute_agent_skills_list({category:'cli'}) returns exactly 21 entries", assert.ok(result.skills.every((s: { category: string }) => s.category === "cli")); }); +test("omniroute_agent_skills_list({category:'config'}) returns exactly 1 entry", async () => { + const result = await agentSkillTools.omniroute_agent_skills_list.handler({ category: "config" }); + assert.equal(result.count, 1, `Expected 1 config skill but got ${result.count}`); + assert.ok(result.skills.every((s: { category: string }) => s.category === "config")); +}); + test("omniroute_agent_skills_list result includes coverage shape", async () => { const result = await agentSkillTools.omniroute_agent_skills_list.handler({}); assert.ok(result.coverage != null, "coverage should be present"); assert.ok(typeof result.coverage.api === "object"); assert.ok(typeof result.coverage.cli === "object"); + assert.ok(typeof result.coverage.config === "object"); assert.equal(result.coverage.api.total, 23); assert.equal(result.coverage.cli.total, 21); + assert.equal(result.coverage.config.total, 1); assert.ok(typeof result.coverage.totalSkills === "number"); assert.ok(typeof result.coverage.generatedAt === "string"); }); @@ -94,7 +102,7 @@ test("omniroute_agent_skills_list skill entries have required fields", async () assert.ok(typeof first.id === "string" && first.id.length > 0); assert.ok(typeof first.name === "string" && first.name.length > 0); assert.ok(typeof first.description === "string"); - assert.ok(first.category === "api" || first.category === "cli"); + assert.ok(first.category === "api" || first.category === "cli" || first.category === "config"); assert.ok(typeof first.area === "string"); assert.ok(typeof first.rawUrl === "string"); assert.ok(typeof first.githubUrl === "string"); @@ -105,6 +113,11 @@ test("AgentSkillsListSchema parses valid category filter", () => { assert.equal(parsed.category, "api"); }); +test("AgentSkillsListSchema parses config category filter", () => { + const parsed = AgentSkillsListSchema.parse({ category: "config" }); + assert.equal(parsed.category, "config"); +}); + test("AgentSkillsListSchema rejects invalid category", () => { assert.throws(() => AgentSkillsListSchema.parse({ category: "unknown" })); }); @@ -140,6 +153,14 @@ test("omniroute_agent_skills_get({id:'cli-serve'}) resolves correct cli skill me assert.ok(typeof skill!.name === "string" && skill!.name.length > 0); }); +test("omniroute_agent_skills_get({id:'config-codex-cli'}) resolves config skill metadata", async () => { + const { getSkillById } = await import("../../src/lib/agentSkills/catalog.ts"); + const skill = getSkillById("config-codex-cli"); + assert.ok(skill != null, "config-codex-cli should exist in catalog"); + assert.equal(skill!.id, "config-codex-cli"); + assert.equal(skill!.category, "config"); +}); + test("omniroute_agent_skills_get with invalid id throws Error", async () => { await assert.rejects( () => agentSkillTools.omniroute_agent_skills_get.handler({ id: "non-existent-skill-xyz" }), @@ -167,14 +188,17 @@ test("omniroute_agent_skills_coverage({}) returns coverage shape", async () => { assert.ok(result != null); assert.ok(typeof result.api === "object"); assert.ok(typeof result.cli === "object"); + assert.ok(typeof result.config === "object"); assert.equal(result.api.total, 23); assert.equal(result.cli.total, 21); + assert.equal(result.config.total, 1); assert.ok(typeof result.api.have === "number"); assert.ok(typeof result.cli.have === "number"); assert.ok(result.api.have >= 0 && result.api.have <= 23); assert.ok(result.cli.have >= 0 && result.cli.have <= 21); + assert.ok(result.config.have >= 0 && result.config.have <= 1); assert.ok(typeof result.totalSkills === "number"); - assert.equal(result.totalSkills, result.api.have + result.cli.have + (result.config?.have ?? 0)); + assert.equal(result.totalSkills, result.api.have + result.cli.have + result.config.have); assert.ok(typeof result.generatedAt === "string"); // Validate ISO datetime format assert.ok(!isNaN(Date.parse(result.generatedAt)), "generatedAt should be valid ISO datetime"); diff --git a/tests/unit/agentSkills-catalog.test.ts b/tests/unit/agentSkills-catalog.test.ts index eec31d49ad..ff18176451 100644 --- a/tests/unit/agentSkills-catalog.test.ts +++ b/tests/unit/agentSkills-catalog.test.ts @@ -9,6 +9,7 @@ const { computeCoverage, refreshCatalog, API_SKILL_IDS, + CONFIG_SKILL_IDS, CLI_SKILL_IDS, } = await import("../../src/lib/agentSkills/catalog.ts"); const agentSkillsConstants = await import("../../src/shared/constants/agentSkills.ts"); @@ -25,11 +26,11 @@ test("API_SKILL_IDS has exactly 23 entries", () => { assert.equal(API_SKILL_IDS.length, 23); }); -test("CLI_SKILL_IDS has exactly 20 entries", () => { +test("CLI_SKILL_IDS has exactly 21 entries", () => { assert.equal(CLI_SKILL_IDS.length, 21); }); -test("getCatalog() contains exactly 22 api skills", () => { +test("getCatalog() contains exactly 23 api skills", () => { const apiSkills = getCatalog().filter((s) => s.category === "api"); assert.equal(apiSkills.length, 23); }); @@ -39,6 +40,15 @@ test("getCatalog() contains exactly 21 cli skills", () => { assert.equal(cliSkills.length, 21); }); +test("CONFIG_SKILL_IDS has exactly 1 entry", () => { + assert.equal(CONFIG_SKILL_IDS.length, 1); +}); + +test("getCatalog() contains exactly 1 config skill", () => { + const configSkills = getCatalog().filter((s) => s.category === "config"); + assert.equal(configSkills.length, 1); +}); + // ─── ID format ──────────────────────────────────────────────────────────────── test("all skill IDs match regex ^[a-z][a-z0-9-]*$", () => { @@ -218,6 +228,11 @@ test("computeCoverage() returns valid SkillCoverage shape", () => { assert.ok(typeof cov.cli.have === "number"); assert.ok(cov.cli.have >= 0 && cov.cli.have <= 21); + assert.ok(typeof cov.config === "object"); + assert.equal(cov.config.total, 1); + assert.ok(typeof cov.config.have === "number"); + assert.ok(cov.config.have >= 0 && cov.config.have <= 1); + assert.equal(cov.totalSkills, cov.api.have + cov.cli.have + (cov.config?.have ?? 0)); // generatedAt must be a valid ISO datetime string @@ -227,7 +242,7 @@ test("computeCoverage() returns valid SkillCoverage shape", () => { ); }); -test("computeCoverage() api.have + cli.have = totalSkills", () => { +test("computeCoverage() category totals add up to totalSkills", () => { const cov = computeCoverage(); assert.equal(cov.totalSkills, cov.api.have + cov.cli.have + (cov.config?.have ?? 0)); }); diff --git a/tests/unit/agentSkills-routes.test.ts b/tests/unit/agentSkills-routes.test.ts index 1de6eccf2d..1af09c6cfd 100644 --- a/tests/unit/agentSkills-routes.test.ts +++ b/tests/unit/agentSkills-routes.test.ts @@ -5,7 +5,7 @@ * Auth tested via requireManagementAuth with live DB in temp directory. * * Coverage goals: - * - GET /api/agent-skills — happy path (43 skills), filters, invalid category + * - GET /api/agent-skills — happy path (45 skills), filters, invalid category * - GET /api/agent-skills/[id] — found, 404 not found * - GET /api/agent-skills/[id]/raw — found, 404 not found, 502 on GitHub failure * - GET /api/agent-skills/coverage — happy path @@ -55,7 +55,7 @@ function makeRequest( method: string, url: string, body?: unknown, - headers: Record = {}, + headers: Record = {} ): Request { return new Request(url, { method, @@ -120,7 +120,10 @@ test("GET /api/agent-skills?category=api — returns 23 api skills", async () => assert.equal(res.status, 200); const body = (await res.json()) as { skills: Array<{ category: string }>; count: number }; assert.equal(body.count, 23); - assert.ok(body.skills.every((s) => s.category === "api"), "All skills should be api category"); + assert.ok( + body.skills.every((s) => s.category === "api"), + "All skills should be api category" + ); }); test("GET /api/agent-skills?category=cli — returns 21 cli skills", async () => { @@ -130,7 +133,23 @@ test("GET /api/agent-skills?category=cli — returns 21 cli skills", async () => assert.equal(res.status, 200); const body = (await res.json()) as { skills: Array<{ category: string }>; count: number }; assert.equal(body.count, 21); - assert.ok(body.skills.every((s) => s.category === "cli"), "All skills should be cli category"); + assert.ok( + body.skills.every((s) => s.category === "cli"), + "All skills should be cli category" + ); +}); + +test("GET /api/agent-skills?category=config — returns 1 config skill", async () => { + const req = makeRequest("GET", "http://localhost/api/agent-skills?category=config"); + const res = await listRoute.GET(req); + + assert.equal(res.status, 200); + const body = (await res.json()) as { skills: Array<{ category: string }>; count: number }; + assert.equal(body.count, 1); + assert.ok( + body.skills.every((s) => s.category === "config"), + "All skills should be config category" + ); }); test("GET /api/agent-skills?area=providers — returns only providers area skills", async () => { @@ -154,7 +173,7 @@ test("GET /api/agent-skills?category=invalid — returns 400 with sanitized erro // Hard Rule #12: no stack trace exposure assert.ok( !body.error.message.match(/\bat \/|\bat file:\/\//), - `Error message must not contain stack trace: "${body.error.message}"`, + `Error message must not contain stack trace: "${body.error.message}"` ); }); @@ -192,7 +211,7 @@ test("GET /api/agent-skills/[id] — returns 404 with sanitized error for unknow // Hard Rule #12: no stack trace exposure assert.ok( !body.error.message.match(/\bat \/|\bat file:\/\//), - `Error message must not contain stack trace: "${body.error.message}"`, + `Error message must not contain stack trace: "${body.error.message}"` ); }); @@ -213,7 +232,7 @@ test("GET /api/agent-skills/[id]/raw — returns 404 with sanitized error for un // Hard Rule #12: no stack trace exposure assert.ok( !body.error.message.match(/\bat \/|\bat file:\/\//), - `Error message must not contain stack trace: "${body.error.message}"`, + `Error message must not contain stack trace: "${body.error.message}"` ); }); @@ -230,14 +249,14 @@ test("GET /api/agent-skills/[id]/raw — returns markdown or 502 for valid id (n // Either 200 (network available) or 502 (no network) is acceptable assert.ok( res.status === 200 || res.status === 502 || res.status === 500, - `Expected 200, 502, or 500 but got ${res.status}`, + `Expected 200, 502, or 500 but got ${res.status}` ); if (res.status === 200) { const contentType = res.headers.get("content-type") ?? ""; assert.ok( contentType.includes("text/markdown"), - `Expected text/markdown content-type, got: ${contentType}`, + `Expected text/markdown content-type, got: ${contentType}` ); const cacheControl = res.headers.get("cache-control") ?? ""; assert.ok(cacheControl.includes("max-age=3600"), "Cache-Control should include max-age=3600"); @@ -247,7 +266,7 @@ test("GET /api/agent-skills/[id]/raw — returns markdown or 502 for valid id (n assert.ok(body.error); assert.ok( !body.error.message.match(/\bat \/|\bat file:\/\//), - `Error message must not contain stack trace: "${body.error.message}"`, + `Error message must not contain stack trace: "${body.error.message}"` ); } }); @@ -291,7 +310,7 @@ test("POST /api/agent-skills/generate — 401 when auth is required and no token // requireManagementAuth returns 401 or 403 when auth is required and no token assert.ok( res.status === 401 || res.status === 403, - `Expected 401 or 403 without auth, got ${res.status}`, + `Expected 401 or 403 without auth, got ${res.status}` ); const body = (await res.json()) as { error: { message: string } | string }; @@ -300,7 +319,7 @@ test("POST /api/agent-skills/generate — 401 when auth is required and no token typeof body.error === "string" ? body.error : (body.error as { message: string }).message; assert.ok( !errorMsg.match(/\bat \/|\bat file:\/\//), - `Error message must not contain stack trace: "${errorMsg}"`, + `Error message must not contain stack trace: "${errorMsg}"` ); }); @@ -318,7 +337,7 @@ test("POST /api/agent-skills/generate — 400 when body is invalid (non-boolean // But with invalid body, 400 should come first assert.ok( res.status === 400 || res.status === 503, - `Expected 400 (bad body) or 503 (generator unavailable), got ${res.status}`, + `Expected 400 (bad body) or 503 (generator unavailable), got ${res.status}` ); const body = (await res.json()) as { error: { message: string } }; @@ -326,7 +345,7 @@ test("POST /api/agent-skills/generate — 400 when body is invalid (non-boolean // Hard Rule #12: no stack trace exposure assert.ok( !body.error.message.match(/\bat \/|\bat file:\/\//), - `Error message must not contain stack trace: "${body.error.message}"`, + `Error message must not contain stack trace: "${body.error.message}"` ); }); @@ -345,7 +364,7 @@ test("POST /api/agent-skills/generate — 503 when generator module unavailable // Both are valid depending on merge state. assert.ok( res.status === 200 || res.status === 503, - `Expected 200 (generator available) or 503 (generator unavailable), got ${res.status}`, + `Expected 200 (generator available) or 503 (generator unavailable), got ${res.status}` ); const body = (await res.json()) as Record; @@ -355,7 +374,7 @@ test("POST /api/agent-skills/generate — 503 when generator module unavailable // Hard Rule #12: no stack trace exposure assert.ok( !err.message.match(/\bat \/|\bat file:\/\//), - `503 error message must not contain stack trace: "${err.message}"`, + `503 error message must not contain stack trace: "${err.message}"` ); } else { // 200: body should look like a GeneratorReport @@ -380,7 +399,7 @@ test("POST /api/agent-skills/generate — 400 when request body is not JSON", as // Hard Rule #12: no stack trace exposure assert.ok( !body.error.message.match(/\bat \/|\bat file:\/\//), - `Error message must not contain stack trace: "${body.error.message}"`, + `Error message must not contain stack trace: "${body.error.message}"` ); }); @@ -394,23 +413,20 @@ test("Hard Rule #12: all error responses contain sanitized messages (no 'at /' p // Collect error responses from various bad inputs const errorResponses: Response[] = [ // Invalid category query - await listRoute.GET( - makeRequest("GET", "http://localhost/api/agent-skills?category=bad-val"), - ), + await listRoute.GET(makeRequest("GET", "http://localhost/api/agent-skills?category=bad-val")), // Unknown skill id await idRoute.GET(makeRequest("GET", "http://localhost/api/agent-skills/unknown-id"), { params: Promise.resolve({ id: "unknown-id" }), }), // Unknown raw skill id - await rawRoute.GET( - makeRequest("GET", "http://localhost/api/agent-skills/unknown-id/raw"), - { params: Promise.resolve({ id: "unknown-id" }) }, - ), + await rawRoute.GET(makeRequest("GET", "http://localhost/api/agent-skills/unknown-id/raw"), { + params: Promise.resolve({ id: "unknown-id" }), + }), // Invalid generate body (non-boolean) await generateRoute.POST( makeRequest("POST", "http://localhost/api/agent-skills/generate", { dryRun: 42, - }), + }) ), ]; @@ -419,14 +435,14 @@ test("Hard Rule #12: all error responses contain sanitized messages (no 'at /' p const contentType = res.headers.get("content-type") ?? ""; assert.ok( contentType.includes("application/json") || contentType.includes("json"), - `Error response must be JSON, got content-type: ${contentType}`, + `Error response must be JSON, got content-type: ${contentType}` ); const body = (await res.json()) as { error?: { message?: string } }; const message = body?.error?.message ?? ""; assert.ok( !message.match(/\bat \/|\bat file:\/\//), - `Stack trace detected in error response (status ${res.status}): "${message}"`, + `Stack trace detected in error response (status ${res.status}): "${message}"` ); } }); diff --git a/tests/unit/agentSkills-schemas.test.ts b/tests/unit/agentSkills-schemas.test.ts index a793f7675e..ea40f9b090 100644 --- a/tests/unit/agentSkills-schemas.test.ts +++ b/tests/unit/agentSkills-schemas.test.ts @@ -136,8 +136,9 @@ test("AgentSkillSchema — .parse throws on invalid input", () => { test("SkillCoverageSchema — valid coverage parses successfully", () => { const input = { api: { have: 23, total: 23 }, - cli: { have: 20, total: 20 }, - totalSkills: 42, + cli: { have: 21, total: 21 }, + config: { have: 1, total: 1 }, + totalSkills: 45, generatedAt: new Date().toISOString(), }; const result = SkillCoverageSchema.safeParse(input); @@ -147,8 +148,9 @@ test("SkillCoverageSchema — valid coverage parses successfully", () => { test("SkillCoverageSchema — wrong total literal (api.total=21) fails", () => { const input = { api: { have: 21, total: 21 }, - cli: { have: 20, total: 20 }, - totalSkills: 41, + cli: { have: 21, total: 21 }, + config: { have: 1, total: 1 }, + totalSkills: 44, generatedAt: new Date().toISOString(), }; const result = SkillCoverageSchema.safeParse(input); @@ -158,8 +160,9 @@ test("SkillCoverageSchema — wrong total literal (api.total=21) fails", () => { test("SkillCoverageSchema — wrong total literal (cli.total=19) fails", () => { const input = { api: { have: 23, total: 23 }, - cli: { have: 19, total: 19 }, - totalSkills: 41, + cli: { have: 20, total: 20 }, + config: { have: 1, total: 1 }, + totalSkills: 44, generatedAt: new Date().toISOString(), }; const result = SkillCoverageSchema.safeParse(input); @@ -169,8 +172,9 @@ test("SkillCoverageSchema — wrong total literal (cli.total=19) fails", () => { test("SkillCoverageSchema — invalid datetime fails", () => { const input = { api: { have: 23, total: 23 }, - cli: { have: 20, total: 20 }, - totalSkills: 42, + cli: { have: 21, total: 21 }, + config: { have: 1, total: 1 }, + totalSkills: 45, generatedAt: "not-a-date", }; const result = SkillCoverageSchema.safeParse(input); @@ -180,8 +184,9 @@ test("SkillCoverageSchema — invalid datetime fails", () => { test("SkillCoverageSchema — negative have value fails", () => { const input = { api: { have: -1, total: 23 }, - cli: { have: 20, total: 20 }, - totalSkills: 42, + cli: { have: 21, total: 21 }, + config: { have: 1, total: 1 }, + totalSkills: 45, generatedAt: new Date().toISOString(), }; const result = SkillCoverageSchema.safeParse(input); @@ -207,6 +212,14 @@ test("ListQuerySchema — valid category parses successfully", () => { } }); +test("ListQuerySchema — config category parses successfully", () => { + const result = ListQuerySchema.safeParse({ category: "config" }); + assert.equal(result.success, true); + if (result.success) { + assert.equal(result.data.category, "config"); + } +}); + test("ListQuerySchema — invalid category fails", () => { const result = ListQuerySchema.safeParse({ category: "invalid" }); assert.equal(result.success, false); diff --git a/tests/unit/check-docs-counts-sync.test.ts b/tests/unit/check-docs-counts-sync.test.ts index 27deae3057..0bcff40e7d 100644 --- a/tests/unit/check-docs-counts-sync.test.ts +++ b/tests/unit/check-docs-counts-sync.test.ts @@ -8,6 +8,9 @@ import { tallyDrift, readProviderTotal, countLocales, + readMcpFactsFromSource, + listLocalizedDocs, + makeRequiredCountsValidator, } from "../../scripts/check/check-docs-counts-sync.mjs"; // Explicit types for the .mjs exports — keep the test at 0 no-explicit-any warnings. @@ -24,6 +27,11 @@ const tally = tallyDrift as ( ) => { strict: number; soft: number; lines: string[] }; const readTotal = readProviderTotal as () => number; const locales = countLocales as () => number; +const mcpFacts = readMcpFactsFromSource as () => { tools: number; scopes: number } | null; +const localizedDocs = listLocalizedDocs as (relativePath: string) => string[]; +const requireCounts = makeRequiredCountsValidator as ( + requirements: { label: string; value: number }[] +) => (content: string) => { ok: boolean; detail: string }; const here = path.dirname(fileURLToPath(import.meta.url)); const GATE = path.resolve(here, "../../scripts/check/check-docs-counts-sync.mjs"); @@ -85,13 +93,38 @@ test("a missing file (null content) registers drift, not a crash", () => { // --- live source readers (smoke) ----------------------------------------------------- test("readProviderTotal reads a real, positive total from the catalog", () => { - assert.ok(readTotal() > 100, "provider catalog total should be > 100"); + assert.ok(readTotal() >= 300, "live provider catalog total should be at least 300"); }); test("countLocales reads a real, positive locale count from config/i18n.json", () => { assert.ok(locales() >= 40, "i18n config should define at least 40 locales"); }); +test("source-only MCP fallback matches the canonical inventory and scope union", () => { + assert.deepEqual(mcpFacts(), { tools: 107, scopes: 32 }); +}); + +test("localized-doc discovery returns every locale root document", () => { + const readmes = localizedDocs("README.md"); + assert.equal(readmes.length, 42); + assert.ok(readmes.includes("docs/i18n/pt-BR/README.md")); + assert.ok(readmes.includes("docs/i18n/zh-CN/README.md")); +}); + +test("required-count validator reports precisely which live markers are missing", () => { + const validate = requireCounts([ + { label: "providers", value: 327 }, + { label: "MCP tools", value: 107 }, + { label: "MCP scopes", value: 32 }, + ]); + assert.equal(validate("327 providers; 107 tools; 32 scopes").ok, true); + const stale = validate("290 providers; 104 tools; 31 scopes"); + assert.equal(stale.ok, false); + assert.match(stale.detail, /providers=327/); + assert.match(stale.detail, /MCP tools=107/); + assert.match(stale.detail, /MCP scopes=32/); +}); + // --- live gate smoke ----------------------------------------------------------------- test("the gate exits 0 against the current (synced) repo state", () => { @@ -105,6 +138,7 @@ test("the gate exits 0 against the current (synced) repo state", () => { // down to 1.37B, because no gate watched that number. import { checkFreeTierHeadline, + checkFreeTierInventory, extractHeadlineClaims, } from "../../scripts/check/check-docs-counts-sync.mjs"; @@ -146,9 +180,27 @@ test("free-tier gate passes when a file carries no headline at all", () => { assert.equal(checkHeadline("no figures here", TOTALS).ok, true); }); +const checkInventory = checkFreeTierInventory as ( + content: string, + totals: { pools: number; models: number } +) => { ok: boolean; detail: string }; + +test("free-tier inventory gate accepts the live pool/model counts", () => { + assert.equal( + checkInventory("43 provider pools / 522 model budget entries", { pools: 43, models: 522 }).ok, + true + ); +}); + +test("free-tier inventory gate rejects stale model counts", () => { + const result = checkInventory("43 provider pools / 516 models", { pools: 43, models: 522 }); + assert.equal(result.ok, false); + assert.match(result.detail, /live catalog has 43 pools \/ 522 model budget entries/); +}); + // --- Generic numeric-claim gate (engines / MCP tools / scopes / CLI) -------- // Extends the same drift guard to the counts that silently drifted in v3.8.49: -// 11→12 engines, 94→104 MCP tools, 30→31 scopes, 26→33 CLI tools. +// 11→12 engines, 94→107 MCP tools, 30→32 scopes, 26→33 CLI tools. import { makeNumberClaimValidator } from "../../scripts/check/check-docs-counts-sync.mjs"; const makeValidator = makeNumberClaimValidator as ( @@ -157,19 +209,19 @@ const makeValidator = makeNumberClaimValidator as ( ) => (content: string) => { ok: boolean; detail: string }; test("MCP-tools gate accepts the aggregate and rejects a stale one", () => { - const v = makeValidator(104, { + const v = makeValidator(107, { what: "MCP tools", pattern: /(\d+) tools/gi, skipBefore: /(tools?|definitions?)\s*\(\s*$/i, skipAfter: /^\s*\(\d+ CLI/, }); - assert.equal(v("MCP Server (104 tools)").ok, true); - assert.equal(v("with 104 tools total").ok, true); + assert.equal(v("MCP Server (107 tools)").ok, true); + assert.equal(v("with 107 tools total").ok, true); assert.equal(v("MCP Server (94 tools)").ok, false); }); test("MCP-tools gate ignores per-module counts and the CLI catalog total", () => { - const v = makeValidator(104, { + const v = makeValidator(107, { what: "MCP tools", pattern: /(\d+) tools/gi, skipBefore: /(tools?|definitions?)\s*\(\s*$/i, diff --git a/tests/unit/listCapabilities-a2a.test.ts b/tests/unit/listCapabilities-a2a.test.ts index 60e636b163..64b1968653 100644 --- a/tests/unit/listCapabilities-a2a.test.ts +++ b/tests/unit/listCapabilities-a2a.test.ts @@ -3,7 +3,7 @@ * * Verifies: * - Return shape matches §3.7 contract - * - Markdown table contains all 44 skill IDs + * - Markdown table contains all 45 skill IDs * - Coverage bounds are within declared totals * - metadata.source === "agent-skills-catalog" * - metadata.generatedAt is an ISO datetime string @@ -13,7 +13,11 @@ import assert from "node:assert/strict"; import { test } from "node:test"; import type { A2ATask } from "../../src/lib/a2a/taskManager.js"; import { executeListCapabilities } from "../../src/lib/a2a/skills/listCapabilities.js"; -import { API_SKILL_IDS, CLI_SKILL_IDS } from "../../src/lib/agentSkills/catalog.js"; +import { + API_SKILL_IDS, + CLI_SKILL_IDS, + CONFIG_SKILL_IDS, +} from "../../src/lib/agentSkills/catalog.js"; // Minimal stub — executeListCapabilities only receives the task arg but does not use it const stubTask = {} as A2ATask; @@ -31,20 +35,21 @@ test("executeListCapabilities returns shape matching §3.7 contract", async () = const { metadata } = result; assert.ok(metadata, "metadata exists"); assert.equal(metadata.source, "agent-skills-catalog", "metadata.source matches"); - assert.equal(metadata.totalSkills, 45, "metadata.totalSkills === 45 (44 + config)"); + assert.equal(metadata.totalSkills, 45, "metadata.totalSkills === 45"); assert.ok(metadata.coverage, "metadata.coverage exists"); assert.ok(metadata.coverage.api, "metadata.coverage.api exists"); assert.ok(metadata.coverage.cli, "metadata.coverage.cli exists"); assert.equal(metadata.coverage.api.total, 23, "api.total === 23"); - assert.equal(metadata.coverage.cli.total, 20, "cli.total === 20"); + assert.equal(metadata.coverage.cli.total, 21, "cli.total === 21"); + assert.equal(metadata.coverage.config.total, 1, "config.total === 1"); }); -test("executeListCapabilities markdown table contains all 44 API+CLI skill IDs", async () => { +test("executeListCapabilities markdown table contains all 45 skill IDs", async () => { const result = await executeListCapabilities(stubTask); const content = result.artifacts[0].content; - const allIds = [...API_SKILL_IDS, ...CLI_SKILL_IDS] as string[]; - assert.equal(allIds.length, 44, "API+CLI catalog declares 44 skill IDs"); + const allIds = [...API_SKILL_IDS, ...CLI_SKILL_IDS, ...CONFIG_SKILL_IDS] as string[]; + assert.equal(allIds.length, 45, "catalog declares 45 skill IDs"); for (const id of allIds) { assert.ok(content.includes(id), `Markdown table missing skill ID: ${id}`); @@ -65,6 +70,13 @@ test("metadata.coverage.cli.have is within [0, 21]", async () => { assert.ok(cli.have <= 21, "cli.have <= 21"); }); +test("metadata.coverage.config.have is within [0, 1]", async () => { + const result = await executeListCapabilities(stubTask); + const { config } = result.metadata.coverage; + assert.ok(config.have >= 0, "config.have >= 0"); + assert.ok(config.have <= 1, "config.have <= 1"); +}); + test("metadata.generatedAt is a valid ISO datetime", async () => { const result = await executeListCapabilities(stubTask); const { generatedAt } = result.metadata;