noslopui 0.0.1 → 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 noslopUI
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,5 +1,79 @@
1
1
  # noslopui
2
2
 
3
- Connect your coding agent to [noslopUI](https://noslopui.com), a catalog of hand-crafted UI components and design systems.
3
+ Connect your coding agent to [noslopUI](https://noslopui.com) hand-crafted UI components and portable design systems, over MCP.
4
4
 
5
- The one-command installer (`npx noslopui init`) is in progress. Until it ships, see [noslopui.com/mcp](https://noslopui.com/mcp) for manual setup.
5
+ ```bash
6
+ npx noslopui@latest init
7
+ ```
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.
10
+
11
+ ## Why
12
+
13
+ Ask any agent for a landing page and you get the same page: gradient headline, purple on purple, three cards, everything centred. noslopUI gives it a real catalog to work from — components someone designed, and design systems that are a set of rules rather than a folder of parts, so a five-page build reads as one site.
14
+
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
+
17
+ ## Commands
18
+
19
+ | | |
20
+ |---|---|
21
+ | `npx noslopui init` | Set up an agent: sign in, add the server, install the skill |
22
+ | `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
+ | `npx noslopui update` | Re-download the skill |
25
+ | `npx noslopui remove` | Take the noslopUI server back out |
26
+
27
+ Options: `--agent <id>`, `--no-skill`, `--no-browser`, `--key <key>`, `-y/--yes`.
28
+
29
+ ## Supported agents
30
+
31
+ **Claude Code.** That's the list.
32
+
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.
34
+
35
+ ## What it does to your machine
36
+
37
+ It edits your agent's user-level config, and nothing else. Concretely, for Claude Code:
38
+
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.
42
+ - **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`.
45
+
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).
47
+
48
+ ### If Claude Code is running
49
+
50
+ `~/.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
+
52
+ ## Your key
53
+
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.
55
+
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.
57
+
58
+ ## The skill
59
+
60
+ The workflow has one source, and it isn't this repo: it's assembled on the server and served at [noslopui.com/skill/SKILL.md](https://noslopui.com/skill/SKILL.md). That's the same text the MCP server sends as its `instructions` on every connection, so an agent gets the rules whether or not the skill is installed.
61
+
62
+ `init` downloads it fresh. The copy in this repo exists so `npx skills add noslopUI/cli` has something to find, and as a fallback when the site is unreachable — `npm run sync-skill` refreshes it and CI fails if it has drifted.
63
+
64
+ ## Development
65
+
66
+ ```bash
67
+ npm install
68
+ npm run build
69
+ npm test # adapter contract tests, against fixture configs in a throwaway HOME
70
+ npm run typecheck
71
+ ```
72
+
73
+ `NOSLOPUI_ORIGIN` points the CLI at a different server, which is how the flow is exercised against a local one.
74
+
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.
76
+
77
+ ## Licence
78
+
79
+ MIT
package/SKILL.md ADDED
@@ -0,0 +1,249 @@
1
+ ---
2
+ name: noslopui
3
+ description: Build a stunning, non-generic UI using noslopUI's hand-crafted catalog over MCP — either from a collection the user curated themselves, or by choosing one Design System and applying it consistently across every component. Use when asked to build a landing page, dashboard, marketing site, or any new page/section from scratch, when the user mentions a noslopUI collection, or when a UI you're building looks like "AI slop" (gradient text, purple-on-purple, bounce easing, everything centered) and needs a real design system instead.
4
+ ---
5
+
6
+ # noslopUI
7
+
8
+ You have access to the **noslopUI MCP server** — a catalog of hand-crafted (not
9
+ AI-generated) UI components, full page examples, and portable Design Systems.
10
+
11
+ ## The rules
12
+
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.
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
+ 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.
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
+
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.
26
+
27
+ ---
28
+
29
+ ## Step 0 — Which path?
30
+
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.
42
+
43
+ ---
44
+
45
+ ## Step 1 — Requirements, including brand
46
+
47
+ Do this on both paths, before choosing a system. Two groups of things.
48
+
49
+ **The build:**
50
+
51
+ 1. **Purpose** — what is this for? (SaaS product, local service business,
52
+ portfolio, storefront, internal dashboard, docs site, …)
53
+ 2. **Audience** — who is it for?
54
+ 3. **Tone** — 2-3 words ("calm and minimal", "bold and playful", "editorial and
55
+ serif-driven", "warm and local"). This is what Step 2 matches against.
56
+ 4. **Required sections** — the concrete list of what must exist.
57
+
58
+ **Their brand** — skip this and you ship a beautiful page that isn't theirs:
59
+
60
+ 5. **Logo or wordmark** — do they have one, and in what format?
61
+ 6. **Brand colours** — hex values if they have them, and whether they are fixed
62
+ (a franchise or a rebrand-in-progress changes the answer).
63
+ 7. **Existing typeface** — anything already in use they need to stay with?
64
+ 8. **Photography or illustration** — real assets to work from, or
65
+ placeholder-and-replace-later?
66
+ 9. **An existing site or profile** to stay consistent with.
67
+
68
+ No brand at all → say so plainly and use the design system's own palette. Don't
69
+ invent a logo.
70
+
71
+ If the user already stated all this, don't re-ask — restate it in one line and
72
+ move on.
73
+
74
+ ---
75
+
76
+ ## Path A — the user curated a collection
77
+
78
+ `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.
81
+
82
+ 1. **The collection's design system wins.** One in it → that is the system, no
83
+ searching; `get_design_system_file` for its DESIGN.md. More than one → ask
84
+ which; they saved both deliberately. None → say so, choose one as in Path B,
85
+ and tell them which and why.
86
+ 2. **Cover each required section from the collection first.** If the user saved
87
+ something that covers a section, use it. Always. **Never substitute a
88
+ catalog component for one they explicitly saved**, even if you found
89
+ 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.
92
+ 4. **Report what you filled in.** "Hero, pricing and footer from your
93
+ collection; you had no testimonial section saved, so I pulled one from the
94
+ catalog and restyled it." They should never have to diff the result to find
95
+ out what they didn't choose.
96
+
97
+ Continue to Step 3.
98
+
99
+ ---
100
+
101
+ ## Path B — you choose
102
+
103
+ ### B1. Shortlist design systems, then ask
104
+
105
+ Design systems are classified by **tag**, not by category — there is no tree to
106
+ walk. Call `search_design_systems` with no arguments first if you don't know
107
+ what exists: it returns every system plus `availableTags`, the full filterable
108
+ vocabulary (`theme:`, `style:`, `industry:` and so on).
109
+
110
+ Then filter with `tags` (ANDed) and/or a free-text `query` built from the tone
111
+ words plus purpose:
112
+
113
+ ```
114
+ search_design_systems({ tags: ["theme:light", "industry:local-business"] })
115
+ search_design_systems({ query: "calm minimal saas", tags: ["theme:light"] })
116
+ ```
117
+
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.
120
+
121
+ Every result comes back with its tags, its token summary (colours, fonts,
122
+ radius) and a `previewUrl`. **Present the 2-3 best matches to the user with
123
+ their names, one-line descriptions, palettes and preview URLs, and ask which
124
+ they want.** Don't pick silently. This single choice shapes every screen, it
125
+ costs one question, and the user has taste you cannot infer from "a roofing
126
+ business".
127
+
128
+ Then `get_design_system` on their pick for the ungated token summary, and
129
+ `get_design_system_file` for the full DESIGN.md.
130
+
131
+ If nothing matches well, say so and ask whether to go with the closest or with
132
+ a system they describe by hand — don't quietly fall back to stock Tailwind
133
+ defaults, which is the "AI slop" this workflow exists to avoid.
134
+
135
+ **Read the whole DESIGN.md before fetching any component.** It's short by
136
+ design — tokens plus a page or two of intent prose, meant to be held in context
137
+ for the entire build. Its **Do's and Don'ts** section is the part doing the real
138
+ work.
139
+
140
+ ### B2. Find components on fit
141
+
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.
147
+
148
+ Then `get_component_code` for the ones you'll use.
149
+
150
+ ---
151
+
152
+ ## Step 3 — Apply the system, and fit the brand into it
153
+
154
+ Two moves, in this order.
155
+
156
+ **A. Restyle everything to the design system.** Same colour variables, same type
157
+ scale, same radius, same spacing scale, same motion durations and easing, across
158
+ every component. Where a fetched component's own defaults conflict with the
159
+ system, the system wins — adapt the component, and never leave both looks in
160
+ play at once.
161
+
162
+ **B. Substitute the brand into the system's tokens.** This is what makes the
163
+ result *theirs* rather than a nice demo:
164
+
165
+ - Their primary colour replaces the system's `accent` token. A secondary, if
166
+ they have one, replaces a supporting token — not a second accent.
167
+ - Their wordmark typeface replaces the `display` face if it suits headlines.
168
+ Body text stays the system's text face unless they insist.
169
+ - Their photography replaces placeholder imagery at the same crops and ratios.
170
+
171
+ **Substitute values, keep the rules.** The system's spacing scale, radius
172
+ discipline, contrast requirements and "one accent moment per screen" all still
173
+ apply after the swap — that is the point of a system: it survives a brand
174
+ change. A roofing company's green goes where the indigo was, and the restraint
175
+ around it is unchanged.
176
+
177
+ **Check contrast after substituting.** A brand colour that fails the system's
178
+ stated contrast pairs is a real problem: tell the user, propose a darker or
179
+ lighter variant of their colour for text, and keep the original for large
180
+ fills. Don't silently ship unreadable text because it was their hex.
181
+
182
+ DESIGN.md files follow the open [DESIGN.md
183
+ format](https://github.com/google-labs-code/design.md): YAML front matter with
184
+ machine-readable tokens (`colors`, `typography`, `rounded`, `spacing`,
185
+ `components`, with `{colors.accent}` references already resolved), then prose
186
+ sections. Take concrete values from the front matter and the rules from the
187
+ prose.
188
+
189
+ ## Step 4 — Polish pass (before calling anything done)
190
+
191
+ Run this against the assembled result. These are the "AI slop" tells noslopUI
192
+ exists to avoid — catch them here rather than shipping them:
193
+
194
+ - **Typography** — one display face used sparingly (headlines only, not buttons
195
+ or body), one text face for everything else. Nothing outside the DESIGN.md's
196
+ stated scale.
197
+ - **Color/contrast** — one accent moment per screen, not accent bleeding into
198
+ every icon, border and badge. Real contrast on text over any background,
199
+ checked against the DESIGN.md's own stated pairs.
200
+ - **Spacing** — compose from the DESIGN.md's spacing scale; don't invent
201
+ in-between values. Prefer more whitespace over a denser grid.
202
+ - **Motion** — only where it communicates a real state change; the DESIGN.md's
203
+ durations and easing, never a default bounce or spring.
204
+ - **Named slop-tells to actively reject**: gradient text, purple-on-purple
205
+ palettes, bounce/spring easing on everything, every layout centered with no
206
+ asymmetry, decorative-only animation.
207
+
208
+ ## Step 5 — Point back to the catalog for variations
209
+
210
+ 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
213
+ Collections tab on `noslopui.com/account` to save the ones they like — which
214
+ turns the next build into Path A. Then repeat from Step 3 with whatever they
215
+ pick.
216
+
217
+ ---
218
+
219
+ ## Quick reference — tool call shape
220
+
221
+ ```
222
+ list_collections({}) -> { collections: [{ id, name, itemCount }] }
223
+ get_collection({ listId?, name? }) -> { collection, designSystems: [{id,name}], items: [{ id, name, kind, ... }], guidance }
224
+
225
+ search_design_systems({ query?, tags?, tag?, limit? }) -> { results: [{ id, name, description, tags, tokens, previewUrl, url }], availableTags }
226
+ get_design_system({ id }) -> { id, name, tokens, previewUrl, detailUrl }
227
+ get_design_system_file({ id }) -> { id, name, designMd } [gated: paid or trial]
228
+
229
+ search_components({ query?, section?, framework?, tags?, tag?, limit? }) -> { results: [{ id, name, kind, tags, url }] }
230
+ get_component({ id }) -> { id, name, dependencies, builtWithDesignSystem, previewUrl, detailUrl }
231
+ get_component_code({ id }) -> { id, name, snippets: [{ language, code }] } [gated: paid or trial]
232
+ get_component_prompt({ id }) -> { id, name, promptText } [gated: paid or trial]
233
+ ```
234
+
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
237
+ every account; `get_design_system_file`, `get_component_code` and
238
+ `get_component_prompt` need the account on a paid plan or inside its free
239
+ 3-day trial.
240
+
241
+ - If a gated call's result carries a `trial` field, that call just started the
242
+ user's trial — tell them, with the end date it gives.
243
+ - A trial covers up to 30 different items per 24 hours, far more than one site
244
+ needs. Fetch what the build uses, not the catalog speculatively; re-fetching
245
+ an item you already opened never counts again.
246
+ - If a gated call says the trial has ended (or the fair-use limit is reached),
247
+ stop and tell the user plainly — don't retry, and don't quietly swap in
248
+ something generated from scratch in its place. Search and collections keep
249
+ working, so the plan itself can still be finished.
@@ -0,0 +1,146 @@
1
+ // Claude Code.
2
+ //
3
+ // User-level MCP servers live at the top level of `~/.claude.json`, under
4
+ // `mcpServers`, as `{ type: 'http', url, headers }`. Project-scoped ones live
5
+ // under `projects["<path>"].mcpServers` — we never write there: a key in a
6
+ // project file is a key that gets committed.
7
+ //
8
+ // WHY WE WRITE THE FILE RATHER THAN SHELLING OUT TO `claude mcp add`:
9
+ // the documented command takes the key as `--header "Authorization: Bearer …"`,
10
+ // which puts the key in the process list for as long as the command runs,
11
+ // where any other process on the machine can read it. Writing the file
12
+ // ourselves keeps the key in two places only — the config and the account.
13
+ // The format is small and we verify our write by reading it back.
14
+ //
15
+ // KNOWN LIMITATION: `~/.claude.json` is also Claude Code's own state file, and
16
+ // a running Claude Code rewrites it from memory on all sorts of events. If it
17
+ // is open while this runs, it can overwrite our entry when it next saves.
18
+ // That's why install tells the user to quit Claude Code first, and why
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');
48
+ }
49
+ 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;
122
+ }
123
+ 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;
145
+ }
146
+ }
@@ -0,0 +1,19 @@
1
+ // The adapter registry.
2
+ //
3
+ // One entry today. Every other agent stays out of this list until it has been
4
+ // installed for real, end to end, on a real machine — an agent that appears
5
+ // here is an agent we're promising works.
6
+ import { ClaudeCodeAdapter } from './claude-code.js';
7
+ export const ADAPTERS = [new ClaudeCodeAdapter()];
8
+ export function adapterById(id) {
9
+ return ADAPTERS.find((a) => a.id === id);
10
+ }
11
+ /** Adapters whose agent is actually on this machine. */
12
+ export async function detectAdapters() {
13
+ const found = [];
14
+ for (const adapter of ADAPTERS) {
15
+ if (await adapter.detect())
16
+ found.push(adapter);
17
+ }
18
+ return found;
19
+ }
@@ -0,0 +1,8 @@
1
+ // The shape every agent adapter fills in.
2
+ //
3
+ // Adding an agent is meant to be this file's worth of work and nothing else:
4
+ // one module implementing `Adapter`, one entry in the registry, one row in the
5
+ // project's Phase 0 table, and one real install before it's offered on the
6
+ // site. Deliberately small, because the interesting part of supporting a new
7
+ // agent is finding out how it actually behaves, not writing code.
8
+ export {};
@@ -0,0 +1,118 @@
1
+ // `noslopui doctor` — what's set up, what's wrong, and what to do about it.
2
+ //
3
+ // The rule for every line: if something is broken, say the one thing that
4
+ // fixes it. A diagnostic that only reports a state leaves the user exactly
5
+ // where they started.
6
+ import { existsSync, readFileSync } from 'node:fs';
7
+ import { checkServer, mcpUrl, origin } from '../lib/api.js';
8
+ import { ADAPTERS } from '../adapters/index.js';
9
+ import { ConfigUnreadableError, readJsonFile } from '../lib/json-file.js';
10
+ import { SERVER_NAME } from '../adapters/claude-code.js';
11
+ import { color, fail, hint, ok, say, warn } from '../lib/ui.js';
12
+ /** Read back the configured key so we can ask the server whether it still works. */
13
+ function configuredKey(configPath) {
14
+ try {
15
+ const config = readJsonFile(configPath);
16
+ const entry = config?.mcpServers?.[SERVER_NAME];
17
+ const headers = (entry?.headers ?? {});
18
+ const auth = typeof headers.Authorization === 'string' ? headers.Authorization : '';
19
+ const match = auth.match(/^Bearer\s+(\S+)$/i);
20
+ return match?.[1] ?? null;
21
+ }
22
+ catch {
23
+ return null;
24
+ }
25
+ }
26
+ export async function doctor() {
27
+ let problems = 0;
28
+ say();
29
+ say(`${color.bold('noslopUI doctor')} ${color.dim(`— ${origin()}`)}`);
30
+ say();
31
+ say(color.bold('Server'));
32
+ const reach = await checkServer();
33
+ if (reach.reachable)
34
+ ok(`${mcpUrl()} — ${reach.detail}`);
35
+ else {
36
+ fail(reach.detail);
37
+ hint('If you are behind a proxy or VPN, that is the usual cause.');
38
+ problems++;
39
+ }
40
+ for (const adapter of ADAPTERS) {
41
+ say();
42
+ say(color.bold(adapter.label));
43
+ if (!(await adapter.detect())) {
44
+ say(`${color.dim('–')} ${color.dim('not installed on this machine')}`);
45
+ continue;
46
+ }
47
+ let status;
48
+ try {
49
+ status = await adapter.status();
50
+ }
51
+ catch (err) {
52
+ if (err instanceof ConfigUnreadableError) {
53
+ fail(err.message);
54
+ hint('Fix that file by hand — this tool will not overwrite a config it cannot read.');
55
+ problems++;
56
+ continue;
57
+ }
58
+ throw err;
59
+ }
60
+ if (!status.installed) {
61
+ warn('noslopUI is not set up here.');
62
+ hint('Run "npx noslopui init".');
63
+ problems++;
64
+ }
65
+ else {
66
+ ok(`Configured in ${status.configPath}`);
67
+ if (status.url !== mcpUrl()) {
68
+ warn(`The configured url is ${status.url}, but this CLI expects ${mcpUrl()}.`);
69
+ hint('Run "npx noslopui init" to update it.');
70
+ problems++;
71
+ }
72
+ if (status.problem) {
73
+ warn(status.problem);
74
+ hint('Run "npx noslopui init" to repair the entry.');
75
+ problems++;
76
+ }
77
+ const key = configuredKey(status.configPath);
78
+ if (key) {
79
+ const probe = await checkServer(key);
80
+ if (probe.keyState === 'ok') {
81
+ ok('The configured key works.');
82
+ }
83
+ else if (probe.keyState === 'rejected') {
84
+ fail(probe.keyDetail ?? 'The configured key was refused.');
85
+ hint(`Create a new key at ${origin()}/account?tab=mcp, or run "npx noslopui login".`);
86
+ problems++;
87
+ }
88
+ }
89
+ }
90
+ const skillPath = adapter.skillPath();
91
+ if (existsSync(skillPath)) {
92
+ let current = '';
93
+ try {
94
+ current = readFileSync(skillPath, 'utf8');
95
+ }
96
+ catch {
97
+ current = '';
98
+ }
99
+ if (current.includes('noslopui'))
100
+ ok(`Skill installed at ${skillPath}`);
101
+ else
102
+ warn(`${skillPath} exists but doesn't look like the noslopUI skill.`);
103
+ }
104
+ else {
105
+ warn('The skill is not installed.');
106
+ hint('Optional, but it is what makes "build me a website" use noslopUI at all. Run "npx noslopui init".');
107
+ }
108
+ }
109
+ say();
110
+ if (problems === 0) {
111
+ ok('Everything checks out.');
112
+ say();
113
+ return 0;
114
+ }
115
+ fail(`${problems} ${problems === 1 ? 'problem' : 'problems'} found.`);
116
+ say();
117
+ return 1;
118
+ }