Imported from yigitkonur/agentshelf (
AGENTS.md). Install upstream withnpx skills add yigitkonur/agentshelf. Copyright stays with the author.
AGENTS.md — AgentShelf
Operational guide for coding agents. Read this before editing. It encodes the architecture, the API/data model, the exact commands, and the non-obvious gotchas that will waste your time if you don't know them.
1. What this is
AgentShelf is a zero-setup local web app. npx agent-shelf starts a Go
loopback server that embeds a built React SPA and scans the user's machine
for AI-agent assets (skills, agents, rules, MCP, instructions, plugins) across
tools (Claude Code .claude, Codex .codex, Cursor .cursor, …). The SPA is a
three-column control room: sidebar (kinds/tools/scope) · virtualized library ·
inspector. The headline workflow is copy a discovered asset into another tool
or project target with a live destination preview, inline conflict resolution,
and a 10-second undo.
Runs entirely on 127.0.0.1. No external network, no telemetry.
2. Commands (use these)
# dev
cd web && pnpm dev # SPA + HMR on :5173 (NO backend — shell only)
cd server && go run ./cmd/agent-shelf serve # Go server; prints http://127.0.0.1:<port>/?token=<hex>
cd server && go run ./cmd/agent-shelf serve --host 0.0.0.0 # expose on the LAN (advertises the LAN IP)
# test (keep green)
make test # go test ./... + vitest --run (24 unit tests)
make e2e # Playwright: 13 tests (real Go servers + Vite shell)
make bench-budget # scan/search/first-paint micro-benchmarks
# build / release
make build # SPA -> embed into spa_dist -> cross-compile 5 platform binaries
# + resync npm/cli binaryShasums + inject version
node scripts/check-binary-size.mjs
node scripts/smoke-npx-timing.mjs
3. Architecture
npx agent-shelf
└─ npm/cli/bin/agent-shelf.js → lib/run.js (launcher: picks the platform
binary from optionalDependencies,
verifies its sha256, execs it)
└─ server/cmd/agent-shelf (Go) (loopback HTTP+WS server)
├─ //go:embed all:spa_dist (the BUILT React SPA, served at /)
├─ scans ~ + ~/.claude/.codex/... (classifies files into asset kinds)
└─ HTTP/JSON + WS deltas (library, install, picker, scan, content)
- server/ — Go.
chirouter,coder/websocket,modernc.org/sqlite(pure-Go DB at~/.agent-index/db.sqlite). Module:github.com/yigitkonur/agentshelf/server. - web/ — React 19 + Vite + TypeScript SPA. Built output is embedded into the
server (see §4).
@tanstack/react-virtualfor the list. - npm/ — the npx distribution:
cli(theagent-shelflauncher),agentshelf(alias),packages/agent-shelf-<os>-<arch>(platform binaries as optionalDependencies). - tests/e2e/ — Playwright. scripts/ — release/bench/smoke tooling.
- specs/001-web-mvp/ — the canonical design spec (data model, HTTP/WS/CLI contracts).
4. Build & embed model — READ THIS
server/internal/http/spa.go has //go:embed all:spa_dist. The SPA is embedded
into the Go binary at COMPILE time. Consequences:
- After ANY change under
web/src, you must rebuild the embed before the server serves it:cd web && pnpm build→node scripts/embed-web-dist.mjs(copiesweb/dist→server/internal/http/spa_dist) → thengo build/go run. - A prebuilt binary serves a FROZEN SPA. Re-running embed updates
spa_diston disk but a binary already built won't reflect it.go runrecompiles each time (picks up the currentspa_dist); a prebuilt binary does not. This silently tests stale UI if you forget — it cost a full debugging detour once. spa_distis committed (so the binary builds without a prior web build).make build(scripts/release-web-mvp.mjs) does build → embed → cross-compile in order.
5. HTTP / WS contract (server is source of truth)
All endpoints bind 127.0.0.1 by default, gated by a per-launch aix_session cookie
(set via GET /?token=<hex>) and an Origin check pinned to the launch origin.
Timestamps are Unix ns; JSON is camelCase. serve --host 0.0.0.0 binds the LAN instead
(advertises the detected LAN IP for the URL + Origin); in that mode the launch token is
reusable so each device can exchange it for its own cookie. Loopback stays single-use.
| Method · path | Purpose |
|---|---|
GET / ?token= |
serve SPA; first hit exchanges token → sets cookie, 303 to / |
POST /scan/start {roots?} |
trigger scan (idempotent); streams progress over WS |
GET /scan/status |
current scan state |
GET /library?q=&kinds=&source_root_kinds=&favorites_only=&limit= |
filtered/searched library (server searches the whole index, returns matchedIndexes, slices to limit≤1000) |
GET /library/facets?q=&kinds=&source_root_kinds=&favorites_only=&tool= |
library-wide faceted counts (byKind/byTool/byScope/total/favorites) over the full in-memory set; each group excludes its own dimension so picking a tool re-scopes the kind counts. Powers the sidebar (counts are NOT computed from the capped client page) |
GET /library/{id}/content |
read-only file preview of an asset's own source path (degrades for uploaded/unreadable/binary) |
POST /library/favorite {asset_id,favorite} |
toggle favorite (persisted) |
GET /targets · POST /targets/project {path} |
install targets (auto-discovered from $HOME/.claude etc.) |
POST /install/from-library · /install/upload · /install/from-path |
three install flows; 409 {destination_path, suggested_new_name} on conflict; resolve with `conflict_resolution: rename |
POST /install/{record}/undo |
10s undo window |
GET /picker?path= |
server-side directory browser |
GET /ws |
library_delta (added/updated/removed) + scan_progress events |
The SPA's search/filter goes server-side (GET /library?q=…&limit=1000) precisely
because the server searches the entire library; a client-only search would miss
assets beyond the loaded page.
6. Data model
- Asset:
id=<kind>:local:<sha256(path)[:16]>(path-stable — excludesnameso a front-matter rename never changes the ID and orphans favorites/installs),kind∈ {skill,agent,rule,mcp,instruction,plugin},name,description?,tags?,source{host,path,rootKind}(rootKind ∈ home|project|cache|upload),mtimeUnixNs,parseError?,matchedIndexes?,isFavorite?. - Tool is NOT a stored field — it is derived from
source.path(matching.claude,.codex, …; else "universal"). Seeweb/src/lib/tools.ts. Keep it honest, never fabricate. - Install destinations are kind-determined (
server/internal/install/destinations.go, mirrored client-side inweb/src/lib/destination.tsfor the preview — keep them in sync): skill→.claude/skills/<n>/SKILL.md, agent→.claude/agents/<n>.md, rule→.cursor/rules/<n>.md, mcp→.mcp.json, instruction→AGENTS.md, plugin→.agents/plugins/<n>.json.
7. Frontend architecture (web/src)
- Design system (
styles/): "Ink & Amber" — warm-dark IDE aesthetic + a light theme via[data-theme]. Tokens instyles/tokens.css; self-hosted fonts (@fontsource-variable/*, bundled — offline-safe). Per-kind colour coding. - Shell:
App.tsx(state + keyboard) →TopBar,Sidebar,LibraryPane(virtualized),Inspector(InstallPaneldoes the copy flow),CommandPalette(⌘K),ImportDialog,SettingsDialog,Toasts. - State:
state/useLibrary.ts(assets + scan + WS deltas; loadslimit=1000, exposestotalMatched; WS deltas coalesce on a trailing flush and freeze while an asset is selected — buffered changes surface via the LibraryPane "N new" pill, so rows don't jump under the cursor mid-scan),state/useLibraryView.ts(server-side search/filter when a query/kind/scope/favorites filter is active; client-side over the loaded set when just browsing; tool is the one client-only filter),state/useFacets.ts(sidebar counts fromGET /library/facets— library-wide + filter-aware; falls back to a client tally when the server is unreachable),useTargets,useTheme,useToasts. - Resilience:
ui/ErrorBoundary.tsxwraps the app (no white-screen-of-death). Hooks defensively guard non-array API responses (Vite dev returns HTML for/library).
8. Gotchas (these bite)
- Embed-at-compile (see §4) — the #1 time-sink. Rebuild the binary after SPA changes.
- chi v5 routes on the raw, still-escaped path, so
chi.URLParamreturns percent-encoded values for ids containing://. Handlers musturl.PathUnescape(seecontent_handlers.go). - E2E
go run+ temp HOME:tests/e2e/go-server.tsoverridesHOMEfor isolation, which would pointGOMODCACHE/GOCACHEat an empty dir → cold re-download → timeout. The harness pins the realgo env GOMODCACHE/GOCACHE. Keep that. - npm publish reads the project
.npmrcfrom the publish command's own directory, NOT walking up. Publish from the repo root withnpm publish ./npm/<pkg>(so the root.npmrctoken applies).cd-ing into a package subdir makes npm fall back to~/.npmrc(often a stale/read-only token) → misleadingE404/E403.npm whoamiworks there regardless, so it masks the problem. - Binary version is ldflags-injected:
var versioninmain.godefaults todev;scripts/release-web-mvp.mjsstamps-X main.version=<npm version>. - Library page caps at 1000 server-side (no cursor pagination); the UI shows an honest "Showing N of M — search to reach the rest" footer for larger libraries. Search/filter DO reach the whole library (server-side).
- Visual QA via a real browser: if you use the
agent-browserCLI, unsetAGENT_BROWSER_PROVIDER(it defaults to a cloud Chrome that can't reach host loopback). Local Chrome reaches127.0.0.1and preserves the cookie/origin checks.
9. Conventions
- TypeScript: match the existing component/hook style; no new deps without reason.
- Go: standard fmt; keep handlers small; tests live beside code (
*_test.go). - Honesty (Principle VII of the spec): never fabricate paths, install state, or counts. Uploaded assets show "Uploaded", unreadable previews say so, derived tool buckets come from real paths.
- Conventional Commits, atomic, scope required (e.g.
feat(web-mvp): …). - Keep
web/src/lib/destination.tsin sync withserver/internal/install/destinations.go. - Keep
server/internal/tooling/tooling.goin sync withweb/src/lib/tools.ts(the path→tool marker map + longest-marker-wins matching); the facets endpoint derives tool server-side, the SPA client-side — they must agree.
10. Release — automated (push to main)
A push to main IS the release. .github/workflows/release.yml (on macos-14, for the darwin
cgo build) runs semantic-release, which reads conventional commits since the last vX.Y.Z tag and:
- computes the next version —
fix/perf→ patch,feat→ minor,BREAKING CHANGE/!→ major (docs/chore/refactor/test/ci→ no release); config in.releaserc.json. prepareCmd:scripts/set-release-version.mjs <v>writes the version into everynpm/**manifest + pins, thenmake buildcross-compiles the 5 binaries + resyncsnpm/clishasums.publishCmd:scripts/publish-release.mjspublishes the 4 macOS/Linux platform packages +cli(tolerating the win32 +agentshelfnpm name-blocks, which never fail the release).- cuts a GitHub Release at the new tag with notes + all 5 binaries attached (Windows ships
here, since npm blocks
agent-shelf-win32-x64). - commits the bumped manifests +
CHANGELOG.mdback tomainaschore(release): <v> [skip ci](the[skip ci]makes Actions skip the re-triggered run — no loop).
Auth: the NPM_TOKEN repo secret → NODE_AUTH_TOKEN on the release step (token, not OIDC). Release
toolchain (semantic-release + plugins) is in the private root package.json (npm ci).
Local dry-run: GITHUB_TOKEN=$(gh auth token) npx semantic-release --dry-run --no-ci.
Current published: agent-shelf@1.0.1 (the 4 macOS/Linux binaries + cli). win32 + the
agentshelf alias remain npm name-blocked (need a scoped/renamed package — open follow-up).