Merge branch 'add-orca-paseo-ade'

This commit is contained in:
tiennm99 committed 2026-09-16 22:27:46 +07:00
commit 1a5792e69e
23 files changed
+823 -121

No files matched your search

+4
View File
@@ -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
+12 -29
View File
@@ -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
View File
@@ -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/
+9 -9
View File
@@ -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` |
+108
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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.
+99
View File
@@ -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
View File
@@ -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).
+33 -5
View File
@@ -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
}
+21
View File
@@ -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 -1
View File
@@ -1,4 +1,4 @@
module github.com/tiennm99/awesome-coding-agents
module github.com/tiennm99/awesome-ai-dev-tools
go 1.23
+9 -1
View File
@@ -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")
+38 -9
View File
@@ -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
View File
@@ -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
+9 -6
View File
@@ -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,
+5
View File
@@ -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
View File
@@ -3,11 +3,11 @@
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Awesome Coding Agents — Rankings &amp; History</title>
<title>Awesome AI Dev Tools — Rankings &amp; 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 &amp; History">
<meta property="og:title" content="Awesome AI Dev Tools — Rankings &amp; 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
View File
@@ -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)
}
+9 -9
View File
@@ -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
View File
@@ -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"}},
}