sdk-canary
Live early-warning system for type-level breaking changes in popular AI SDKs on npm.
sdk-canary downloads every published version of @anthropic-ai/sdk, openai, Vercel ai, @modelcontextprotocol/sdk, langchain, and ~20 other AI SDKs straight from the npm registry, extracts the .d.ts files inside each tarball, parses every top-level export, and computes a per-version diff. The dashboard surfaces what's been added, removed, and modified — and flags releases that ship a breaking change in a non-major bump as a silent break.
- Live:
https://holyai.me/sdk-canary/ - Port:
4897 - Base path:
/sdk-canary - Auth: none (read and write are public)
Why this exists
The AI SDK ecosystem ships every 5–10 days. messages[].content quietly changes from string to (string | ContentBlock[]); beta.messages.create is moved to messages.create; tool-result enums get renamed. Nobody plots that history publicly. Devs find out at runtime, after the bump.
sdk-canary is the missing source of truth — a TypeScript-diff equivalent of an npm release feed.
Data sources (no mocks, no seeds, no Math.random())
Every export and diff in the database is traceable to a real fetch from one of these public endpoints. If a fetch fails the prior good value is preserved and last_error is recorded; the next cron retries. The dashboard never shows fabricated numbers.
| Source | URL | Auth | Cadence |
|------------------------------------:|----------------------------------------------------------|------|-------------------------------|
| npm registry — package metadata | https://registry.npmjs.org/<pkg> | none | every 2 h per package |
| npm registry — package tarball | <tarball_url> from registry response | none | once per version (cached) |
| npm downloads (weekly point) | https://api.npmjs.org/downloads/point/last-week/<pkg> | none | every 2 h (rotated 12 at a time) |
| daily full sweep | (combined above) | none | 03:13 UTC |
Stack
- Node.js 20+ (uses global
fetch), ESM - Express 4 +
helmet+compression better-sqlite3(WAL)node-crontarfor tarball extractionsemverfor honest-semver detection- Vanilla JS SPA — no framework, no build step, dark theme, English labels
Quick start
cp .env.example .env
npm install
npm start
# open http://127.0.0.1:4897/sdk-canary/
The DB is created at ./data.db on first boot. The seed list lives at data/seed-packages.json — add/remove entries there, no code changes required.
On first boot the dashboard shows the empty-state copy First fetch in progress — refresh in 2 minutes. The boot kicks off one immediate meta+tarball cycle so the table populates within ~30 s for the first few packages and within ~2 weeks at the polite default cadence for the full backlog.
Endpoints (all public)
| Method | Path | Purpose |
|-------:|-------------------------------------------------|------------------------------------------------------------------------|
| GET | /sdk-canary/health | {ok:true, uptime, version} |
| GET | /sdk-canary/ | SPA |
| GET | /sdk-canary/api/stats | Totals + last-fetch timestamp |
| GET | /sdk-canary/api/packages | All tracked packages with 30-day breaking score |
| GET | /sdk-canary/api/packages/:name | One package: metadata + recent versions + recent diffs |
| GET | /sdk-canary/api/packages/:name/versions | All versions of one package |
| GET | /sdk-canary/api/packages/:name/diff?from=A&to=B | Stored or on-the-fly diff between two versions |
| GET | /sdk-canary/api/changes?days=7&breakingOnly=1 | Recent breaking-change events across all packages |
| GET | /sdk-canary/api/search?q=foo | Search export names in the latest version of every package |
| GET | /sdk-canary/api/runs?limit=50 | Last N fetch-run log entries |
| GET | /sdk-canary/api/feed.json | JSON Feed of recent diffs |
| GET | /sdk-canary/api/feed.atom | Atom feed |
| POST | /sdk-canary/api/refresh | Manual full refresh (debounced 5 min) |
| POST | /sdk-canary/api/refresh/:name | Manual single-package refresh (debounced 60 s) |
Schema
packages → one row per tracked npm package
versions → one row per (package, version)
exports → one row per (version, kind, name) parsed from .d.ts
diffs → one row per (package, from_version, to_version) with scores
diff_entries → per-export added / removed / modified records
fetches → audit log: every HTTP call with status, code, ms, bytes
Scoring
For each from → to pair:
breaking_score = removed * 10 + modified * 5
honest_semver = 1 iff (breaking_score == 0) OR (bump in [major, premajor]) OR (from < 1.0.0)
Diffs with honest_semver = 0 are the flagship signal — a breaking change shipped in a patch or minor bump.
Known limitations
- The
.d.tsparser inlib/parseDts.jsis line + brace-balance based; ~5–10% of exotic patterns (computed property names, deeply nested conditional types,declare globalblocks) are missed. The next release that touches the same export will re-record it correctly. - A version with no
.d.tsfiles in its tarball (JS-only packages) is recorded withexport_count = 0and produces no diff. - The default polite cadence (5 tarballs/h) means the first full backfill of ~25 packages × ~15 recent versions takes ~2 weeks.
POST /api/refreshaccelerates manually.
License
MIT.