noslopui 0.0.1 → 0.2.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,119 @@
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 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
+
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
+ ## 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
+
37
+ ## Commands
38
+
39
+ **Set up**
40
+
41
+ | | |
42
+ |---|---|
43
+ | `npx noslopui init` | Set up your agents: one sign-in, then the server and the skill for each |
44
+ | `npx noslopui doctor` | Check what's set up, and say what to do about anything that isn't |
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>`) |
47
+ | `npx noslopui remove` | Take the noslopUI server back out |
48
+
49
+ **Account**
50
+
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 components and blocks (`--tag style:editorial`, `--framework`, `--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 |
69
+
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.
71
+
72
+ Options: `--agent <id>` (repeatable), `--no-skill`, `--no-browser`, `--key <key>`, `--json`, `--force`, `-y/--yes`.
73
+
74
+ ## What it does to your machine
75
+
76
+ It edits your agents' user-level config, and nothing else:
77
+
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.
81
+ - **Never writes to a project config.** A key in a project file is a key that gets committed.
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.
85
+
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).
87
+
88
+ ### If Claude Code is running
89
+
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.
91
+
92
+ ## Your keys
93
+
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.
95
+
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.
97
+
98
+ ## The skill
99
+
100
+ 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.
101
+
102
+ `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.
103
+
104
+ ## Development
105
+
106
+ ```bash
107
+ npm install
108
+ npm run build
109
+ npm test # adapter contract tests, against fixture configs in a throwaway HOME
110
+ npm run typecheck
111
+ ```
112
+
113
+ `NOSLOPUI_ORIGIN` points the CLI at a different server, which is how the flow is exercised against a local one.
114
+
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`.
116
+
117
+ ## Licence
118
+
119
+ MIT
package/SKILL.md ADDED
@@ -0,0 +1,245 @@
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, 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 the noslopUI tools aren't available: the user runs
21
+ `npx noslopui@latest init`, or adds `https://noslopui.com/api/mcp` in their
22
+ agent and signs in when it asks (steps for each agent: `noslopui.com/mcp`).
23
+ Every call runs as the user's account. Search, metadata and collections are
24
+ free on every account. Code, prompts and DESIGN.md files need a paid plan or
25
+ the account's free 3-day trial, which the first such call 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
+
36
+ Several collections and no obvious match → ask. Don't guess between two curated
37
+ sets.
38
+
39
+ ---
40
+
41
+ ## Step 1 — Requirements, including brand
42
+
43
+ Do this on both paths, before choosing a system. Two groups of things.
44
+
45
+ **The build:**
46
+
47
+ 1. **Purpose** — what is this for? (SaaS product, local service business,
48
+ portfolio, storefront, internal dashboard, docs site, …)
49
+ 2. **Audience** — who is it for?
50
+ 3. **Tone** — 2-3 words ("calm and minimal", "bold and playful", "editorial and
51
+ serif-driven", "warm and local"). This is what Step 2 matches against.
52
+ 4. **Required sections** — the concrete list of what must exist.
53
+
54
+ **Their brand** — skip this and you ship a beautiful page that isn't theirs:
55
+
56
+ 5. **Logo or wordmark** — do they have one, and in what format?
57
+ 6. **Brand colours** — hex values if they have them, and whether they are fixed
58
+ (a franchise or a rebrand-in-progress changes the answer).
59
+ 7. **Existing typeface** — anything already in use they need to stay with?
60
+ 8. **Photography or illustration** — real assets to work from, or
61
+ placeholder-and-replace-later?
62
+ 9. **An existing site or profile** to stay consistent with.
63
+
64
+ No brand at all → say so plainly and use the design system's own palette. Don't
65
+ invent a logo.
66
+
67
+ If the user already stated all this, don't re-ask — restate it in one line and
68
+ move on.
69
+
70
+ ---
71
+
72
+ ## Path A — the user curated a collection
73
+
74
+ `get_collection({ listId })` (or `name`). Every item carries a `kind` —
75
+ `design_systems` or `ui_blocks` — plus a top-level `designSystems` array and a
76
+ `guidance` line.
77
+
78
+ 1. **The collection's design system wins.** One in it → that is the system, no
79
+ searching; `get_design_system_file` for its DESIGN.md. More than one → ask
80
+ which; they saved both deliberately. None → say so, choose one as in Path B,
81
+ and tell them which and why.
82
+ 2. **Cover each required section from the collection first.** If the user saved
83
+ something that covers a section, use it. Always. **Never substitute a
84
+ catalog component for one they explicitly saved**, even if you found
85
+ something you think is better — that was their call, not an oversight.
86
+ 3. **Fill the gaps with `search_components`**, searching the whole catalog on
87
+ fit, and restyle what you find to the collection's design system.
88
+ 4. **Report what you filled in.** "Hero, pricing and footer from your
89
+ collection; you had no testimonial section saved, so I pulled one from the
90
+ catalog and restyled it." They should never have to diff the result to find
91
+ out what they didn't choose.
92
+
93
+ Continue to Step 3.
94
+
95
+ ---
96
+
97
+ ## Path B — you choose
98
+
99
+ ### B1. Shortlist design systems, then ask
100
+
101
+ Design systems are classified by **tag**, not by category — there is no tree to
102
+ walk. Call `search_design_systems` with no arguments first if you don't know
103
+ what exists: it returns every system plus `availableTags`, the full filterable
104
+ vocabulary (`theme:`, `style:`, `industry:` and so on).
105
+
106
+ Then filter with `tags` (ANDed) and/or a free-text `query` built from the tone
107
+ words plus purpose:
108
+
109
+ ```
110
+ search_design_systems({ tags: ["theme:light", "industry:local-business"] })
111
+ search_design_systems({ query: "calm minimal saas", tags: ["theme:light"] })
112
+ ```
113
+
114
+ A system can carry several `industry:` tags — a developer-tools system is
115
+ often a SaaS system too — so don't treat one tag as excluding the others.
116
+
117
+ Every result comes back with its tags, its token summary (colours, fonts,
118
+ radius) and a `previewUrl`. **Present the 2-3 best matches to the user with
119
+ their names, one-line descriptions, palettes and preview URLs, and ask which
120
+ they want.** Don't pick silently. This single choice shapes every screen, it
121
+ costs one question, and the user has taste you cannot infer from "a roofing
122
+ business".
123
+
124
+ Then `get_design_system` on their pick for the ungated token summary, and
125
+ `get_design_system_file` for the full DESIGN.md.
126
+
127
+ If nothing matches well, say so and ask whether to go with the closest or with
128
+ a system they describe by hand — don't quietly fall back to stock Tailwind
129
+ defaults, which is the "AI slop" this workflow exists to avoid.
130
+
131
+ **Read the whole DESIGN.md before fetching any component.** It's short by
132
+ design — tokens plus a page or two of intent prose, meant to be held in context
133
+ for the entire build. Its **Do's and Don'ts** section is the part doing the real
134
+ work.
135
+
136
+ ### B2. Find components on fit
137
+
138
+ For each required section, call `search_components` — `section`, `framework`
139
+ and `tags` filters as needed, with `tags` ANDed the same way
140
+ (`["style:editorial"]`, `["animation:carousel"]`). Pick the best
141
+ **structural** match: the thing that actually does what the section needs.
142
+ Largely ignore how it currently looks; you are restyling it regardless.
143
+
144
+ Then `get_component_code` for the ones you'll use.
145
+
146
+ ---
147
+
148
+ ## Step 3 — Apply the system, and fit the brand into it
149
+
150
+ Two moves, in this order.
151
+
152
+ **A. Restyle everything to the design system.** Same colour variables, same type
153
+ scale, same radius, same spacing scale, same motion durations and easing, across
154
+ every component. Where a fetched component's own defaults conflict with the
155
+ system, the system wins — adapt the component, and never leave both looks in
156
+ play at once.
157
+
158
+ **B. Substitute the brand into the system's tokens.** This is what makes the
159
+ result *theirs* rather than a nice demo:
160
+
161
+ - Their primary colour replaces the system's `accent` token. A secondary, if
162
+ they have one, replaces a supporting token — not a second accent.
163
+ - Their wordmark typeface replaces the `display` face if it suits headlines.
164
+ Body text stays the system's text face unless they insist.
165
+ - Their photography replaces placeholder imagery at the same crops and ratios.
166
+
167
+ **Substitute values, keep the rules.** The system's spacing scale, radius
168
+ discipline, contrast requirements and "one accent moment per screen" all still
169
+ apply after the swap — that is the point of a system: it survives a brand
170
+ change. A roofing company's green goes where the indigo was, and the restraint
171
+ around it is unchanged.
172
+
173
+ **Check contrast after substituting.** A brand colour that fails the system's
174
+ stated contrast pairs is a real problem: tell the user, propose a darker or
175
+ lighter variant of their colour for text, and keep the original for large
176
+ fills. Don't silently ship unreadable text because it was their hex.
177
+
178
+ DESIGN.md files follow the open [DESIGN.md
179
+ format](https://github.com/google-labs-code/design.md): YAML front matter with
180
+ machine-readable tokens (`colors`, `typography`, `rounded`, `spacing`,
181
+ `components`, with `{colors.accent}` references already resolved), then prose
182
+ sections. Take concrete values from the front matter and the rules from the
183
+ prose.
184
+
185
+ ## Step 4 — Polish pass (before calling anything done)
186
+
187
+ Run this against the assembled result. These are the "AI slop" tells noslopUI
188
+ exists to avoid — catch them here rather than shipping them:
189
+
190
+ - **Typography** — one display face used sparingly (headlines only, not buttons
191
+ or body), one text face for everything else. Nothing outside the DESIGN.md's
192
+ stated scale.
193
+ - **Color/contrast** — one accent moment per screen, not accent bleeding into
194
+ every icon, border and badge. Real contrast on text over any background,
195
+ checked against the DESIGN.md's own stated pairs.
196
+ - **Spacing** — compose from the DESIGN.md's spacing scale; don't invent
197
+ in-between values. Prefer more whitespace over a denser grid.
198
+ - **Motion** — only where it communicates a real state change; the DESIGN.md's
199
+ durations and easing, never a default bounce or spring.
200
+ - **Named slop-tells to actively reject**: gradient text, purple-on-purple
201
+ palettes, bounce/spring easing on everything, every layout centered with no
202
+ asymmetry, decorative-only animation.
203
+
204
+ ## Step 5 — Point back to the catalog for variations
205
+
206
+ If the result doesn't land, don't re-prompt the same component blind. Point the
207
+ user at `noslopui.com/explore` (cross-catalog search across UI Blocks and
208
+ Design Systems in one grid) to browse real alternatives, and at the
209
+ Collections tab on `noslopui.com/account` to save the ones they like — which
210
+ turns the next build into Path A. Then repeat from Step 3 with whatever they
211
+ pick.
212
+
213
+ ---
214
+
215
+ ## Quick reference — tool call shape
216
+
217
+ ```
218
+ list_collections({}) -> { collections: [{ id, name, itemCount }] }
219
+ get_collection({ listId?, name? }) -> { collection, designSystems: [{id,name}], items: [{ id, name, kind, ... }], guidance }
220
+
221
+ search_design_systems({ query?, tags?, tag?, limit? }) -> { results: [{ id, name, description, tags, tokens, previewUrl, url }], availableTags }
222
+ get_design_system({ id }) -> { id, name, tokens, previewUrl, detailUrl }
223
+ get_design_system_file({ id }) -> { id, name, designMd } [gated: paid or trial]
224
+
225
+ search_components({ query?, section?, framework?, tags?, tag?, limit? }) -> { results: [{ id, name, kind, tags, url }] }
226
+ get_component({ id }) -> { id, name, dependencies, builtWithDesignSystem, previewUrl, detailUrl }
227
+ get_component_code({ id }) -> { id, name, snippets: [{ language, code }] } [gated: paid or trial]
228
+ get_component_prompt({ id }) -> { id, name, promptText } [gated: paid or trial]
229
+ ```
230
+
231
+ The account rides on the connection (a key or a sign-in, sent as the
232
+ `Authorization` header), so no tool takes a credential argument. Every search, metadata and collection tool is free on
233
+ every account; `get_design_system_file`, `get_component_code` and
234
+ `get_component_prompt` need the account on a paid plan or inside its free
235
+ 3-day trial.
236
+
237
+ - If a gated call's result carries a `trial` field, that call just started the
238
+ user's trial — tell them, with the end date it gives.
239
+ - A trial covers up to 30 different items per 24 hours, far more than one site
240
+ needs. Fetch what the build uses, not the catalog speculatively; re-fetching
241
+ an item you already opened never counts again.
242
+ - If a gated call says the trial has ended (or the fair-use limit is reached),
243
+ stop and tell the user plainly — don't retry, and don't quietly swap in
244
+ something generated from scratch in its place. Search and collections keep
245
+ working, so the plan itself can still be finished.
@@ -0,0 +1,46 @@
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 { 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
+ });
39
+ }
40
+ skillPath() {
41
+ return join(home(), '.claude', 'skills', 'noslopui', 'SKILL.md');
42
+ }
43
+ async installSkill(markdown) {
44
+ return installSkillAt(this.skillPath(), markdown);
45
+ }
46
+ }
@@ -0,0 +1,196 @@
1
+ // Codex (the CLI, the IDE extension and the app share one config).
2
+ //
3
+ // User-level MCP servers live in `~/.codex/config.toml`, one table each:
4
+ //
5
+ // [mcp_servers.noslopui]
6
+ // url = "https://noslopui.com/api/mcp"
7
+ // http_headers = { "Authorization" = "Bearer nsui_…" }
8
+ //
9
+ // Checked against learn.chatgpt.com/docs/extend/mcp (2026-09-21): `url`,
10
+ // `http_headers` (a static map) and `bearer_token_env_var` are all supported.
11
+ // We write the static header, like every other agent, so the key lives in the
12
+ // config and nowhere else — an env var would have to be set in every shell
13
+ // Codex is ever started from, which is exactly the fragility this tool exists
14
+ // to remove.
15
+ //
16
+ // NO TOML PARSER. This package has zero runtime dependencies, so instead of
17
+ // parsing the file we edit one table and leave every other byte as it was:
18
+ // find `[mcp_servers.noslopui]` (and any `[mcp_servers.noslopui.*]`
19
+ // sub-tables), replace our keys in it, keep any other key the user put there.
20
+ // If our server appears in a form we don't recognise — an inline table under
21
+ // `[mcp_servers]`, or a dotted key — we stop and say so, rather than guess.
22
+ // Skills: Codex reads user-level skills from ~/.agents/skills
23
+ // (learn.chatgpt.com/docs/build-skills).
24
+ import { existsSync, readFileSync } from 'node:fs';
25
+ import { join } from 'node:path';
26
+ import { backupFile, ConfigUnreadableError, writeTextFile } from '../lib/json-file.js';
27
+ import { home, onPath, sharedSkillPath } from '../lib/paths.js';
28
+ import { installSkillAt, removeSkillAt } from '../lib/skill-file.js';
29
+ import { SERVER_NAME } from './json-adapter.js';
30
+ /** `[mcp_servers.noslopui]` or one of its sub-tables, in any quoting TOML allows. */
31
+ const OUR_HEADER = new RegExp(`^\\s*\\[\\s*mcp_servers\\s*\\.\\s*(?:${SERVER_NAME}|"${SERVER_NAME}"|'${SERVER_NAME}')\\s*(\\]|\\.)`);
32
+ /** `[mcp_servers.noslopui.http_headers]` — replaced by our inline map, or TOML would see the key twice. */
33
+ const OUR_HEADERS_SUBTABLE = new RegExp(`^\\s*\\[\\s*mcp_servers\\s*\\.\\s*["']?${SERVER_NAME}["']?\\s*\\.\\s*["']?http_headers["']?\\s*\\]`);
34
+ const ANY_HEADER = /^\s*\[/;
35
+ /** Our server declared some other way: `noslopui = {…}` or `mcp_servers.noslopui.url = …`. */
36
+ const FOREIGN_FORM = new RegExp(`^\\s*(?:mcp_servers\\s*\\.\\s*)?["']?${SERVER_NAME}["']?\\s*(?:=|\\.)`);
37
+ /** Keys we own inside our table; every other key the user wrote there is kept. */
38
+ const OUR_KEYS = /^\s*(url|http_headers|bearer_token_env_var)\s*=/;
39
+ const tomlString = (s) => `"${s.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`;
40
+ function parse(path) {
41
+ if (!existsSync(path))
42
+ return null;
43
+ let raw;
44
+ try {
45
+ raw = readFileSync(path, 'utf8');
46
+ }
47
+ catch (err) {
48
+ throw new ConfigUnreadableError(path, err);
49
+ }
50
+ const eol = raw.includes('\r\n') ? '\r\n' : '\n';
51
+ const lines = raw.split(/\r?\n/);
52
+ const blocks = [];
53
+ let open = null;
54
+ lines.forEach((line, i) => {
55
+ if (ANY_HEADER.test(line)) {
56
+ if (open !== null) {
57
+ blocks.push([open, i]);
58
+ open = null;
59
+ }
60
+ if (OUR_HEADER.test(line))
61
+ open = i;
62
+ return;
63
+ }
64
+ if (open === null && !line.trim().startsWith('#') && FOREIGN_FORM.test(line)) {
65
+ throw new ConfigUnreadableError(path, new Error(`"${SERVER_NAME}" is declared in a form this tool doesn't edit`));
66
+ }
67
+ });
68
+ if (open !== null)
69
+ blocks.push([open, lines.length]);
70
+ return { lines, eol, blocks };
71
+ }
72
+ function ourLines(parsed) {
73
+ return parsed.blocks.flatMap(([a, b]) => parsed.lines.slice(a, b));
74
+ }
75
+ export class CodexAdapter {
76
+ id = 'codex';
77
+ label = 'Codex';
78
+ configPath() {
79
+ return join(home(), '.codex', 'config.toml');
80
+ }
81
+ skillPath() {
82
+ return sharedSkillPath();
83
+ }
84
+ async detect() {
85
+ try {
86
+ return onPath('codex') || existsSync(join(home(), '.codex'));
87
+ }
88
+ catch {
89
+ return false;
90
+ }
91
+ }
92
+ async install(entry) {
93
+ const path = this.configPath();
94
+ const parsed = parse(path);
95
+ const backupPath = backupFile(path);
96
+ const lines = parsed?.lines ?? [];
97
+ const eol = parsed?.eol ?? '\n';
98
+ const blocks = parsed?.blocks ?? [];
99
+ const replacedExisting = blocks.length > 0;
100
+ // Keys the user added to our main table (enabled, tool_timeout_sec, …)
101
+ // and any sub-table other than http_headers survive the rewrite.
102
+ const kept = [];
103
+ const keptSubtables = [];
104
+ for (const [a, b] of blocks) {
105
+ const header = lines[a] ?? '';
106
+ const body = lines.slice(a + 1, b);
107
+ if (OUR_HEADERS_SUBTABLE.test(header))
108
+ continue;
109
+ if (/\]\s*(#.*)?$/.test(header) && new RegExp(`${SERVER_NAME}["']?\\s*\\]`).test(header)) {
110
+ kept.push(...body.filter((l) => !OUR_KEYS.test(l)));
111
+ }
112
+ else {
113
+ keptSubtables.push(header, ...body);
114
+ }
115
+ }
116
+ while (kept.length && !kept[kept.length - 1].trim())
117
+ kept.pop();
118
+ const block = [
119
+ `[mcp_servers.${SERVER_NAME}]`,
120
+ `url = ${tomlString(entry.url)}`,
121
+ `http_headers = { "Authorization" = ${tomlString(`Bearer ${entry.key}`)} }`,
122
+ ...kept,
123
+ '',
124
+ ...keptSubtables,
125
+ ];
126
+ while (block.length && !block[block.length - 1].trim())
127
+ block.pop();
128
+ let out;
129
+ if (blocks.length) {
130
+ const first = blocks[0][0];
131
+ const inBlocks = (i) => blocks.some(([a, b]) => i >= a && i < b);
132
+ out = [...lines.slice(0, first).filter((_, i) => !inBlocks(i)), ...block, '', ...lines.slice(first).filter((_, j) => !inBlocks(first + j))];
133
+ }
134
+ else {
135
+ const body = [...lines];
136
+ while (body.length && !body[body.length - 1].trim())
137
+ body.pop();
138
+ out = body.length ? [...body, '', ...block] : [...block];
139
+ }
140
+ while (out.length && !out[out.length - 1].trim())
141
+ out.pop();
142
+ writeTextFile(path, out.join(eol) + eol);
143
+ const after = await this.status();
144
+ if (!after.installed || after.url !== entry.url || !after.hasKey) {
145
+ throw new Error(`The entry was written to ${path} but reading it back didn't show it.`);
146
+ }
147
+ return { configPath: path, backupPath, method: 'config-file', replacedExisting };
148
+ }
149
+ async status() {
150
+ const path = this.configPath();
151
+ const parsed = parse(path);
152
+ if (!parsed || !parsed.blocks.length)
153
+ return { installed: false, configPath: path, url: null, hasKey: false };
154
+ const text = ourLines(parsed).join('\n');
155
+ const url = text.match(/^\s*url\s*=\s*"([^"]*)"/m)?.[1] ?? null;
156
+ const hasKey = /"?Authorization"?\s*=\s*"Bearer\s+\S+"/i.test(text) || /^\s*bearer_token_env_var\s*=/m.test(text);
157
+ let problem;
158
+ if (!url)
159
+ problem = 'The entry has no url.';
160
+ else if (!hasKey)
161
+ problem = 'The entry has no Authorization header, so Codex will be asked to sign in ("codex mcp login noslopui").';
162
+ return { installed: true, configPath: path, url, hasKey, ...(problem ? { problem } : {}) };
163
+ }
164
+ async configuredKey() {
165
+ try {
166
+ const parsed = parse(this.configPath());
167
+ if (!parsed)
168
+ return null;
169
+ return ourLines(parsed).join('\n').match(/"?Authorization"?\s*=\s*"Bearer\s+([^"\s]+)"/i)?.[1] ?? null;
170
+ }
171
+ catch {
172
+ return null;
173
+ }
174
+ }
175
+ async uninstall() {
176
+ const path = this.configPath();
177
+ const parsed = parse(path);
178
+ if (!parsed || !parsed.blocks.length)
179
+ return false;
180
+ backupFile(path);
181
+ const inBlocks = (i) => parsed.blocks.some(([a, b]) => i >= a && i < b);
182
+ const out = parsed.lines.filter((_, i) => !inBlocks(i));
183
+ // Collapse the blank lines our table leaves behind.
184
+ const tidy = out.filter((l, i) => l.trim() || (out[i - 1]?.trim() ?? '') !== '');
185
+ while (tidy.length && !tidy[tidy.length - 1].trim())
186
+ tidy.pop();
187
+ writeTextFile(path, tidy.length ? tidy.join(parsed.eol) + parsed.eol : '');
188
+ return true;
189
+ }
190
+ async installSkill(markdown) {
191
+ return installSkillAt(this.skillPath(), markdown);
192
+ }
193
+ async removeSkill() {
194
+ return removeSkillAt(this.skillPath());
195
+ }
196
+ }
@@ -0,0 +1,30 @@
1
+ // The adapter registry — every agent `npx noslopui init` can set up.
2
+ //
3
+ // The server keeps the same list (CLI_AGENTS in the website's lib/cli-auth.ts)
4
+ // and refuses a key for any agent not on it, so the two change together.
5
+ // Order is the order they're offered in.
6
+ import { ClaudeCodeAdapter } from './claude-code.js';
7
+ import { CodexAdapter } from './codex.js';
8
+ import { antigravity, cursor, devin, opencode, vscode, windsurf } from './json-agents.js';
9
+ export const ADAPTERS = [
10
+ new ClaudeCodeAdapter(),
11
+ new CodexAdapter(),
12
+ cursor(),
13
+ vscode(),
14
+ devin(),
15
+ windsurf(),
16
+ opencode(),
17
+ antigravity(),
18
+ ];
19
+ export function adapterById(id) {
20
+ return ADAPTERS.find((a) => a.id === id);
21
+ }
22
+ /** Adapters whose agent is actually on this machine. */
23
+ export async function detectAdapters() {
24
+ const found = [];
25
+ for (const adapter of ADAPTERS) {
26
+ if (await adapter.detect())
27
+ found.push(adapter);
28
+ }
29
+ return found;
30
+ }