mirror of
https://github.com/tiennm99/awesome-ai-dev-tools.git
synced 2026-10-11 03:13:05 +00:00
Merge branch 'add-orca-paseo-ade'
This commit is contained in:
commit
1a5792e69e
23 files changed
+823
-121
No files matched your search
@@ -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
|
||||
|
||||
@@ -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
|
||||
+5
-3
@@ -1,5 +1,5 @@
|
||||
# Go build artifacts
|
||||
/awesome-coding-agents
|
||||
/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/
|
||||
@@ -1,9 +1,9 @@
|
||||
# Awesome Coding Agents
|
||||
# Awesome AI Dev Tools
|
||||
|
||||
> 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/)**
|
||||
📊 **[Interactive dashboard with star-history charts →](https://awesome-ai-dev-tools.pages.dev/)**
|
||||
|
||||
**Last updated:** 2026-09-16 14:01 UTC · **Tracked:** 41 repos
|
||||
**Top 7-day mover:** [earendil-works/pi](https://github.com/earendil-works/pi) (+2964 stars)
|
||||
@@ -60,8 +60,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/awesome-coding-agents/) (`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.
|
||||
|
||||
@@ -69,8 +69,8 @@
|
||||
|
||||
**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.
|
||||
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).
|
||||
@@ -84,13 +84,13 @@ 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://awesome-ai-dev-tools.pages.dev/) 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` |
|
||||
| Workflow | `interactive` · `autonomous` · `review` · `app-builder` · `research` · `orchestration` |
|
||||
| Integration | `mcp` · `acp` · `headless` |
|
||||
| Origin (at most one) | `vendor` · `community` |
|
||||
|
||||
|
||||
@@ -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
|
||||
})
|
||||
}
|
||||
+239
@@ -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", "<html>dashboard</html>")
|
||||
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)
|
||||
}
|
||||
}
|
||||
+18
-29
@@ -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,7 +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, 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."
|
||||
+42
-2
@@ -24,6 +24,41 @@ 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
|
||||
|
||||
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:
|
||||
|
||||
- **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.
|
||||
|
||||
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.
|
||||
|
||||
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
|
||||
|
||||
**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
|
||||
|
||||
Tags replaced the old single-select `category` field, because one slot cannot
|
||||
@@ -31,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.
|
||||
|
||||
@@ -55,6 +90,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.
|
||||
|
||||
@@ -88,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
|
||||
|
||||
@@ -95,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.
|
||||
@@ -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.
|
||||
+36
-4
@@ -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., "awesome-coding-agents")
|
||||
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).
|
||||
@@ -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
|
||||
|
||||
@@ -149,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
|
||||
@@ -244,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", "awesome-coding-agents-updater")
|
||||
req.Header.Set("User-Agent", "awesome-ai-dev-tools-updater")
|
||||
|
||||
resp, err := httpClient.Do(req)
|
||||
if err != nil {
|
||||
@@ -272,3 +284,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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
module github.com/tiennm99/awesome-coding-agents
|
||||
module github.com/tiennm99/awesome-ai-dev-tools
|
||||
|
||||
go 1.23
|
||||
|
||||
|
||||
+9
-1
@@ -195,8 +195,16 @@ func computeDeltas(history []Snapshot, current Snapshot) map[string]int {
|
||||
// the delta would be misleadingly labeled "Δ<days>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")
|
||||
|
||||
|
||||
@@ -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 {
|
||||
@@ -52,7 +76,12 @@ func run() error {
|
||||
return err
|
||||
}
|
||||
|
||||
snapshots, deltas7, deltas30, err := appendHistory(historyPath, stats)
|
||||
stats = enforceStarFloor(stats)
|
||||
if len(stats) == 0 {
|
||||
return fmt.Errorf("no agents in %s meet the %d-star minimum", agentsPath, minStars)
|
||||
}
|
||||
|
||||
_, deltas7, _, err := appendHistory(historyPath, stats)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -61,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
|
||||
}
|
||||
|
||||
|
||||
+112
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
+9
-9
@@ -3,11 +3,11 @@
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Awesome Coding Agents — Rankings & History</title>
|
||||
<title>Awesome AI Dev Tools — Rankings & History</title>
|
||||
<meta name="description" content="Daily-refreshed ranking of open-source AI coding agents by GitHub stars, with 7-day deltas and star-history charts.">
|
||||
<meta property="og:title" content="Awesome Coding Agents — Rankings & History">
|
||||
<meta property="og:title" content="Awesome AI Dev Tools — Rankings & History">
|
||||
<meta property="og:description" content="Daily-refreshed ranking of open-source AI coding agents by GitHub stars, with 7-day deltas and star-history charts.">
|
||||
<meta property="og:url" content="https://tiennm99.github.io/awesome-coding-agents/">
|
||||
<meta property="og:url" content="https://awesome-ai-dev-tools.pages.dev/">
|
||||
<meta property="og:type" content="website">
|
||||
<meta name="theme-color" content="#f8fafc" media="(prefers-color-scheme: light)">
|
||||
<meta name="theme-color" content="#0f172a" media="(prefers-color-scheme: dark)">
|
||||
@@ -211,11 +211,11 @@
|
||||
<div>
|
||||
<h1>
|
||||
<svg width="22" height="22" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M12 2l2.9 6.26L21.5 9.1l-4.9 4.5 1.3 6.7L12 17l-5.9 3.3 1.3-6.7-4.9-4.5 6.6-.84L12 2z"/></svg>
|
||||
Awesome Coding Agents
|
||||
Awesome AI Dev Tools
|
||||
</h1>
|
||||
<div class="sub">Curated ranking of open-source AI coding agents by GitHub stars, refreshed daily. <span id="meta"></span></div>
|
||||
</div>
|
||||
<a class="ghlink" href="https://github.com/tiennm99/awesome-coding-agents">
|
||||
<a class="ghlink" href="https://github.com/tiennm99/awesome-ai-dev-tools">
|
||||
<svg width="18" height="18" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true"><path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27s1.36.09 2 .27c1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.01 8.01 0 0 0 16 8c0-4.42-3.58-8-8-8z"/></svg>
|
||||
GitHub
|
||||
</a>
|
||||
@@ -225,7 +225,7 @@
|
||||
<noscript>
|
||||
<p>This dashboard needs JavaScript to render the ranking table and charts.
|
||||
You can view the same ranking in the
|
||||
<a href="https://github.com/tiennm99/awesome-coding-agents#readme">project README on GitHub</a>.</p>
|
||||
<a href="https://github.com/tiennm99/awesome-ai-dev-tools#readme">project README on GitHub</a>.</p>
|
||||
</noscript>
|
||||
|
||||
<div class="stats" id="stats" hidden>
|
||||
@@ -272,8 +272,8 @@
|
||||
</section>
|
||||
|
||||
<footer>
|
||||
<span>Data refreshed daily by <a href="https://github.com/tiennm99/awesome-coding-agents/actions">GitHub Actions</a>.</span>
|
||||
<span>Add an agent via <a href="https://github.com/tiennm99/awesome-coding-agents#contributing">a pull request</a>.</span>
|
||||
<span>Data refreshed daily by <a href="https://github.com/tiennm99/awesome-ai-dev-tools/actions">GitHub Actions</a>.</span>
|
||||
<span>Add an agent via <a href="https://github.com/tiennm99/awesome-ai-dev-tools#contributing">a pull request</a>.</span>
|
||||
</footer>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.9/dist/chart.umd.min.js"></script>
|
||||
@@ -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
|
||||
|
||||
+1
-1
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
# Awesome Coding Agents
|
||||
# Awesome AI Dev Tools
|
||||
|
||||
> 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/)**
|
||||
📊 **[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/awesome-coding-agents/) (`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,8 +33,8 @@
|
||||
|
||||
**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.
|
||||
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*. 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.
|
||||
|
||||
Archived or abandoned repos are kept only when they are historically significant, and are marked as such in [`data/agents.yml`](data/agents.yml).
|
||||
@@ -48,13 +48,13 @@ 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://awesome-ai-dev-tools.pages.dev/) 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` |
|
||||
| Workflow | `interactive` · `autonomous` · `review` · `app-builder` · `research` · `orchestration` |
|
||||
| Integration | `mcp` · `acp` · `headless` |
|
||||
| Origin (at most one) | `vendor` · `community` |
|
||||
|
||||
|
||||
+2
-2
@@ -28,11 +28,11 @@ 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"}},
|
||||
{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"}},
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user