mirror of
https://github.com/tiennm99/awesome-ai-dev-tools.git
synced 2026-10-11 03:13:05 +00:00
feat: replace category with a multi-facet tag vocabulary
A single category could not describe tools that ship as a CLI, an editor plugin and a desktop app at once, and 7 of the 40 entries were filed under a surface they only partly match — cline is "an SDK, IDE extension or CLI assistant" in one `extension` slot, Reasonix ships CLI, desktop and VS Code under `cli`, warp is a terminal filed as `ide`. Tags cover five facets: surface (at least one), model access, workflow, integration, and origin (at most one). tagVocabulary in validate.go is the single source of truth — the validator, its error messages, and the dashboard's filter chips all derive from it, the last via a new facets field in site/data.json. The dashboard now filters on tags with multi-select chips grouped by facet: OR within a facet, AND across facets, plus a clear-filters control, and search matches tags as well as name and description. Only tags some row actually carries get a chip, so chips and rows cannot disagree. Tag pills are tinted per facet, replacing the category badge. All 40 entries are tagged from each repo's own README, topics and docs; CONTRIBUTING documents every tag and the evidence rule for applying one. A leftover `category:` key now fails validation with a message naming its replacement, rather than being silently ignored. README.md and data/history.jsonl are the regenerated updater output.
This commit is contained in:
1 parent
e49c487700
commit
eb12bcc2cb
16 files changed
+462
-205
No files matched your search
@@ -5,25 +5,25 @@
|
||||
|
||||
📊 **[Interactive dashboard with star-history charts →](https://tiennm99.github.io/awesome-coding-agents/)**
|
||||
|
||||
**Last updated:** 2026-09-11 07:37 UTC · **Tracked:** 40 repos
|
||||
**Top 7-day mover:** [anomalyco/opencode](https://github.com/anomalyco/opencode) (+2981 stars)
|
||||
**Last updated:** 2026-09-11 08:40 UTC · **Tracked:** 40 repos
|
||||
**Top 7-day mover:** [anomalyco/opencode](https://github.com/anomalyco/opencode) (+2992 stars)
|
||||
|
||||
| # | Repo | Stars | Δ7d | Language | Last push | Description |
|
||||
|---|------|------:|----:|----------|-----------|-------------|
|
||||
| 1 | [deepseek-ai/deepseek-harness](https://github.com/deepseek-ai/deepseek-harness) | 219.7k | — | TypeScript | 2026-09-11 | DeepSeek Harness: Everything is a Plugin. |
|
||||
| 2 | [anomalyco/opencode](https://github.com/anomalyco/opencode) | 206.6k | +2981 | TypeScript | 2026-09-11 | The open source coding agent. |
|
||||
| 3 | [anthropics/claude-code](https://github.com/anthropics/claude-code) | 144.7k | +722 | Python | 2026-09-10 | Claude Code is an agentic coding tool that lives in your terminal, understands your codebase, and helps you code faster by executing routine tasks, explaining complex code, and handling git workflows - all through natural language commands. |
|
||||
| 4 | [openai/codex](https://github.com/openai/codex) | 123.2k | +1905 | Rust | 2026-09-11 | Lightweight coding agent that runs in your terminal |
|
||||
| 5 | [google-gemini/gemini-cli](https://github.com/google-gemini/gemini-cli) | 106.9k | +103 | TypeScript | 2026-09-11 | An open-source AI agent that brings the power of Gemini directly into your terminal. |
|
||||
| 6 | [earendil-works/pi](https://github.com/earendil-works/pi) | 104.0k | +2460 | TypeScript | 2026-09-10 | AI agent toolkit: unified LLM API, agent loop, TUI, coding agent CLI |
|
||||
| 7 | [zed-industries/zed](https://github.com/zed-industries/zed) | 90.1k | +353 | Rust | 2026-09-11 | Code at the speed of thought – Zed is a high-performance, multiplayer code editor from the creators of Atom and Tree-sitter. |
|
||||
| 8 | [OpenHands/OpenHands](https://github.com/OpenHands/OpenHands) | 87.3k | +1197 | TypeScript | 2026-09-11 | 🙌 OpenHands: AI-Driven Development |
|
||||
| 1 | [deepseek-ai/deepseek-harness](https://github.com/deepseek-ai/deepseek-harness) | 219.8k | — | TypeScript | 2026-09-11 | DeepSeek Harness: Everything is a Plugin. |
|
||||
| 2 | [anomalyco/opencode](https://github.com/anomalyco/opencode) | 206.6k | +2992 | TypeScript | 2026-09-11 | The open source coding agent. |
|
||||
| 3 | [anthropics/claude-code](https://github.com/anthropics/claude-code) | 144.7k | +725 | Python | 2026-09-10 | Claude Code is an agentic coding tool that lives in your terminal, understands your codebase, and helps you code faster by executing routine tasks, explaining complex code, and handling git workflows - all through natural language commands. |
|
||||
| 4 | [openai/codex](https://github.com/openai/codex) | 123.3k | +1944 | Rust | 2026-09-11 | Lightweight coding agent that runs in your terminal |
|
||||
| 5 | [google-gemini/gemini-cli](https://github.com/google-gemini/gemini-cli) | 106.9k | +102 | TypeScript | 2026-09-11 | An open-source AI agent that brings the power of Gemini directly into your terminal. |
|
||||
| 6 | [earendil-works/pi](https://github.com/earendil-works/pi) | 104.0k | +2484 | TypeScript | 2026-09-11 | AI agent toolkit: unified LLM API, agent loop, TUI, coding agent CLI |
|
||||
| 7 | [zed-industries/zed](https://github.com/zed-industries/zed) | 90.1k | +359 | Rust | 2026-09-11 | Code at the speed of thought – Zed is a high-performance, multiplayer code editor from the creators of Atom and Tree-sitter. |
|
||||
| 8 | [OpenHands/OpenHands](https://github.com/OpenHands/OpenHands) | 87.3k | +1211 | TypeScript | 2026-09-11 | 🙌 OpenHands: AI-Driven Development |
|
||||
| 9 | [openinterpreter/openinterpreter](https://github.com/openinterpreter/openinterpreter) | 68.3k | — | Rust | 2026-09-09 | A coding agent for open models like Kimi K3 and GLM 5.3 |
|
||||
| 10 | [cline/cline](https://github.com/cline/cline) | 67.8k | +380 | TypeScript | 2026-09-11 | Autonomous coding agent as an SDK, IDE extension, or CLI assistant. |
|
||||
| 10 | [cline/cline](https://github.com/cline/cline) | 67.8k | +384 | TypeScript | 2026-09-11 | Autonomous coding agent as an SDK, IDE extension, or CLI assistant. |
|
||||
| 11 | [warpdotdev/warp](https://github.com/warpdotdev/warp) | 64.9k | +160 | Rust | 2026-09-11 | Warp is an agentic development environment, born out of the terminal. |
|
||||
| 12 | [AntonOsika/gpt-engineer](https://github.com/AntonOsika/gpt-engineer) | 55.1k | -4 | Python | 2025-05-14 | CLI platform to experiment with codegen. Precursor to: https://lovable.dev |
|
||||
| 13 | [aaif-goose/goose](https://github.com/aaif-goose/goose) | 54.1k | +216 | Rust | 2026-09-11 | an open source, extensible AI agent that goes beyond code suggestions - install, execute, edit, and test with any LLM |
|
||||
| 14 | [Aider-AI/aider](https://github.com/Aider-AI/aider) | 48.9k | +173 | Python | 2026-05-22 | aider is AI pair programming in your terminal |
|
||||
| 13 | [aaif-goose/goose](https://github.com/aaif-goose/goose) | 54.1k | +219 | Rust | 2026-09-11 | an open source, extensible AI agent that goes beyond code suggestions - install, execute, edit, and test with any LLM |
|
||||
| 14 | [Aider-AI/aider](https://github.com/Aider-AI/aider) | 48.9k | +174 | Python | 2026-05-22 | aider is AI pair programming in your terminal |
|
||||
| 15 | [Hmbown/Codewhale](https://github.com/Hmbown/Codewhale) | 40.9k | — | Rust | 2026-09-11 | Open-source coding agent for your terminal, built in Rust and on a journey of continuous community improvement. Issues and PRs welcome. |
|
||||
| 16 | [continuedev/continue](https://github.com/continuedev/continue) | 35.9k | +120 | TypeScript | 2026-09-08 | open-source coding agent |
|
||||
| 17 | [esengine/DeepSeek-Reasonix](https://github.com/esengine/DeepSeek-Reasonix) | 35.5k | — | Go | 2026-09-11 | DeepSeek-native AI coding agent for your terminal. Engineered around prefix-cache stability — leave it running. |
|
||||
@@ -31,24 +31,24 @@
|
||||
| 19 | [Pythagora-io/gpt-pilot](https://github.com/Pythagora-io/gpt-pilot) | 33.7k | +4 | Python | 2026-06-18 | The first real AI developer |
|
||||
| 20 | [Gitlawb/openclaude](https://github.com/Gitlawb/openclaude) | 33.1k | — | TypeScript | 2026-09-09 | runs anywhere. uses anything |
|
||||
| 21 | [can1357/oh-my-pi](https://github.com/can1357/oh-my-pi) | 30.6k | — | TypeScript | 2026-09-11 | ⌥ Coding agent with the IDE wired in |
|
||||
| 22 | [voideditor/void](https://github.com/voideditor/void) | 28.8k | -10 | TypeScript | 2026-06-02 | |
|
||||
| 23 | [charmbracelet/crush](https://github.com/charmbracelet/crush) | 28.0k | +118 | Go | 2026-09-11 | Glamourous agentic coding for all 💘 |
|
||||
| 22 | [voideditor/void](https://github.com/voideditor/void) | 28.8k | -11 | TypeScript | 2026-06-02 | |
|
||||
| 23 | [charmbracelet/crush](https://github.com/charmbracelet/crush) | 28.0k | +122 | Go | 2026-09-11 | Glamourous agentic coding for all 💘 |
|
||||
| 24 | [QwenLM/qwen-code](https://github.com/QwenLM/qwen-code) | 27.8k | +134 | TypeScript | 2026-09-11 | An open-source AI coding agent that lives in your terminal. |
|
||||
| 25 | [Kilo-Org/kilocode](https://github.com/Kilo-Org/kilocode) | 27.3k | +90 | TypeScript | 2026-09-11 | Kilo is the all-in-one agentic engineering platform. Build, ship, and iterate faster with the most popular open source coding agent. |
|
||||
| 25 | [Kilo-Org/kilocode](https://github.com/Kilo-Org/kilocode) | 27.3k | +93 | TypeScript | 2026-09-11 | Kilo is the all-in-one agentic engineering platform. Build, ship, and iterate faster with the most popular open source coding agent. |
|
||||
| 26 | [onlook-dev/onlook](https://github.com/onlook-dev/onlook) | 26.7k | +81 | TypeScript | 2026-08-25 | The Cursor for Designers • An Open-Source AI-First Design tool • Visually build, style, and edit your React App with AI |
|
||||
| 27 | [xai-org/grok-build](https://github.com/xai-org/grok-build) | 26.7k | — | Rust | 2026-09-09 | SpaceXAI's coding agent harness and TUI. Fullscreen, mouse interactive, extensible. |
|
||||
| 28 | [RooCodeInc/Roo-Code](https://github.com/RooCodeInc/Roo-Code) | 24.3k | -9 | TypeScript | 2026-05-15 | Roo Code gives you a whole dev team of AI agents in your code editor. |
|
||||
| 29 | [alibaba/open-code-review](https://github.com/alibaba/open-code-review) | 22.2k | — | Go | 2026-09-11 | Fast, efficient, battle-tested at Alibaba's scale. Hybrid architecture code review tool: deterministic pipelines + LLM Agent, precise line-level comments, built-in multi-language ruleset (NPE, thread-safety, XSS, SQL injection), OpenAI & Anthropic compatible. |
|
||||
| 30 | [dyad-sh/dyad](https://github.com/dyad-sh/dyad) | 21.5k | +86 | TypeScript | 2026-09-10 | Local, open-source AI app builder for power users ✨ v0 / Lovable / Replit / Bolt alternative 🌟 Star if you like it! |
|
||||
| 31 | [PrimeIntellect-ai/prime-agent](https://github.com/PrimeIntellect-ai/prime-agent) | 20.5k | — | TypeScript | 2026-09-11 | A self-improving RLM agent for coding workflows and long-running autonomous tasks. |
|
||||
| 32 | [SWE-agent/SWE-agent](https://github.com/SWE-agent/SWE-agent) | 20.3k | +82 | Python | 2026-09-07 | SWE-agent takes a GitHub issue and tries to automatically fix it, using your LM of choice. It can also be employed for offensive cybersecurity or competitive coding challenges. [NeurIPS 2024] |
|
||||
| 33 | [kortix-ai/suna](https://github.com/kortix-ai/suna) | 20.2k | +28 | TypeScript | 2026-09-11 | The open-source AI Management System |
|
||||
| 32 | [SWE-agent/SWE-agent](https://github.com/SWE-agent/SWE-agent) | 20.3k | +83 | Python | 2026-09-07 | SWE-agent takes a GitHub issue and tries to automatically fix it, using your LM of choice. It can also be employed for offensive cybersecurity or competitive coding challenges. [NeurIPS 2024] |
|
||||
| 33 | [kortix-ai/suna](https://github.com/kortix-ai/suna) | 20.2k | +29 | TypeScript | 2026-09-11 | The open-source AI Management System |
|
||||
| 34 | [1jehuang/jcode](https://github.com/1jehuang/jcode) | 19.5k | — | Rust | 2026-09-11 | The most RAM efficient harness |
|
||||
| 35 | [avante-corp/avante.nvim](https://github.com/avante-corp/avante.nvim) | 18.2k | +6 | Lua | 2026-09-09 | Use your Neovim like using Cursor AI IDE! |
|
||||
| 36 | [HKUDS/DeepCode](https://github.com/HKUDS/DeepCode) | 16.5k | — | Python | 2026-09-09 | "DeepCode: Open Agentic Coding (Agent Harness & Loop Engineering & Multi-Agent Orchestration)" |
|
||||
| 37 | [CodebuffAI/freebuff](https://github.com/CodebuffAI/freebuff) | 11.9k | — | TypeScript | 2026-09-11 | The free coding agent |
|
||||
| 38 | [MoonshotAI/kimi-cli](https://github.com/MoonshotAI/kimi-cli) | 11.3k | — | Python | 2026-09-01 | Kimi Code CLI is your next CLI agent. |
|
||||
| 39 | [github/copilot-cli](https://github.com/github/copilot-cli) | 11.2k | +20 | Shell | 2026-09-10 | GitHub Copilot CLI brings the power of Copilot coding agent directly to your terminal. |
|
||||
| 39 | [github/copilot-cli](https://github.com/github/copilot-cli) | 11.2k | +21 | Shell | 2026-09-10 | GitHub Copilot CLI brings the power of Copilot coding agent directly to your terminal. |
|
||||
| 40 | [Kuberwastaken/claurst](https://github.com/Kuberwastaken/claurst) | 10.3k | — | Rust | 2026-09-02 | Agentic Coding for Builders who Ship |
|
||||
|
||||
---
|
||||
@@ -80,12 +80,24 @@ Add an agent to [`data/agents.yml`](data/agents.yml):
|
||||
agents:
|
||||
- owner: github-username-or-org
|
||||
repo: repository-name
|
||||
category: cli # cli | ide | extension | library | research | web
|
||||
tags: [terminal, byo-model, interactive, community]
|
||||
```
|
||||
|
||||
**Tags** describe a tool across five facets — the [dashboard](https://tiennm99.github.io/awesome-coding-agents/) filters on them, OR within a facet and AND across facets:
|
||||
|
||||
| Facet | Tags |
|
||||
|-------|------|
|
||||
| Surface (at least one) | `terminal` · `editor-plugin` · `ide` · `desktop` · `web` · `self-hosted` |
|
||||
| Model access | `byo-model` · `single-vendor` · `local-models` |
|
||||
| Workflow | `interactive` · `autonomous` · `review` · `app-builder` · `research` |
|
||||
| Integration | `mcp` · `acp` · `headless` |
|
||||
| Origin (at most one) | `vendor` · `community` |
|
||||
|
||||
Apply a tag only when the repo's own README, docs, or topics back it up. See [`docs/CONTRIBUTING.md`](docs/CONTRIBUTING.md) for what each tag means.
|
||||
|
||||
Open a PR. The next daily run picks it up automatically.
|
||||
|
||||
PRs are validated automatically by CI (`go run . -check`): owner/repo must be non-empty and look like a real GitHub slug, category must be one of the values above, and duplicates (case-insensitive) are rejected.
|
||||
PRs are validated automatically by CI (`go run . -check`): owner/repo must be non-empty and look like a real GitHub slug, every tag must come from the vocabulary above with at least one surface tag and at most one origin tag, and duplicates (case-insensitive) are rejected.
|
||||
|
||||
## License
|
||||
|
||||
|
||||
@@ -8,10 +8,15 @@ import (
|
||||
)
|
||||
|
||||
type Agent struct {
|
||||
Owner string `yaml:"owner"`
|
||||
Repo string `yaml:"repo"`
|
||||
Owner string `yaml:"owner"`
|
||||
Repo string `yaml:"repo"`
|
||||
Tags []string `yaml:"tags"`
|
||||
Notes string `yaml:"notes,omitempty"`
|
||||
|
||||
// Category is the retired single-select field that tags replaced. It is
|
||||
// still parsed so a stale entry fails validation with a message naming
|
||||
// the replacement, rather than being silently ignored.
|
||||
Category string `yaml:"category,omitempty"`
|
||||
Notes string `yaml:"notes,omitempty"`
|
||||
}
|
||||
|
||||
func loadAgents(path string) ([]Agent, error) {
|
||||
|
||||
+2
-2
@@ -19,7 +19,7 @@ func TestLoadAgents_ValidationErrors(t *testing.T) {
|
||||
yaml: `agents:
|
||||
- owner: org
|
||||
repo: repo1
|
||||
category: tools
|
||||
tags: [terminal, community]
|
||||
`,
|
||||
expectErr: false,
|
||||
desc: "should load valid entry",
|
||||
@@ -69,7 +69,7 @@ func TestLoadAgents_ValidationErrors(t *testing.T) {
|
||||
repo: repo1
|
||||
- owner: org2
|
||||
repo: repo2
|
||||
category: ai
|
||||
tags: [terminal, community]
|
||||
`,
|
||||
expectErr: false,
|
||||
desc: "should load multiple valid entries",
|
||||
|
||||
+46
-41
@@ -1,136 +1,141 @@
|
||||
# Curated list of AI agent coding tools.
|
||||
# Edit this file to add, remove, or recategorize entries.
|
||||
# Edit this file to add, remove, or retag entries.
|
||||
# The daily GitHub Actions run will refresh stars and regenerate README.md.
|
||||
#
|
||||
# owner/repo must be the CURRENT canonical GitHub slug. When a tracked repo is
|
||||
# renamed, update it here AND add a key migration in history.go so historical
|
||||
# star data stays attached.
|
||||
#
|
||||
# tags describe each tool across five facets — surface (at least one required),
|
||||
# model access, workflow, integration, and origin (at most one). The full
|
||||
# vocabulary and the evidence rule for applying a tag live in
|
||||
# docs/CONTRIBUTING.md; `go run . -check` rejects anything outside it.
|
||||
|
||||
agents:
|
||||
- owner: Aider-AI
|
||||
repo: aider
|
||||
category: cli
|
||||
tags: [terminal, byo-model, local-models, interactive, community]
|
||||
- owner: cline
|
||||
repo: cline
|
||||
category: extension
|
||||
tags: [editor-plugin, terminal, byo-model, local-models, interactive, mcp, community]
|
||||
- owner: continuedev
|
||||
repo: continue
|
||||
category: extension
|
||||
tags: [editor-plugin, terminal, byo-model, local-models, interactive, mcp, community]
|
||||
- owner: anomalyco
|
||||
repo: opencode
|
||||
category: cli
|
||||
tags: [terminal, desktop, byo-model, local-models, interactive, headless, mcp, community]
|
||||
- owner: charmbracelet
|
||||
repo: crush
|
||||
category: cli
|
||||
tags: [terminal, byo-model, local-models, interactive, mcp, community]
|
||||
- owner: google-gemini
|
||||
repo: gemini-cli
|
||||
category: cli
|
||||
tags: [terminal, single-vendor, interactive, headless, mcp, vendor]
|
||||
notes: Google announced a closed-source successor; OSS repo may be retired
|
||||
- owner: openai
|
||||
repo: codex
|
||||
category: cli
|
||||
tags: [terminal, editor-plugin, single-vendor, interactive, headless, mcp, vendor]
|
||||
- owner: aaif-goose
|
||||
repo: goose
|
||||
category: cli
|
||||
tags: [terminal, desktop, byo-model, local-models, interactive, mcp, acp, community]
|
||||
notes: Moved to the Agentic AI Foundation (Linux Foundation)
|
||||
- owner: AntonOsika
|
||||
repo: gpt-engineer
|
||||
category: cli
|
||||
tags: [terminal, byo-model, autonomous, community]
|
||||
notes: Archived upstream (read-only); kept for historical significance
|
||||
- owner: SWE-agent
|
||||
repo: SWE-agent
|
||||
category: research
|
||||
tags: [terminal, byo-model, autonomous, research, community]
|
||||
- owner: RooCodeInc
|
||||
repo: Roo-Code
|
||||
category: extension
|
||||
tags: [editor-plugin, byo-model, interactive, mcp, community]
|
||||
notes: Archived upstream (read-only)
|
||||
- owner: TabbyML
|
||||
repo: tabby
|
||||
category: ide
|
||||
tags: [self-hosted, editor-plugin, local-models, interactive, community]
|
||||
- owner: zed-industries
|
||||
repo: zed
|
||||
category: ide
|
||||
tags: [ide, byo-model, local-models, interactive, acp, community]
|
||||
- owner: voideditor
|
||||
repo: void
|
||||
category: ide
|
||||
tags: [ide, byo-model, local-models, interactive, community]
|
||||
notes: Archived upstream (read-only)
|
||||
- owner: anthropics
|
||||
repo: claude-code
|
||||
category: cli
|
||||
tags: [terminal, editor-plugin, single-vendor, interactive, headless, mcp, vendor]
|
||||
- owner: avante-corp
|
||||
repo: avante.nvim
|
||||
category: extension
|
||||
tags: [editor-plugin, byo-model, local-models, interactive, mcp, acp, community]
|
||||
- owner: kortix-ai
|
||||
repo: suna
|
||||
category: web
|
||||
tags: [web, self-hosted, byo-model, autonomous, mcp, community]
|
||||
- owner: earendil-works
|
||||
repo: pi
|
||||
category: cli
|
||||
tags: [terminal, byo-model, interactive, community]
|
||||
- owner: OpenHands
|
||||
repo: OpenHands
|
||||
category: web
|
||||
tags: [web, self-hosted, terminal, byo-model, autonomous, acp, community]
|
||||
- owner: warpdotdev
|
||||
repo: warp
|
||||
category: ide
|
||||
tags: [terminal, desktop, byo-model, interactive, community]
|
||||
notes: Agentic terminal; client open-sourced Apr 2026 (AGPL-3.0)
|
||||
- owner: Pythagora-io
|
||||
repo: gpt-pilot
|
||||
category: cli
|
||||
tags: [terminal, editor-plugin, byo-model, autonomous, community]
|
||||
- owner: QwenLM
|
||||
repo: qwen-code
|
||||
category: cli
|
||||
tags: [terminal, editor-plugin, desktop, byo-model, local-models, interactive, headless, mcp, acp, vendor]
|
||||
- owner: Kilo-Org
|
||||
repo: kilocode
|
||||
category: extension
|
||||
tags: [editor-plugin, terminal, byo-model, interactive, mcp, community]
|
||||
- owner: onlook-dev
|
||||
repo: onlook
|
||||
category: web
|
||||
tags: [desktop, web, byo-model, app-builder, mcp, community]
|
||||
- owner: dyad-sh
|
||||
repo: dyad
|
||||
category: web
|
||||
tags: [desktop, byo-model, local-models, app-builder, community]
|
||||
- owner: github
|
||||
repo: copilot-cli
|
||||
category: cli
|
||||
tags: [terminal, single-vendor, interactive, headless, mcp, vendor]
|
||||
- owner: deepseek-ai
|
||||
repo: deepseek-harness
|
||||
category: cli
|
||||
tags: [terminal, single-vendor, interactive, vendor]
|
||||
notes: Developer preview; compatibility-breaking changes expected
|
||||
- owner: openinterpreter
|
||||
repo: openinterpreter
|
||||
category: cli
|
||||
tags: [terminal, byo-model, local-models, interactive, mcp, acp, community]
|
||||
- owner: Hmbown
|
||||
repo: Codewhale
|
||||
category: cli
|
||||
tags: [terminal, editor-plugin, desktop, byo-model, local-models, interactive, mcp, community]
|
||||
- owner: esengine
|
||||
repo: DeepSeek-Reasonix
|
||||
category: cli
|
||||
tags: [terminal, desktop, web, editor-plugin, byo-model, interactive, autonomous, mcp, acp, community]
|
||||
- owner: Gitlawb
|
||||
repo: openclaude
|
||||
category: cli
|
||||
tags: [terminal, byo-model, local-models, interactive, headless, mcp, community]
|
||||
- owner: can1357
|
||||
repo: oh-my-pi
|
||||
category: cli
|
||||
tags: [terminal, editor-plugin, byo-model, local-models, interactive, mcp, acp, headless, community]
|
||||
- owner: xai-org
|
||||
repo: grok-build
|
||||
category: cli
|
||||
tags: [terminal, editor-plugin, single-vendor, interactive, headless, mcp, acp, vendor]
|
||||
- owner: alibaba
|
||||
repo: open-code-review
|
||||
category: cli
|
||||
tags: [terminal, byo-model, review, mcp, vendor]
|
||||
- owner: PrimeIntellect-ai
|
||||
repo: prime-agent
|
||||
category: cli
|
||||
tags: [terminal, byo-model, autonomous, headless, vendor]
|
||||
- owner: 1jehuang
|
||||
repo: jcode
|
||||
category: cli
|
||||
tags: [terminal, byo-model, local-models, interactive, headless, mcp, community]
|
||||
- owner: HKUDS
|
||||
repo: DeepCode
|
||||
category: research
|
||||
tags: [terminal, desktop, web, byo-model, local-models, autonomous, research, mcp, headless, community]
|
||||
- owner: CodebuffAI
|
||||
repo: freebuff
|
||||
category: cli
|
||||
tags: [terminal, interactive, community]
|
||||
- owner: MoonshotAI
|
||||
repo: kimi-cli
|
||||
category: cli
|
||||
tags: [terminal, editor-plugin, single-vendor, interactive, mcp, acp, vendor]
|
||||
- owner: Kuberwastaken
|
||||
repo: claurst
|
||||
category: cli
|
||||
tags: [terminal, byo-model, interactive, headless, acp, community]
|
||||
+1
-1
@@ -116,4 +116,4 @@
|
||||
{"date":"2026-09-08","stars":{"Aider-AI/aider":48823,"AntonOsika/gpt-engineer":55106,"Kilo-Org/kilocode":27218,"OpenHands/OpenHands":86718,"Pythagora-io/gpt-pilot":33679,"QwenLM/qwen-code":27704,"RooCodeInc/Roo-Code":24305,"SWE-agent/SWE-agent":20273,"TabbyML/tabby":33867,"aaif-goose/goose":54012,"anomalyco/opencode":205723,"anthropics/claude-code":144384,"avante-corp/avante.nvim":18154,"bytedance/trae-agent":12071,"charmbracelet/crush":27950,"cline/cline":67649,"continuedev/continue":35829,"dyad-sh/dyad":21395,"earendil-works/pi":102830,"github/copilot-cli":11143,"google-gemini/gemini-cli":106856,"kortix-ai/suna":20180,"onlook-dev/onlook":26658,"openai/codex":122299,"plandex-ai/plandex":15627,"stackblitz/bolt.new":16537,"voideditor/void":28808,"warpdotdev/warp":64864,"zed-industries/zed":89912}}
|
||||
{"date":"2026-09-09","stars":{"Aider-AI/aider":48834,"AntonOsika/gpt-engineer":55101,"Kilo-Org/kilocode":27228,"OpenHands/OpenHands":86949,"Pythagora-io/gpt-pilot":33680,"QwenLM/qwen-code":27713,"RooCodeInc/Roo-Code":24306,"SWE-agent/SWE-agent":20286,"TabbyML/tabby":33874,"aaif-goose/goose":54041,"anomalyco/opencode":205984,"anthropics/claude-code":144483,"avante-corp/avante.nvim":18156,"bytedance/trae-agent":12077,"charmbracelet/crush":27962,"cline/cline":67689,"continuedev/continue":35835,"dyad-sh/dyad":21418,"earendil-works/pi":103216,"github/copilot-cli":11147,"google-gemini/gemini-cli":106875,"kortix-ai/suna":20183,"onlook-dev/onlook":26675,"openai/codex":122602,"plandex-ai/plandex":15628,"stackblitz/bolt.new":16539,"voideditor/void":28807,"warpdotdev/warp":64890,"zed-industries/zed":89980}}
|
||||
{"date":"2026-09-10","stars":{"Aider-AI/aider":48862,"AntonOsika/gpt-engineer":55100,"Kilo-Org/kilocode":27237,"OpenHands/OpenHands":87134,"Pythagora-io/gpt-pilot":33678,"QwenLM/qwen-code":27733,"RooCodeInc/Roo-Code":24305,"SWE-agent/SWE-agent":20292,"TabbyML/tabby":33875,"aaif-goose/goose":54074,"anomalyco/opencode":206201,"anthropics/claude-code":144585,"avante-corp/avante.nvim":18156,"bytedance/trae-agent":12082,"charmbracelet/crush":27982,"cline/cline":67757,"continuedev/continue":35852,"dyad-sh/dyad":21443,"earendil-works/pi":103554,"github/copilot-cli":11152,"google-gemini/gemini-cli":106884,"kortix-ai/suna":20192,"onlook-dev/onlook":26697,"openai/codex":122903,"plandex-ai/plandex":15630,"stackblitz/bolt.new":16540,"voideditor/void":28801,"warpdotdev/warp":64917,"zed-industries/zed":90022}}
|
||||
{"date":"2026-09-11","stars":{"1jehuang/jcode":19470,"Aider-AI/aider":48890,"AntonOsika/gpt-engineer":55104,"CodebuffAI/freebuff":11933,"Gitlawb/openclaude":33134,"HKUDS/DeepCode":16518,"Hmbown/Codewhale":40946,"Kilo-Org/kilocode":27253,"Kuberwastaken/claurst":10300,"MoonshotAI/kimi-cli":11339,"OpenHands/OpenHands":87316,"PrimeIntellect-ai/prime-agent":20529,"Pythagora-io/gpt-pilot":33681,"QwenLM/qwen-code":27764,"RooCodeInc/Roo-Code":24303,"SWE-agent/SWE-agent":20297,"TabbyML/tabby":33874,"aaif-goose/goose":54114,"alibaba/open-code-review":22238,"anomalyco/opencode":206578,"anthropics/claude-code":144703,"avante-corp/avante.nvim":18153,"can1357/oh-my-pi":30590,"charmbracelet/crush":28005,"cline/cline":67815,"continuedev/continue":35870,"deepseek-ai/deepseek-harness":219729,"dyad-sh/dyad":21464,"earendil-works/pi":103982,"esengine/DeepSeek-Reasonix":35497,"github/copilot-cli":11157,"google-gemini/gemini-cli":106910,"kortix-ai/suna":20197,"onlook-dev/onlook":26713,"openai/codex":123215,"openinterpreter/openinterpreter":68295,"voideditor/void":28803,"warpdotdev/warp":64946,"xai-org/grok-build":26662,"zed-industries/zed":90083}}
|
||||
{"date":"2026-09-11","stars":{"1jehuang/jcode":19471,"Aider-AI/aider":48891,"AntonOsika/gpt-engineer":55104,"CodebuffAI/freebuff":11933,"Gitlawb/openclaude":33135,"HKUDS/DeepCode":16518,"Hmbown/Codewhale":40945,"Kilo-Org/kilocode":27256,"Kuberwastaken/claurst":10300,"MoonshotAI/kimi-cli":11340,"OpenHands/OpenHands":87330,"PrimeIntellect-ai/prime-agent":20531,"Pythagora-io/gpt-pilot":33681,"QwenLM/qwen-code":27764,"RooCodeInc/Roo-Code":24303,"SWE-agent/SWE-agent":20298,"TabbyML/tabby":33874,"aaif-goose/goose":54117,"alibaba/open-code-review":22243,"anomalyco/opencode":206589,"anthropics/claude-code":144706,"avante-corp/avante.nvim":18153,"can1357/oh-my-pi":30608,"charmbracelet/crush":28009,"cline/cline":67819,"continuedev/continue":35870,"deepseek-ai/deepseek-harness":219829,"dyad-sh/dyad":21464,"earendil-works/pi":104006,"esengine/DeepSeek-Reasonix":35497,"github/copilot-cli":11158,"google-gemini/gemini-cli":106909,"kortix-ai/suna":20198,"onlook-dev/onlook":26713,"openai/codex":123254,"openinterpreter/openinterpreter":68295,"voideditor/void":28802,"warpdotdev/warp":64946,"xai-org/grok-build":26662,"zed-industries/zed":90089}}
|
||||
+57
-11
@@ -8,10 +8,10 @@ Edit [`data/agents.yml`](../data/agents.yml) and add an entry:
|
||||
agents:
|
||||
- owner: github-username-or-org
|
||||
repo: repository-name
|
||||
category: cli
|
||||
tags: [terminal, byo-model, interactive, community]
|
||||
```
|
||||
|
||||
Required fields: `owner`, `repo`, `category`
|
||||
Required fields: `owner`, `repo`, `tags`
|
||||
|
||||
Optional fields: `notes` (for clarifications or caveats)
|
||||
|
||||
@@ -21,21 +21,67 @@ Optional fields: `notes` (for clarifications or caveats)
|
||||
|-------|------|----------|-------------|
|
||||
| `owner` | string | Yes | GitHub user or organization that owns the repo |
|
||||
| `repo` | string | Yes | Repository name on GitHub |
|
||||
| `category` | string | Yes | One of the values listed below |
|
||||
| `tags` | list | Yes | Tags from the vocabulary below; at least one surface tag, at most one origin tag |
|
||||
| `notes` | string | No | Additional context or disclaimers |
|
||||
|
||||
## Valid Categories
|
||||
## Tag Vocabulary
|
||||
|
||||
- **cli** — Command-line tools and CLI wrappers
|
||||
- **ide** — Standalone editors and IDEs
|
||||
- **extension** — Editor extensions (VS Code, Neovim, etc.)
|
||||
- **library** — Libraries and SDKs
|
||||
- **research** — Research papers and proof-of-concept projects
|
||||
- **web** — Web-based tools and online IDEs
|
||||
Tags replaced the old single-select `category` field, because one slot cannot
|
||||
describe a tool that ships as a CLI, an editor plugin and a desktop app at the
|
||||
same time — which most of them now do. An entry carries several tags across
|
||||
five facets. `data/agents.yml` is the source of truth; the vocabulary itself
|
||||
lives in `tagVocabulary` (`validate.go`) and reaches the dashboard through
|
||||
`site/data.json`, so it is defined exactly once.
|
||||
|
||||
**Surface** — where you run it. At least one required.
|
||||
|
||||
- **terminal** — a CLI or TUI you run in a shell
|
||||
- **editor-plugin** — extension for an existing editor (VS Code, JetBrains, Neovim)
|
||||
- **ide** — a standalone editor or IDE
|
||||
- **desktop** — a native or Electron/Tauri desktop app
|
||||
- **web** — runs in a browser, hosted or local
|
||||
- **self-hosted** — a server you deploy, with clients or editor plugins on top
|
||||
|
||||
**Model access** — which models it can drive.
|
||||
|
||||
- **byo-model** — bring your own: multiple providers, OpenAI-compatible endpoints, or OpenRouter
|
||||
- **single-vendor** — built for one lab's models
|
||||
- **local-models** — runs against local inference (Ollama, llama.cpp, vLLM, SGLang)
|
||||
|
||||
**Workflow** — how you work with it.
|
||||
|
||||
- **interactive** — conversational pair programming, you stay in the loop
|
||||
- **autonomous** — takes a goal or issue and runs long stretches unattended
|
||||
- **review** — reviews diffs or existing code rather than writing it
|
||||
- **app-builder** — prompt-to-app scaffolding, with preview and deploy
|
||||
- **research** — published as a research artifact or proof of concept
|
||||
|
||||
**Integration** — what it plugs into.
|
||||
|
||||
- **mcp** — speaks Model Context Protocol
|
||||
- **acp** — speaks Agent Client Protocol
|
||||
- **headless** — a non-interactive mode for scripting or CI
|
||||
|
||||
**Origin** — who publishes it. At most one.
|
||||
|
||||
- **vendor** — first-party tool from a model lab (Anthropic, OpenAI, Google, xAI, DeepSeek, Alibaba, Moonshot, …)
|
||||
- **community** — everyone else
|
||||
|
||||
### The evidence rule
|
||||
|
||||
Apply a tag only when the repo's own README, docs, or GitHub topics support it.
|
||||
Do not tag from reputation or from a blog post. Under-tagging is better than a
|
||||
wrong tag: a missing tag hides a row from one filter, a wrong one sends someone
|
||||
to a tool that cannot do what they need.
|
||||
|
||||
Deliberately **not** tags: model names (`gpt-4`, `sonnet`, `r1`) because they
|
||||
churn within months, and implementation stacks (`rust`, `nextjs`) because they
|
||||
say nothing about choosing the tool. GitHub topics are a drafting aid only —
|
||||
12 of the tracked repos have no topics at all, including several in the top ten.
|
||||
|
||||
## Handling Duplicates and Changes
|
||||
|
||||
**Duplicate repos:** CI rejects them. `go run . -check` fails on a case-insensitive `owner/repo` match, so a duplicate never reaches a daily run.
|
||||
**Duplicate repos:** CI rejects them. `go run . -check` fails on a case-insensitive `owner/repo` match, so a duplicate never reaches a daily run. It also rejects unknown tags, a repeated tag, an entry with no surface tag, two origin tags, and any leftover `category:` key.
|
||||
|
||||
**Renamed repos:** GitHub redirects the old slug, so the updater keeps working — it prints a `::warning::` naming the new slug. Update `owner`/`repo` in `data/agents.yml` to the new slug and add the old key to `canonicalKeyMigrations` in `history.go`, or the repo's star history detaches and its deltas show `—`.
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ type Stat struct {
|
||||
CanonicalKey string
|
||||
Owner string
|
||||
Repo string
|
||||
Category string
|
||||
Tags []string
|
||||
Notes string
|
||||
Description string
|
||||
Stars int
|
||||
@@ -137,7 +137,7 @@ func fetchStats(token string, agents []Agent) ([]Stat, error) {
|
||||
CanonicalKey: canonicalKey,
|
||||
Owner: a.Owner,
|
||||
Repo: a.Repo,
|
||||
Category: a.Category,
|
||||
Tags: a.Tags,
|
||||
Notes: a.Notes,
|
||||
Description: node.Description,
|
||||
Stars: node.StargazerCount,
|
||||
|
||||
+12
-12
@@ -49,7 +49,7 @@ func TestFetchStats_ChunkingHappyPath(t *testing.T) {
|
||||
const n = 55
|
||||
agents := make([]Agent, n)
|
||||
for i := range agents {
|
||||
agents[i] = Agent{Owner: "org", Repo: fmt.Sprintf("repo%02d", i), Category: "cli"}
|
||||
agents[i] = Agent{Owner: "org", Repo: fmt.Sprintf("repo%02d", i), Tags: []string{"terminal", "community"}}
|
||||
}
|
||||
|
||||
var requestCount int
|
||||
@@ -118,9 +118,9 @@ func TestFetchStats_ChunkingHappyPath(t *testing.T) {
|
||||
|
||||
func TestFetchStats_MissingNodeNamesTheRepo(t *testing.T) {
|
||||
agents := []Agent{
|
||||
{Owner: "foo", Repo: "bar0", Category: "cli"},
|
||||
{Owner: "foo", Repo: "bar1", Category: "cli"},
|
||||
{Owner: "foo", Repo: "bar2", Category: "cli"},
|
||||
{Owner: "foo", Repo: "bar0", Tags: []string{"terminal", "community"}},
|
||||
{Owner: "foo", Repo: "bar1", Tags: []string{"terminal", "community"}},
|
||||
{Owner: "foo", Repo: "bar2", Tags: []string{"terminal", "community"}},
|
||||
}
|
||||
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -155,9 +155,9 @@ func TestFetchStats_MissingNodeNamesTheRepo(t *testing.T) {
|
||||
|
||||
func TestFetchStats_GraphQLErrorNamesTheRepo(t *testing.T) {
|
||||
agents := []Agent{
|
||||
{Owner: "foo", Repo: "bar0", Category: "cli"},
|
||||
{Owner: "foo", Repo: "bar1", Category: "cli"},
|
||||
{Owner: "foo", Repo: "bar2", Category: "cli"},
|
||||
{Owner: "foo", Repo: "bar0", Tags: []string{"terminal", "community"}},
|
||||
{Owner: "foo", Repo: "bar1", Tags: []string{"terminal", "community"}},
|
||||
{Owner: "foo", Repo: "bar2", Tags: []string{"terminal", "community"}},
|
||||
}
|
||||
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -193,8 +193,8 @@ func TestFetchStats_DriftWarnings(t *testing.T) {
|
||||
// still succeeds and reports the drift as a warning (checked via
|
||||
// captured stdout) rather than failing the whole daily update.
|
||||
agents := []Agent{
|
||||
{Owner: "old-owner", Repo: "renamed-repo", Category: "cli"},
|
||||
{Owner: "org", Repo: "archived-repo", Category: "cli"},
|
||||
{Owner: "old-owner", Repo: "renamed-repo", Tags: []string{"terminal", "community"}},
|
||||
{Owner: "org", Repo: "archived-repo", Tags: []string{"terminal", "community"}},
|
||||
}
|
||||
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -261,9 +261,9 @@ func TestFetchStats_StaleWarning(t *testing.T) {
|
||||
defer func() { timeNow = origNow }()
|
||||
|
||||
agents := []Agent{
|
||||
{Owner: "org", Repo: "fresh", Category: "cli"},
|
||||
{Owner: "org", Repo: "stale", Category: "cli"},
|
||||
{Owner: "org", Repo: "stale-and-archived", Category: "cli"},
|
||||
{Owner: "org", Repo: "fresh", Tags: []string{"terminal", "community"}},
|
||||
{Owner: "org", Repo: "stale", Tags: []string{"terminal", "community"}},
|
||||
{Owner: "org", Repo: "stale-and-archived", Tags: []string{"terminal", "community"}},
|
||||
}
|
||||
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
@@ -18,7 +18,6 @@ type Row struct {
|
||||
Language string
|
||||
PushedAt string
|
||||
Description string
|
||||
Category string
|
||||
}
|
||||
|
||||
// TopMover is the biggest 7-day gainer, surfaced as a one-line callout above
|
||||
@@ -47,7 +46,6 @@ func renderReadme(tmplPath, outPath string, stats []Stat, deltas map[string]int)
|
||||
Language: s.Language,
|
||||
PushedAt: s.PushedAt.Format("2006-01-02"),
|
||||
Description: sanitizeCell(s.Description),
|
||||
Category: s.Category,
|
||||
}
|
||||
if has && (!topMover.HasMover || delta > topMover.Delta) {
|
||||
topMover = TopMover{NameWithOwner: s.NameWithOwner, URL: s.URL, Delta: delta, HasMover: true}
|
||||
|
||||
+4
-4
@@ -130,11 +130,11 @@ func TestRenderReadme_TableRowsAndCallout(t *testing.T) {
|
||||
|
||||
stats := []Stat{
|
||||
{CanonicalKey: "org/big", NameWithOwner: "org/big", URL: "https://github.com/org/big",
|
||||
Stars: 1_500_000, Language: "Go", PushedAt: fixed, Description: "huge | agent", Category: "cli"},
|
||||
Stars: 1_500_000, Language: "Go", PushedAt: fixed, Description: "huge | agent", Tags: []string{"terminal", "community"}},
|
||||
{CanonicalKey: "org/mid", NameWithOwner: "org/mid", URL: "https://github.com/org/mid",
|
||||
Stars: 1234, Language: "Rust", PushedAt: fixed, Description: "mid agent", Category: "cli"},
|
||||
Stars: 1234, Language: "Rust", PushedAt: fixed, Description: "mid agent", Tags: []string{"terminal", "community"}},
|
||||
{CanonicalKey: "org/small", NameWithOwner: "org/small", URL: "https://github.com/org/small",
|
||||
Stars: 999, Language: "", PushedAt: fixed, Description: "small agent", Category: "cli"},
|
||||
Stars: 999, Language: "", PushedAt: fixed, Description: "small agent", Tags: []string{"terminal", "community"}},
|
||||
}
|
||||
// org/small has no delta: its row must show the em dash, and it must not
|
||||
// win the top-mover callout.
|
||||
@@ -171,7 +171,7 @@ func TestRenderReadme_NoDeltasOmitsTopMover(t *testing.T) {
|
||||
defer func() { timeNow = orig }()
|
||||
|
||||
stats := []Stat{{CanonicalKey: "org/repo", NameWithOwner: "org/repo",
|
||||
URL: "https://github.com/org/repo", Stars: 10, PushedAt: fixed, Category: "cli"}}
|
||||
URL: "https://github.com/org/repo", Stars: 10, PushedAt: fixed, Tags: []string{"terminal", "community"}}}
|
||||
|
||||
out := t.TempDir() + "/README.md"
|
||||
if err := renderReadme("templates/readme.tmpl", out, stats, map[string]int{}); err != nil {
|
||||
|
||||
@@ -7,26 +7,29 @@ import (
|
||||
|
||||
// siteRow is one ranked repo in site/data.json, consumed by site/index.html.
|
||||
type siteRow struct {
|
||||
Key string `json:"key"` // canonical owner/repo, matches history keys
|
||||
NameWithOwner string `json:"nameWithOwner"`
|
||||
URL string `json:"url"`
|
||||
Stars int `json:"stars"`
|
||||
Delta7d int `json:"delta7d"`
|
||||
HasDelta bool `json:"hasDelta"`
|
||||
Delta30d int `json:"delta30d"`
|
||||
HasDelta30 bool `json:"hasDelta30"`
|
||||
Language string `json:"language"`
|
||||
PushedAt string `json:"pushedAt"`
|
||||
Description string `json:"description"`
|
||||
Category string `json:"category"`
|
||||
Notes string `json:"notes,omitempty"`
|
||||
Archived bool `json:"archived"`
|
||||
Key string `json:"key"` // canonical owner/repo, matches history keys
|
||||
NameWithOwner string `json:"nameWithOwner"`
|
||||
URL string `json:"url"`
|
||||
Stars int `json:"stars"`
|
||||
Delta7d int `json:"delta7d"`
|
||||
HasDelta bool `json:"hasDelta"`
|
||||
Delta30d int `json:"delta30d"`
|
||||
HasDelta30 bool `json:"hasDelta30"`
|
||||
Language string `json:"language"`
|
||||
PushedAt string `json:"pushedAt"`
|
||||
Description string `json:"description"`
|
||||
Tags []string `json:"tags"`
|
||||
Notes string `json:"notes,omitempty"`
|
||||
Archived bool `json:"archived"`
|
||||
}
|
||||
|
||||
type siteData struct {
|
||||
UpdatedAt string `json:"updatedAt"`
|
||||
Rows []siteRow `json:"rows"`
|
||||
History []Snapshot `json:"history"`
|
||||
// Facets drives the dashboard's filter chips, so the tag vocabulary is
|
||||
// defined once in Go rather than duplicated in site/index.html.
|
||||
Facets []tagFacet `json:"facets"`
|
||||
}
|
||||
|
||||
// writeSiteData emits the JSON payload for the GitHub Pages dashboard.
|
||||
@@ -49,7 +52,7 @@ func writeSiteData(path string, stats []Stat, deltas7, deltas30 map[string]int,
|
||||
Language: s.Language,
|
||||
PushedAt: s.PushedAt.Format("2006-01-02"),
|
||||
Description: s.Description,
|
||||
Category: s.Category,
|
||||
Tags: s.Tags,
|
||||
Notes: s.Notes,
|
||||
Archived: s.IsArchived,
|
||||
}
|
||||
@@ -60,6 +63,7 @@ func writeSiteData(path string, stats []Stat, deltas7, deltas30 map[string]int,
|
||||
UpdatedAt: timeNow().UTC().Format("2006-01-02 15:04 UTC"),
|
||||
Rows: rows,
|
||||
History: history,
|
||||
Facets: tagVocabulary,
|
||||
})
|
||||
})
|
||||
}
|
||||
+108
-39
@@ -115,17 +115,35 @@
|
||||
a.repo { color: var(--link); text-decoration: none; font-weight: 500; white-space: nowrap; font-size: .92rem; }
|
||||
a.repo:hover { text-decoration: underline; }
|
||||
.pos { color: var(--pos); } .neg { color: var(--neg); }
|
||||
/* Category badges: tinted per category; text label carries the meaning */
|
||||
.cat {
|
||||
display: inline-block; font-size: .74rem; font-weight: 500; padding: .1rem .55rem;
|
||||
/* Tag pills: tinted per facet; the text label carries the meaning */
|
||||
td.tags { max-width: 20rem; }
|
||||
.taglist { display: flex; flex-wrap: wrap; gap: .25rem; }
|
||||
.tag {
|
||||
display: inline-block; font-size: .72rem; font-weight: 500; padding: .1rem .5rem;
|
||||
border: 1px solid var(--border); border-radius: 999px; color: var(--muted); background: var(--surface-2);
|
||||
white-space: nowrap;
|
||||
}
|
||||
.cat-cli { border-color: color-mix(in srgb, var(--primary) 45%, var(--border)); color: var(--link); background: color-mix(in srgb, var(--primary) 12%, var(--surface)); }
|
||||
.cat-ide { border-color: color-mix(in srgb, #9333ea 45%, var(--border)); background: color-mix(in srgb, #9333ea 12%, var(--surface)); }
|
||||
.cat-extension { border-color: color-mix(in srgb, #0d9488 45%, var(--border)); background: color-mix(in srgb, #0d9488 12%, var(--surface)); }
|
||||
.cat-web { border-color: color-mix(in srgb, #ea580c 45%, var(--border)); background: color-mix(in srgb, #ea580c 12%, var(--surface)); }
|
||||
.cat-research { border-color: color-mix(in srgb, #db2777 45%, var(--border)); background: color-mix(in srgb, #db2777 12%, var(--surface)); }
|
||||
.cat-ide, .cat-extension, .cat-web, .cat-research { color: var(--fg); }
|
||||
.tag-surface { border-color: color-mix(in srgb, var(--primary) 45%, var(--border)); color: var(--link); background: color-mix(in srgb, var(--primary) 12%, var(--surface)); }
|
||||
.tag-model { border-color: color-mix(in srgb, #9333ea 45%, var(--border)); background: color-mix(in srgb, #9333ea 12%, var(--surface)); }
|
||||
.tag-workflow { border-color: color-mix(in srgb, #0d9488 45%, var(--border)); background: color-mix(in srgb, #0d9488 12%, var(--surface)); }
|
||||
.tag-integration { border-color: color-mix(in srgb, #ea580c 45%, var(--border)); background: color-mix(in srgb, #ea580c 12%, var(--surface)); }
|
||||
.tag-origin { border-color: color-mix(in srgb, #db2777 45%, var(--border)); background: color-mix(in srgb, #db2777 12%, var(--surface)); }
|
||||
.tag-model, .tag-workflow, .tag-integration, .tag-origin { color: var(--fg); }
|
||||
/* Facet filter rows */
|
||||
.facets { margin: 0 0 1rem; display: grid; gap: .4rem; }
|
||||
.facet { display: flex; flex-wrap: wrap; gap: .4rem; align-items: center; }
|
||||
.facet > .flabel {
|
||||
font-size: .72rem; text-transform: uppercase; letter-spacing: .06em; color: var(--muted);
|
||||
font-weight: 600; min-width: 6.5rem;
|
||||
}
|
||||
.facet button {
|
||||
border: 1px solid var(--border); background: var(--surface); color: var(--fg);
|
||||
border-radius: 999px; padding: .22rem .7rem; cursor: pointer; font: inherit; font-size: .82rem;
|
||||
transition: background-color .15s ease, border-color .15s ease, color .15s ease;
|
||||
}
|
||||
.facet button:hover { border-color: var(--primary); }
|
||||
.facet button[aria-pressed="true"] { background: var(--primary); color: var(--primary-fg); border-color: var(--primary); }
|
||||
#clearfilters[hidden] { display: none; }
|
||||
tr.stale td { color: var(--muted); }
|
||||
.stalebadge {
|
||||
display: inline-block; font-size: .7rem; padding: 0 .45rem; margin-left: .4rem;
|
||||
@@ -188,13 +206,14 @@
|
||||
</div>
|
||||
|
||||
<div class="toolbar">
|
||||
<label class="vh" for="q">Search repositories by name or description</label>
|
||||
<label class="vh" for="q">Search repositories by name, description, or tag</label>
|
||||
<div class="searchbox">
|
||||
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" aria-hidden="true"><circle cx="11" cy="11" r="7"/><path d="m21 21-4.3-4.3"/></svg>
|
||||
<input id="q" type="search" placeholder="Search repos…" autocomplete="off">
|
||||
</div>
|
||||
<button type="button" class="btn" id="clearfilters" hidden>Clear filters</button>
|
||||
</div>
|
||||
<div class="chips" id="chips" role="group" aria-label="Filter by category"></div>
|
||||
<div class="facets" id="facets"></div>
|
||||
|
||||
<div class="tablewrap"><table id="tbl">
|
||||
<thead><tr>
|
||||
@@ -203,7 +222,7 @@
|
||||
<th scope="col" data-k="stars" class="num" tabindex="0">Stars</th>
|
||||
<th scope="col" data-k="delta7d" class="num" tabindex="0">Δ7d</th>
|
||||
<th scope="col" data-k="delta30d" class="num" tabindex="0">Δ30d</th>
|
||||
<th scope="col" data-k="category" tabindex="0">Category</th>
|
||||
<th scope="col">Tags</th>
|
||||
<th scope="col" data-k="language" tabindex="0">Language</th>
|
||||
<th scope="col" data-k="pushedAt" tabindex="0">Last push</th>
|
||||
<th scope="col">Description</th>
|
||||
@@ -218,7 +237,7 @@
|
||||
</div>
|
||||
<div class="chartwrap"><canvas id="chart" role="img"
|
||||
aria-label="Line chart of GitHub star history over time for the repositories listed in the table above."></canvas></div>
|
||||
<p class="hint">Top 12 repos shown by default — click legend entries to toggle series. The category filter and search above also filter the chart.</p>
|
||||
<p class="hint">Top 12 repos shown by default — click legend entries to toggle series. The tag filters and search above also filter the chart.</p>
|
||||
</section>
|
||||
|
||||
<footer>
|
||||
@@ -257,28 +276,74 @@
|
||||
staleCutoff.setMonth(staleCutoff.getMonth() - 6);
|
||||
const isStale = r => r.pushedAt && new Date(r.pushedAt) < staleCutoff;
|
||||
|
||||
// --- category filter chips ---
|
||||
const cats = ['all', ...new Set(rows.map(r => r.category).filter(Boolean).sort())];
|
||||
let activeCat = 'all';
|
||||
const chipBox = document.getElementById('chips');
|
||||
for (const c of cats) {
|
||||
const b = document.createElement('button');
|
||||
b.type = 'button';
|
||||
b.textContent = c;
|
||||
b.className = c === activeCat ? 'on' : '';
|
||||
b.setAttribute('aria-pressed', String(c === activeCat));
|
||||
b.onclick = () => {
|
||||
activeCat = c;
|
||||
chipBox.querySelectorAll('button').forEach(x => {
|
||||
const on = x === b;
|
||||
x.classList.toggle('on', on);
|
||||
x.setAttribute('aria-pressed', String(on));
|
||||
});
|
||||
renderTable(); renderChart();
|
||||
};
|
||||
chipBox.appendChild(b);
|
||||
// --- facet filter chips (multi-select) ---
|
||||
// Vocabulary comes from site/data.json, which the Go updater fills from
|
||||
// tagVocabulary — no second copy of the tag list lives in this file.
|
||||
const facets = Array.isArray(data.facets) ? data.facets : [];
|
||||
// Only offer a tag some row actually carries, so the rows never disagree
|
||||
// with the chips after an agents.yml edit.
|
||||
const inUse = new Set(rows.flatMap(r => r.tags || []));
|
||||
const active = new Set();
|
||||
const facetBox = document.getElementById('facets');
|
||||
const clearBtn = document.getElementById('clearfilters');
|
||||
|
||||
const syncChips = () => {
|
||||
facetBox.querySelectorAll('button[data-tag]').forEach(b => {
|
||||
b.setAttribute('aria-pressed', String(active.has(b.dataset.tag)));
|
||||
});
|
||||
clearBtn.hidden = active.size === 0;
|
||||
};
|
||||
|
||||
for (const f of facets) {
|
||||
const usable = (f.tags || []).filter(t => inUse.has(t));
|
||||
if (!usable.length) continue;
|
||||
const row = document.createElement('div');
|
||||
row.className = 'facet';
|
||||
row.setAttribute('role', 'group');
|
||||
row.setAttribute('aria-label', 'Filter by ' + f.label);
|
||||
const label = document.createElement('span');
|
||||
label.className = 'flabel';
|
||||
label.textContent = f.label;
|
||||
row.appendChild(label);
|
||||
for (const tag of usable) {
|
||||
const b = document.createElement('button');
|
||||
b.type = 'button';
|
||||
b.dataset.tag = tag;
|
||||
b.dataset.facet = f.id;
|
||||
b.textContent = tag;
|
||||
b.setAttribute('aria-pressed', 'false');
|
||||
b.onclick = () => {
|
||||
active.has(tag) ? active.delete(tag) : active.add(tag);
|
||||
syncChips(); renderTable(); renderChart();
|
||||
};
|
||||
row.appendChild(b);
|
||||
}
|
||||
facetBox.appendChild(row);
|
||||
}
|
||||
|
||||
clearBtn.onclick = () => { active.clear(); syncChips(); renderTable(); renderChart(); };
|
||||
|
||||
// Selected tags grouped by facet: a row must match at least one tag in every
|
||||
// facet that has a selection (OR inside a facet, AND across facets).
|
||||
const selectionsByFacet = () => {
|
||||
const groups = new Map();
|
||||
for (const f of facets) {
|
||||
const picked = (f.tags || []).filter(t => active.has(t));
|
||||
if (picked.length) groups.set(f.id, picked);
|
||||
}
|
||||
return groups;
|
||||
};
|
||||
|
||||
const matchesTags = r => {
|
||||
const groups = selectionsByFacet();
|
||||
if (!groups.size) return true;
|
||||
const own = new Set(r.tags || []);
|
||||
for (const picked of groups.values()) {
|
||||
if (!picked.some(t => own.has(t))) return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
// --- text search (filters table + chart) ---
|
||||
let query = '';
|
||||
const qInput = document.getElementById('q');
|
||||
@@ -292,10 +357,11 @@
|
||||
});
|
||||
|
||||
const visible = () => rows.filter(r =>
|
||||
(activeCat === 'all' || r.category === activeCat) &&
|
||||
matchesTags(r) &&
|
||||
(!query ||
|
||||
r.nameWithOwner.toLowerCase().includes(query) ||
|
||||
String(r.description || '').toLowerCase().includes(query)));
|
||||
String(r.description || '').toLowerCase().includes(query) ||
|
||||
(r.tags || []).some(t => t.includes(query))));
|
||||
|
||||
// --- sortable table ---
|
||||
let sortKey = 'rank', sortAsc = true;
|
||||
@@ -303,7 +369,7 @@
|
||||
const activate = () => {
|
||||
const k = th.dataset.k;
|
||||
if (sortKey === k) sortAsc = !sortAsc;
|
||||
else { sortKey = k; sortAsc = (k === 'rank' || k === 'nameWithOwner' || k === 'category' || k === 'language'); }
|
||||
else { sortKey = k; sortAsc = (k === 'rank' || k === 'nameWithOwner' || k === 'language'); }
|
||||
renderTable();
|
||||
};
|
||||
th.onclick = activate;
|
||||
@@ -312,8 +378,9 @@
|
||||
});
|
||||
});
|
||||
|
||||
// category → badge class; strict allowlist so third-party data can't inject class names
|
||||
const catClass = c => /^[a-z][a-z0-9-]*$/.test(c || '') ? ' cat-' + c : '';
|
||||
// tag → facet class; strict allowlist so third-party data can't inject class names
|
||||
const facetOf = new Map(facets.flatMap(f => (f.tags || []).map(t => [t, f.id])));
|
||||
const tagClass = t => /^[a-z][a-z0-9-]*$/.test(facetOf.get(t) || '') ? ' tag-' + facetOf.get(t) : '';
|
||||
|
||||
function renderTable() {
|
||||
const list = [...visible()].sort((a, b) => {
|
||||
@@ -351,7 +418,9 @@
|
||||
<td class="num">${fmtStars(r.stars)}</td>
|
||||
<td class="num ${r.hasDelta && r.delta7d > 0 ? 'pos' : r.hasDelta && r.delta7d < 0 ? 'neg' : ''}">${r.hasDelta ? (r.delta7d > 0 ? '+' : '') + r.delta7d : '—'}</td>
|
||||
<td class="num ${r.hasDelta30 && r.delta30d > 0 ? 'pos' : r.hasDelta30 && r.delta30d < 0 ? 'neg' : ''}">${r.hasDelta30 ? (r.delta30d > 0 ? '+' : '') + r.delta30d : '—'}</td>
|
||||
<td><span class="cat${catClass(r.category)}">${esc(r.category || '—')}</span></td>
|
||||
<td class="tags">${(r.tags || []).length
|
||||
? '<div class="taglist">' + r.tags.map(t => `<span class="tag${tagClass(t)}">${esc(t)}</span>`).join('') + '</div>'
|
||||
: '—'}</td>
|
||||
<td class="lang">${esc(r.language || '')}</td>
|
||||
<td>${esc(r.pushedAt)}${isStale(r) ? ' <span class="stalebadge">stale</span>' : ''}${r.archived ? ' <span class="stalebadge">archived</span>' : ''}</td>
|
||||
<td class="desc">${esc(r.description)}${r.notes ? ' · <em>' + esc(r.notes) + '</em>' : ''}</td>
|
||||
|
||||
+3
-3
@@ -18,7 +18,7 @@ func TestWriteSiteData_JSONShapeAndDeltaFields(t *testing.T) {
|
||||
CanonicalKey: "org/repo1",
|
||||
Owner: "org",
|
||||
Repo: "repo1",
|
||||
Category: "cli",
|
||||
Tags: []string{"terminal", "community"},
|
||||
Notes: "some note",
|
||||
Description: "a repo",
|
||||
Stars: 100,
|
||||
@@ -32,7 +32,7 @@ func TestWriteSiteData_JSONShapeAndDeltaFields(t *testing.T) {
|
||||
CanonicalKey: "org/repo2",
|
||||
Owner: "org",
|
||||
Repo: "repo2",
|
||||
Category: "web",
|
||||
Tags: []string{"web", "community"},
|
||||
Description: "another repo",
|
||||
Stars: 50,
|
||||
Language: "TypeScript",
|
||||
@@ -115,7 +115,7 @@ func TestWriteSiteData_JSONShapeAndDeltaFields(t *testing.T) {
|
||||
if !ok {
|
||||
t.Fatalf("expected row to be an object, got %T", rows[0])
|
||||
}
|
||||
for _, key := range []string{"key", "nameWithOwner", "url", "stars", "delta7d", "hasDelta", "delta30d", "hasDelta30", "language", "pushedAt", "description", "category", "archived"} {
|
||||
for _, key := range []string{"key", "nameWithOwner", "url", "stars", "delta7d", "hasDelta", "delta30d", "hasDelta30", "language", "pushedAt", "description", "tags", "archived"} {
|
||||
if _, ok := firstRow[key]; !ok {
|
||||
t.Errorf("expected JSON field %q in row, got keys: %v", key, mapKeys(firstRow))
|
||||
}
|
||||
|
||||
+14
-2
@@ -45,12 +45,24 @@ Add an agent to [`data/agents.yml`](data/agents.yml):
|
||||
agents:
|
||||
- owner: github-username-or-org
|
||||
repo: repository-name
|
||||
category: cli # cli | ide | extension | library | research | web
|
||||
tags: [terminal, byo-model, interactive, community]
|
||||
```
|
||||
|
||||
**Tags** describe a tool across five facets — the [dashboard](https://tiennm99.github.io/awesome-coding-agents/) filters on them, OR within a facet and AND across facets:
|
||||
|
||||
| Facet | Tags |
|
||||
|-------|------|
|
||||
| Surface (at least one) | `terminal` · `editor-plugin` · `ide` · `desktop` · `web` · `self-hosted` |
|
||||
| Model access | `byo-model` · `single-vendor` · `local-models` |
|
||||
| Workflow | `interactive` · `autonomous` · `review` · `app-builder` · `research` |
|
||||
| Integration | `mcp` · `acp` · `headless` |
|
||||
| Origin (at most one) | `vendor` · `community` |
|
||||
|
||||
Apply a tag only when the repo's own README, docs, or topics back it up. See [`docs/CONTRIBUTING.md`](docs/CONTRIBUTING.md) for what each tag means.
|
||||
|
||||
Open a PR. The next daily run picks it up automatically.
|
||||
|
||||
PRs are validated automatically by CI (`go run . -check`): owner/repo must be non-empty and look like a real GitHub slug, category must be one of the values above, and duplicates (case-insensitive) are rejected.
|
||||
PRs are validated automatically by CI (`go run . -check`): owner/repo must be non-empty and look like a real GitHub slug, every tag must come from the vocabulary above with at least one surface tag and at most one origin tag, and duplicates (case-insensitive) are rejected.
|
||||
|
||||
## License
|
||||
|
||||
|
||||
+88
-14
@@ -18,15 +18,60 @@ var ownerPattern = regexp.MustCompile(`^[A-Za-z0-9]+(-[A-Za-z0-9]+)*$`)
|
||||
// underscore, hyphen.
|
||||
var repoPattern = regexp.MustCompile(`^[A-Za-z0-9._-]+$`)
|
||||
|
||||
// validCategories mirrors the category enum documented in
|
||||
// templates/readme.tmpl's Contributing section.
|
||||
var validCategories = map[string]bool{
|
||||
"cli": true,
|
||||
"ide": true,
|
||||
"extension": true,
|
||||
"library": true,
|
||||
"research": true,
|
||||
"web": true,
|
||||
// tagVocabulary is the closed tag list, ordered for display. Tags describe a
|
||||
// tool along orthogonal facets, so an entry carries several; the grouping is
|
||||
// what lets the dashboard offer OR-within-facet, AND-across-facet filters.
|
||||
//
|
||||
// Deliberately excluded: model names (gpt-4, sonnet, r1 — they churn within
|
||||
// months) and implementation stacks (rust, nextjs — they say nothing about
|
||||
// choosing the tool).
|
||||
//
|
||||
// This slice is the single source of truth: the lookup map below, the
|
||||
// validation messages, and the dashboard's filter chips (shipped in
|
||||
// site/data.json) are all derived from it.
|
||||
var tagVocabulary = []tagFacet{
|
||||
{ID: facetSurface, Label: "Surface", Tags: []string{"terminal", "editor-plugin", "ide", "desktop", "web", "self-hosted"}},
|
||||
{ID: facetModel, Label: "Model access", Tags: []string{"byo-model", "single-vendor", "local-models"}},
|
||||
{ID: facetWorkflow, Label: "Workflow", Tags: []string{"interactive", "autonomous", "review", "app-builder", "research"}},
|
||||
{ID: facetIntegration, Label: "Integration", Tags: []string{"mcp", "acp", "headless"}},
|
||||
{ID: facetOrigin, Label: "Origin", Tags: []string{"vendor", "community"}},
|
||||
}
|
||||
|
||||
// tagFacet is one group of related tags, also the shape the dashboard consumes.
|
||||
type tagFacet struct {
|
||||
ID string `json:"id"`
|
||||
Label string `json:"label"`
|
||||
Tags []string `json:"tags"`
|
||||
}
|
||||
|
||||
const (
|
||||
facetSurface = "surface"
|
||||
facetModel = "model"
|
||||
facetWorkflow = "workflow"
|
||||
facetIntegration = "integration"
|
||||
facetOrigin = "origin"
|
||||
)
|
||||
|
||||
// tagFacetIDs indexes tag -> facet ID for O(1) validation.
|
||||
var tagFacetIDs = func() map[string]string {
|
||||
m := make(map[string]string)
|
||||
for _, f := range tagVocabulary {
|
||||
for _, t := range f.Tags {
|
||||
m[t] = f.ID
|
||||
}
|
||||
}
|
||||
return m
|
||||
}()
|
||||
|
||||
// facetTags lists one facet's legal tags, for error messages that tell a
|
||||
// contributor exactly what they may write.
|
||||
func facetTags(id string) []string {
|
||||
for _, f := range tagVocabulary {
|
||||
if f.ID == id {
|
||||
return f.Tags
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// validateAgents checks data/agents.yml entries offline (no network, no
|
||||
@@ -53,12 +98,10 @@ func validateAgents(agents []Agent) []string {
|
||||
violations = append(violations, fmt.Sprintf("%s: repo %q contains characters not allowed in a GitHub repo name (allowed: letters, digits, '.', '_', '-')", ref, a.Repo))
|
||||
}
|
||||
|
||||
switch {
|
||||
case strings.TrimSpace(a.Category) == "":
|
||||
violations = append(violations, fmt.Sprintf("%s: category is required (one of: cli, ide, extension, library, research, web)", ref))
|
||||
case !validCategories[a.Category]:
|
||||
violations = append(violations, fmt.Sprintf("%s: category %q is not one of cli, ide, extension, library, research, web", ref, a.Category))
|
||||
if a.Category != "" {
|
||||
violations = append(violations, fmt.Sprintf("%s: category %q is no longer a field — replace it with tags, e.g. tags: [terminal, byo-model, interactive, community]", ref, a.Category))
|
||||
}
|
||||
violations = append(violations, validateTags(ref, a.Tags)...)
|
||||
|
||||
key := strings.ToLower(a.Owner + "/" + a.Repo)
|
||||
if first, dup := seen[key]; dup {
|
||||
@@ -92,3 +135,34 @@ func runCheck(path string) error {
|
||||
fmt.Printf("%s: %d agents valid\n", path, len(agents))
|
||||
return nil
|
||||
}
|
||||
|
||||
// validateTags checks one entry's tags against the closed vocabulary: every
|
||||
// tag known, no repeats, at least one surface tag (the dashboard groups rows
|
||||
// by where you run them), and at most one origin tag (a tool has one
|
||||
// publisher).
|
||||
func validateTags(ref string, tags []string) []string {
|
||||
var violations []string
|
||||
seen := make(map[string]bool, len(tags))
|
||||
facetCount := map[string]int{}
|
||||
|
||||
for _, tag := range tags {
|
||||
switch facet, known := tagFacetIDs[tag]; {
|
||||
case !known:
|
||||
violations = append(violations, fmt.Sprintf("%s: tag %q is not in the vocabulary (see docs/CONTRIBUTING.md)", ref, tag))
|
||||
case seen[tag]:
|
||||
violations = append(violations, fmt.Sprintf("%s: tag %q is repeated", ref, tag))
|
||||
default:
|
||||
seen[tag] = true
|
||||
facetCount[facet]++
|
||||
}
|
||||
}
|
||||
|
||||
if facetCount[facetSurface] == 0 {
|
||||
violations = append(violations, fmt.Sprintf("%s: needs at least one surface tag (one of: %s)", ref, strings.Join(facetTags(facetSurface), ", ")))
|
||||
}
|
||||
if facetCount[facetOrigin] > 1 {
|
||||
violations = append(violations, fmt.Sprintf("%s: has %d origin tags, expected at most one (%s)", ref, facetCount[facetOrigin], strings.Join(facetTags(facetOrigin), ", ")))
|
||||
}
|
||||
|
||||
return violations
|
||||
}
|
||||
+65
-33
@@ -8,9 +8,9 @@ import (
|
||||
|
||||
func TestValidateAgents_ValidEntriesNoViolations(t *testing.T) {
|
||||
agents := []Agent{
|
||||
{Owner: "aider-ai", Repo: "aider", Category: "cli"},
|
||||
{Owner: "cline", Repo: "cline", Category: "extension"},
|
||||
{Owner: "a", Repo: "b.c-d_e", Category: "web"},
|
||||
{Owner: "aider-ai", Repo: "aider", Tags: []string{"terminal", "community"}},
|
||||
{Owner: "cline", Repo: "cline", Tags: []string{"terminal", "community"}},
|
||||
{Owner: "a", Repo: "b.c-d_e", Tags: []string{"terminal", "community"}},
|
||||
}
|
||||
violations := validateAgents(agents)
|
||||
if len(violations) != 0 {
|
||||
@@ -19,7 +19,7 @@ func TestValidateAgents_ValidEntriesNoViolations(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestValidateAgents_EmptyOwner(t *testing.T) {
|
||||
violations := validateAgents([]Agent{{Owner: "", Repo: "repo1", Category: "cli"}})
|
||||
violations := validateAgents([]Agent{{Owner: "", Repo: "repo1", Tags: []string{"terminal", "community"}}})
|
||||
if !anyContains(violations, "owner is empty") {
|
||||
t.Errorf("expected 'owner is empty' violation, got %v", violations)
|
||||
}
|
||||
@@ -29,7 +29,7 @@ func TestValidateAgents_InvalidOwnerChars(t *testing.T) {
|
||||
tests := []string{"foo_bar", "-leading", "trailing-", "double--hyphen", "foo owner"}
|
||||
for _, owner := range tests {
|
||||
t.Run(owner, func(t *testing.T) {
|
||||
violations := validateAgents([]Agent{{Owner: owner, Repo: "repo1", Category: "cli"}})
|
||||
violations := validateAgents([]Agent{{Owner: owner, Repo: "repo1", Tags: []string{"terminal", "community"}}})
|
||||
if !anyContains(violations, "does not look like a valid GitHub username/org") {
|
||||
t.Errorf("owner %q: expected invalid-owner violation, got %v", owner, violations)
|
||||
}
|
||||
@@ -38,7 +38,7 @@ func TestValidateAgents_InvalidOwnerChars(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestValidateAgents_EmptyRepo(t *testing.T) {
|
||||
violations := validateAgents([]Agent{{Owner: "org", Repo: "", Category: "cli"}})
|
||||
violations := validateAgents([]Agent{{Owner: "org", Repo: "", Tags: []string{"terminal", "community"}}})
|
||||
if !anyContains(violations, "repo is empty") {
|
||||
t.Errorf("expected 'repo is empty' violation, got %v", violations)
|
||||
}
|
||||
@@ -48,7 +48,7 @@ func TestValidateAgents_InvalidRepoChars(t *testing.T) {
|
||||
tests := []string{"foo/bar", "foo bar", "foo@bar", "foo#bar"}
|
||||
for _, repo := range tests {
|
||||
t.Run(repo, func(t *testing.T) {
|
||||
violations := validateAgents([]Agent{{Owner: "org", Repo: repo, Category: "cli"}})
|
||||
violations := validateAgents([]Agent{{Owner: "org", Repo: repo, Tags: []string{"terminal", "community"}}})
|
||||
if !anyContains(violations, "characters not allowed in a GitHub repo name") {
|
||||
t.Errorf("repo %q: expected invalid-repo violation, got %v", repo, violations)
|
||||
}
|
||||
@@ -56,35 +56,67 @@ func TestValidateAgents_InvalidRepoChars(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateAgents_MissingCategory(t *testing.T) {
|
||||
violations := validateAgents([]Agent{{Owner: "org", Repo: "repo1", Category: ""}})
|
||||
if !anyContains(violations, "category is required") {
|
||||
t.Errorf("expected 'category is required' violation, got %v", violations)
|
||||
func TestValidateAgents_LegacyCategoryFieldIsRejected(t *testing.T) {
|
||||
// An entry left on the retired schema must fail loudly, not be ignored.
|
||||
violations := validateAgents([]Agent{{Owner: "org", Repo: "repo1", Category: "cli", Tags: []string{"terminal"}}})
|
||||
if !anyContains(violations, "no longer a field") {
|
||||
t.Errorf("expected a legacy-category violation, got %v", violations)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateAgents_InvalidCategory(t *testing.T) {
|
||||
violations := validateAgents([]Agent{{Owner: "org", Repo: "repo1", Category: "framework"}})
|
||||
if !anyContains(violations, "is not one of cli, ide, extension, library, research, web") {
|
||||
t.Errorf("expected invalid-category violation, got %v", violations)
|
||||
func TestValidateAgents_MissingSurfaceTag(t *testing.T) {
|
||||
violations := validateAgents([]Agent{{Owner: "org", Repo: "repo1", Tags: []string{"byo-model", "community"}}})
|
||||
if !anyContains(violations, "at least one surface tag") {
|
||||
t.Errorf("expected a missing-surface violation, got %v", violations)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateAgents_AllValidCategoriesAccepted(t *testing.T) {
|
||||
for _, cat := range []string{"cli", "ide", "extension", "library", "research", "web"} {
|
||||
t.Run(cat, func(t *testing.T) {
|
||||
violations := validateAgents([]Agent{{Owner: "org", Repo: "repo1", Category: cat}})
|
||||
if len(violations) != 0 {
|
||||
t.Errorf("category %q: expected no violations, got %v", cat, violations)
|
||||
}
|
||||
})
|
||||
func TestValidateAgents_NoTagsAtAll(t *testing.T) {
|
||||
violations := validateAgents([]Agent{{Owner: "org", Repo: "repo1"}})
|
||||
if !anyContains(violations, "at least one surface tag") {
|
||||
t.Errorf("expected a missing-surface violation, got %v", violations)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateAgents_UnknownTag(t *testing.T) {
|
||||
violations := validateAgents([]Agent{{Owner: "org", Repo: "repo1", Tags: []string{"terminal", "gpt-4"}}})
|
||||
if !anyContains(violations, `tag "gpt-4" is not in the vocabulary`) {
|
||||
t.Errorf("expected an unknown-tag violation, got %v", violations)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateAgents_RepeatedTag(t *testing.T) {
|
||||
violations := validateAgents([]Agent{{Owner: "org", Repo: "repo1", Tags: []string{"terminal", "terminal"}}})
|
||||
if !anyContains(violations, `tag "terminal" is repeated`) {
|
||||
t.Errorf("expected a repeated-tag violation, got %v", violations)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateAgents_TwoOriginTags(t *testing.T) {
|
||||
violations := validateAgents([]Agent{{Owner: "org", Repo: "repo1", Tags: []string{"terminal", "vendor", "community"}}})
|
||||
if !anyContains(violations, "origin tags, expected at most one") {
|
||||
t.Errorf("expected a multiple-origin violation, got %v", violations)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateAgents_EveryVocabularyTagAccepted(t *testing.T) {
|
||||
// Each legal tag must pass alongside a surface tag, so a typo in
|
||||
// tagFacets can't silently make a documented tag unusable.
|
||||
for tag := range tagFacetIDs {
|
||||
tags := []string{"terminal", tag}
|
||||
if tag == "terminal" {
|
||||
tags = []string{"terminal"}
|
||||
}
|
||||
if violations := validateAgents([]Agent{{Owner: "org", Repo: "repo1", Tags: tags}}); len(violations) > 0 {
|
||||
t.Errorf("tag %q: expected no violations, got %v", tag, violations)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateAgents_DuplicateCaseInsensitive(t *testing.T) {
|
||||
agents := []Agent{
|
||||
{Owner: "Foo", Repo: "Bar", Category: "cli"},
|
||||
{Owner: "foo", Repo: "bar", Category: "cli"},
|
||||
{Owner: "Foo", Repo: "Bar", Tags: []string{"terminal", "community"}},
|
||||
{Owner: "foo", Repo: "bar", Tags: []string{"terminal", "community"}},
|
||||
}
|
||||
violations := validateAgents(agents)
|
||||
if !anyContains(violations, "duplicate of entry 0") {
|
||||
@@ -94,8 +126,8 @@ func TestValidateAgents_DuplicateCaseInsensitive(t *testing.T) {
|
||||
|
||||
func TestValidateAgents_NoDuplicateForDistinctRepos(t *testing.T) {
|
||||
agents := []Agent{
|
||||
{Owner: "foo", Repo: "bar", Category: "cli"},
|
||||
{Owner: "foo", Repo: "baz", Category: "cli"},
|
||||
{Owner: "foo", Repo: "bar", Tags: []string{"terminal", "community"}},
|
||||
{Owner: "foo", Repo: "baz", Tags: []string{"terminal", "community"}},
|
||||
}
|
||||
violations := validateAgents(agents)
|
||||
if len(violations) != 0 {
|
||||
@@ -105,14 +137,14 @@ func TestValidateAgents_NoDuplicateForDistinctRepos(t *testing.T) {
|
||||
|
||||
func TestValidateAgents_CollectsAllViolationsNotJustFirst(t *testing.T) {
|
||||
agents := []Agent{
|
||||
{Owner: "", Repo: "", Category: ""},
|
||||
{Owner: "org", Repo: "repo1", Category: "not-a-category"},
|
||||
{Owner: "", Repo: "", Tags: nil},
|
||||
{Owner: "org", Repo: "repo1", Tags: []string{"terminal", "nonsense", "vendor", "community"}},
|
||||
}
|
||||
violations := validateAgents(agents)
|
||||
// entry 0: owner empty, repo empty, category empty = 3 violations.
|
||||
// entry 1: invalid category = 1 violation.
|
||||
if len(violations) != 4 {
|
||||
t.Errorf("expected 4 violations collected across both entries, got %d: %v", len(violations), violations)
|
||||
// entry 0: owner empty, repo empty, no surface tag = 3 violations.
|
||||
// entry 1: unknown tag, two origin tags = 2 violations.
|
||||
if len(violations) != 5 {
|
||||
t.Errorf("expected 5 violations collected across both entries, got %d: %v", len(violations), violations)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user