feat(skills): mna skills install — detect AI clients and install the skill + MCP server - #7
Merged
Merged
Conversation
…e skill + MCP server
Wrangler-style agent integration. `mna skills install` detects the AI coding
clients on the machine, shows exactly what it will write where, and asks once.
After a successful interactive `mna login` the same offer appears behind a
single `y`.
Supported targets (skill dirs follow the same table Wrangler uses via
rosie-skills; MCP shapes verified per client):
claude-code ~/.claude/skills/mna ~/.claude.json {type:http,url}
cursor ~/.cursor/skills/mna ~/.cursor/mcp.json {url}
claude-desktop — claude_desktop_config npx mcp-remote
windsurf ~/.codeium/windsurf/skills mcp_config.json {serverUrl}
vscode — <user>/mcp.json servers.{type:http}
agents ~/.agents/skills/mna —
codex ~/.codex/skills/mna — (TOML, untouched)
opencode ~/.config/opencode/skills —
gemini-cli ~/.gemini/skills/mna ~/.gemini/settings.json {httpUrl}
Safety properties, all covered by tests:
- idempotent — byte-identical files are left alone
- JSON configs are merged into, never rewritten wholesale
- anything modified is copied to <file>.mna-backup-<timestamp> first
- an unparseable config blocks that client with a clear message instead of
being replaced
- --dry-run touches nothing; --json refuses to write without --yes
- the post-login offer is skipped entirely for --json, non-TTY and CI, and is
suppressible with `mna config set skills.prompt false`
The skill payload is inlined into the bundle via Bun's text loader, so install
works identically from npm, the compiled Homebrew binary, and a source checkout.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AsdP2hvt4XQhYxkdNREXUq
…C, 0600, error isolation
Addresses the FIX-FIRST review. The headline bug was a false success claim.
CRITICAL
- Blocked client silently skipped its skill files, then reported success.
`plan.blocked` gated *all* of applyPlan but was only ever set by the MCP
change, so a corrupt ~/.claude.json meant the preview promised SKILL.md,
nothing was written, and the CLI still printed "✓ Installed ... for Claude
Code, Cursor" with exit 0. Renamed to `mcpBlocked` and scoped to the MCP
change alone; the success line is now built from the actual `applied`
results, so it names only clients that really got something.
- Writes are now atomic (temp file + rename) instead of truncate-in-place.
An interrupt or ENOSPC mid-write can no longer truncate ~/.claude.json,
which holds Claude Code's entire user state and is routinely multi-MB.
IMPORTANT
- Credentials: any config we write an API key into is chmod 0600, and the
CLI now says plainly that the key is stored in plain text. The earlier PR
description implied otherwise; it has been corrected.
- Backups are 0600 (they can contain OAuth tokens) and pruned to the 3 most
recent instead of accumulating forever.
- Per-client, per-change error isolation: one EACCES no longer aborts the
run. Failures are collected, reported per client with the backup path, and
the command exits non-zero.
- JSONC configs are merged, not refused. VS Code's mcp.json and Gemini CLI's
settings.json routinely carry // comments; jsonc-parser edits the text in
place so comments, key order, and surrounding formatting survive (the
object being merged into may be re-indented — documented).
- A non-object at `mcpServers` is a hard stop (ConfigConflictError) rather
than being silently replaced with {} and reported as "create".
HONESTY
- Verified every skill directory against vendor docs. One was wrong: Codex
CLI reads $HOME/.agents/skills, NOT ~/.codex/skills — the latter is a
third-party compatibility claim OpenAI's own docs never make. Removed the
bogus client; `--client codex` now aliases the shared ~/.agents entry.
- Windsurf, Gemini CLI and OpenCode skill paths all confirmed correct.
- Claude Desktop's Linux config path is NOT vendor-documented (the build is
official beta, the path is convention). Flagged (?) in the plan and table.
- Machinery added to mark any unverified path rather than print "up-to-date"
for somewhere a client may never read.
ALSO
- `mna skills uninstall`.
- --json failures emit JSON and a non-zero exit.
- CI builds dist/mna.js and smoke-tests it on Node: jsonc-parser's UMD main
has a lazy require that bundles into a broken artifact, which unit tests
could never have caught. Import pinned to the ESM entry.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AsdP2hvt4XQhYxkdNREXUq
…skill paths Third review round. N1 — success line still over-claimed, in a new shape. `succeeded` was "any change written", with no notion of *what* was written, so a client whose three skill files all failed EACCES but whose MCP entry landed still produced "✓ Installed the mna skill for Claude Code". Reproduced before fixing. Root cause was structural: AppliedChange carried no label, so the command literally could not tell a skill write from an MCP write. Added `label` to AppliedChange and split the summary into "Installed the mna skill for …" (gated on real skill writes) and "Registered the MNA MCP server for …". N2 — the dist smoke test didn't cover the class it was added for. On CI's pristine $HOME no client is detected, so none of the three commands ever reached parse/modify/applyEdits and a lazy require inside them would still have shipped. The step now seeds a temp $HOME with a commented ~/.cursor/mcp.json, runs a real `skills install --client cursor --yes` against the built bundle, and asserts the comment, the existing server, the new entry and the SKILL.md all survive. Verified by running the extracted script locally. N3 — honesty. `skillPathVerified` was set on zero clients, so the (?) marker and both legends were inert for skills while the README asserted every path was vendor-documented. Rather than soften the claim, I verified each path against the vendor's own page and recorded the URL in the registry as `skillPathDocs`: claude-code code.claude.com/docs/en/skills "Personal | ~/.claude/skills/…" cursor cursor.com/docs/skills user: ~/.cursor/skills/ windsurf docs.devin.ai/desktop/cascade/skills scope table: Global | ~/.codeium/windsurf/skills/ opencode opencode.ai/docs/skills/ "~/.config/opencode/skills/*/SKILL.md" agents learn.chatgpt.com/docs/build-skills Codex USER scope = $HOME/.agents/skills gemini-cli github.com/google-gemini/gemini-cli "User skills: ~/.gemini/skills/" A test now fails if a client with a skill directory has no citation. The URLs are surfaced in `skills list --json`, so the README's claim is checkable. The ~/.agents comment no longer reads as a hedge: five vendors documenting the same path is not the same as a spec guarantee, and it now says exactly that. The (?) machinery stays live via Claude Desktop's undocumented Linux config path. Minors: ApplyError.backup is populated (ApplyFailedError carries it) so the "your original is at …" branch is no longer dead — proved with a pure toApplyError test, since an atomic rename makes it unreachable via the filesystem; --json shapes shared between list and install via clientJsonView; secrets are written with mode 0600 at creation instead of chmod-after, closing a brief world-readable window; backup names are millisecond-precision and collision-proof (a second-granularity name silently overwrote the earlier backup within one run). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AsdP2hvt4XQhYxkdNREXUq
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Wrangler-inspired agent integration.
mna skills installdetects the AI coding clients on themachine, shows exactly what it will write where, and asks once. After a successful interactive
mna loginthe same offer appears behind a singley.Replaces the manual
cp -r skills/mna ~/.claude/skills/instructions in the README (kept as afallback section).
Supported clients
Skill directories follow the same table Wrangler itself uses (via
rosie-skills) for itspost-login install — verified first-hand against the trees
wrangler loginleft in~/.claude/skills/and~/.cursor/skills/on the dev machine. MCP entry shapes were verifiedper client, because they genuinely differ:
--clientclaude-code~/.claude/skills/mna/~/.claude.json→mcpServers{type:"http",url}cursor~/.cursor/skills/mna/~/.cursor/mcp.json→mcpServers{url}claude-desktopclaude_desktop_config.json(per-OS)npx -y mcp-remotewindsurf~/.codeium/windsurf/skills/mna/~/.codeium/windsurf/mcp_config.json{serverUrl}vscode<user dir>/mcp.json→servers{type:"http",url}agents,codex~/.agents/skills/mna/opencode~/.config/opencode/skills/mna/gemini-cli~/.gemini/skills/mna/~/.gemini/settings.json→mcpServers{httpUrl}Cline was deliberately left out — its settings path is contested between the docs
(
~/.cline/data/settings/…) and the shipping extension (VS CodeglobalStorage), so there isno path we can write with confidence.
~/.codeium/windsurf/is used for Windsurf because it isthe only probe that survives the June 2026 Devin Desktop rename.
Codex CLI correction (review follow-up): the first version of this PR shipped
~/.codex/skills/. OpenAI's docs document$HOME/.agents/skillsand never mention~/.codex/skills— that path comes from Cursor's compatibility docs, and is where most of thefolklore online traces to. The bogus client was removed;
--client codexnow aliases the shared~/.agentsentry. Windsurf, Gemini CLI and OpenCode paths were checked the same way and arecorrect.
Claude Desktop on Linux is flagged
(?): the Linux build is official (beta), but noAnthropic doc says it reads
~/.config/Claude/claude_desktop_config.json— only the macOS andWindows paths are documented.
mnawrites the conventional path and tells you it's unverifiedrather than reporting "up-to-date" for somewhere the client may never read.
MCP servers point at
https://mcp.mynextadventure.cloud/mcp. If credentials exist, the APIkey is written into that client's config file in plain text — as an
X-API-Keyheader, or inenvfor themcp-remotebridge. That is simply how these clients take credentials; there is nosecret store to point them at. What
mnadoes about it: any config it writes a key into is setto mode
0600, and the CLI says so on screen before writing. Users who'd rather not have the keyon disk can use
--no-mcp, or install beforemna loginand let the client do its own OAuth.(An earlier revision of this description implied the key was handled more discreetly than it is.
It wasn't, and the reviewer was right to call it out.)
Safety properties (all covered by tests)
Already up to date.mcpServers.my-next-adventureis set. JSONC configs (VS Code,Gemini CLI) are edited in place via
jsonc-parser, so//comments, trailing commas and keyorder survive. The object being merged into may be re-indented; nothing else is touched.
rename(). An interrupt or a full disk cannot truncate amulti-megabyte
~/.claude.json.<file>.mna-backup-<timestamp>at mode0600(they can contain OAuth tokens), and only the 3 most recent are kept.
where
mcpServersshould be, is skipped with a clear message. Crucially this skips only theMCP entry: the skill files are independent and still install.
client (with the backup path) and the command exits non-zero.
--dry-runwrites nothing;--jsonrefuses to write without--yes, and emits JSON (not abare stack trace) on failure; a non-TTY without
--yeserrors instead of hanging on a prompt.--json, non-TTY and CI, is skill-only (nevertouches MCP configs), doesn't re-ask once the skill is current, and is suppressible with
mna config set skills.prompt false.Review fixes (second commit)
The first commit had a bug worth naming: a blocked client silently skipped its skill files and
then reported success.
plan.blockedgated all ofapplyPlanbut was only ever set by the MCPchange, so a corrupt
~/.claude.jsonproduced a preview promisingSKILL.md, wrote nothing, andstill printed
✓ Installed the mna skill for Claude Code, Cursorwith exit 0. Fixed by scoping itto
mcpBlockedand building the summary from actualappliedresults. Also fixed in that pass:non-atomic writes, world-readable configs/backups holding credentials, unpruned backups, no error
isolation, JSONC configs being refused as "corrupt", and
setAtPathsilently replacing anon-object
mcpServers. Each has a regression test.One more found while verifying:
jsonc-parser'smainis a UMD bundle whose lazyrequire('./impl/format')bundles into a brokendist/mna.js— the unit tests all passedwhile the actual published artifact threw on startup. The import is pinned to the ESM entry, and
CI now builds the bundle and smoke-tests it on Node so this class of bug can't recur.
Implementation notes
with { type: 'text' }),so
skills installworks identically from npm, the compiled Homebrew binary, and a sourcecheckout — none of which agree on where
skills/mnasits relative to the entrypoint.~/.config/mna/settings.jsonfor non-secret prefs, soskills.promptsurviveslogoutand can be set before a first login.
mna config get|setlearned the key.mna logingained--json(house convention: every command supports it), which alsosuppresses the prompt.
Demo transcript
Merging into an existing
~/.claude.jsonthat already hadmcpServers.existing:{ "numStartups": 42, "mcpServers": { "existing": { "command": "npx" }, "my-next-adventure": { "type": "http", "url": "https://mcp.mynextadventure.cloud/mcp" } } }And the Claude Desktop bridge form, with credentials present:
{ "mcpServers": { "my-next-adventure": { "command": "npx", "args": ["-y", "mcp-remote", "https://mcp.mynextadventure.cloud/mcp", "--header", "X-API-Key:${MNA_API_KEY}"], "env": { "MNA_API_KEY": "mna_test_key" } } } }Review round 3
The success line over-claimed again, in a new shape.
succeededmeant "wrote anything",so a client whose three skill files all failed
EACCESwhile its MCP entry landed stillprinted
✓ Installed the mna skill for Claude Code. Structural cause:AppliedChangecarriedno label, so the command couldn't tell a skill write from an MCP write. Added
label; thesummary is now split into "Installed the mna skill for …" (gated on real skill writes) and
"Registered the MNA MCP server for …".
The dist smoke test didn't cover the class it was added for. On CI's pristine
$HOMEnothing is detected, so none of the three commands reached
parse/modify/applyEdits. Itnow seeds a temp
$HOMEwith a commented~/.cursor/mcp.jsonand runs a realskills install --client cursor --yesagainst the built bundle, asserting the comment, thepre-existing server, the new entry and
SKILL.mdall survive. Confirmed inCI.
Skill paths are now cited, not asserted.
skillPathVerifiedwas set on zero clients, sothe
(?)marker was inert for skills while the README claimed every path was vendor-documented.I verified each against the vendor's own page and recorded the URL in the registry
(
skillPathDocs), surfaced inskills list --jsonand linked from the README table:~/.claude/skills/~/.claude/skills/<skill-name>/SKILL.md"~/.cursor/skills/~/.codeium/windsurf/skills/~/.config/opencode/skills/agents~/.agents/skills/~/.gemini/skills/A test fails if a client with a skill directory has no citation. The
~/.agentscomment nolonger hedges — five vendors documenting the same path is not a spec guarantee, and it now says
exactly that. (Worth noting the search snippets claimed Windsurf had no global skills dir;
fetching the vendor page showed that was the cross-agent paragraph quoted out of context.)
Minors:
ApplyError.backuppopulated so the "your original is at …" branch isn't dead;--jsonshapes shared betweenlistandinstall; secrets written with mode0600atcreation rather than chmod-after; backup names are millisecond-precision and collision-proof
— a same-second name silently overwrote the earlier backup within a single run, which my own
new test caught.
Verification
bun run typecheck+bun run lint+bun testall green — 128 tests, 0 fail (58 new acrosschanges,clients,plan,post-login-prompt,settings). New regression tests cover theblocked-client no-op, atomic-write failure cleanup,
0600modes on configs and backups, backupretention, JSONC comment preservation,
ConfigConflictErroron a non-objectmcpServers, andper-change error isolation.
Exercised for real on macOS against throwaway
$HOMEs:~/.claude.json→ skill files now install, the corrupt configis byte-identical afterwards, the MCP skip is reported on stderr, and the success line is
accurate.
mcp.jsonwith// commentsand a trailing comma merged cleanly, commentsintact, file and backup both
0600.chmod 500on~/.claude/skills→ Cursor still installed, Claude Code's threefailures reported individually, success line named Cursor only, exit
2,--jsonok:false.--jsonwithout--yes→ JSON error object, exit 1.skills uninstall --dry-run/--yes.bun run build+node dist/mna.jsverified working after the jsonc-parser fix.The real
~/.claude/skills/(11 Wrangler-installed skills),~/.claude.jsonand~/.cursor/remain unmodified — no
mnaskill dir, no backup files, nomcpServerskey added.Not verified: the
bun build --compilebinaries run. They compile, but this sandbox SIGKILLs anyfreshly-compiled binary (a hello-world does the same), so I can't claim the Homebrew artifact was
executed. The npm bundle on Node is verified and now gated in CI.
🤖 Generated with Claude Code
https://claude.ai/code/session_01AsdP2hvt4XQhYxkdNREXUq