Agent Astronomer
Query your local Claude Code skill, agent, and plugin library from any conversation. Wraps the Agent Astronomer CLI as MCP tools.
Install
claude plugin marketplace add tyroneross/RossLabs-AI-Toolkit
claude plugin install agent-astronomer@rosslabs-ai-toolkit
The Problem
A growing plugin collection is also a growing memory problem. Which skills exist? Which duplicates drifted? Which agent fires on which trigger? The answer lives on disk but the model cannot see it, so users default to loading everything or none of it.
What I Built
Agent Astronomer turns your installed skills, agents, and plugins into a searchable catalog any conversation can query. No more “which skill did that again?”

Impact
The plugin does not bundle the scanner. All catalog reads go through the CLI, so plugin and web UI stay consistent. Node 22+ runs the TypeScript directly via --experimental-strip-types, with no build step and zero npm dependencies on the MCP side.
Architecture
Agent Astronomer is a local-first, single-user, no-cloud system with three surfaces reading and writing the same on-disk state: a Next.js web app, a native macOS menu-bar app (SwiftUI), and this MCP plugin for Claude Code / Codex. There is no server to deploy and no account — every surface resolves state under ~/.astronomer/ (settings, links, the scan cache, per-file shadow-git repos) directly on the machine it runs on. An earlier cloud build (Neon Postgres + Vercel Blob + Neon Auth) was amputated in April 2026; lib/db/client.ts still sits on disk as dormant, unused scaffolding for a possible future cloud rebuild, and throws immediately if called without a DATABASE_URL.
flowchart LR
A["Scan roots<br/>~/.claude/skills, plugin cache,<br/>marketplaces, linked project folders"] --> B["Scanner<br/>lib/scanner.ts / plugin-scanner.ts<br/>walk + parse frontmatter"]
B --> C["Catalog<br/>skills · agents · plugins<br/>djb2 id + sha256 content hash"]
C --> D["scan-cache.json<br/>~/.astronomer/, cross-process locked"]
D --> E1[Next.js web UI]
D --> E2["macOS menu-bar app<br/>(Swift, mirror scanners)"]
D --> E3["bin/aa CLI<br/>--json"]
E3 --> F["MCP server<br/>plugin/mcp-server.mjs<br/>stdio JSON-RPC, no SDK"]
F --> G["Claude Code / Codex<br/>aa_search, aa_stats, aa_similar,<br/>aa_drift, aa_scan_folder, aa_list, aa_graph"]
C --> H["Trigram Jaccard similarity<br/>(pure stdlib, no embeddings)"]
C --> I["Drift compute<br/>lib/library.ts + drift-cache.ts, 30s TTL"]
J["Edit in web/Mac UI"] --> K["Shadow git repo per file<br/>lib/shadow-git.ts, spawns system git"]
K -->|explicit promote| L["Real file on disk"]
How it works
- Each of the three surfaces reads the same
.astronomer/settings.jsonscan-path list (default: a project folder,~/.claude/skills/, the plugin cache, and installed marketplaces) and walks it forSKILL.mdfiles (lib/scanner.ts) andplugin.json/.claude-plugin/plugin.jsonfiles (lib/plugin-scanner.ts), plus linked folders, single files, or shallow-cloned GitHub repos added vialib/links.ts. - Each hit is parsed with a hand-rolled frontmatter parser (
lib/parser.ts, nogray-matterdependency), assigned a stabledjb2id from its absolute path (lib/id.ts) and asha256content hash, and classified by platform (web/ios/macos/shared) vialib/platform-detect.ts. - The walk result is written to a disk-backed, cross-process-locked cache (
lib/scan-cache.ts,~/.astronomer/scan-cache.json) so the Next.js app and the Swift Mac app can share one fingerprinted catalog instead of each re-walking the filesystem —lib/cross-process-lock.tsimplements the lock itself withO_EXCLsentinel files and stale-holder detection, stdlib only. - Three derived views run over that catalog on demand: lexical-similarity clustering for near-duplicate detection (
lib/similarity.ts— character-trigram Jaccard with union-find, no embeddings, chosen because the catalog is ~100-200 items and O(n²) trigram comparison is cheap enough that adding a vector index would be solving a problem that doesn’t exist yet), drift detection for stale plugin-cache copies (lib/library.ts+lib/drift-cache.ts, 30-second TTL keyed on(id, contentHash, source, lastModified)), and usage/fork grouping by name and content hash (lib/skill-usage.ts). - The web app and Mac app serve this catalog for browsing, search, and editing; edits default to a per-file shadow-git repo (
lib/shadow-git.ts, keyed bydjb2(realPath), shelling out to the systemgitbinary viachild_process.spawnrather than addingsimple-git/isomorphic-git) so every change is versioned and revertible before it ever touches the real file — only an explicit “promote” write copies the shadow HEAD back to disk. - The MCP plugin never touches the filesystem itself.
plugin/mcp-server.mjsis a hand-rolled JSON-RPC 2.0 server over stdio (pure Node stdlib, no MCP SDK) that shells out tobin/aa <command> --jsonin a sibling clone of this repo and relays the CLI’s stdout back as the tool result — the same code path the web UI’s API routes call, so plugin answers can’t drift from the web catalog. bin/aais ashwrapper that runsaa.tsdirectly via Node 22+‘s--experimental-strip-types/--experimental-transform-types, with a tiny custom module-resolution hook so extensionless relative imports (./scanner→./scanner.ts) work with zero build step and zero added npm dependencies on that path.
Models: none. No LLM, embedding model, or external inference call exists anywhere in the scan, similarity, drift, or MCP path — verified by grep across lib/, app/, plugin/, and bin/ for LLM/provider SDK usage, which returns nothing. Catalog matching is deterministic (hash equality, substring search, trigram Jaccard); nothing in this product is probabilistic.
Tools & infra — which, why
| Component | Why |
|---|---|
| Node.js 22+, native TS stripping | Runs .ts sources directly (--experimental-strip-types) — no tsc/tsx/build step for the CLI or MCP server |
| Next.js 16 + React 19 | Local web UI surface (catalog browse/search/edit, history, links, context) |
Swift / SwiftUI (mac/) | Native menu-bar/Dock app mirroring the same scanners in Swift, sharing scan-cache.json with the web app so both surfaces agree on catalog state |
System git binary (via child_process.spawn) | Per-file shadow-git version history and rollback — deliberately no simple-git/isomorphic-git dependency (AGENTS.md invariant) |
Filesystem JSON store (.astronomer/*.json) | Settings, links, propagation opt-outs, dependency graph — atomic temp-file+rename writes (lib/fs-atomic.ts) and per-key in-process mutexes (lib/mutex.ts) guard read-modify-write cycles |
Cross-process file lock (lib/cross-process-lock.ts) | Stdlib-only O_EXCL sentinel locking so the web app, Mac app, and CLI never interleave writes to shared .astronomer/ state |
Neon Postgres + Drizzle ORM (@neondatabase/serverless, drizzle-orm) | Dormant — kept on disk from the pre-April-2026 cloud build; getDb() throws unless DATABASE_URL is set, and nothing in the active app calls it |
| MCP (hand-rolled JSON-RPC 2.0 over stdio) | No MCP SDK dependency — the plugin surface is a thin protocol shim over the CLI, not a second implementation of scanning logic |
Tech stack
Next.js 16 + React 19 (web UI), Node.js 22+ with native TypeScript stripping (CLI, MCP server — zero build step, zero MCP-side npm deps), Swift/SwiftUI (native macOS menu-bar app, mac/), system git binary via child_process.spawn (shadow-git version history), filesystem JSON state under .astronomer/ with atomic writes and cross-process locking. Neon Postgres + Drizzle ORM are present but dormant (unused since the April 2026 cloud-to-local-first reshape). No LLM or embedding model anywhere in the stack.
MCP Tools
The plugin ships an MCP server that shells out to the bin/aa CLI in a sibling clone of the agent-astronomer repository. All reads go through the CLI, so plugin behavior matches the web UI.
| Tool | Purpose |
|---|---|
aa_stats | Catalog totals by kind with drift counts |
aa_search | Substring search over names, descriptions, triggers |
aa_similar | Lexical-similarity clusters containing a given name |
aa_drift | Stale plugin-cache copies |
aa_scan_folder | Classify candidates in a folder: new, duplicate, update, conflict |
aa_list | Full catalog list by kind |
aa_graph | Dependency-graph slice for skills matching a name substring |