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:
tiennm99 committed 2026-09-11 15:40:18 +07:00
1 parent e49c487700
commit eb12bcc2cb
16 files changed
+462 -205

No files matched your search

+33 -21
View File
@@ -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 -3
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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 `—`.
+2 -2
View File
@@ -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
View File
@@ -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) {
-2
View File
@@ -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
View File
@@ -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 {
+19 -15
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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)
}
}