From 21ad0d47a5f20059f862c23964ff77a6469d04c7 Mon Sep 17 00:00:00 2001
From: tiennm99
Date: Wed, 16 Sep 2026 21:11:54 +0700
Subject: [PATCH 1/4] feat: admit agent development environments; add Orca and
Paseo
Criterion 1 previously excluded "workspaces for agents", which ruled out
the ADE category entirely. ADEs are the surface a developer actually works
in, the same way a coding agent is, so they now qualify on their own terms.
Libraries, SDKs, skill collections and observability-only layers stay out.
- Widen inclusion criterion 1 in templates/readme.tmpl and document the
two admitted kinds in docs/CONTRIBUTING.md.
- Add `orchestration` to the Workflow facet in validate.go so ADEs are
filterable as a distinct class on the dashboard.
- Add stablyai/orca (70.0k) and getpaseo/paseo (17.5k).
README.md is regenerated from the template by the daily run; its
Contributing section is updated here so it is not stale in the meantime.
go run . -check: 43 agents valid. go test ./...: ok.
---
README.md | 4 ++--
data/agents.yml | 8 ++++++++
docs/CONTRIBUTING.md | 18 ++++++++++++++++++
templates/readme.tmpl | 4 ++--
validate.go | 2 +-
5 files changed, 31 insertions(+), 5 deletions(-)
diff --git a/README.md b/README.md
index 4919ebb..568150f 100644
--- a/README.md
+++ b/README.md
@@ -68,7 +68,7 @@
**Inclusion criteria** — a repo belongs on this list when it is:
-1. **An AI coding agent or assistant itself** — a tool that autonomously writes, edits, or reviews code. Frameworks, wrappers, observability layers, and workspaces *for* agents are out of scope.
+1. **An AI coding agent, or an environment for running them** — either a tool that autonomously writes, edits, or reviews code, or an *agent development environment* (ADE) that runs and coordinates those agents as its primary purpose. Libraries, SDKs, prompt/skill collections, and observability-only layers remain out of scope.
2. **Notable**: roughly **10,000+ GitHub stars** (the current list floor).
3. **Open source and actively maintained** — no push in **6 months** means the entry is dropped. The daily run flags anything past **3 months** for review, so the ranking reflects tools people can actually use today.
@@ -89,7 +89,7 @@ agents:
|-------|------|
| 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` |
+| Workflow | `interactive` · `autonomous` · `review` · `app-builder` · `research` · `orchestration` |
| Integration | `mcp` · `acp` · `headless` |
| Origin (at most one) | `vendor` · `community` |
diff --git a/data/agents.yml b/data/agents.yml
index 811574b..5e586b4 100644
--- a/data/agents.yml
+++ b/data/agents.yml
@@ -143,3 +143,11 @@ agents:
repo: Orkas
tags: [desktop, byo-model, interactive, community]
notes: "Open-source, local-first desktop AI workforce coordinated by a Commander through one chat; supports local coding-agent workflows."
+ - owner: stablyai
+ repo: orca
+ tags: [desktop, terminal, byo-model, orchestration, headless, community]
+ notes: "ADE: runs Codex, Claude Code, OpenCode and Pi side-by-side, each in its own git worktree, with a mobile companion app."
+ - owner: getpaseo
+ repo: paseo
+ tags: [desktop, web, terminal, self-hosted, byo-model, orchestration, headless, community]
+ notes: "ADE: self-hosted daemon that runs Claude Code, Codex, Copilot, OpenCode and Pi on your own machines, driven from desktop, mobile, web or CLI."
diff --git a/docs/CONTRIBUTING.md b/docs/CONTRIBUTING.md
index 59c5bb5..3529c9a 100644
--- a/docs/CONTRIBUTING.md
+++ b/docs/CONTRIBUTING.md
@@ -24,6 +24,23 @@ Optional fields: `notes` (for clarifications or caveats)
| `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 |
+## Scope
+
+Two kinds of project belong here:
+
+1. **Coding agents** — they write, edit, or review code themselves.
+2. **Agent development environments (ADEs)** — their primary purpose is running and
+ coordinating those agents: parallel worktrees, session management, remote or mobile
+ control. Tag these `orchestration`.
+
+An ADE earns a row because it is the surface a developer actually works in, the same way
+a coding agent is. What stays out is anything that only *assists* agents without being a
+place you run them: libraries and SDKs, prompt or skill collections, dashboards and
+observability-only layers, and single-purpose wrappers around one agent's config.
+
+The other two criteria in the [README](../README.md#contributing) — roughly 10,000+ stars
+and active maintenance — apply to both kinds equally.
+
## Tag Vocabulary
Tags replaced the old single-select `category` field, because one slot cannot
@@ -55,6 +72,7 @@ lives in `tagVocabulary` (`validate.go`) and reaches the dashboard through
- **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
+- **orchestration** — runs and coordinates *other* coding agents as its primary purpose (an ADE), rather than editing code itself
**Integration** — what it plugs into.
diff --git a/templates/readme.tmpl b/templates/readme.tmpl
index 680b225..2b1f070 100644
--- a/templates/readme.tmpl
+++ b/templates/readme.tmpl
@@ -33,7 +33,7 @@
**Inclusion criteria** — a repo belongs on this list when it is:
-1. **An AI coding agent or assistant itself** — a tool that autonomously writes, edits, or reviews code. Frameworks, wrappers, observability layers, and workspaces *for* agents are out of scope.
+1. **An AI coding agent, or an environment for running them** — either a tool that autonomously writes, edits, or reviews code, or an *agent development environment* (ADE) that runs and coordinates those agents as its primary purpose. Libraries, SDKs, prompt/skill collections, and observability-only layers remain out of scope.
2. **Notable**: roughly **10,000+ GitHub stars** (the current list floor).
3. **Open source and actively maintained** — no push in **6 months** means the entry is dropped. The daily run flags anything past **3 months** for review, so the ranking reflects tools people can actually use today.
@@ -54,7 +54,7 @@ agents:
|-------|------|
| 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` |
+| Workflow | `interactive` · `autonomous` · `review` · `app-builder` · `research` · `orchestration` |
| Integration | `mcp` · `acp` · `headless` |
| Origin (at most one) | `vendor` · `community` |
diff --git a/validate.go b/validate.go
index f25cf74..caf5fe3 100644
--- a/validate.go
+++ b/validate.go
@@ -32,7 +32,7 @@ var repoPattern = regexp.MustCompile(`^[A-Za-z0-9._-]+$`)
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: facetWorkflow, Label: "Workflow", Tags: []string{"interactive", "autonomous", "review", "app-builder", "research", "orchestration"}},
{ID: facetIntegration, Label: "Integration", Tags: []string{"mcp", "acp", "headless"}},
{ID: facetOrigin, Label: "Origin", Tags: []string{"vendor", "community"}},
}
From c5d8561d8f783c9a65cccc5bcfe85b552ff058af Mon Sep 17 00:00:00 2001
From: tiennm99
Date: Wed, 16 Sep 2026 21:21:12 +0700
Subject: [PATCH 2/4] feat: widen scope to AI developer tools; enforce hard
1,000-star floor
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
The list was already drifting past "coding agents" — ADEs were admitted in
the previous commit. Rather than keep widening criterion 1 one category at a
time, it now describes the actual subject: developer tools built around AI.
The dividing line becomes tools you use vs. building blocks you import, which
keeps libraries, SDKs, model weights and skill collections out.
The star floor drops from a soft "roughly 10,000+" to a hard 1,000:
- enforceStarFloor drops any below-floor entry from the ranking and emits an
::error:: annotation, so the published list can never violate the rule.
Dropping rather than failing keeps one bad entry from blocking the refresh
of every other repo.
- -check cannot catch this (star counts need the API, -check runs offline);
docs/CONTRIBUTING.md says so explicitly.
Side effect: Orkas (1,998 stars) now clears the floor it previously missed.
go run . -check: 43 agents valid. go test ./...: ok.
---
README.md | 6 +++---
docs/CONTRIBUTING.md | 37 ++++++++++++++++++++++++++-----------
github.go | 23 +++++++++++++++++++++++
github_test.go | 21 +++++++++++++++++++++
main.go | 5 +++++
templates/readme.tmpl | 6 +++---
6 files changed, 81 insertions(+), 17 deletions(-)
diff --git a/README.md b/README.md
index 568150f..5ad65a3 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,6 @@
# Awesome Coding Agents
-> Curated ranking of AI agent coding tools, sorted by GitHub stars.
+> Curated ranking of AI-powered developer tools, sorted by GitHub stars.
> Updated daily by GitHub Actions.
📊 **[Interactive dashboard with star-history charts →](https://tiennm99.github.io/awesome-coding-agents/)**
@@ -68,8 +68,8 @@
**Inclusion criteria** — a repo belongs on this list when it is:
-1. **An AI coding agent, or an environment for running them** — either a tool that autonomously writes, edits, or reviews code, or an *agent development environment* (ADE) that runs and coordinates those agents as its primary purpose. Libraries, SDKs, prompt/skill collections, and observability-only layers remain out of scope.
-2. **Notable**: roughly **10,000+ GitHub stars** (the current list floor).
+1. **A developer tool built around AI** — something a developer uses to build software, where an LLM is central to what it does. That covers agents that write, edit, or review code; agent development environments that run and coordinate them; and AI-assisted editors, terminals, and review tools. Libraries, SDKs, model weights, and prompt or skill collections are out of scope — this list ranks tools you *use*, not building blocks you *import*.
+2. **At least 1,000 GitHub stars** — a hard floor, enforced by the updater. An entry below it is dropped from the ranking automatically, so the threshold is never waived for an individual entry.
3. **Open source and actively maintained** — no push in **6 months** means the entry is dropped. The daily run flags anything past **3 months** for review, so the ranking reflects tools people can actually use today.
Archived or abandoned repos are kept only when they are historically significant, and are marked as such in [`data/agents.yml`](data/agents.yml).
diff --git a/docs/CONTRIBUTING.md b/docs/CONTRIBUTING.md
index 3529c9a..de079f8 100644
--- a/docs/CONTRIBUTING.md
+++ b/docs/CONTRIBUTING.md
@@ -26,20 +26,35 @@ Optional fields: `notes` (for clarifications or caveats)
## Scope
-Two kinds of project belong here:
+This list ranks **developer tools built around AI** — things a developer uses to
+build software, where an LLM is central to what the tool does. In practice that
+spans several shapes:
-1. **Coding agents** — they write, edit, or review code themselves.
-2. **Agent development environments (ADEs)** — their primary purpose is running and
- coordinating those agents: parallel worktrees, session management, remote or mobile
- control. Tag these `orchestration`.
+- **Coding agents** — they write, edit, or review code themselves.
+- **Agent development environments (ADEs)** — their primary purpose is running and
+ coordinating those agents: parallel worktrees, session management, remote or
+ mobile control. Tag these `orchestration`.
+- **AI-assisted editors, terminals, and review tools** — the developer stays in the
+ driver's seat and the model accelerates the work.
-An ADE earns a row because it is the surface a developer actually works in, the same way
-a coding agent is. What stays out is anything that only *assists* agents without being a
-place you run them: libraries and SDKs, prompt or skill collections, dashboards and
-observability-only layers, and single-purpose wrappers around one agent's config.
+The dividing line is **tools you use vs. building blocks you import**. Out of scope:
+libraries and SDKs, agent frameworks meant to be built on, model weights, prompt or
+skill collections, and dashboards that only observe a tool without being one.
-The other two criteria in the [README](../README.md#contributing) — roughly 10,000+ stars
-and active maintenance — apply to both kinds equally.
+A tool also has to be *about* software development. General-purpose assistants and
+chat UIs do not qualify just because a developer could use them.
+
+### The star floor is hard
+
+**1,000 stars minimum.** This is not a judgement call and is not waived for
+individual entries, however good the tool is. The updater enforces it: any entry
+below the floor is dropped from the ranking and reported as an `::error::` in the
+Actions log (`enforceStarFloor` in `github.go`). Because star counts require the
+API, `go run . -check` cannot catch this offline — a below-floor entry passes CI
+and is then dropped by the next daily run, so check the count before opening a PR.
+
+Maintenance requirements apply to every entry equally: no push in 6 months means
+removal, and the daily run warns past 3 months.
## Tag Vocabulary
diff --git a/github.go b/github.go
index 3a621d7..3e1919a 100644
--- a/github.go
+++ b/github.go
@@ -74,6 +74,13 @@ var graphqlURL = "https://api.github.com/graphql"
// annotates the run.
const staleWarnAfter = 90 * 24 * time.Hour
+// minStars is the hard inclusion floor from the README's criteria. Unlike the
+// staleness window, this is not a judgement call: an entry below the floor is
+// dropped from the published ranking, so the list can never show a repo that
+// does not meet it. It lives here rather than in -check because star counts
+// need the API and -check runs offline.
+const minStars = 1000
+
// chunkSize is the max aliases per GraphQL request (GitHub node-limit safety margin).
const chunkSize = 50
@@ -272,3 +279,19 @@ func doWithRetry(token string, body []byte) ([]byte, int, error) {
}
return nil, 0, fmt.Errorf("all %d attempts failed; last error: %w", maxRetries, lastErr)
}
+
+// enforceStarFloor drops entries below minStars and annotates each one as a run
+// error. Dropping rather than failing the run keeps one below-floor entry from
+// blocking the refresh of every other repo, while the ::error:: annotation
+// makes the removal impossible to miss in the Actions log.
+func enforceStarFloor(stats []Stat) []Stat {
+ kept := make([]Stat, 0, len(stats))
+ for _, s := range stats {
+ if s.Stars < minStars {
+ fmt.Printf("::error::repo %s has %d stars, below the %d minimum — dropped from the ranking; remove its entry from data/agents.yml\n", s.CanonicalKey, s.Stars, minStars)
+ continue
+ }
+ kept = append(kept, s)
+ }
+ return kept
+}
diff --git a/github_test.go b/github_test.go
index 27d63bc..7287702 100644
--- a/github_test.go
+++ b/github_test.go
@@ -312,3 +312,24 @@ func TestFetchStats_StaleWarning(t *testing.T) {
t.Errorf("missing archived warning %q in:\n%s", want, logs)
}
}
+
+func TestEnforceStarFloor(t *testing.T) {
+ in := []Stat{
+ {CanonicalKey: "a/above", Stars: minStars + 1},
+ {CanonicalKey: "b/exactly", Stars: minStars},
+ {CanonicalKey: "c/below", Stars: minStars - 1},
+ {CanonicalKey: "d/zero", Stars: 0},
+ }
+
+ got := enforceStarFloor(in)
+
+ want := []string{"a/above", "b/exactly"}
+ if len(got) != len(want) {
+ t.Fatalf("kept %d entries, want %d: %+v", len(got), len(want), got)
+ }
+ for i, w := range want {
+ if got[i].CanonicalKey != w {
+ t.Errorf("kept[%d] = %s, want %s", i, got[i].CanonicalKey, w)
+ }
+ }
+}
diff --git a/main.go b/main.go
index b756b24..6ab7151 100644
--- a/main.go
+++ b/main.go
@@ -52,6 +52,11 @@ func run() error {
return err
}
+ stats = enforceStarFloor(stats)
+ if len(stats) == 0 {
+ return fmt.Errorf("no agents in %s meet the %d-star minimum", agentsPath, minStars)
+ }
+
snapshots, deltas7, deltas30, err := appendHistory(historyPath, stats)
if err != nil {
return err
diff --git a/templates/readme.tmpl b/templates/readme.tmpl
index 2b1f070..6e28e2b 100644
--- a/templates/readme.tmpl
+++ b/templates/readme.tmpl
@@ -1,6 +1,6 @@
# Awesome Coding Agents
-> Curated ranking of AI agent coding tools, sorted by GitHub stars.
+> Curated ranking of AI-powered developer tools, sorted by GitHub stars.
> Updated daily by GitHub Actions.
📊 **[Interactive dashboard with star-history charts →](https://tiennm99.github.io/awesome-coding-agents/)**
@@ -33,8 +33,8 @@
**Inclusion criteria** — a repo belongs on this list when it is:
-1. **An AI coding agent, or an environment for running them** — either a tool that autonomously writes, edits, or reviews code, or an *agent development environment* (ADE) that runs and coordinates those agents as its primary purpose. Libraries, SDKs, prompt/skill collections, and observability-only layers remain out of scope.
-2. **Notable**: roughly **10,000+ GitHub stars** (the current list floor).
+1. **A developer tool built around AI** — something a developer uses to build software, where an LLM is central to what it does. That covers agents that write, edit, or review code; agent development environments that run and coordinate them; and AI-assisted editors, terminals, and review tools. Libraries, SDKs, model weights, and prompt or skill collections are out of scope — this list ranks tools you *use*, not building blocks you *import*.
+2. **At least 1,000 GitHub stars** — a hard floor, enforced by the updater. An entry below it is dropped from the ranking automatically, so the threshold is never waived for an individual entry.
3. **Open source and actively maintained** — no push in **6 months** means the entry is dropped. The daily run flags anything past **3 months** for review, so the ranking reflects tools people can actually use today.
Archived or abandoned repos are kept only when they are historically significant, and are marked as such in [`data/agents.yml`](data/agents.yml).
From 3310ed36d434dfd358e11b76cf4f8721b907028a Mon Sep 17 00:00:00 2001
From: tiennm99
Date: Wed, 16 Sep 2026 21:40:23 +0700
Subject: [PATCH 3/4] chore: rename to ai-dev-tools
"awesome-coding-agents" no longer described the contents once the scope
widened past coding agents to AI-powered developer tools generally. Renaming
now was near-free: 3 stars, 4 forks, 135 unique visitors, four months old.
Name selection avoided the crowded awesome-* namespace, where the obvious
candidates are held by established lists (awesome-ai-devtools 3.9k,
awesome-ai-tools 6.2k, awesome-ai-coding-tools 2.1k). "ai-dev-tools" has no
incumbent at the exact name.
- Repo renamed; GitHub description and homepage updated.
- Module path, User-Agent, .gitignore, H1, site /og:title and every
GitHub Pages URL updated across README.md, templates/readme.tmpl,
docs/LOCAL_DEV.md and site/index.html.
- plans/reports/ left untouched: it is a dated historical record.
go build: ok. go run . -check: 43 agents valid. go test ./...: ok.
---
.gitignore | 2 +-
README.md | 8 ++++----
docs/LOCAL_DEV.md | 2 +-
github.go | 2 +-
go.mod | 2 +-
site/index.html | 16 ++++++++--------
templates/readme.tmpl | 8 ++++----
7 files changed, 20 insertions(+), 20 deletions(-)
diff --git a/.gitignore b/.gitignore
index f60ce43..cf8ff6a 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,5 +1,5 @@
# Go build artifacts
-/awesome-coding-agents
+/ai-dev-tools
*.exe
*.test
*.out
diff --git a/README.md b/README.md
index 5ad65a3..ff794cb 100644
--- a/README.md
+++ b/README.md
@@ -1,9 +1,9 @@
-# Awesome Coding Agents
+# AI Dev Tools
> Curated ranking of AI-powered developer tools, sorted by GitHub stars.
> Updated daily by GitHub Actions.
-📊 **[Interactive dashboard with star-history charts →](https://tiennm99.github.io/awesome-coding-agents/)**
+📊 **[Interactive dashboard with star-history charts →](https://tiennm99.github.io/ai-dev-tools/)**
**Last updated:** 2026-09-16 03:54 UTC · **Tracked:** 40 repos
**Top 7-day mover:** [earendil-works/pi](https://github.com/earendil-works/pi) (+2642 stars)
@@ -60,7 +60,7 @@
3. The updater fetches live repo metadata via the GitHub GraphQL API in one batched query.
4. Star counts are appended to `data/history.jsonl` for 7-day delta computation.
5. This `README.md` is regenerated from `templates/readme.tmpl` and committed back to the repo.
-6. `site/data.json` is regenerated and the [dashboard](https://tiennm99.github.io/awesome-coding-agents/) (`site/index.html`) is redeployed to GitHub Pages.
+6. `site/data.json` is regenerated and the [dashboard](https://tiennm99.github.io/ai-dev-tools/) (`site/index.html`) is redeployed to GitHub Pages.
**Δ7d:** Change in stars over the past 7 days; `—` means fewer than 7 days of history.
@@ -83,7 +83,7 @@ agents:
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:
+**Tags** describe a tool across five facets — the [dashboard](https://tiennm99.github.io/ai-dev-tools/) filters on them, OR within a facet and AND across facets:
| Facet | Tags |
|-------|------|
diff --git a/docs/LOCAL_DEV.md b/docs/LOCAL_DEV.md
index a3f42fb..31c9b68 100644
--- a/docs/LOCAL_DEV.md
+++ b/docs/LOCAL_DEV.md
@@ -9,7 +9,7 @@
1. Go to [https://github.com/settings/tokens](https://github.com/settings/tokens)
2. Click "Generate new token" → "Generate new token (classic)"
-3. Give it a name (e.g., "awesome-coding-agents")
+3. Give it a name (e.g., "ai-dev-tools")
4. Select scope: **`public_repo`** (needed to read public repo metadata)
5. Click "Generate token" and copy the value
diff --git a/github.go b/github.go
index 3e1919a..6079ba4 100644
--- a/github.go
+++ b/github.go
@@ -251,7 +251,7 @@ func doWithRetry(token string, body []byte) ([]byte, int, error) {
}
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("Content-Type", "application/json")
- req.Header.Set("User-Agent", "awesome-coding-agents-updater")
+ req.Header.Set("User-Agent", "ai-dev-tools-updater")
resp, err := httpClient.Do(req)
if err != nil {
diff --git a/go.mod b/go.mod
index 7d02bfe..655bf88 100644
--- a/go.mod
+++ b/go.mod
@@ -1,4 +1,4 @@
-module github.com/tiennm99/awesome-coding-agents
+module github.com/tiennm99/ai-dev-tools
go 1.23
diff --git a/site/index.html b/site/index.html
index 4e84b00..5c92037 100644
--- a/site/index.html
+++ b/site/index.html
@@ -3,11 +3,11 @@
-Awesome Coding Agents — Rankings & History
+AI Dev Tools — Rankings & History
-
+
-
+
@@ -211,11 +211,11 @@
- Awesome Coding Agents
+ AI Dev Tools
Curated ranking of open-source AI coding agents by GitHub stars, refreshed daily.
@@ -272,8 +272,8 @@
diff --git a/templates/readme.tmpl b/templates/readme.tmpl
index 6e28e2b..8c7f678 100644
--- a/templates/readme.tmpl
+++ b/templates/readme.tmpl
@@ -1,9 +1,9 @@
-# Awesome Coding Agents
+# AI Dev Tools
> Curated ranking of AI-powered developer tools, sorted by GitHub stars.
> Updated daily by GitHub Actions.
-📊 **[Interactive dashboard with star-history charts →](https://tiennm99.github.io/awesome-coding-agents/)**
+📊 **[Interactive dashboard with star-history charts →](https://tiennm99.github.io/ai-dev-tools/)**
**Last updated:** {{.UpdatedAt}} · **Tracked:** {{.Total}} repos
{{- if .TopMover.HasMover }}
@@ -25,7 +25,7 @@
3. The updater fetches live repo metadata via the GitHub GraphQL API in one batched query.
4. Star counts are appended to `data/history.jsonl` for 7-day delta computation.
5. This `README.md` is regenerated from `templates/readme.tmpl` and committed back to the repo.
-6. `site/data.json` is regenerated and the [dashboard](https://tiennm99.github.io/awesome-coding-agents/) (`site/index.html`) is redeployed to GitHub Pages.
+6. `site/data.json` is regenerated and the [dashboard](https://tiennm99.github.io/ai-dev-tools/) (`site/index.html`) is redeployed to GitHub Pages.
**Δ7d:** Change in stars over the past 7 days; `—` means fewer than 7 days of history.
@@ -48,7 +48,7 @@ agents:
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:
+**Tags** describe a tool across five facets — the [dashboard](https://tiennm99.github.io/ai-dev-tools/) filters on them, OR within a facet and AND across facets:
| Facet | Tags |
|-------|------|
From 00c90c5be989e4b6a8de515b2448e8f35c1c897e Mon Sep 17 00:00:00 2001
From: tiennm99
Date: Wed, 16 Sep 2026 22:24:48 +0700
Subject: [PATCH 4/4] feat: split data update from site build; publish via
Cloudflare Pages
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
The updater fetched GitHub metadata and rendered the dashboard payload in
a single step, so anything that published the site needed a GITHUB_TOKEN.
Cloudflare Pages builds from a Git webhook and has no business holding
one.
Split the tool into three modes. The default update step still fetches
GitHub and now records every API-sourced field in data/metadata.json,
which is committed alongside README.md and data/history.jsonl. The new
-build mode joins that snapshot with data/agents.yml and
data/history.jsonl to render dist/ with no network access and no token;
-check is unchanged.
Supporting changes:
- computeDeltaAt anchors the delta windows to when the data was fetched
rather than the wall clock, so a redeploy days later reproduces the
same Δ7d instead of sliding the window past its slack allowance.
- writeSiteData takes updatedAt explicitly for the same reason: the
timestamp labels data freshness, not build time.
- sortStats is extracted from fetchStats so the build step re-ranks
identically from committed metadata.
- An agents.yml entry with no metadata yet is omitted with a warning
instead of failing the build, which would otherwise block every deploy
between merging a new entry and the next nightly run.
- update.yml drops the GitHub Pages deploy steps and commits
data/metadata.json; ci.yml runs `go run . -build` so a build that would
break on deploy breaks in CI first.
- site/_headers stops the edge serving a stale data.json after a refresh.
- docs/DEPLOY.md covers the Cloudflare setup, including the one-time
bootstrap of data/metadata.json that the build depends on.
Also carries the in-progress curation work already in the tree: the
module rename to awesome-ai-dev-tools, retagged entries, and removal of
the archived Roo-Code, void, continue and suna entries.
---
.github/workflows/ci.yml | 4 +
.github/workflows/update.yml | 41 +--
.gitignore | 8 +-
README.md | 10 +-
build.go | 108 ++++++++
build_test.go | 239 ++++++++++++++++++
data/agents.yml | 41 +--
docs/CONTRIBUTING.md | 15 +-
docs/DEPLOY.md | 99 ++++++++
docs/LOCAL_DEV.md | 40 ++-
github.go | 15 +-
go.mod | 2 +-
history.go | 10 +-
main.go | 42 ++-
metadata.go | 112 ++++++++
.../research-260911-1328-agents-refresh.md | 4 +-
site.go | 15 +-
site/_headers | 5 +
site/index.html | 18 +-
site_test.go | 2 +-
templates/readme.tmpl | 12 +-
validate.go | 2 +-
22 files changed, 728 insertions(+), 116 deletions(-)
create mode 100644 build.go
create mode 100644 build_test.go
create mode 100644 docs/DEPLOY.md
create mode 100644 metadata.go
create mode 100644 site/_headers
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 2df21bc..28ab949 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -11,6 +11,7 @@ on:
- '.github/workflows/ci.yml'
- 'data/agents.yml'
- 'templates/**'
+ - 'site/**'
permissions:
contents: read
@@ -30,6 +31,9 @@ jobs:
- run: go test ./...
- run: go build ./...
- run: go run . -check
+ # Runs exactly what Cloudflare Pages runs, so a build that would fail on
+ # deploy fails here instead. Needs no token — that is the point of the split.
+ - run: go run . -build
lint:
runs-on: ubuntu-latest
diff --git a/.github/workflows/update.yml b/.github/workflows/update.yml
index 85b97a6..4203d91 100644
--- a/.github/workflows/update.yml
+++ b/.github/workflows/update.yml
@@ -8,7 +8,6 @@ on:
branches: [main]
paths:
- 'data/agents.yml'
- - 'site/**'
- 'templates/**'
- '**.go'
- 'go.mod'
@@ -18,11 +17,11 @@ permissions:
# Uses GITHUB_TOKEN intentionally — a PAT would cause infinite trigger loops
# on the auto-commit ("chore: daily ranking refresh") because GITHUB_TOKEN-authored
# pushes do NOT re-trigger workflows, whereas a PAT would.
+ #
+ # This job only refreshes data. Publishing is Cloudflare Pages' job: it builds
+ # from the commit this job pushes (webhook-driven, so the GITHUB_TOKEN caveat
+ # above does not apply to it) by running `go run . -build`, which needs no token.
contents: write
- # Pages deploy happens in THIS workflow (a separate push-triggered Pages
- # workflow would never fire — see GITHUB_TOKEN note above).
- pages: write
- id-token: write
concurrency:
group: update-rankings
@@ -31,9 +30,6 @@ concurrency:
jobs:
update:
runs-on: ubuntu-latest
- environment:
- name: github-pages
- url: ${{ steps.deploy.outputs.page_url }}
steps:
- uses: actions/checkout@v7
@@ -42,6 +38,9 @@ jobs:
go-version: 'stable'
cache: true
+ # The only step that touches the network or needs a token. It refreshes
+ # README.md, data/history.jsonl and data/metadata.json; rendering the
+ # site from those files happens later, on Cloudflare.
- name: Run updater
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
@@ -51,7 +50,7 @@ jobs:
run: |
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
- git add README.md data/history.jsonl
+ git add README.md data/history.jsonl data/metadata.json
if git diff --staged --quiet; then
echo "no changes"
exit 0
@@ -59,7 +58,7 @@ jobs:
git commit -m "chore: daily ranking refresh"
# Rebase + retry guards against the race where two runs commit near-simultaneously.
# Strategy: plain rebase first; only force-resolve bot-generated files (README.md,
- # data/history.jsonl) when they are the sole conflicts. If any human-maintained file
+ # data/history.jsonl, data/metadata.json) when they are the sole conflicts. If any human-maintained file
# (e.g. data/agents.yml) conflicts, abort and fail loudly so a human can investigate.
for attempt in 1 2 3; do
if git push; then exit 0; fi
@@ -72,31 +71,15 @@ jobs:
conflicted=$(git diff --name-only --diff-filter=U)
echo "conflicted files: $conflicted"
# Only auto-resolve if ALL conflicts are in bot-generated files.
- non_bot=$(echo "$conflicted" | grep -v -E '^(README\.md|data/history\.jsonl)$' || true)
+ non_bot=$(echo "$conflicted" | grep -v -E '^(README\.md|data/history\.jsonl|data/metadata\.json)$' || true)
if [ -n "$non_bot" ]; then
echo "ERROR: conflict in human-maintained file(s): $non_bot — aborting rebase"
git rebase --abort
exit 1
fi
# Safe to force-resolve: keep our (freshest) bot-generated content.
- git checkout --theirs README.md data/history.jsonl 2>/dev/null || true
- git add README.md data/history.jsonl
+ git checkout --theirs README.md data/history.jsonl data/metadata.json 2>/dev/null || true
+ git add README.md data/history.jsonl data/metadata.json
GIT_EDITOR=true git rebase --continue
done
exit 1
-
- # site/data.json was generated by the updater run above; deploy the
- # static dashboard (site/) to GitHub Pages every run.
- - name: Configure Pages
- uses: actions/configure-pages@v6
- with:
- enablement: true
-
- - name: Upload Pages artifact
- uses: actions/upload-pages-artifact@v5
- with:
- path: site
-
- - name: Deploy to GitHub Pages
- id: deploy
- uses: actions/deploy-pages@v5
diff --git a/.gitignore b/.gitignore
index cf8ff6a..53b3b40 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,5 +1,5 @@
# Go build artifacts
-/ai-dev-tools
+/awesome-ai-dev-tools
*.exe
*.test
*.out
@@ -9,5 +9,7 @@
.idea/
.vscode/
-# Generated by the updater each run; deployed to Pages, never committed
-site/data.json
+# Build output from `go run . -build`; regenerated on every deploy.
+# data/metadata.json IS committed — it is the update step's handoff to the
+# build step, and the reason the build needs no GITHUB_TOKEN.
+/dist/
diff --git a/README.md b/README.md
index ff794cb..c144efc 100644
--- a/README.md
+++ b/README.md
@@ -1,9 +1,9 @@
-# AI Dev Tools
+# Awesome AI Dev Tools
> Curated ranking of AI-powered developer tools, sorted by GitHub stars.
> Updated daily by GitHub Actions.
-📊 **[Interactive dashboard with star-history charts →](https://tiennm99.github.io/ai-dev-tools/)**
+📊 **[Interactive dashboard with star-history charts →](https://awesome-ai-dev-tools.pages.dev/)**
**Last updated:** 2026-09-16 03:54 UTC · **Tracked:** 40 repos
**Top 7-day mover:** [earendil-works/pi](https://github.com/earendil-works/pi) (+2642 stars)
@@ -59,8 +59,8 @@
2. A daily GitHub Actions workflow (`.github/workflows/update.yml`) runs the Go updater.
3. The updater fetches live repo metadata via the GitHub GraphQL API in one batched query.
4. Star counts are appended to `data/history.jsonl` for 7-day delta computation.
-5. This `README.md` is regenerated from `templates/readme.tmpl` and committed back to the repo.
-6. `site/data.json` is regenerated and the [dashboard](https://tiennm99.github.io/ai-dev-tools/) (`site/index.html`) is redeployed to GitHub Pages.
+5. This `README.md` and `data/metadata.json` are regenerated and committed back to the repo.
+6. That push triggers Cloudflare Pages, which runs `go run . -build` to render the [dashboard](https://awesome-ai-dev-tools.pages.dev/) from the committed data — no API token needed at build time. See [docs/DEPLOY.md](./docs/DEPLOY.md).
**Δ7d:** Change in stars over the past 7 days; `—` means fewer than 7 days of history.
@@ -83,7 +83,7 @@ agents:
tags: [terminal, byo-model, interactive, community]
```
-**Tags** describe a tool across five facets — the [dashboard](https://tiennm99.github.io/ai-dev-tools/) filters on them, OR within a facet and AND across facets:
+**Tags** describe a tool across five facets — the [dashboard](https://awesome-ai-dev-tools.pages.dev/) filters on them, OR within a facet and AND across facets:
| Facet | Tags |
|-------|------|
diff --git a/build.go b/build.go
new file mode 100644
index 0000000..11f0642
--- /dev/null
+++ b/build.go
@@ -0,0 +1,108 @@
+package main
+
+import (
+ "fmt"
+ "io"
+ "os"
+ "path/filepath"
+)
+
+// runBuild renders the static dashboard into distDir from committed inputs
+// only — data/agents.yml, data/metadata.json and data/history.jsonl. It makes
+// no network calls and needs no GITHUB_TOKEN, which is what lets an untrusted
+// build environment (Cloudflare Pages) run it.
+//
+// Splitting this out from the update step also means a pure curation change
+// (retagging an entry, editing a note) republishes immediately on push,
+// reusing the last fetched star figures instead of waiting for the nightly run.
+func runBuild(agentsPath, metadataPath, historyPath, siteDir, distDir string) error {
+ agents, err := loadAgents(agentsPath)
+ if err != nil {
+ return err
+ }
+
+ meta, err := readMetadata(metadataPath)
+ if err != nil {
+ return err
+ }
+
+ stats := statsFromMetadata(agents, meta)
+ stats = enforceStarFloor(stats)
+ if len(stats) == 0 {
+ return fmt.Errorf("no entries left to publish after joining %s with %s", agentsPath, metadataPath)
+ }
+ sortStats(stats)
+
+ history, err := readSnapshots(historyPath)
+ if err != nil {
+ return err
+ }
+
+ // Anchor the delta windows to when the data was fetched, not to wall clock.
+ // A rebuild triggered days later (a docs push, a manual redeploy) must show
+ // the same "Δ7d" the nightly run computed, not a window silently slid
+ // forward past its slack allowance.
+ current := Snapshot{
+ Date: meta.FetchedAt.UTC().Format("2006-01-02"),
+ Stars: make(map[string]int, len(stats)),
+ }
+ for _, s := range stats {
+ current.Stars[s.CanonicalKey] = s.Stars
+ }
+ deltas7 := computeDeltaAt(history, current, meta.FetchedAt, 7, 3)
+ deltas30 := computeDeltaAt(history, current, meta.FetchedAt, 30, 5)
+
+ if err := copyDir(siteDir, distDir); err != nil {
+ return fmt.Errorf("copy %s to %s: %w", siteDir, distDir, err)
+ }
+
+ updatedAt := meta.FetchedAt.UTC().Format("2006-01-02 15:04 UTC")
+ if err := writeSiteData(filepath.Join(distDir, "data.json"), updatedAt, stats, deltas7, deltas30, history); err != nil {
+ return err
+ }
+
+ fmt.Printf("built %s: %d entries, data fetched %s\n", distDir, len(stats), updatedAt)
+ return nil
+}
+
+// copyDir replaces dst with a fresh copy of src. The wipe matters: dist/ is
+// build output, and a file deleted from site/ must not survive in a deploy
+// because a previous build left it there.
+func copyDir(src, dst string) error {
+ if err := os.RemoveAll(dst); err != nil {
+ return err
+ }
+ return filepath.WalkDir(src, func(path string, d os.DirEntry, err error) error {
+ if err != nil {
+ return err
+ }
+ rel, err := filepath.Rel(src, path)
+ if err != nil {
+ return err
+ }
+ target := filepath.Join(dst, rel)
+ if d.IsDir() {
+ return os.MkdirAll(target, 0o755)
+ }
+ if !d.Type().IsRegular() {
+ return nil // skip symlinks and other irregular entries
+ }
+ return copyFile(path, target)
+ })
+}
+
+func copyFile(src, dst string) error {
+ in, err := os.Open(src)
+ if err != nil {
+ return err
+ }
+ defer func() { _ = in.Close() }() // read-only; close error is not actionable
+
+ if err := os.MkdirAll(filepath.Dir(dst), 0o755); err != nil {
+ return err
+ }
+ return atomicWriteFile(dst, func(w io.Writer) error {
+ _, err := io.Copy(w, in)
+ return err
+ })
+}
diff --git a/build_test.go b/build_test.go
new file mode 100644
index 0000000..57df813
--- /dev/null
+++ b/build_test.go
@@ -0,0 +1,239 @@
+package main
+
+import (
+ "encoding/json"
+ "os"
+ "path/filepath"
+ "strings"
+ "testing"
+ "time"
+)
+
+// writeBuildFixture lays down a minimal repo layout the build step can read:
+// agents.yml (curation), metadata.json (fetched figures), history.jsonl
+// (delta baselines) and a site/ source directory.
+func writeBuildFixture(t *testing.T, fetchedAt time.Time) (dir string) {
+ t.Helper()
+ dir = t.TempDir()
+
+ mustWrite := func(rel, content string) {
+ t.Helper()
+ path := filepath.Join(dir, rel)
+ if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
+ t.Fatalf("mkdir for %s: %v", rel, err)
+ }
+ if err := os.WriteFile(path, []byte(content), 0o644); err != nil {
+ t.Fatalf("write %s: %v", rel, err)
+ }
+ }
+
+ mustWrite("data/agents.yml", `agents:
+ - owner: org
+ repo: small
+ tags: [terminal, community]
+ - owner: org
+ repo: big
+ tags: [web, vendor]
+ notes: a note
+`)
+
+ meta := metadataFile{
+ FetchedAt: fetchedAt,
+ Repos: map[string]repoMeta{
+ "org/big": {
+ NameWithOwner: "org/big", URL: "https://github.com/org/big",
+ Stars: 5000, Language: "Go", PushedAt: fetchedAt,
+ Description: "big one",
+ },
+ "org/small": {
+ NameWithOwner: "org/small", URL: "https://github.com/org/small",
+ Stars: 2000, Language: "Rust", PushedAt: fetchedAt,
+ Description: "small one", IsArchived: true,
+ },
+ },
+ }
+ raw, err := json.Marshal(meta)
+ if err != nil {
+ t.Fatalf("marshal metadata: %v", err)
+ }
+ mustWrite("data/metadata.json", string(raw))
+
+ // Baseline 7 days before fetchedAt (2026-09-09) so deltas resolve.
+ mustWrite("data/history.jsonl", `{"date":"2026-09-09","stars":{"org/big":4900,"org/small":1990}}
+{"date":"2026-09-16","stars":{"org/big":5000,"org/small":2000}}
+`)
+
+ mustWrite("site/index.html", "dashboard")
+ mustWrite("site/_headers", "/data.json\n Cache-Control: no-cache\n")
+
+ return dir
+}
+
+func readBuiltSiteData(t *testing.T, dir string) siteData {
+ t.Helper()
+ raw, err := os.ReadFile(filepath.Join(dir, "dist", "data.json"))
+ if err != nil {
+ t.Fatalf("read dist/data.json: %v", err)
+ }
+ var got siteData
+ if err := json.Unmarshal(raw, &got); err != nil {
+ t.Fatalf("unmarshal dist/data.json: %v", err)
+ }
+ return got
+}
+
+func runBuildIn(t *testing.T, dir string) error {
+ t.Helper()
+ return runBuild(
+ filepath.Join(dir, "data/agents.yml"),
+ filepath.Join(dir, "data/metadata.json"),
+ filepath.Join(dir, "data/history.jsonl"),
+ filepath.Join(dir, "site"),
+ filepath.Join(dir, "dist"),
+ )
+}
+
+func TestRunBuild_JoinsCurationWithMetadataAndCopiesSite(t *testing.T) {
+ fetchedAt := time.Date(2026, 9, 16, 22, 10, 0, 0, time.UTC)
+ dir := writeBuildFixture(t, fetchedAt)
+
+ if err := runBuildIn(t, dir); err != nil {
+ t.Fatalf("runBuild: %v", err)
+ }
+
+ got := readBuiltSiteData(t, dir)
+
+ // updatedAt must label the fetch, not the build.
+ if got.UpdatedAt != "2026-09-16 22:10 UTC" {
+ t.Errorf("UpdatedAt: expected the metadata fetch time, got %q", got.UpdatedAt)
+ }
+
+ if len(got.Rows) != 2 {
+ t.Fatalf("expected 2 rows, got %d", len(got.Rows))
+ }
+ // Ranked by stars descending, independent of agents.yml order.
+ if got.Rows[0].Key != "org/big" || got.Rows[1].Key != "org/small" {
+ t.Errorf("expected big ranked above small, got %q then %q", got.Rows[0].Key, got.Rows[1].Key)
+ }
+ // Curation comes from agents.yml...
+ if got.Rows[0].Notes != "a note" {
+ t.Errorf("expected notes from agents.yml, got %q", got.Rows[0].Notes)
+ }
+ // ...figures from metadata.json.
+ if got.Rows[0].Stars != 5000 || got.Rows[0].Language != "Go" || got.Rows[0].Description != "big one" {
+ t.Errorf("row0 metadata not applied: %+v", got.Rows[0])
+ }
+ if !got.Rows[1].Archived {
+ t.Error("expected archived flag to survive the round trip")
+ }
+ if !got.Rows[0].HasDelta || got.Rows[0].Delta7d != 100 {
+ t.Errorf("row0 delta7d: expected +100 from the 2026-09-09 baseline, got hasDelta=%v delta=%d", got.Rows[0].HasDelta, got.Rows[0].Delta7d)
+ }
+ if len(got.Facets) == 0 {
+ t.Error("expected tag facets in the payload")
+ }
+
+ // Every static file in site/ must land in dist/ alongside data.json.
+ for _, name := range []string{"index.html", "_headers"} {
+ if _, err := os.Stat(filepath.Join(dir, "dist", name)); err != nil {
+ t.Errorf("expected dist/%s: %v", name, err)
+ }
+ }
+}
+
+// The build must not depend on the clock: it runs whenever Cloudflare happens
+// to redeploy, which may be long after the data was fetched.
+func TestRunBuild_DeltasAnchoredToFetchTimeNotWallClock(t *testing.T) {
+ fetchedAt := time.Date(2026, 9, 16, 22, 10, 0, 0, time.UTC)
+ dir := writeBuildFixture(t, fetchedAt)
+
+ orig := timeNow
+ // Simulate a redeploy 40 days later; a wall-clock anchor would slide the
+ // 7-day window past its slack and drop the delta entirely.
+ timeNow = func() time.Time { return fetchedAt.AddDate(0, 0, 40) }
+ defer func() { timeNow = orig }()
+
+ if err := runBuildIn(t, dir); err != nil {
+ t.Fatalf("runBuild: %v", err)
+ }
+
+ got := readBuiltSiteData(t, dir)
+ if !got.Rows[0].HasDelta || got.Rows[0].Delta7d != 100 {
+ t.Errorf("delta should be unchanged by a late rebuild, got hasDelta=%v delta=%d", got.Rows[0].HasDelta, got.Rows[0].Delta7d)
+ }
+ if got.UpdatedAt != "2026-09-16 22:10 UTC" {
+ t.Errorf("UpdatedAt should still report the fetch time, got %q", got.UpdatedAt)
+ }
+}
+
+// A newly contributed entry has no metadata until the next update run. The
+// build must degrade to omitting it, not fail — otherwise every deploy is
+// blocked between the PR merge and the nightly refresh.
+func TestRunBuild_SkipsEntriesWithoutMetadataYet(t *testing.T) {
+ fetchedAt := time.Date(2026, 9, 16, 22, 10, 0, 0, time.UTC)
+ dir := writeBuildFixture(t, fetchedAt)
+
+ agentsPath := filepath.Join(dir, "data/agents.yml")
+ existing, err := os.ReadFile(agentsPath)
+ if err != nil {
+ t.Fatalf("read agents.yml: %v", err)
+ }
+ added := string(existing) + " - owner: org\n repo: brandnew\n tags: [terminal]\n"
+ if err := os.WriteFile(agentsPath, []byte(added), 0o644); err != nil {
+ t.Fatalf("write agents.yml: %v", err)
+ }
+
+ if err := runBuildIn(t, dir); err != nil {
+ t.Fatalf("runBuild should tolerate a missing metadata entry, got: %v", err)
+ }
+
+ got := readBuiltSiteData(t, dir)
+ if len(got.Rows) != 2 {
+ t.Fatalf("expected the un-fetched entry to be omitted, got %d rows", len(got.Rows))
+ }
+ for _, r := range got.Rows {
+ if r.Key == "org/brandnew" {
+ t.Error("org/brandnew has no metadata and must not be published")
+ }
+ }
+}
+
+// dist/ is build output: a file removed from site/ must not survive there.
+func TestRunBuild_WipesStaleDistFiles(t *testing.T) {
+ fetchedAt := time.Date(2026, 9, 16, 22, 10, 0, 0, time.UTC)
+ dir := writeBuildFixture(t, fetchedAt)
+
+ stale := filepath.Join(dir, "dist", "old.html")
+ if err := os.MkdirAll(filepath.Dir(stale), 0o755); err != nil {
+ t.Fatalf("mkdir dist: %v", err)
+ }
+ if err := os.WriteFile(stale, []byte("stale"), 0o644); err != nil {
+ t.Fatalf("write stale file: %v", err)
+ }
+
+ if err := runBuildIn(t, dir); err != nil {
+ t.Fatalf("runBuild: %v", err)
+ }
+
+ if _, err := os.Stat(stale); !os.IsNotExist(err) {
+ t.Errorf("expected stale dist file to be removed, stat err = %v", err)
+ }
+}
+
+func TestRunBuild_MissingMetadataIsActionable(t *testing.T) {
+ dir := t.TempDir()
+ if err := os.MkdirAll(filepath.Join(dir, "data"), 0o755); err != nil {
+ t.Fatalf("mkdir: %v", err)
+ }
+ if err := os.WriteFile(filepath.Join(dir, "data/agents.yml"), []byte("agents:\n - owner: org\n repo: big\n"), 0o644); err != nil {
+ t.Fatalf("write agents.yml: %v", err)
+ }
+
+ err := runBuildIn(t, dir)
+ if err == nil {
+ t.Fatal("expected an error when metadata.json is absent")
+ }
+ if !strings.Contains(err.Error(), "metadata.json") || !strings.Contains(err.Error(), "GITHUB_TOKEN") {
+ t.Errorf("error should name the missing file and how to produce it, got: %v", err)
+ }
+}
diff --git a/data/agents.yml b/data/agents.yml
index 5e586b4..e44cbde 100644
--- a/data/agents.yml
+++ b/data/agents.yml
@@ -17,10 +17,7 @@ agents:
tags: [terminal, byo-model, local-models, interactive, community]
- owner: cline
repo: cline
- tags: [editor-plugin, terminal, byo-model, local-models, interactive, mcp, community]
- - owner: continuedev
- repo: continue
- tags: [editor-plugin, terminal, byo-model, local-models, interactive, mcp, community]
+ tags: [editor-plugin, terminal, desktop, byo-model, local-models, interactive, headless, mcp, community]
- owner: anomalyco
repo: opencode
tags: [terminal, desktop, byo-model, local-models, interactive, headless, mcp, community]
@@ -30,44 +27,32 @@ agents:
- owner: google-gemini
repo: gemini-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
- tags: [terminal, editor-plugin, single-vendor, interactive, headless, mcp, vendor]
+ tags: [terminal, editor-plugin, desktop, single-vendor, interactive, headless, mcp, vendor]
- owner: aaif-goose
repo: goose
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
- tags: [terminal, byo-model, autonomous, community]
+ tags: [terminal, byo-model, local-models, autonomous, community]
notes: Archived upstream (read-only); kept for historical significance
- owner: SWE-agent
repo: SWE-agent
tags: [terminal, byo-model, autonomous, research, community]
- - owner: RooCodeInc
- repo: Roo-Code
- tags: [editor-plugin, byo-model, interactive, mcp, community]
- notes: Archived upstream (read-only)
- owner: TabbyML
repo: tabby
tags: [self-hosted, editor-plugin, local-models, interactive, community]
- owner: zed-industries
repo: zed
tags: [ide, byo-model, local-models, interactive, acp, community]
- - owner: voideditor
- repo: void
- tags: [ide, byo-model, local-models, interactive, community]
- notes: Archived upstream (read-only)
- owner: anthropics
repo: claude-code
- tags: [terminal, editor-plugin, single-vendor, interactive, headless, mcp, vendor]
+ tags: [terminal, editor-plugin, desktop, single-vendor, interactive, headless, mcp, vendor]
- owner: avante-corp
repo: avante.nvim
tags: [editor-plugin, byo-model, local-models, interactive, mcp, acp, community]
- - owner: kortix-ai
- repo: suna
- tags: [web, self-hosted, byo-model, autonomous, mcp, community]
- owner: earendil-works
repo: pi
tags: [terminal, byo-model, interactive, community]
@@ -83,10 +68,10 @@ agents:
tags: [terminal, editor-plugin, byo-model, autonomous, community]
- owner: QwenLM
repo: qwen-code
- tags: [terminal, editor-plugin, desktop, byo-model, local-models, interactive, headless, mcp, acp, vendor]
+ tags: [terminal, editor-plugin, desktop, web, byo-model, local-models, interactive, headless, mcp, acp, vendor]
- owner: Kilo-Org
repo: kilocode
- tags: [editor-plugin, terminal, byo-model, interactive, mcp, community]
+ tags: [editor-plugin, terminal, byo-model, interactive, autonomous, headless, mcp, community]
- owner: onlook-dev
repo: onlook
tags: [desktop, web, byo-model, app-builder, mcp, community]
@@ -98,7 +83,7 @@ agents:
tags: [terminal, single-vendor, interactive, headless, mcp, vendor]
- owner: deepseek-ai
repo: deepseek-harness
- tags: [terminal, single-vendor, interactive, vendor]
+ tags: [terminal, web, single-vendor, interactive, vendor]
notes: Developer preview; compatibility-breaking changes expected
- owner: openinterpreter
repo: openinterpreter
@@ -108,10 +93,10 @@ agents:
tags: [terminal, editor-plugin, desktop, byo-model, local-models, interactive, mcp, community]
- owner: esengine
repo: DeepSeek-Reasonix
- tags: [terminal, desktop, web, editor-plugin, byo-model, interactive, autonomous, mcp, acp, community]
+ tags: [terminal, desktop, web, editor-plugin, single-vendor, interactive, autonomous, mcp, acp, community]
- owner: Gitlawb
repo: openclaude
- tags: [terminal, byo-model, local-models, interactive, headless, mcp, community]
+ tags: [terminal, editor-plugin, byo-model, local-models, interactive, headless, mcp, community]
- owner: can1357
repo: oh-my-pi
tags: [terminal, editor-plugin, byo-model, local-models, interactive, mcp, acp, headless, community]
@@ -120,7 +105,7 @@ agents:
tags: [terminal, editor-plugin, single-vendor, interactive, headless, mcp, acp, vendor]
- owner: alibaba
repo: open-code-review
- tags: [terminal, byo-model, review, mcp, vendor]
+ tags: [terminal, byo-model, review, headless, mcp, vendor]
- owner: PrimeIntellect-ai
repo: prime-agent
tags: [terminal, byo-model, autonomous, headless, vendor]
@@ -139,15 +124,11 @@ agents:
- owner: Kuberwastaken
repo: claurst
tags: [terminal, byo-model, interactive, headless, acp, community]
- - owner: Orkas-AI
- repo: Orkas
- tags: [desktop, byo-model, interactive, community]
- notes: "Open-source, local-first desktop AI workforce coordinated by a Commander through one chat; supports local coding-agent workflows."
- owner: stablyai
repo: orca
tags: [desktop, terminal, byo-model, orchestration, headless, community]
notes: "ADE: runs Codex, Claude Code, OpenCode and Pi side-by-side, each in its own git worktree, with a mobile companion app."
- owner: getpaseo
repo: paseo
- tags: [desktop, web, terminal, self-hosted, byo-model, orchestration, headless, community]
+ tags: [desktop, web, terminal, editor-plugin, self-hosted, byo-model, orchestration, headless, mcp, community]
notes: "ADE: self-hosted daemon that runs Claude Code, Codex, Copilot, OpenCode and Pi on your own machines, driven from desktop, mobile, web or CLI."
diff --git a/docs/CONTRIBUTING.md b/docs/CONTRIBUTING.md
index de079f8..c196e81 100644
--- a/docs/CONTRIBUTING.md
+++ b/docs/CONTRIBUTING.md
@@ -41,8 +41,11 @@ The dividing line is **tools you use vs. building blocks you import**. Out of sc
libraries and SDKs, agent frameworks meant to be built on, model weights, prompt or
skill collections, and dashboards that only observe a tool without being one.
-A tool also has to be *about* software development. General-purpose assistants and
-chat UIs do not qualify just because a developer could use them.
+A tool also has to be *about* software development. General-purpose assistants,
+chat UIs, and multi-agent "digital workforce" apps — the ones whose specialists write
+reports, decks, and marketing copy — do not qualify just because a developer could use
+them, or because one of their agents happens to touch code. Judge the tool by what it is
+built to do, not by the widest thing it can be pointed at.
### The star floor is hard
@@ -63,7 +66,7 @@ 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.
+the generated `dist/data.json`, so it is defined exactly once.
**Surface** — where you run it. At least one required.
@@ -121,6 +124,9 @@ say nothing about choosing the tool. GitHub topics are a drafting aid only —
**Deprecation:** To remove an agent, delete its entry from `data/agents.yml`. The next run drops it from the README; its history stays in `data/history.jsonl`, so re-adding the entry later restores its star chart.
**Staleness:** An entry with no push in **6 months** is dropped. Past **3 months** the updater prints a `::warning::` naming the repo and its days idle, so the daily run surfaces candidates without anyone auditing the list by hand. Removal stays a human decision: a repo can go quiet between releases, and a historically significant one (`gpt-engineer`) is kept with a `notes` marker instead.
+A repo the maintainers have **archived or declared deprecated** is different: it will never
+be pushed again, so it is removed without waiting out the 6 months unless it earns the same
+historical-significance exception.
## PR Review
@@ -128,4 +134,5 @@ say nothing about choosing the tool. GitHub topics are a drafting aid only —
- The daily GitHub Actions workflow (runs at 00:00 UTC) picks up merged PRs automatically
- No manual review required; the updater regenerates the README after your PR merges
-For local testing before opening a PR, see [LOCAL_DEV.md](./LOCAL_DEV.md).
+For local testing before opening a PR, see [LOCAL_DEV.md](./LOCAL_DEV.md). A tag
+or note change needs only `go run . -build` — no GitHub token.
diff --git a/docs/DEPLOY.md b/docs/DEPLOY.md
new file mode 100644
index 0000000..dfb98c8
--- /dev/null
+++ b/docs/DEPLOY.md
@@ -0,0 +1,99 @@
+# Deploying to Cloudflare Pages
+
+The site is published by Cloudflare Pages' Git integration. Cloudflare builds
+from the repository on every push to `main` and never holds a GitHub token.
+
+## How the split works
+
+Data refresh and site build are two separate steps, in two separate places:
+
+| Step | Command | Runs where | Needs a token? | Writes |
+| --- | --- | --- | --- | --- |
+| Update | `go run .` | GitHub Actions (`update.yml`, nightly + manual) | Yes — `GITHUB_TOKEN` | `README.md`, `data/history.jsonl`, `data/metadata.json` (all committed) |
+| Build | `go run . -build` | Cloudflare Pages | No | `dist/` (never committed) |
+
+`data/metadata.json` is the handoff. The update step records every GitHub-sourced
+field there — stars, language, description, push date, archived flag — so the
+build step can render the dashboard from committed files alone.
+
+That has two consequences worth knowing:
+
+- Cloudflare's build environment never sees a GitHub token, because it has no
+ reason to call the GitHub API.
+- A pure curation change (retagging an entry, editing a note in
+ `data/agents.yml`) republishes as soon as you push, reusing the last fetched
+ star figures. You do not wait for the nightly run.
+
+The nightly Actions run commits refreshed data to `main`; that push fires
+Cloudflare's build webhook, which redeploys with the new numbers.
+
+## One-time setup
+
+### 1. Bootstrap `data/metadata.json`
+
+The build fails without it, so generate it before connecting Cloudflare. Either
+trigger the **Update rankings** workflow manually (Actions tab →
+*Update rankings* → *Run workflow*), or run the updater locally and commit:
+
+```bash
+export GITHUB_TOKEN=ghp_your_token_here
+go run .
+git add data/metadata.json data/history.jsonl README.md
+git commit -m "chore: bootstrap metadata snapshot"
+git push
+```
+
+### 2. Create the Pages project
+
+Cloudflare dashboard → **Workers & Pages** → **Create** → **Pages** →
+**Connect to Git** → pick this repository, then set:
+
+| Setting | Value |
+| --- | --- |
+| Production branch | `main` |
+| Framework preset | None |
+| Build command | `go run . -build` |
+| Build output directory | `dist` |
+| Root directory | *(leave blank)* |
+
+### 3. Set the build environment variable
+
+Under **Settings → Environment variables → Production** (and Preview, if you
+want PR previews) add:
+
+| Variable | Value |
+| --- | --- |
+| `GO_VERSION` | `1.23` |
+
+Do **not** add `GITHUB_TOKEN` — the build does not use one, and adding it would
+hand a credential to an environment that has no need for it.
+
+Cloudflare's build image ships Go and honours `GO_VERSION`. Even on an older
+image, Go's toolchain directive in `go.mod` downloads the matching toolchain
+automatically.
+
+### 4. Deploy
+
+Save and deploy. Subsequent pushes to `main` — yours and the nightly bot's —
+redeploy automatically.
+
+## Caching
+
+`site/_headers` marks `data.json` as `must-revalidate`, so the edge cannot serve
+yesterday's ranking after a refresh. `index.html` and everything else in `site/`
+are copied into `dist/` as-is and use Cloudflare's defaults.
+
+## Troubleshooting
+
+**Build fails with "data/metadata.json not found"** — the bootstrap in step 1
+has not been committed yet.
+
+**A newly added tool is missing from the site** — expected between merging the
+`agents.yml` entry and the next update run. The build logs a warning and omits
+entries it has no metadata for, rather than failing the deploy. Trigger the
+*Update rankings* workflow manually to fetch it immediately.
+
+**Stars look stale** — check the Actions tab: publishing is healthy, but the
+update workflow has not committed recently. The dashboard's "updated" timestamp
+reports when the data was fetched, not when the site was built, so a redeploy
+never makes stale figures look fresh.
diff --git a/docs/LOCAL_DEV.md b/docs/LOCAL_DEV.md
index 31c9b68..4de6d95 100644
--- a/docs/LOCAL_DEV.md
+++ b/docs/LOCAL_DEV.md
@@ -9,25 +9,56 @@
1. Go to [https://github.com/settings/tokens](https://github.com/settings/tokens)
2. Click "Generate new token" → "Generate new token (classic)"
-3. Give it a name (e.g., "ai-dev-tools")
+3. Give it a name (e.g., "awesome-ai-dev-tools")
4. Select scope: **`public_repo`** (needed to read public repo metadata)
5. Click "Generate token" and copy the value
The updater reads public repos only; `public_repo` scope is sufficient and safe.
-## Running Locally
+## The two steps
+
+The tool separates fetching data from rendering the site, so only the fetch
+needs a token. See [DEPLOY.md](./DEPLOY.md) for why.
+
+### Update (needs a token, hits the network)
```bash
export GITHUB_TOKEN=ghp_your_token_here
go run .
```
-The tool will:
+This will:
- Read `data/agents.yml`
-- Fetch live metadata from GitHub GraphQL API
+- Fetch live metadata from the GitHub GraphQL API
- Append star counts to `data/history.jsonl`
+- Write the fetched fields to `data/metadata.json`
- Regenerate `README.md` from `templates/readme.tmpl`
+### Build (offline, no token)
+
+```bash
+go run . -build
+```
+
+This joins `data/agents.yml` (tags and notes) with `data/metadata.json` (stars
+and repo metadata) and renders `dist/` — a copy of `site/` plus the generated
+`dist/data.json`. This is exactly what Cloudflare Pages runs.
+
+Preview it with any static server:
+
+```bash
+go run . -build && python3 -m http.server -d dist 8080
+```
+
+If you only touched `site/index.html` or the tags in `data/agents.yml`, the
+build step alone is enough — no token required.
+
+### Validate (offline, no token)
+
+```bash
+go run . -check
+```
+
## Reverting Local Changes
Before opening a PR, undo local modifications:
@@ -48,3 +79,4 @@ Always set `GITHUB_TOKEN` when testing locally to avoid hitting the unauthentica
## Next Steps
To add agents, see [CONTRIBUTING.md](./CONTRIBUTING.md).
+To publish the site, see [DEPLOY.md](./DEPLOY.md).
diff --git a/github.go b/github.go
index 6079ba4..5f6a040 100644
--- a/github.go
+++ b/github.go
@@ -156,16 +156,21 @@ func fetchStats(token string, agents []Agent) ([]Stat, error) {
})
}
- // Sort by stars descending. Ties are ordered by CanonicalKey for determinism
- // regardless of map-iteration or agents.yml order.
+ sortStats(stats)
+
+ return stats, nil
+}
+
+// sortStats orders the ranking by stars descending. Ties break on CanonicalKey
+// for determinism regardless of map-iteration or agents.yml order. Shared with
+// the build step, which re-ranks from committed metadata.
+func sortStats(stats []Stat) {
sort.Slice(stats, func(i, j int) bool {
if stats[i].Stars != stats[j].Stars {
return stats[i].Stars > stats[j].Stars
}
return stats[i].CanonicalKey < stats[j].CanonicalKey
})
-
- return stats, nil
}
// fetchChunk sends one GraphQL request for a slice of agents. aliasOffset
@@ -251,7 +256,7 @@ func doWithRetry(token string, body []byte) ([]byte, int, error) {
}
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("Content-Type", "application/json")
- req.Header.Set("User-Agent", "ai-dev-tools-updater")
+ req.Header.Set("User-Agent", "awesome-ai-dev-tools-updater")
resp, err := httpClient.Do(req)
if err != nil {
diff --git a/go.mod b/go.mod
index 655bf88..a96e582 100644
--- a/go.mod
+++ b/go.mod
@@ -1,4 +1,4 @@
-module github.com/tiennm99/ai-dev-tools
+module github.com/tiennm99/awesome-ai-dev-tools
go 1.23
diff --git a/history.go b/history.go
index cb99382..4e340a5 100644
--- a/history.go
+++ b/history.go
@@ -195,8 +195,16 @@ func computeDeltas(history []Snapshot, current Snapshot) map[string]int {
// the delta would be misleadingly labeled "Δd", so we return no delta
// for that repo instead.
func computeDeltaOver(history []Snapshot, current Snapshot, days, slackDays int) map[string]int {
+ return computeDeltaAt(history, current, timeNow(), days, slackDays)
+}
+
+// computeDeltaAt is computeDeltaOver with an explicit anchor instead of the
+// wall clock. The build step passes the time the data was fetched, so
+// rebuilding an unchanged dataset later reproduces identical deltas rather
+// than sliding the window forward off its baseline.
+func computeDeltaAt(history []Snapshot, current Snapshot, anchor time.Time, days, slackDays int) map[string]int {
deltas := map[string]int{}
- now := timeNow().UTC()
+ now := anchor.UTC()
cutoff := now.AddDate(0, 0, -days).Format("2006-01-02")
lowerBound := now.AddDate(0, 0, -(days + slackDays)).Format("2006-01-02")
diff --git a/main.go b/main.go
index 6ab7151..0d829dc 100644
--- a/main.go
+++ b/main.go
@@ -11,28 +11,52 @@ import (
const (
agentsPath = "data/agents.yml"
historyPath = "data/history.jsonl"
+ metadataPath = "data/metadata.json"
readmeTmpl = "templates/readme.tmpl"
readmePath = "README.md"
- siteDataPath = "site/data.json"
+ siteDir = "site"
+ distDir = "dist"
)
+// The tool has three modes, deliberately separated so the only step that needs
+// network access and a GITHUB_TOKEN is the one that runs in CI:
+//
+// (default) update — fetch GitHub, refresh history/metadata/README
+// -build — render dist/ from committed data, offline
+// -check — validate data/agents.yml, offline
+//
+// The split is what lets an untrusted build environment (Cloudflare Pages)
+// publish the site without ever holding a token.
func main() {
check := flag.Bool("check", false, "validate data/agents.yml offline (no network, no token) and exit")
+ build := flag.Bool("build", false, "render the static site into dist/ from committed data (no network, no token) and exit")
flag.Parse()
- if *check {
+ if *check && *build {
+ log.Print("-check and -build are mutually exclusive")
+ os.Exit(1)
+ }
+
+ switch {
+ case *check:
if err := runCheck(agentsPath); err != nil {
log.Printf("check failed: %v", err)
os.Exit(1)
}
- return
- }
-
- if err := run(); err != nil {
- log.Fatalf("update failed: %v", err)
+ case *build:
+ if err := runBuild(agentsPath, metadataPath, historyPath, siteDir, distDir); err != nil {
+ log.Printf("build failed: %v", err)
+ os.Exit(1)
+ }
+ default:
+ if err := run(); err != nil {
+ log.Fatalf("update failed: %v", err)
+ }
}
}
+// run is the update step: it is the only mode that talks to GitHub. It writes
+// data/metadata.json, which the build step later consumes offline.
func run() error {
agents, err := loadAgents(agentsPath)
if err != nil {
@@ -57,7 +81,7 @@ func run() error {
return fmt.Errorf("no agents in %s meet the %d-star minimum", agentsPath, minStars)
}
- snapshots, deltas7, deltas30, err := appendHistory(historyPath, stats)
+ _, deltas7, _, err := appendHistory(historyPath, stats)
if err != nil {
return err
}
@@ -66,7 +90,7 @@ func run() error {
return err
}
- if err := writeSiteData(siteDataPath, stats, deltas7, deltas30, snapshots); err != nil {
+ if err := writeMetadata(metadataPath, stats); err != nil {
return err
}
diff --git a/metadata.go b/metadata.go
new file mode 100644
index 0000000..c58be8f
--- /dev/null
+++ b/metadata.go
@@ -0,0 +1,112 @@
+package main
+
+import (
+ "encoding/json"
+ "fmt"
+ "io"
+ "os"
+ "time"
+)
+
+// repoMeta is the GitHub-sourced half of a ranking row: every field the
+// dashboard needs that cannot be derived from data/agents.yml. It is written
+// by the update step (which has a token) and committed, so the build step can
+// render the site offline.
+type repoMeta struct {
+ NameWithOwner string `json:"nameWithOwner"`
+ URL string `json:"url"`
+ Stars int `json:"stars"`
+ Language string `json:"language"`
+ PushedAt time.Time `json:"pushedAt"`
+ Description string `json:"description"`
+ IsArchived bool `json:"archived"`
+}
+
+// metadataFile is the on-disk shape of data/metadata.json. Repos is keyed by
+// canonical owner/repo (the same key history.jsonl uses), so entries survive
+// renames and Go's sorted map marshalling keeps commit diffs minimal.
+type metadataFile struct {
+ FetchedAt time.Time `json:"fetchedAt"`
+ Repos map[string]repoMeta `json:"repos"`
+}
+
+// writeMetadata persists the fetched GitHub metadata. Unlike the old
+// site/data.json this file IS committed: it is the handoff between the update
+// step and the build step, and the only reason the build can run without a
+// GITHUB_TOKEN.
+func writeMetadata(path string, stats []Stat) error {
+ repos := make(map[string]repoMeta, len(stats))
+ for _, s := range stats {
+ repos[s.CanonicalKey] = repoMeta{
+ NameWithOwner: s.NameWithOwner,
+ URL: s.URL,
+ Stars: s.Stars,
+ Language: s.Language,
+ PushedAt: s.PushedAt,
+ Description: s.Description,
+ IsArchived: s.IsArchived,
+ }
+ }
+
+ return atomicWriteFile(path, func(w io.Writer) error {
+ enc := json.NewEncoder(w)
+ // Indented so the committed file reviews as a readable diff.
+ enc.SetIndent("", " ")
+ return enc.Encode(metadataFile{
+ FetchedAt: timeNow().UTC(),
+ Repos: repos,
+ })
+ })
+}
+
+func readMetadata(path string) (metadataFile, error) {
+ var m metadataFile
+ data, err := os.ReadFile(path)
+ if err != nil {
+ if os.IsNotExist(err) {
+ return m, fmt.Errorf("%s not found — run the updater (`go run .` with GITHUB_TOKEN set) to generate it", path)
+ }
+ return m, err
+ }
+ if err := json.Unmarshal(data, &m); err != nil {
+ return m, fmt.Errorf("parse %s: %w", path, err)
+ }
+ if len(m.Repos) == 0 {
+ return m, fmt.Errorf("%s contains no repos", path)
+ }
+ return m, nil
+}
+
+// statsFromMetadata rejoins the two halves of a row: curation (order-independent
+// tags and notes) from agents.yml, live figures from metadata.json.
+//
+// A repo present in agents.yml but absent from metadata.json is skipped with a
+// warning rather than failing the build. That is the normal state between
+// "contributor adds an entry" and "the next nightly update fetches it", and a
+// hard failure there would block every Cloudflare deploy in the meantime.
+func statsFromMetadata(agents []Agent, meta metadataFile) []Stat {
+ stats := make([]Stat, 0, len(agents))
+ for _, a := range agents {
+ key := a.Owner + "/" + a.Repo
+ m, ok := meta.Repos[key]
+ if !ok {
+ fmt.Fprintf(os.Stderr, "warning: %s has no entry in metadata yet — omitted from this build; the next updater run will add it\n", key)
+ continue
+ }
+ stats = append(stats, Stat{
+ CanonicalKey: key,
+ Owner: a.Owner,
+ Repo: a.Repo,
+ Tags: a.Tags,
+ Notes: a.Notes,
+ Description: m.Description,
+ Stars: m.Stars,
+ Language: m.Language,
+ PushedAt: m.PushedAt,
+ URL: m.URL,
+ NameWithOwner: m.NameWithOwner,
+ IsArchived: m.IsArchived,
+ })
+ }
+ return stats
+}
diff --git a/plans/reports/research-260911-1328-agents-refresh.md b/plans/reports/research-260911-1328-agents-refresh.md
index 515154f..5ca1895 100644
--- a/plans/reports/research-260911-1328-agents-refresh.md
+++ b/plans/reports/research-260911-1328-agents-refresh.md
@@ -1,6 +1,6 @@
-# Research Report: awesome-coding-agents list refresh (additions + stale cleanup)
+# Research Report: awesome-ai-dev-tools list refresh (additions + stale cleanup)
-**Conducted:** 2026-09-11 13:28 (+07) · **Repo:** `tiennm99/awesome-coding-agents` · **Tracked now:** 29
+**Conducted:** 2026-09-11 13:28 (+07) · **Repo:** `tiennm99/awesome-ai-dev-tools` · **Tracked now:** 29
## Executive Summary
diff --git a/site.go b/site.go
index f596356..e1bb90b 100644
--- a/site.go
+++ b/site.go
@@ -5,7 +5,7 @@ import (
"io"
)
-// siteRow is one ranked repo in site/data.json, consumed by site/index.html.
+// siteRow is one ranked repo in the generated dist/data.json, consumed by site/index.html.
type siteRow struct {
Key string `json:"key"` // canonical owner/repo, matches history keys
NameWithOwner string `json:"nameWithOwner"`
@@ -32,10 +32,13 @@ type siteData struct {
Facets []tagFacet `json:"facets"`
}
-// writeSiteData emits the JSON payload for the GitHub Pages dashboard.
-// The file is generated fresh on every updater run and is not committed;
-// the Pages deploy step in the workflow picks it up from the working tree.
-func writeSiteData(path string, stats []Stat, deltas7, deltas30 map[string]int, history []Snapshot) error {
+// writeSiteData emits the JSON payload the dashboard fetches at runtime.
+// It is build output (dist/data.json), never committed.
+//
+// updatedAt is passed in rather than read from the clock because it labels
+// data freshness, not build time: a redeploy of unchanged data must not claim
+// the figures are newer than the fetch that produced them.
+func writeSiteData(path string, updatedAt string, stats []Stat, deltas7, deltas30 map[string]int, history []Snapshot) error {
rows := make([]siteRow, len(stats))
for i, s := range stats {
delta7, has7 := deltas7[s.CanonicalKey]
@@ -60,7 +63,7 @@ func writeSiteData(path string, stats []Stat, deltas7, deltas30 map[string]int,
return atomicWriteFile(path, func(w io.Writer) error {
return json.NewEncoder(w).Encode(siteData{
- UpdatedAt: timeNow().UTC().Format("2006-01-02 15:04 UTC"),
+ UpdatedAt: updatedAt,
Rows: rows,
History: history,
Facets: tagVocabulary,
diff --git a/site/_headers b/site/_headers
new file mode 100644
index 0000000..8cd044c
--- /dev/null
+++ b/site/_headers
@@ -0,0 +1,5 @@
+# data.json is regenerated on every deploy and read by the dashboard at load.
+# Cloudflare's edge would otherwise keep serving yesterday's ranking after a
+# nightly refresh; the page's own `cache: 'no-cache'` only covers the browser.
+/data.json
+ Cache-Control: public, max-age=0, must-revalidate
diff --git a/site/index.html b/site/index.html
index 5c92037..e8ffa44 100644
--- a/site/index.html
+++ b/site/index.html
@@ -3,11 +3,11 @@
-AI Dev Tools — Rankings & History
+Awesome AI Dev Tools — Rankings & History
-
+
-
+
@@ -211,11 +211,11 @@
- AI Dev Tools
+ Awesome AI Dev Tools
Curated ranking of open-source AI coding agents by GitHub stars, refreshed daily.
@@ -272,8 +272,8 @@
@@ -309,7 +309,7 @@
const isStale = r => r.pushedAt && new Date(r.pushedAt) < staleCutoff;
// --- facet filter chips (multi-select) ---
- // Vocabulary comes from site/data.json, which the Go updater fills from
+ // Vocabulary comes from the generated data.json, which the Go build step 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
diff --git a/site_test.go b/site_test.go
index 24159ed..5f93a7e 100644
--- a/site_test.go
+++ b/site_test.go
@@ -51,7 +51,7 @@ func TestWriteSiteData_JSONShapeAndDeltaFields(t *testing.T) {
tmpDir := t.TempDir()
tmpFile := tmpDir + "/data.json"
- if err := writeSiteData(tmpFile, stats, deltas7, deltas30, history); err != nil {
+ if err := writeSiteData(tmpFile, "2026-08-09 12:00 UTC", stats, deltas7, deltas30, history); err != nil {
t.Fatalf("writeSiteData: %v", err)
}
diff --git a/templates/readme.tmpl b/templates/readme.tmpl
index 8c7f678..4809156 100644
--- a/templates/readme.tmpl
+++ b/templates/readme.tmpl
@@ -1,9 +1,9 @@
-# AI Dev Tools
+# Awesome AI Dev Tools
> Curated ranking of AI-powered developer tools, sorted by GitHub stars.
> Updated daily by GitHub Actions.
-📊 **[Interactive dashboard with star-history charts →](https://tiennm99.github.io/ai-dev-tools/)**
+📊 **[Interactive dashboard with star-history charts →](https://awesome-ai-dev-tools.pages.dev/)**
**Last updated:** {{.UpdatedAt}} · **Tracked:** {{.Total}} repos
{{- if .TopMover.HasMover }}
@@ -24,8 +24,8 @@
2. A daily GitHub Actions workflow (`.github/workflows/update.yml`) runs the Go updater.
3. The updater fetches live repo metadata via the GitHub GraphQL API in one batched query.
4. Star counts are appended to `data/history.jsonl` for 7-day delta computation.
-5. This `README.md` is regenerated from `templates/readme.tmpl` and committed back to the repo.
-6. `site/data.json` is regenerated and the [dashboard](https://tiennm99.github.io/ai-dev-tools/) (`site/index.html`) is redeployed to GitHub Pages.
+5. This `README.md` and `data/metadata.json` are regenerated and committed back to the repo.
+6. That push triggers Cloudflare Pages, which runs `go run . -build` to render the [dashboard](https://awesome-ai-dev-tools.pages.dev/) from the committed data — no API token needed at build time. See [docs/DEPLOY.md](./docs/DEPLOY.md).
**Δ7d:** Change in stars over the past 7 days; `—` means fewer than 7 days of history.
@@ -33,7 +33,7 @@
**Inclusion criteria** — a repo belongs on this list when it is:
-1. **A developer tool built around AI** — something a developer uses to build software, where an LLM is central to what it does. That covers agents that write, edit, or review code; agent development environments that run and coordinate them; and AI-assisted editors, terminals, and review tools. Libraries, SDKs, model weights, and prompt or skill collections are out of scope — this list ranks tools you *use*, not building blocks you *import*.
+1. **A developer tool built around AI** — something a developer uses to build software, where an LLM is central to what it does. That covers agents that write, edit, or review code; agent development environments that run and coordinate them; and AI-assisted editors, terminals, and review tools. Libraries, SDKs, model weights, and prompt or skill collections are out of scope — this list ranks tools you *use*, not building blocks you *import*. General-purpose assistants and multi-agent "digital workforce" apps are out as well, even when one of their agents can write code — the tool itself has to be aimed at building software.
2. **At least 1,000 GitHub stars** — a hard floor, enforced by the updater. An entry below it is dropped from the ranking automatically, so the threshold is never waived for an individual entry.
3. **Open source and actively maintained** — no push in **6 months** means the entry is dropped. The daily run flags anything past **3 months** for review, so the ranking reflects tools people can actually use today.
@@ -48,7 +48,7 @@ agents:
tags: [terminal, byo-model, interactive, community]
```
-**Tags** describe a tool across five facets — the [dashboard](https://tiennm99.github.io/ai-dev-tools/) filters on them, OR within a facet and AND across facets:
+**Tags** describe a tool across five facets — the [dashboard](https://awesome-ai-dev-tools.pages.dev/) filters on them, OR within a facet and AND across facets:
| Facet | Tags |
|-------|------|
diff --git a/validate.go b/validate.go
index caf5fe3..e929386 100644
--- a/validate.go
+++ b/validate.go
@@ -28,7 +28,7 @@ var repoPattern = regexp.MustCompile(`^[A-Za-z0-9._-]+$`)
//
// 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.
+// the generated dist/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"}},