@skill-router/cli
v0.4.31
Published
SKILLROUTER CLI (sr) — search engine for agent skills: sr resolve ranks screened third-party skills and SkillRouter's own website tools; sr ensure installs the pick, hash-checked. Free for individuals. Pair with `sr connect`, search with `sr resolve`, ins
Maintainers
Readme
@skill-router/cli
sr — the SKILLROUTER command line. SkillRouter is a search engine for agent skills: sr resolve
ranks crawled third-party skills together with SkillRouter's own website tools for a task, and
sr ensure verifies and installs the pick. Search and install are free for individuals; a browser
is one ordinary candidate, never the only path. Third-party skills run under their own manual.
The pivot is being implemented in the integration queue. Check the installed CLI and deployed backend before relying on a new feature. The site-adapter examples below describe one existing capability, not the platform's full scope; browser use is not a prerequisite for every task.
npm i -g @skill-router/cli
sr connect # pair this machine — approve in your browser, key auto-saved
sr catalog # skills + contracts
sr account # account and membership status
sr ensure boss-web-cli # install one authorized execution component on demand
sr xiaohongshu search "agent tools" # call a listed skill directly (account key, free for individuals)
sr update # diff-only CLI/MCP/runtime + skills/sitedata update; never waits for site login- Credentials live in
~/.skillrouter/credentials.json, shared with@skill-router/mcp— log in once, every surface works. - Manuals are agent-readable; there is nothing for you to learn. Point your agent at https://skillrouter.org/SKILL.md.
- Self-hosted/dev:
SKILLROUTER_BASE=http://localhost:9000/api/sr sr catalog.
Not every task has a suitable skill, and not every skill is installable: installation follows the
licence, pack availability and local script authorization, and sr resolve can legitimately return
rank.no_match: true. Say so rather than promising a hit.
On-demand adapters
sr <site> <command> first checks the local runtime. If the exact site and command are in the
public index but its execution component is missing, the CLI downloads the bundle, verifies SHA-256
plus the ZIP/manifest structure, installs it once under a bundle lock, and then continues the
original argv. Already installed components run locally without a catalog request, including
offline and after a supporter membership lapses.
sr ensure needs no sr connect: a machine without an account installs anonymously through the
same endpoint (an account key is sent when one exists). sr connect is only required for
sr community submit / claim and for sr account / sr run. When a key is rejected (401/409)
the CLI drops it and retries once anonymously; the failure code stays connect_required only if
that retry fails too. An old backend that still gates a package answers 402/403, which surface as
payment_required / not_available — never as a silent success.
sr ensure boss-web-cli --check --json # authorize/validate only; no install
sr ensure boss-web-cli --json # idempotent first install
SR_AUTO_ENSURE=0 sr boss search ... # rollback switch: keep legacy missing-plugin behaviorThird-party skills
sr resolve can list "kind": "external" candidates: public GitHub skills the crawler screened and
mirrors. They are third-party — SkillRouter does not maintain or run them. Install one by id, which
fetches the mirror pack, recomputes the content hash locally, and keeps it in SkillRouter's cache
(~/.skillrouter/external/<name>), not in your host's skill directories, so it does not load into every
turn: the agent reads the returned skill_md when it needs it. Skills installed before this change stay
bridged as they were. A pack that bundles scripts is refused until you pass --allow-scripts explicitly.
Nothing from a pack is ever executed or imported. Each pack carries the skill's original license files;
a skill that cannot be mirrored (for example over the size caps) stays link-only, with source_url as the place to read it.
sr ensure gh/owner/repo --json # fetch, verify the content hash, cache; prints skill_md
sr ensure gh/owner/repo --check --json # local readiness only; writes nothing
sr ensure gh/owner/repo --refresh # refetch and upgrade; never overwrites local edits
sr ensure gh/owner/repo --allow-scripts # required once, only after the user agrees
sr external list # installed third-party skills, per-turn cost, stale (--json)
sr external remove <name> # remove only entries whose ownership verifiesUnlike sr ensure <skill>, a third-party install needs no Chrome and no execution runtime: a
documentation-only pack installs on a bare machine. --check writes nothing, sends no receipt and
executes nothing. A locally modified install reports blocked / local_modified instead of being
overwritten, and an interrupted install is rolled back on the next run. Every install also appends
the usual ensure journal line; the optional install receipt is skipped under
SR_EXTERNAL_NO_REPORT=1 (or when local outcome reporting is off) and can never change the exit code.
execution.bundle and execution.runtime_plugin come from the catalog contract. The CLI never
derives a download from a prompt, manual URL, or site-name suffix. sr setup --bundle <path> remains
available for offline/development bundles; pre-downloading every adapter is no longer onboarding.
Every sr ensure call also appends one line — skill, target, state, code, changed, source —
to the local journal ~/.skillrouter/journal/ensure.jsonl; it never leaves the machine.
Submitting a skill repository
SkillRouter indexes public GitHub repositories: a folder with a SKILL.md anywhere in the repo
becomes a candidate skill. Hand the CLI the repository address; the crawler picks it up on the next
round. Submitting an address uploads nothing from this machine (to share local-only skills, see below).
sr connect # submissions need your account key
sr community submit https://github.com/owner/repo # or owner/repo; queued for the next crawl
sr community submissions # what you submitted (--withdraw <repo> takes one back)
sr author claim owner/repo # write .skillrouter-claim at the repo root, then:
sr author verify owner/repo # commit the file first; verify reads it from GitHub
sr author stats owner/repo # per-skill listing state, unlisted reason, 30-day shown/top1/installsSharing skills that live only on this machine
sr community share # preview: classifies every local skill, uploads nothing
sr community share --accept-terms # once per terms version, after reading them
sr community share --send <name...> # or --send --all; --as <handle> sets your author name once
sr community share --list # your uploads; --withdraw <name> takes one backThe preview scans each host's skills folder, ~/.agents/skills and the current project's .claude/skills /
.agents/skills. Skills installed by SkillRouter are skipped; skills whose content is already in the catalog are
skipped (only content hashes are sent for this check, and only with a key); skills with a public GitHub origin are
submitted as repository addresses instead of uploaded. A skill matching a secret pattern (API key, token, password
assignment, private key, credentials in a URL) is never uploaded, and the output names the rule and line, not the
value; the patterns cannot catch every secret, so the preview is yours to check. Home paths,
emails and internal hosts are flagged for you to decide. Symlinks inside a skill are not followed; .git,
node_modules, .env*, binaries and files over 256 KiB are left out; a skill over 2 MiB or 200 files is refused.
Upload requests carry skill-relative paths and file contents only — no absolute paths, host names or user names.
Uploaded skills are kept in a SkillRouter-held hosting repository under <handle>/<name>/, then indexed and installed
through SkillRouter like any other catalog skill; skills without a licence file are shared under CC BY 4.0.
sr community submit <draft-dir> (the old adapter-package flow) is refused with
adapter_submissions_closed and exit code 2 — it reads nothing, packs nothing, and makes no request.
Skill discovery across agent harnesses
sr snapshot reads the skills already present on this machine, and sr integrate wires
SkillRouter into Codex and Claude Code. SkillRouter no longer injects anything per turn: a
SessionStart warmup primes the local index, and the skillrouter-router Skill finds the
right skill for any task on demand with sr resolve (plus sr ensure for the one candidate
it picks). Website work in the user's own Chrome is one candidate among others: the
sr-browser Skill, ranked like everything else.
Run sr resolve before any specialised task, even when an installed skill seems to fit; the
agent then compares its candidates with the installed skills in its own context and picks the
better one — installed skill names, descriptions and paths are never uploaded.
sr snapshot # JSON: skills found, name collisions, invalid ones, catalog matches
sr snapshot --format hook # compact projection injected at session start
sr resolve "<need>" --json # candidates for a task; site is not required
sr resolve "<need>" --site <domain>,<brand> --json # optional filter for an explicitly named site
sr integrate --dry-run # show exactly what would be written — nothing is touched
sr integrate # router-only; asks for confirmation first
sr integrate verify # health check; non-zero exit if something is broken
sr integrate remove # undo — removes only what SkillRouter addedsr integrate adds only marked SkillRouter entries and preserves every existing setting:
Claude Code hook in
~/.claude/settings.json:SessionStartwarms the local index.sr integratealso migrates away anyUserPromptSubmit/Stopfragment written by cli ≤ 0.4.22 (third-party entries in the same arrays are left byte-for-byte intact).Codex hooks in
~/.codex/hooks.json: no SkillRouter hooks are needed; the same migration removes old per-turn fragments and deletes the keys it empties.Small infrastructure Skills linked into
~/.agents/skillsand every detected host's native skill root:skillrouter-router(find and run a candidate),sr-browser(drive a website in the user's own Chrome; its browser, login, write, workspace and failure-handoff rules live in its ownSKILL.md) andcommunity-submit. The count follows the shipped manuals, not a fixed number.~/.agents/skillsis written when a host that reads that shared directory is detected (Codex, Cursor, Gemini CLI, …), or when no host is detected at all. Aclaude-only machine therefore no longer gets~/.agents/skills— that directory only existed for hosts that read it.Upgrading from a build without the
timeoutfield onSessionStart:sr integrate --dry-runwill report Claude'sSessionStartasupdate, becausesr integraterewrites our own fragment in place to add the current timeout (entries marked_skillrouter; foreign entries are never touched).sr integrate verifystays green throughout — the recorded hash is refreshed when the fragment is written, so a drift line only appears if someone edits our entry by hand.
Run sr integrate --list-hosts to see which hosts this machine detects. Hosts are only written
when detected, and a host with a hook surface is verified after every write:
| id | tier | writes | notes |
|---|---|---|---|
| claude | hook | ~/.claude/settings.json | SessionStart only; Claude Code asks you to approve externally added hooks once |
| codex | hook | ~/.codex/hooks.json | no SkillRouter hooks; the router skill reaches Codex through ~/.agents/skills, and only old fragments are cleaned up |
| gemini | hook | ~/.gemini/settings.json | SessionStart only, timeout in milliseconds. Only ~/.gemini/settings.json or gemini on PATH counts — a bare ~/.gemini/ (Antigravity) is ignored |
| qwen | hook | ~/.qwen/settings.json | SessionStart only |
| cursor | hook | ~/.cursor/hooks.json | sessionStart writes {additional_context}; beforeSubmitPrompt cannot inject and is not used |
| copilot, kiro, factory | skills | each host's skills root | router Skill linked next to the host's own skills (Kiro does not read ~/.agents/skills) |
| opencode, amp, zed, warp, roo, junie, goose, kimi, trae, openclaw, hermes, cline | skills | ~/.agents/skills | these read the shared ~/.agents/skills directory; no per-host file is written this batch |
Use sr integrate --host <id,…> to limit a run to specific hosts (for example
--host gemini). sr integrate --dry-run shows the exact plan without writing.
Integration is router-only. Product skills stay in the local SkillRouter index and their execution
component installs on first use, so the native skill roots do not grow with the catalog. Machines
integrated by an older CLI that still carry per-product native stubs are migrated by sr integrate
or sr update: only stubs SkillRouter wrote and never modified are removed; edited or foreign
entries are left in place and reported.
Settings files are backed up to ~/.skillrouter/backup/. sr integrate remove removes only
entries and links whose identity/content still matches what SkillRouter installed.
Scanning only ever reads the name and description from each SKILL.md. Skill bodies are
never read at startup and skill scripts are never executed.
What leaves your machine
One anonymous catalog request is cached for 15 minutes. Public bundle updates work the same with
or without a saved key. Matching stays
on your machine: project paths, source code, local skill names, and user prompts are never
sent to SkillRouter.
For manual sr snapshot diagnostics, pass --no-available to skip that fetch entirely;
local skills keep working with no network at all.
If SkillRouter is unreachable, the last successful catalog is reused and the snapshot is
marked incomplete — your local skills are never affected.
sr resolve lists every matching candidate — nothing is withheld — and owned is installed,
community or available. --all is kept as a compatibility no-op (the default list is already
the full public list). sr ensure is the final gate: it verifies the runtime, the local readiness
and the install. Nothing is ever bought automatically.
Every sr resolve call also appends one line — need, site, access, k, coverage, next,
excluded (always null since the authorization gate was removed), plus
skill/site/command/score/owned for the first ten candidates — to the local
journal ~/.skillrouter/journal/resolve.jsonl; it never leaves the machine.
Uncovered sites and community feedback
sr resolve may return no candidate that can do the task, and rank.no_match: true says the
ranking model found nothing applicable. The sr-browser Skill is not a special fallback channel:
it is one candidate among the others, ranked with them, so a website task can also come up empty.
When that happens the Router says so in one line and the agent finishes with the tools and skills it
already has (a browser skill or browser tool included), re-searches with a changed need, or records
the gap. SkillRouter never installs or remotely controls a browser. Writes still require
confirmation, and login, CAPTCHA, risk control, payments, credentials, and other red-zone workflows
are never explored.
When sr resolve returns no candidate at all, the CLI records that miss by itself (one bounded,
best-effort POST of the need text; it never changes the exit code or the output). There is no
separate report command any more.
For the closing line the agent uses sr feedback done: it appends one line to the
local journal ~/.skillrouter/journal/feedback.jsonl — the skill or skill/command, the outcome
(ok, wrong, nofit, blocked, fallback) and the user's own sentence verbatim via --said —
and never uploads it. It prints { ok: true, journal: <path|null> } and exits 0 even when the local
write fails (SR_JOURNAL=off reports journal: null).
sr feedback status
sr feedback off
sr feedback on
sr feedback done <skill>[/<command>] <ok|wrong|nofit|blocked|fallback> --said "<user's words>" # local journal onlyFeedback is independent from the default-off sr signal health ping. A supporting integrated
client shows one single-line disclosure once; there is no per-task prompt. Public explanation:
https://skillrouter.org/us/docs.
Anonymous outcome events (on by default, one switch off)
When a cloud sr resolve returns a rid, the CLI turns the results a user actually sees into
anonymous outcome events and queues them on this machine:
pick—sr ensurereally succeeded (installed/ready_local) for a candidate of aresolvefrom the last 60 minutes;--checkand a side install that matched no candidate never count;ok/wrong/nofit/blocked/fallback— an explicitsr feedback doneconclusion (nofitlands on that resolve's first candidate);run_ok/run_fail/blocked— new lines of the localruns.jsonlmapped back to their canonicalskill/commandid (auth/arg/env/timeoutareblocked, never a quality vote;emptyconcludes nothing; verify/audit runs and unmappable sites are skipped);requery— a re-search within 10 minutes of the previous one, in the same identifiable local session, when that session's candidate had been picked and nofeedback doneconclusion followed.
An event is exactly { id, ev, day, rid?, pos? } (plus the opt-in q below). id is a canonical public id only — a bare skill
id, an official skill/command, an external gh/owner/repo[/path], or builtin/sr-browser — taken
from the public index or from a resolve response; arbitrary text, URLs and private skill names never
become telemetry ids. day is a UTC date within the last seven days, pos is 1–10, and rid is a
16-hex local correlation id (the server only records that a rid was present). Your request sentence,
--said, tasks, argv, arguments, results, URLs, paths, account ids and API keys have no
representation in the payload, the queue or the request.
Optional q (off by default). Only if you set SR_OUTCOME_QUERY=1 (exactly 1; unset, empty,
0 or any other value keeps it off), the ok, run_ok and wrong events that belong to a recent
resolve also carry a sixth key q: your need sentence in normalized form (NFKC, control characters
removed, lower-cased, whitespace collapsed, at most 128 characters) — the same normalization the
server uses for missed searches. It is used to improve search (which wording finds which skill). No
other event ever carries q. Locally, resolve keeps only that normalized need_norm next to the
rid (never the raw sentence); it is sent only while SR_OUTCOME_QUERY=1, both when the event is
queued and when it is uploaded. Deploy the backend before a CLI that sends q: an older server drops
an event that carries q.
Delivery is best effort and never changes a command's output or exit code. The queue is
~/.skillrouter/outcome/queue.jsonl (at most 500 rows, 7 UTC days, ≤512 bytes per row, written under
a lock so concurrent sr processes cannot corrupt it); a batch is packed to the whole request body's
8 KiB and 50-event limits rather than blindly taking 50 rows, and only a 204 removes it. Anything
else is kept and retried with bounded backoff — local state is never lost to a failed send, and an
upload never blocks the command (it runs in a detached worker that receives no unrelated environment
variables).
sr feedback outcome status # is it on, what is queued, where it goes
sr feedback outcome off # stop: clears pending, aborts an in-flight upload
sr feedback outcome onSR_OUTCOME=off, SR_JOURNAL=off and SR_EXTERNAL_NO_REPORT=1 each disable outcome events too,
and sr feedback off turns the whole client-reporting chain (outcome events) off in one step. Turning reporting off clears the pending queue and an upload already in flight can
never write it back. The server side aggregates separately: turning the switch off stops future
sends, it does not erase what an earlier run already delivered. Installed/failed receipts remain
client self-reports, download is a server-verified download, and shown/top1 come from the final
rank response — none of them is independent proof that a user saw or ran anything.
sr diagnose
Reads the four local journals (resolve, ensure, runs, feedback, rotation files included),
stitches them into one chain — listed → picked → ran → acknowledged — and prints a five-section
report on this machine only:
sr diagnose [--since <days>=30] [--window <minutes>=30] [--json]
--export <file> counts-only JSON export (0600)
--with-said also export the feedback quotes — the user's own words
--merge <file>... corroborate several exports; >= --k distinct accounts make a suspect
--k <n>=2 distinct non-empty accounts a (site, cmd) failure needs
--proposals <file> default ~/.skillrouter/patrol/status-proposals.json (whole-file, 0600)
--requests <file> default ~/.skillrouter/patrol/requests.jsonl (append, 0600)- 列了没挑 — resolves that had candidates but no
ensure/runswithin the window, plus thenofitcount and the top needs.needis user text and is shown only here. - 挑了没跑成 —
runsby(site, cmd, outcome),outcome ≠ okonly, with the objective suspect set{selector_miss, shape_mismatch, crash}marked. - agent 主观 — feedback by
(skill, command, outcome); a lead, never a weight. - 混淆对 — where the top-1 candidate was not the skill that was actually ensured/run.
- 覆盖缺口 —
coverage: none/next: search_then_siteby site (old journals may still carry the retired withheld-candidate reason, which diagnose keeps grouping), plus feedbackfallbackby skill.
An ensure/runs/feedback line belongs to the nearest preceding resolve inside the window, so
nothing is counted twice. Sites that no resolve candidate maps are grouped as site:<site>; a skill
name is never guessed.
--export writes counts only: runs, feedback, confusions, unrouted and an account that is
sha256("sr-diagnose:" + <api key>) truncated to 16 hex (or null without a key). It contains no
need, no quotes, no per-line timestamps, no pid and no paths. --with-said is the only way quotes
enter the file (each ≤ 200 characters) and it must be asked for explicitly; it prints one reminder
line — on stdout normally, and on stderr when --json is used, so the JSON report on stdout stays
machine-parseable. Nothing here touches the network, and diagnose never writes the journal, the index
or sitedata.
The export and the proposal file are replaced atomically (same-directory temp file + rename), so a
failed write leaves the previous file byte-identical instead of truncating it; a refused or failed
target never turns into a partial file, and the --requests append says so when it cannot promise
that. Only a directory diagnose itself creates is tightened to 0700 and the files are 0600: a
directory you already had (a shared or synced one you pointed --export at) keeps its mode.
--merge corroborates exports collected from several machines: a (site, cmd) whose objective
failure appears under at least --k distinct non-null accounts becomes a suspect proposal. One
account counted twice, account: null, and the empty outcome never do. Proposals overwrite the
proposals file; each new proposal is also appended to the patrol request file as
{ts, skill, site, cmd, reason:"suspect", accounts, source:"diagnose"} — the same (site, cmd)
within 7 days is not repeated. The request file is a local hand-off for a maintainer; the automated
patrol sweep that used to consume it was retired on 2026-09-22.
sr manual <skill>
Looks the skill up in the catalog (by its skill id, legacy handle, or title). The CLI then requests
the manual endpoint directly — with no account too; a saved key is sent when one exists. On success
the full guide lands in the private ordinary cache ~/.skillrouter/manuals/<skill>/{SKILL.md,meta.json}.
The CLI verifies the endpoint SHA-256 before an atomic write; this directory is never added to native
skill roots. The Router reads it only after a request selects that product. Old
~/.skillrouter/skills/<skill>/SKILL.md files remain a read-only fallback and are never overwritten by
sr manual. When the server still answers 401/403 (an older backend), the response falls back to the
public product-page URL instead of the full text. Unreleased skills (status: coming) are not
downloadable.
Pass --print to emit the verified Markdown body after caching it; the default JSON response returns
the readable body and metadata paths.
Compatibility notes
The webnav runner keeps two legacy output columns, charged_points and free_trial, for schema
compatibility. They are always 0 / false; use member: true as the access signal.
