noslopui 0.1.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -6,7 +6,7 @@ Connect your coding agent to [noslopUI](https://noslopui.com) — hand-crafted U
6
6
  npx noslopui@latest init
7
7
  ```
8
8
 
9
- That one command finds your agent, signs you in through the browser, adds the MCP server to your agent's **user-level** config, and installs the workflow skill. Restart your agent and ask it to build something.
9
+ That one command finds the agents on your machine, signs you in once through the browser, adds the MCP server to each agent's **user-level** config, and installs the workflow skill. Restart your agents and ask one to build something.
10
10
 
11
11
  ## Why
12
12
 
@@ -14,46 +14,86 @@ Ask any agent for a landing page and you get the same page: gradient headline, p
14
14
 
15
15
  The MCP server is what your agent calls. The skill is what makes it think to call it at all when you say "build me a website" — so `init` installs both, and you can skip the skill with `--no-skill`.
16
16
 
17
+ ## Supported agents
18
+
19
+ | Agent | `--agent` | Config it writes | Skill it installs |
20
+ |---|---|---|---|
21
+ | Claude Code | `claude-code` | `~/.claude.json` | `~/.claude/skills/noslopui/` |
22
+ | Codex | `codex` | `~/.codex/config.toml` | `~/.agents/skills/noslopui/` |
23
+ | Cursor | `cursor` | `~/.cursor/mcp.json` | `~/.agents/skills/noslopui/` |
24
+ | VS Code (Copilot) | `vscode` | your user `mcp.json` ¹ | `~/.agents/skills/noslopui/` |
25
+ | Devin Desktop | `devin` | `~/.config/devin/mcp_config.json` ² | `~/.codeium/windsurf/skills/noslopui/` |
26
+ | Windsurf (Cascade) | `windsurf` | `~/.codeium/windsurf/mcp_config.json` | `~/.codeium/windsurf/skills/noslopui/` |
27
+ | OpenCode | `opencode` | `~/.config/opencode/opencode.json` | `~/.agents/skills/noslopui/` |
28
+ | Antigravity | `antigravity` | `~/.gemini/config/mcp_config.json` | `~/.gemini/config/skills/noslopui/` |
29
+
30
+ ¹ `%APPDATA%\Code\User\mcp.json` on Windows, `~/Library/Application Support/Code/User/mcp.json` on macOS, `~/.config/Code/User/mcp.json` on Linux.
31
+ ² `%APPDATA%\devin\mcp_config.json` on Windows. Windsurf became Devin Desktop in June 2026: its default agent (Devin Local) reads this file, the older Cascade agent reads the Windsurf one, so they're set up separately.
32
+
33
+ Every path and field comes from that agent's own documentation. Several agents read the same skills folder, so one copy there serves all of them.
34
+
35
+ Anything else that speaks MCP over HTTP can be connected by hand — add `https://noslopui.com/api/mcp` and sign in when it asks. Steps for each client: [noslopui.com/mcp](https://noslopui.com/mcp).
36
+
17
37
  ## Commands
18
38
 
39
+ **Set up**
40
+
19
41
  | | |
20
42
  |---|---|
21
- | `npx noslopui init` | Set up an agent: sign in, add the server, install the skill |
43
+ | `npx noslopui init` | Set up your agents: one sign-in, then the server and the skill for each |
22
44
  | `npx noslopui doctor` | Check what's set up, and say what to do about anything that isn't |
23
- | `npx noslopui login` | Get a fresh key for an agent that's already set up |
24
45
  | `npx noslopui update` | Re-download the skill |
46
+ | `npx noslopui skill` | Install only the skill, for an agent you connected by hand (`--agent <id>`) |
25
47
  | `npx noslopui remove` | Take the noslopUI server back out |
26
48
 
27
- Options: `--agent <id>`, `--no-skill`, `--no-browser`, `--key <key>`, `-y/--yes`.
49
+ **Account**
28
50
 
29
- ## Supported agents
51
+ | | |
52
+ |---|---|
53
+ | `npx noslopui login` | Sign this CLI in, for the catalog commands below |
54
+ | `npx noslopui login --agent <id>` | A fresh key for an agent that's already set up |
55
+ | `npx noslopui whoami` | The account and plan this CLI is signed in to |
56
+ | `npx noslopui logout` | Forget this CLI's sign-in |
57
+
58
+ **Catalog**
59
+
60
+ | | |
61
+ |---|---|
62
+ | `npx noslopui search <query>` | Search UI blocks and Motion Lab effects (`--section ui-blocks\|motion-lab`, `--tag style:editorial`, `--limit`) |
63
+ | `npx noslopui get <id>` | Print a component's code, or `--write src/Hero.tsx` (`--format tsx\|html\|vue`) |
64
+ | `npx noslopui prompt <id>` | Print a component's AI-prompt version |
65
+ | `npx noslopui design-systems [query]` | Search design systems (`--tag theme:dark`) |
66
+ | `npx noslopui design-system <id>` | Print a DESIGN.md, or `--write DESIGN.md` |
67
+ | `npx noslopui collections` | Your saved collections |
68
+ | `npx noslopui collection <name>` | What's in one |
30
69
 
31
- **Claude Code.** That's the list.
70
+ The catalog commands call the same MCP tools your agent does, with the same access: search and collections are free on every account; code, prompts and DESIGN.md files need a paid plan or the account's free trial. Output is only the thing you asked for, so `get <id> > Hero.tsx` works; `--json` gives the raw result.
32
71
 
33
- Other agents are added one at a time, and only after a real install has been done on a real machine, end to end — an agent named here is one we're claiming works. The server enforces the same list: asking for a key for an agent that hasn't passed gets refused, so an out-of-date copy of this CLI can't get one either.
72
+ Options: `--agent <id>` (repeatable), `--no-skill`, `--no-browser`, `--key <key>`, `--json`, `--force`, `-y/--yes`.
34
73
 
35
74
  ## What it does to your machine
36
75
 
37
- It edits your agent's user-level config, and nothing else. Concretely, for Claude Code:
76
+ It edits your agents' user-level config, and nothing else:
38
77
 
39
- - Adds one entry, `noslopui`, under `mcpServers` in `~/.claude.json`.
40
- - **Copies the file first.** The backup is written next to the original as `.claude.json.noslopui-backup-<timestamp>`, and its path is printed.
41
- - **Never touches another server's entry**, or any of the other state Claude Code keeps in that file.
78
+ - Adds one entry, `noslopui`, to each agent you choose, in the file listed above.
79
+ - **Copies the file first.** The backup sits next to the original as `<file>.noslopui-backup-<timestamp>`, and its path is printed.
80
+ - **Never touches another server's entry**, or anything else in the file. Codex's `config.toml` is edited table by table: every line outside `[mcp_servers.noslopui]` is left byte for byte as it was.
42
81
  - **Never writes to a project config.** A key in a project file is a key that gets committed.
43
- - **Stops if the config doesn't parse.** It will not overwrite a file it couldn't read — that file is the only copy of your setup, and guessing at its contents would be worse than doing nothing.
44
- - Writes the skill to `~/.claude/skills/noslopui/SKILL.md`.
82
+ - **Stops if the config doesn't parse**, or if the `noslopui` entry is written in a form it doesn't recognise. It will not overwrite a file it couldn't read — that file is the only copy of your setup.
83
+ - Writes the skill to the folders listed above.
84
+ - Saves the CLI's own sign-in in `~/.config/noslopui/credentials.json` (`%APPDATA%\noslopui\` on Windows), readable only by you.
45
85
 
46
- `remove` takes the entry back out and leaves everything else, including the skill unless you pass `--skill`. It does **not** revoke your key: the key belongs to your account, not to this machine. Revoke it at [noslopui.com/account](https://noslopui.com/account?tab=mcp).
86
+ `remove` takes the entry back out and leaves everything else, including the skill unless you pass `--skill` — and even then it keeps a skill folder another configured agent still reads. It does **not** revoke your key: the key belongs to your account, not to this machine. Revoke it at [noslopui.com/account](https://noslopui.com/account?tab=mcp).
47
87
 
48
88
  ### If Claude Code is running
49
89
 
50
90
  `~/.claude.json` is also Claude Code's own state file, and a running Claude Code rewrites it from memory. Quit it before running `init`, or the entry can be overwritten seconds later. `doctor` re-reads the file rather than trusting the write, so it will tell you if that happened.
51
91
 
52
- ## Your key
92
+ ## Your keys
53
93
 
54
- `init` issues one key per agent, named after the agent and this machine, so you can tell them apart and revoke one without breaking the others. The key is written to your agent's config and nowhere else — this tool never prints it in full, and never sends it anywhere except noslopui.com.
94
+ `init` issues one key per agent, named after the agent and this machine, so you can tell them apart on the account page and revoke one without breaking the others — plus one for the CLI itself if it isn't signed in yet. Keys are written to each agent's config and nowhere else; this tool never prints one in full, and never sends it anywhere except noslopui.com.
55
95
 
56
- `--key <key>` skips the browser and uses a key you already made on the account page, for CI or a machine with no browser.
96
+ `--key <key>` skips the browser and uses a key you already made on the account page, for CI or a machine with no browser. `NOSLOPUI_API_KEY` does the same for the catalog commands.
57
97
 
58
98
  ## The skill
59
99
 
@@ -72,7 +112,7 @@ npm run typecheck
72
112
 
73
113
  `NOSLOPUI_ORIGIN` points the CLI at a different server, which is how the flow is exercised against a local one.
74
114
 
75
- Adding an agent is one module implementing `Adapter` (`src/adapters/types.ts`), one line in the registry, and a real install before it goes in the list.
115
+ Adding an agent is its facts in `src/adapters/json-agents.ts` (or its own module, if its config isn't JSON), one line in the registry, the same id in the server's `CLI_AGENTS`, and a row in `test/agents.test.ts`.
76
116
 
77
117
  ## Licence
78
118
 
package/SKILL.md CHANGED
@@ -11,34 +11,39 @@ AI-generated) UI components, full page examples, and portable Design Systems.
11
11
  ## The rules
12
12
 
13
13
  1. **Choose one design system before fetching any component, then restyle every component to it** — colours, type scale, spacing, radius, motion. One system applied everywhere is what stops a multi-page build reading as a collage.
14
- 2. **Search the whole catalog on fit.** A design system is a set of rules, not an inventory of parts: any component can be built in any system, so nothing is off-limits once one is chosen.
14
+ 2. **Search the catalog before hand-building a section, not instead of hand-building it.** Check `search_components` for every required section first — having the design system's tokens and prose isn't a reason to skip that, and any component can be built in any system, so nothing is off-limits once one is chosen. Only build a section yourself if nothing in the catalog fits it, and tell the user that's what happened.
15
15
  3. **Never substitute a catalog component for one the user explicitly saved** in a collection. Their pick was a decision, not an oversight.
16
16
  4. **Ask about their brand before building** — logo, brand colours, typeface, real photography. Put their values into the system's tokens (their colour replaces `accent`) and keep the system's rules.
17
- 5. **Check for a collection first:** call `list_collections`. If one fits the request, build from it. If none fits, or no key is configured, don't guess a collection — shortlist two or three design systems with `search_design_systems` and let the user pick.
17
+ 5. **A collection existing is not the same as the user asking for it.** `list_collections` is free to call, but only build from what it returns when the user asked for their saved items by name or in substance, or confirmed it when you asked. The default "Favorites" list is just where the save button drops a single item with zero setup — a lone design system sitting in it is not a decision to build the whole thing around. One collection with content and no explicit ask → surface it as a question, don't assume either answer. Nothing confirmed → shortlist two or three design systems with `search_design_systems` and let the user pick.
18
18
  6. **If a gated tool says the trial has ended or the fair-use limit is reached, tell the user and stop** — don't retry, and don't swap in something generated from scratch.
19
+ 7. **A Motion Lab effect is a signature moment, not a section.** Reach for one (`search_components` with `section: "motion_lab"`) when the user asks for an effect, motion, 3D or a showpiece, or the brief clearly calls for one — then use one per page unless they ask for more. Recolour it through its `colorVars` to the design system's tokens, follow its `usage`, and keep its reduced-motion fallback.
19
20
 
20
- Setup, if a tool says no key is configured: the user creates an API key at
21
- `noslopui.com/account` (MCP tab) and adds it to this MCP server's config as an
22
- `Authorization: Bearer <key>` header. Search, metadata and design-system token
23
- tools work without one. Code, prompts and DESIGN.md files need a key on a paid
24
- plan or inside the account's free 3-day trial, which the first such call
25
- starts.
21
+ Setup, if the noslopUI tools aren't available: the user runs
22
+ `npx noslopui@latest init`, or adds `https://noslopui.com/api/mcp` in their
23
+ agent and signs in when it asks (steps for each agent: `noslopui.com/mcp`).
24
+ Every call runs as the user's account. Search, metadata and collections are
25
+ free on every account. Code, prompts and DESIGN.md files need a paid plan or
26
+ the account's free 3-day trial, which the first such call starts.
26
27
 
27
28
  ---
28
29
 
29
30
  ## Step 0 — Which path?
30
31
 
31
- **Call `list_collections` first.** It's free on every account.
32
-
33
- - A relevant collection exists → **Path A**. The user already did the choosing.
34
- - No collection, or nothing relevant → **Path B**.
35
- - It says no key is configured → **Path B**, without calling `get_collection`
36
- (it needs the same key, and a guessed collection name finds nothing). Tell the
37
- user once that code and DESIGN.md files will need a key — see Setup above —
38
- and carry on with everything that works without one.
39
-
40
- Several collections and no obvious match → ask. Don't guess between two curated
41
- sets.
32
+ **Call `list_collections` first.** It's free on every account. But a
33
+ collection existing is not the same as the user asking you to build from it —
34
+ the default "Favorites" list is just where the save button puts a single item
35
+ with zero setup, so a design system sitting there alone is very often a saved
36
+ reference, not a decision. Don't let its mere presence override what the user
37
+ actually asked for.
38
+
39
+ - The user already said to use their saved items ("build from my collection",
40
+ "use what I've saved", names a list by name) → **Path A**, no need to ask.
41
+ - Nothing was said either way, but exactly one collection has content that
42
+ looks relevant → **ask**: name what's in it and let them choose Path A or
43
+ Path B. Don't assume silently in either direction.
44
+ - No collection, or nothing in any of them fits → **Path B**.
45
+ - Several collections and no obvious match → ask which one. Don't guess
46
+ between two curated sets.
42
47
 
43
48
  ---
44
49
 
@@ -68,6 +73,17 @@ Do this on both paths, before choosing a system. Two groups of things.
68
73
  No brand at all → say so plainly and use the design system's own palette. Don't
69
74
  invent a logo.
70
75
 
76
+ No real photography → don't ship a grey box. Use a seeded placeholder from
77
+ [Picsum](https://picsum.photos) — `https://picsum.photos/seed/<brand-slug>-<slot-name>/<w>/<h>`
78
+ — so the same build gets the same photo on every regenerate instead of a new
79
+ random one. Mark every one with `<!-- TODO: Replace with real photo, target
80
+ size <w>x<h> -->` immediately above it, and write alt text for the *intended*
81
+ subject ("Hand-poured ceramic mug, studio lighting"), not the placeholder
82
+ ("Picsum image"), so it's already correct once they swap it in. Skip
83
+ photography altogether where the chosen design system's own register is
84
+ photo-free (type-led, editorial, most SaaS) rather than forcing a stock photo
85
+ in where the system doesn't call for one.
86
+
71
87
  If the user already stated all this, don't re-ask — restate it in one line and
72
88
  move on.
73
89
 
@@ -76,8 +92,9 @@ move on.
76
92
  ## Path A — the user curated a collection
77
93
 
78
94
  `get_collection({ listId })` (or `name`). Every item carries a `kind` —
79
- `design_systems` or `ui_blocks` — plus a top-level `designSystems` array and a
80
- `guidance` line.
95
+ `design_systems`, `ui_blocks` or `motion_lab` (an effect) — plus a top-level
96
+ `designSystems` array and a `guidance` line. A saved effect is a pick like any
97
+ other: use it where it fits, restyled as in Step 3.
81
98
 
82
99
  1. **The collection's design system wins.** One in it → that is the system, no
83
100
  searching; `get_design_system_file` for its DESIGN.md. More than one → ask
@@ -87,8 +104,10 @@ move on.
87
104
  something that covers a section, use it. Always. **Never substitute a
88
105
  catalog component for one they explicitly saved**, even if you found
89
106
  something you think is better — that was their call, not an oversight.
90
- 3. **Fill the gaps with `search_components`**, searching the whole catalog on
91
- fit, and restyle what you find to the collection's design system.
107
+ 3. **Fill the gaps with `search_components`, checked before you hand-build
108
+ anything.** Search the whole catalog on fit, and restyle what you find to
109
+ the collection's design system. Only build a gap section yourself if
110
+ nothing in the catalog fits it, and say so in step 4's report.
92
111
  4. **Report what you filled in.** "Hero, pricing and footer from your
93
112
  collection; you had no testimonial section saved, so I pulled one from the
94
113
  catalog and restyled it." They should never have to diff the result to find
@@ -111,12 +130,27 @@ Then filter with `tags` (ANDed) and/or a free-text `query` built from the tone
111
130
  words plus purpose:
112
131
 
113
132
  ```
114
- search_design_systems({ tags: ["theme:light", "industry:local-business"] })
115
- search_design_systems({ query: "calm minimal saas", tags: ["theme:light"] })
133
+ search_design_systems({ tags: ["industry:hair-beauty"] })
134
+ search_design_systems({ tags: ["industry:dentist", "theme:light"] })
135
+ search_design_systems({ query: "calm minimal", tags: ["industry:saas"] })
116
136
  ```
117
137
 
118
- A system can carry several `industry:` tags — a developer-tools system is
119
- often a SaaS system too — so don't treat one tag as excluding the others.
138
+ `industry:` is a two-level tree, shaped like a template marketplace's: a
139
+ **field** (`food-drink`, `medical`, `home-services`, `technology`…) and, under
140
+ it, **niches** (`restaurant`, `dentist`, `roofing`, `saas`…). Every system
141
+ tagged with a niche is also tagged with its field, so start with the niche if
142
+ the brief names one and widen to the field when that returns fewer than three.
143
+
144
+ **"Local business" is not an industry — name the trade.** A plumber is
145
+ `home-services`, a barber `hair-beauty`, a café `food-drink`, a physio
146
+ `medical`, a yoga studio `wellness`, an accountant `professional-services`.
147
+ Each of those looks and reads differently, and filtering on the specific field
148
+ is what keeps two local businesses from getting the same site.
149
+
150
+ A system can carry several fields — a warm serif system serves a restaurant, a
151
+ wedding planner and a spa alike — so don't treat one tag as excluding the
152
+ others, and don't pick the same system for every brief in a field: read the
153
+ descriptions and match the tone the user described.
120
154
 
121
155
  Every result comes back with its tags, its token summary (colours, fonts,
122
156
  radius) and a `previewUrl`. **Present the 2-3 best matches to the user with
@@ -139,11 +173,32 @@ work.
139
173
 
140
174
  ### B2. Find components on fit
141
175
 
142
- For each required section, call `search_components` — `section`, `framework`
143
- and `tags` filters as needed, with `tags` ANDed the same way
144
- (`["style:editorial"]`, `["animation:carousel"]`). Pick the best
145
- **structural** match: the thing that actually does what the section needs.
146
- Largely ignore how it currently looks; you are restyling it regardless.
176
+ **Never write a section's markup from scratch before checking the catalog.**
177
+ Having the DESIGN.md in hand is not the same as being done searching — for
178
+ *every* required section, call `search_components` first — `section` and
179
+ `tags` filters as needed, with `tags` ANDed the same way (`["style:editorial"]`,
180
+ `["animation:carousel"]`). Pick the best **structural** match: the thing that
181
+ actually does what the section needs. Largely ignore how it currently looks;
182
+ you are restyling it regardless. Only once nothing in the catalog fits a
183
+ section is hand-building it acceptable — and say so, don't pass it off as a
184
+ catalog pick.
185
+
186
+ Every result is published as React/TSX with Tailwind classes, whatever stack
187
+ the build target is. That's not a limitation — the markup and styling aren't
188
+ React-specific, so converting a result to plain HTML, Vue, or anything else is
189
+ a mechanical step (`className` to `class`, drop event-handler props, etc.),
190
+ not a rebuild. Never skip a structurally-right component, or conclude the
191
+ catalog has nothing for a non-React project, over its source format.
192
+
193
+ **Effects are a separate kind.** `section: "motion_lab"` searches Motion Lab:
194
+ shader backgrounds, 3D, liquid glass, text, cursor and scroll effects. An
195
+ effect isn't a section of the page — it's the one moment people remember the
196
+ page for. Use one where the brief asks for it (an effect, motion, 3D, "make it
197
+ pop") or clearly calls for a showpiece, and one per page unless the user asks
198
+ for more. `get_component` tells you how: `usage` says where it earns its
199
+ place, `props` are its knobs, `colorVars` are the CSS custom properties its
200
+ colours come from, and `libraries` are the exact npm packages and versions to
201
+ install.
147
202
 
148
203
  Then `get_component_code` for the ones you'll use.
149
204
 
@@ -168,6 +223,13 @@ result *theirs* rather than a nice demo:
168
223
  Body text stays the system's text face unless they insist.
169
224
  - Their photography replaces placeholder imagery at the same crops and ratios.
170
225
 
226
+ **C. Effects take the system's colours through their variables.** A Motion
227
+ Lab effect exposes each colour as a CSS custom property (`colorVars`, e.g.
228
+ `--ml-accent`) and as a prop. Set those from the system's tokens — the brand
229
+ accent, the darkest surface — and tune its `props` toward the system's motion
230
+ rules (slower if the system is calm). Never recolour inside a shader or
231
+ script, and never remove its reduced-motion fallback.
232
+
171
233
  **Substitute values, keep the rules.** The system's spacing scale, radius
172
234
  discipline, contrast requirements and "one accent moment per screen" all still
173
235
  apply after the swap — that is the point of a system: it survives a brand
@@ -200,7 +262,9 @@ exists to avoid — catch them here rather than shipping them:
200
262
  - **Spacing** — compose from the DESIGN.md's spacing scale; don't invent
201
263
  in-between values. Prefer more whitespace over a denser grid.
202
264
  - **Motion** — only where it communicates a real state change; the DESIGN.md's
203
- durations and easing, never a default bounce or spring.
265
+ durations and easing, never a default bounce or spring. The one exception is
266
+ a Motion Lab effect placed where its `usage` says it earns its place — one
267
+ per page, recoloured to the system.
204
268
  - **Named slop-tells to actively reject**: gradient text, purple-on-purple
205
269
  palettes, bounce/spring easing on everything, every layout centered with no
206
270
  asymmetry, decorative-only animation.
@@ -208,8 +272,8 @@ exists to avoid — catch them here rather than shipping them:
208
272
  ## Step 5 — Point back to the catalog for variations
209
273
 
210
274
  If the result doesn't land, don't re-prompt the same component blind. Point the
211
- user at `noslopui.com/explore` (cross-catalog search across UI Blocks and
212
- Design Systems in one grid) to browse real alternatives, and at the
275
+ user at `noslopui.com/explore` (every UI block in one grid, with Motion Lab and
276
+ design-system shelves) to browse real alternatives, and at the
213
277
  Collections tab on `noslopui.com/account` to save the ones they like — which
214
278
  turns the next build into Path A. Then repeat from Step 3 with whatever they
215
279
  pick.
@@ -222,18 +286,19 @@ pick.
222
286
  list_collections({}) -> { collections: [{ id, name, itemCount }] }
223
287
  get_collection({ listId?, name? }) -> { collection, designSystems: [{id,name}], items: [{ id, name, kind, ... }], guidance }
224
288
 
225
- search_design_systems({ query?, tags?, tag?, limit? }) -> { results: [{ id, name, description, tags, tokens, previewUrl, url }], availableTags }
289
+ search_design_systems({ query?, tags?, tag?, limit? }) -> { results: [{ id, name, description, tags, tokens, previewUrl, url }], availableTags, industryTree }
226
290
  get_design_system({ id }) -> { id, name, tokens, previewUrl, detailUrl }
227
291
  get_design_system_file({ id }) -> { id, name, designMd } [gated: paid or trial]
228
292
 
229
- search_components({ query?, section?, framework?, tags?, tag?, limit? }) -> { results: [{ id, name, kind, tags, url }] }
293
+ search_components({ query?, section?, tags?, tag?, limit? }) -> { results: [{ id, name, kind, tags, url }] }
230
294
  get_component({ id }) -> { id, name, dependencies, builtWithDesignSystem, previewUrl, detailUrl }
295
+ motion_lab items add { usage, props, colorVars, libraries }
231
296
  get_component_code({ id }) -> { id, name, snippets: [{ language, code }] } [gated: paid or trial]
232
297
  get_component_prompt({ id }) -> { id, name, promptText } [gated: paid or trial]
233
298
  ```
234
299
 
235
- The key rides on the connection (the `Authorization` header), so no tool needs
236
- an `apiKey` argument. Every search, metadata and collection tool is free on
300
+ The account rides on the connection (a key or a sign-in, sent as the
301
+ `Authorization` header), so no tool takes a credential argument. Every search, metadata and collection tool is free on
237
302
  every account; `get_design_system_file`, `get_component_code` and
238
303
  `get_component_prompt` need the account on a paid plan or inside its free
239
304
  3-day trial.
@@ -17,130 +17,30 @@
17
17
  // is open while this runs, it can overwrite our entry when it next saves.
18
18
  // That's why install tells the user to quit Claude Code first, and why
19
19
  // `doctor` re-checks rather than trusting that the write stuck.
20
- import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
21
- import { homedir } from 'node:os';
22
- import { delimiter, dirname, join } from 'node:path';
23
- import { backupFile, detectIndent, readJsonFile, writeJsonFile } from '../lib/json-file.js';
24
- /** The name the server appears under. Also what the user types: `/mcp__noslopui__build_ui`. */
25
- export const SERVER_NAME = 'noslopui';
26
- const SKILL_DIR_NAME = 'noslopui';
27
- function onPath(binary) {
28
- const paths = (process.env.PATH ?? '').split(delimiter).filter(Boolean);
29
- const suffixes = process.platform === 'win32' ? (process.env.PATHEXT ?? '.EXE;.CMD;.BAT').split(';') : [''];
30
- for (const dir of paths) {
31
- for (const suffix of suffixes) {
32
- try {
33
- if (existsSync(join(dir, binary + suffix)))
34
- return true;
35
- }
36
- catch {
37
- // An unreadable PATH entry is not our problem; keep looking.
38
- }
39
- }
40
- }
41
- return false;
42
- }
43
- export class ClaudeCodeAdapter {
44
- id = 'claude-code';
45
- label = 'Claude Code';
46
- configPath() {
47
- return join(homedir(), '.claude.json');
20
+ import { join } from 'node:path';
21
+ import { JsonConfigAdapter, SERVER_NAME } from './json-adapter.js';
22
+ import { home } from '../lib/paths.js';
23
+ import { installSkillAt } from '../lib/skill-file.js';
24
+ export { SERVER_NAME };
25
+ export class ClaudeCodeAdapter extends JsonConfigAdapter {
26
+ constructor() {
27
+ super({
28
+ id: 'claude-code',
29
+ label: 'Claude Code',
30
+ configPath: () => join(home(), '.claude.json'),
31
+ container: 'mcpServers',
32
+ urlField: 'url',
33
+ fixed: { type: 'http' },
34
+ detectPaths: () => [join(home(), '.claude.json'), join(home(), '.claude')],
35
+ detectBinaries: ['claude'],
36
+ skillPath: () => join(home(), '.claude', 'skills', 'noslopui', 'SKILL.md'),
37
+ readBackHint: 'If Claude Code is running, quit it and run this again.',
38
+ });
48
39
  }
49
40
  skillPath() {
50
- return join(homedir(), '.claude', 'skills', SKILL_DIR_NAME, 'SKILL.md');
51
- }
52
- async detect() {
53
- try {
54
- return onPath('claude') || existsSync(this.configPath()) || existsSync(join(homedir(), '.claude'));
55
- }
56
- catch {
57
- return false;
58
- }
59
- }
60
- async install(entry) {
61
- const path = this.configPath();
62
- const config = readJsonFile(path) ?? {};
63
- const backupPath = backupFile(path);
64
- const servers = (typeof config.mcpServers === 'object' && config.mcpServers !== null && !Array.isArray(config.mcpServers)
65
- ? config.mcpServers
66
- : {});
67
- const replacedExisting = SERVER_NAME in servers;
68
- // Spread the existing entry so anything Claude Code (or the user) added to
69
- // it that we don't know about survives the update.
70
- const existing = (typeof servers[SERVER_NAME] === 'object' && servers[SERVER_NAME] !== null ? servers[SERVER_NAME] : {});
71
- const existingHeaders = (typeof existing.headers === 'object' && existing.headers !== null ? existing.headers : {});
72
- servers[SERVER_NAME] = {
73
- ...existing,
74
- type: 'http',
75
- url: entry.url,
76
- headers: { ...existingHeaders, Authorization: `Bearer ${entry.key}` },
77
- };
78
- config.mcpServers = servers;
79
- writeJsonFile(path, config, { indent: existsSync(path) ? detectIndent(path) : 2 });
80
- // Read back: this file has another writer, so a successful write is not
81
- // the same as a config that says what we think it says.
82
- const after = await this.status();
83
- if (!after.installed || after.url !== entry.url || !after.hasKey) {
84
- throw new Error(`The entry was written to ${path} but reading it back didn't show it. If Claude Code is running, quit it and run this again.`);
85
- }
86
- return { configPath: path, backupPath, method: 'config-file', replacedExisting };
87
- }
88
- async status() {
89
- const path = this.configPath();
90
- const config = readJsonFile(path);
91
- if (!config)
92
- return { installed: false, configPath: path, url: null, hasKey: false };
93
- const servers = config.mcpServers;
94
- const entry = servers?.[SERVER_NAME];
95
- if (!entry)
96
- return { installed: false, configPath: path, url: null, hasKey: false };
97
- const url = typeof entry.url === 'string' ? entry.url : null;
98
- const headers = (entry.headers ?? {});
99
- const auth = typeof headers.Authorization === 'string' ? headers.Authorization : '';
100
- const hasKey = /^Bearer\s+\S+/i.test(auth);
101
- let problem;
102
- if (!url)
103
- problem = 'The entry has no url.';
104
- else if (!hasKey)
105
- problem = 'The entry has no Authorization header, so gated tools will be refused.';
106
- else if (entry.type !== 'http')
107
- problem = `The entry's type is "${String(entry.type)}", expected "http".`;
108
- return { installed: true, configPath: path, url, hasKey, ...(problem ? { problem } : {}) };
109
- }
110
- async uninstall() {
111
- const path = this.configPath();
112
- const config = readJsonFile(path);
113
- if (!config)
114
- return false;
115
- const servers = config.mcpServers;
116
- if (!servers || !(SERVER_NAME in servers))
117
- return false;
118
- backupFile(path);
119
- delete servers[SERVER_NAME];
120
- writeJsonFile(path, config, { indent: detectIndent(path) });
121
- return true;
41
+ return join(home(), '.claude', 'skills', 'noslopui', 'SKILL.md');
122
42
  }
123
43
  async installSkill(markdown) {
124
- const path = this.skillPath();
125
- if (existsSync(path)) {
126
- try {
127
- if (readFileSync(path, 'utf8') === markdown)
128
- return { path, changed: false };
129
- }
130
- catch {
131
- // Unreadable but present: fall through and replace it.
132
- }
133
- }
134
- mkdirSync(dirname(path), { recursive: true });
135
- writeFileSync(path, markdown, 'utf8');
136
- return { path, changed: true };
137
- }
138
- async removeSkill() {
139
- const path = this.skillPath();
140
- if (!existsSync(path))
141
- return false;
142
- // Only our own folder, and only because we created it.
143
- rmSync(dirname(path), { recursive: true, force: true });
144
- return true;
44
+ return installSkillAt(this.skillPath(), markdown);
145
45
  }
146
46
  }