noslopui 0.1.0 → 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/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 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 |
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
@@ -14,15 +14,15 @@ AI-generated) UI components, full page examples, and portable Design Systems.
14
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
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. **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
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
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.
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
26
 
27
27
  ---
28
28
 
@@ -32,10 +32,6 @@ starts.
32
32
 
33
33
  - A relevant collection exists → **Path A**. The user already did the choosing.
34
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
35
 
40
36
  Several collections and no obvious match → ask. Don't guess between two curated
41
37
  sets.
@@ -232,8 +228,8 @@ get_component_code({ id }) -> { id, name
232
228
  get_component_prompt({ id }) -> { id, name, promptText } [gated: paid or trial]
233
229
  ```
234
230
 
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
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
237
233
  every account; `get_design_system_file`, `get_component_code` and
238
234
  `get_component_prompt` need the account on a paid plan or inside its free
239
235
  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
  }
@@ -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
+ }
@@ -1,10 +1,21 @@
1
- // The adapter registry.
1
+ // The adapter registry — every agent `npx noslopui init` can set up.
2
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.
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
6
  import { ClaudeCodeAdapter } from './claude-code.js';
7
- export const ADAPTERS = [new ClaudeCodeAdapter()];
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
+ ];
8
19
  export function adapterById(id) {
9
20
  return ADAPTERS.find((a) => a.id === id);
10
21
  }