@1tool/js-boost 1.1.0 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -13,9 +13,11 @@ Instead of manually maintaining separate instruction files for each AI agent, yo
13
13
  ├── guidelines/
14
14
  │ ├── general.md ← coding conventions
15
15
  │ └── testing.md ← testing standards
16
- └── skills/
17
- └── my-skill/
18
- └── SKILL.md ← on-demand skill (loaded when relevant)
16
+ ├── skills/
17
+ └── my-skill/
18
+ └── SKILL.md ← on-demand skill (loaded when relevant)
19
+ └── mcp/
20
+ └── mcp.json ← MCP server definitions
19
21
  ```
20
22
 
21
23
  Run `npx @1tool/js-boost generate` and get the right file for each configured agent:
@@ -70,37 +72,43 @@ npx @1tool/js-boost agents
70
72
 
71
73
  ### `@1tool/js-boost mcp`
72
74
 
73
- Interactive wizard to add, remove, or toggle MCP servers. Changes are saved to `js-boost.config.json` run `generate` afterwards to apply them to agent files.
75
+ Interactive wizard for managing MCP servers. Team server definitions are stored in `.ai/mcp/mcp.json` (committed). Per-developer enable/disable state is stored in `.js-boost.json` (gitignored). Run `generate` afterwards to apply changes to agent files.
74
76
 
75
77
  ```bash
76
78
  npx @1tool/js-boost mcp
77
79
  ```
78
80
 
79
- **Add a remote server:**
81
+ The wizard offers three actions:
82
+
83
+ **Add a remote server** — HTTP/SSE endpoint with optional auth headers:
80
84
  ```
81
- ✔ Server key: my-api
82
- ✔ Server type: Remote (HTTP / SSE url)
83
- ✔ URL: https://my-mcp.com/mcp
84
- Description (optional): Internal API tools
85
+ ✔ Server key my-api
86
+ ✔ Server type Remote (HTTP / SSE url)
87
+ ✔ URL https://my-mcp.com/mcp
88
+ Headers Authorization: Bearer YOUR_TOKEN_HERE
89
+ ✔ Description Internal API tools
85
90
  ```
86
91
 
87
- **Add a local (stdio) server:**
92
+ **Add a local server** — stdio process with optional args and env vars:
88
93
  ```
89
- ✔ Server key: local-tools
90
- ✔ Server type: Local (stdio process)
91
- ✔ Command: node
92
- ✔ Arguments: ./mcp-server.js --port 3000
93
- ✔ Environment variables: API_KEY=secret,NODE_ENV=production
94
+ ✔ Server key local-tools
95
+ ✔ Server type Local (stdio process)
96
+ ✔ Command node
97
+ ✔ Arguments ./mcp-server.js --port 3000
98
+ ✔ Environment variables API_KEY=secret,NODE_ENV=production
94
99
  ```
95
100
 
101
+ **Enable / disable servers locally** — multiselect over all configured servers. Unchecked servers are added to `disabledMcpServers` in `.js-boost.json` and excluded from your generated files without affecting teammates.
102
+
96
103
  ### `@1tool/js-boost generate`
97
104
 
98
- Reads `.ai/guidelines/*.md` and `.ai/skills/*/SKILL.md`, then generates files for all selected agents. Falls back to generating all supported formats if no agents are configured.
105
+ Reads `.ai/guidelines/*.md` and `.ai/skills/*/SKILL.md`, then generates files for all selected agents. On first run (no `.js-boost.json`), prompts for agent selection inline.
99
106
 
100
107
  ```bash
101
108
  npx @1tool/js-boost generate
102
- npx @1tool/js-boost gen # alias
103
- npx @1tool/js-boost generate --verbose
109
+ npx @1tool/js-boost gen # alias
110
+ npx @1tool/js-boost generate --verbose # show skipped files
111
+ npx @1tool/js-boost generate --agents claude_code,cursor # CI one-off, not saved
104
112
  ```
105
113
 
106
114
  ### `@1tool/js-boost watch`
@@ -141,36 +149,71 @@ During `init` (and `agents`), installed agents are pre-selected automatically ba
141
149
 
142
150
  ## Configuration
143
151
 
144
- `js-boost.config.json` is managed by the CLI commands (`init`, `agents`, `mcp`) and committed to your repo.
152
+ ### `.ai/mcp/mcp.json` team MCP servers
153
+
154
+ Committed to the repo. Defines the MCP servers available to the whole team. Managed by `js-boost mcp`.
145
155
 
146
156
  ```json
147
157
  {
148
- "projectName": "my-app",
149
- "projectDescription": "",
150
- "agents": ["claude_code", "cursor", "codex"],
151
158
  "mcpServers": {
152
- "my-api": {
153
- "type": "remote",
159
+ "my-remote": {
160
+ "type": "http",
154
161
  "url": "https://my-mcp.com/mcp",
155
- "description": "Internal API tools"
162
+ "headers": {
163
+ "Authorization": "Bearer YOUR_TOKEN_HERE"
164
+ }
156
165
  },
157
- "local-tools": {
158
- "type": "stdio",
166
+ "my-local": {
159
167
  "command": "node",
160
- "args": ["./mcp-server.js"],
161
- "env": { "API_KEY": "secret" }
168
+ "args": ["./mcp-server.js", "--port", "3000"],
169
+ "env": {
170
+ "API_KEY": "secret"
171
+ }
172
+ },
173
+ "npm-package": {
174
+ "command": "npx",
175
+ "args": ["-y", "@some/mcp-server"]
162
176
  }
163
- },
164
- "disableMcpServers": []
177
+ }
165
178
  }
166
179
  ```
167
180
 
168
- MCP servers are written to the right format per agent:
181
+ Server types:
169
182
 
170
- | Agent | Format | Remote servers |
171
- |---|---|---|
172
- | Claude Code, Codex | `.mcp.json` | wrapped in `mcp-remote` |
173
- | Junie | `.junie/mcp.json` | referenced by URL directly |
183
+ | Field | When to use |
184
+ |---|---|
185
+ | `type: "http"` + `url` | Remote HTTP/SSE server |
186
+ | `command` (no type) | Local stdio process |
187
+ | `headers` | Auth headers for remote servers (e.g. `Authorization`) |
188
+ | `args` | Command-line arguments |
189
+ | `env` | Environment variables injected into the process |
190
+
191
+ Remote servers are written differently per agent:
192
+
193
+ | Agent file | Remote format |
194
+ |---|---|
195
+ | `.mcp.json` (Claude Code, Codex) | Wrapped in `mcp-remote` with `--header` args |
196
+ | `.junie/mcp.json` | URL referenced directly |
197
+
198
+ ### `.js-boost.json` — per-developer config
199
+
200
+ Gitignored. Created by `js-boost init`, updated by `js-boost agents` and `js-boost mcp`.
201
+
202
+ ```json
203
+ {
204
+ "agents": ["claude_code", "cursor", "codex"],
205
+ "guidelines": true,
206
+ "skills": ["example-skill"],
207
+ "disabledMcpServers": ["my-remote"]
208
+ }
209
+ ```
210
+
211
+ | Field | Description |
212
+ |---|---|
213
+ | `agents` | Which agents to generate files for |
214
+ | `guidelines` | Set to `true` after first successful generate |
215
+ | `skills` | Snapshot of skill names at last generate |
216
+ | `disabledMcpServers` | Server keys to exclude from your generated files |
174
217
 
175
218
  ---
176
219
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@1tool/js-boost",
3
- "version": "1.1.0",
3
+ "version": "1.3.0",
4
4
  "description": "Laravel Boost-inspired CLI for JavaScript projects — generates agent files from your .ai/ folder",
5
5
  "main": "src/index.js",
6
6
  "type": "module",
package/src/cli.js CHANGED
@@ -7,13 +7,13 @@ import { watch } from './watch.js';
7
7
 
8
8
  program
9
9
  .name('js-boost')
10
- .description('Generate agent files (AGENTS.md, CLAUDE.md, .mcp.json, Junie, Cursor) from your .ai/ folder')
11
- .version('1.0.0');
10
+ .description('Generate agent files from your .ai/ folder')
11
+ .version('1.2.0');
12
12
 
13
13
  // ─── js-boost init ───────────────────────────────────────────────────────────
14
14
  program
15
15
  .command('init')
16
- .description('Scaffold the .ai/ folder with example guidelines and skills')
16
+ .description('Scaffold .ai/ folder, select agents, create .js-boost.json')
17
17
  .option('--force', 'Overwrite existing files')
18
18
  .option('--dir <path>', 'Project directory', process.cwd())
19
19
  .action(async (options) => {
@@ -25,13 +25,14 @@ program
25
25
  program
26
26
  .command('generate')
27
27
  .alias('gen')
28
- .description('Generate all agent files from .ai/guidelines/ and .ai/skills/')
28
+ .description('Generate agent files from .ai/guidelines/ and .ai/skills/')
29
29
  .option('--dir <path>', 'Project directory', process.cwd())
30
+ .option('--agents <list>', 'Comma-separated agent keys — one-off override, not saved (e.g. claude_code,cursor)')
30
31
  .option('--verbose', 'Show skipped files')
31
32
  .action(async (options) => {
32
33
  const projectDir = path.resolve(options.dir);
33
34
  try {
34
- await generate(projectDir, { verbose: options.verbose });
35
+ await generate(projectDir, { verbose: options.verbose, agents: options.agents });
35
36
  } catch (err) {
36
37
  console.error(chalk.red('Error:'), err.message);
37
38
  process.exit(1);
@@ -51,29 +52,26 @@ program
51
52
  // ─── js-boost agents ─────────────────────────────────────────────────────────
52
53
  program
53
54
  .command('agents')
54
- .description('Select which AI agents to configure (updates js-boost.config.json)')
55
+ .description('Re-select AI agents and update .js-boost.json')
55
56
  .option('--dir <path>', 'Project directory', process.cwd())
56
57
  .action(async (options) => {
57
58
  const projectDir = path.resolve(options.dir);
58
- const { readConfig, writeFile } = await import('./utils/reader.js');
59
- const configPath = `${projectDir}/js-boost.config.json`;
60
- const config = readConfig(projectDir);
59
+ const { readLocalConfig, writeLocalConfig } = await import('./utils/reader.js');
60
+ const localConfig = readLocalConfig(projectDir);
61
61
 
62
- const selected = await selectAgents(projectDir, config.agents ?? null);
63
- config.agents = selected;
62
+ const selected = await selectAgents(projectDir, localConfig.agents ?? null);
63
+ writeLocalConfig(projectDir, { ...localConfig, agents: selected });
64
64
 
65
- writeFile(configPath, JSON.stringify(config, null, 2));
66
65
  console.log('');
67
- console.log(chalk.green(' ✓ Agent configuration saved to js-boost.config.json'));
68
- console.log('');
69
- console.log(chalk.dim(` Selected: ${selected.join(', ')}`));
66
+ console.log(chalk.green(' ✓ Saved to .js-boost.json'));
67
+ console.log(chalk.dim(` Agents: ${selected.join(', ')}`));
70
68
  console.log('');
71
69
  });
72
70
 
73
71
  // ─── js-boost mcp ────────────────────────────────────────────────────────────
74
72
  program
75
73
  .command('mcp')
76
- .description('Configure MCP servers — add, remove, or toggle built-in defaults')
74
+ .description('Configure MCP servers — add/remove team servers or toggle built-in defaults')
77
75
  .option('--dir <path>', 'Project directory', process.cwd())
78
76
  .action(async (options) => {
79
77
  const projectDir = path.resolve(options.dir);
@@ -84,22 +82,22 @@ program
84
82
  // ─── js-boost status ─────────────────────────────────────────────────────────
85
83
  program
86
84
  .command('status')
87
- .description('Show what .ai/ contains and what would be generated')
85
+ .description('Show configured agents, guidelines, skills, MCP servers, and output files')
88
86
  .option('--dir <path>', 'Project directory', process.cwd())
89
87
  .action(async (options) => {
90
88
  const projectDir = path.resolve(options.dir);
91
- const { readGuidelines, readSkills, readConfig } = await import('./utils/reader.js');
89
+ const { readGuidelines, readSkills, readLocalConfig, readMcpConfig } = await import('./utils/reader.js');
92
90
  const { buildMcpServers } = await import('./utils/mcp.js');
93
91
  const { AGENTS, AGENTS_MD_CONSUMERS, MCP_JSON_CONSUMERS } = await import('./agents.js');
94
92
  const aiDir = path.join(projectDir, '.ai');
95
- const config = readConfig(projectDir);
96
93
 
94
+ const localConfig = readLocalConfig(projectDir);
97
95
  const guidelines = await readGuidelines(aiDir);
98
96
  const skills = await readSkills(aiDir);
99
- const mcpServers = buildMcpServers(config);
97
+ const mcpServers = buildMcpServers(readMcpConfig(aiDir), localConfig);
100
98
 
101
99
  const allAgentKeys = Object.keys(AGENTS);
102
- const activeAgents = new Set(config.agents ?? allAgentKeys);
100
+ const activeAgents = new Set(localConfig.agents ?? allAgentKeys);
103
101
  const has = (key) => activeAgents.has(key);
104
102
  const hasAny = (keys) => keys.some(k => activeAgents.has(k));
105
103
 
@@ -113,12 +111,12 @@ program
113
111
  const active = activeAgents.has(key);
114
112
  const icon = active ? chalk.green('✓') : chalk.dim('–');
115
113
  const label = active ? chalk.cyan(agent.name) : chalk.dim(agent.name);
116
- const hint = chalk.dim(`(${agent.hint})`);
117
- console.log(` ${icon} ${label} ${hint}`);
114
+ console.log(` ${icon} ${label} ${chalk.dim(`(${agent.hint})`)}`);
118
115
  }
119
- if (config.agents == null) console.log(chalk.dim(' (all agents — run `js-boost agents` to configure)'));
116
+ if (!localConfig.agents) console.log(chalk.dim(' (none configured — run `js-boost init` or `js-boost agents`)'));
120
117
  console.log('');
121
118
 
119
+ // Guidelines
122
120
  console.log(chalk.bold(' Guidelines') + chalk.dim(` (${guidelines.length})`));
123
121
  for (const g of guidelines) {
124
122
  console.log(` ${chalk.green('•')} ${chalk.cyan(g.filename)} — ${chalk.dim(g.title)}`);
@@ -126,6 +124,7 @@ program
126
124
  if (guidelines.length === 0) console.log(chalk.dim(' none — add .ai/guidelines/*.md'));
127
125
  console.log('');
128
126
 
127
+ // Skills
129
128
  console.log(chalk.bold(' Skills') + chalk.dim(` (${skills.length})`));
130
129
  for (const s of skills) {
131
130
  console.log(` ${chalk.green('•')} ${chalk.cyan(s.name)} — ${chalk.dim(s.description || s.dir)}`);
@@ -133,21 +132,24 @@ program
133
132
  if (skills.length === 0) console.log(chalk.dim(' none — add .ai/skills/<name>/SKILL.md'));
134
133
  console.log('');
135
134
 
135
+ // MCP servers
136
136
  console.log(chalk.bold(' MCP Servers') + chalk.dim(` (${Object.keys(mcpServers).length})`));
137
137
  for (const [key, srv] of Object.entries(mcpServers)) {
138
- const url = srv.url || `${srv.command} ${(srv.args || []).join(' ')}`;
139
- console.log(` ${chalk.green('•')} ${chalk.cyan(key)} — ${chalk.dim(url)}`);
138
+ const addr = srv.type === 'http' ? srv.url : `${srv.command} ${(srv.args || []).join(' ')}`;
139
+ console.log(` ${chalk.green('•')} ${chalk.cyan(key)} — ${chalk.dim(addr)}`);
140
140
  }
141
+ if (Object.keys(mcpServers).length === 0) console.log(chalk.dim(' none — run `js-boost mcp` to configure'));
141
142
  console.log('');
142
143
 
144
+ // Will generate
143
145
  console.log(chalk.bold(' Will generate:'));
144
146
  const willGenerate = [
145
- hasAny(AGENTS_MD_CONSUMERS) && ['AGENTS.md', 'Amp, Codex, Copilot, Gemini, OpenCode'],
146
- has('claude_code') && ['CLAUDE.md', 'Claude Code'],
147
- hasAny(MCP_JSON_CONSUMERS) && ['.mcp.json', 'Claude Code + Codex MCP'],
147
+ hasAny(AGENTS_MD_CONSUMERS) && ['AGENTS.md', 'Amp, Codex, Copilot, Gemini, OpenCode'],
148
+ has('claude_code') && ['CLAUDE.md', 'Claude Code'],
149
+ hasAny(MCP_JSON_CONSUMERS) && ['.mcp.json', 'Claude Code + Codex'],
148
150
  has('junie') && ['.junie/guidelines.md + .junie/mcp.json', 'JetBrains Junie'],
149
151
  has('cursor') && ['.cursor/rules/js-boost.mdc + .cursorrules', 'Cursor'],
150
- has('kiro') && ['.kiro/steering/guidelines.md', 'Kiro'],
152
+ has('kiro') && ['.kiro/steering/guidelines.md', 'Kiro'],
151
153
  ].filter(Boolean);
152
154
 
153
155
  for (const [file, label] of willGenerate) {
@@ -2,11 +2,11 @@ import path from 'path';
2
2
  import chalk from 'chalk';
3
3
  import { text, select, multiselect, isCancel } from '@clack/prompts';
4
4
  import { DEFAULT_MCP_SERVERS } from '../utils/mcp.js';
5
- import { readConfig, writeFile } from '../utils/reader.js';
5
+ import { readMcpConfig, writeMcpConfig, readLocalConfig, writeLocalConfig } from '../utils/reader.js';
6
6
 
7
- function displayServers(config) {
8
- const userServers = config.mcpServers || {};
9
- const disabled = new Set(config.disableMcpServers || []);
7
+ function displayServers(mcpConfig, localConfig) {
8
+ const userServers = mcpConfig.mcpServers || {};
9
+ const disabled = new Set(localConfig.disabledMcpServers || []);
10
10
  const builtinKeys = Object.keys(DEFAULT_MCP_SERVERS);
11
11
 
12
12
  if (builtinKeys.length > 0) {
@@ -19,28 +19,29 @@ function displayServers(config) {
19
19
  console.log('');
20
20
  }
21
21
 
22
- console.log(chalk.bold(' Custom'));
22
+ console.log(chalk.bold(' Custom') + chalk.dim(' → .ai/mcp/mcp.json'));
23
23
  const userEntries = Object.entries(userServers);
24
24
  if (userEntries.length === 0) {
25
25
  console.log(chalk.dim(' (none)'));
26
26
  } else {
27
27
  for (const [key, srv] of userEntries) {
28
- const addr = srv.type === 'remote'
28
+ const addr = srv.type === 'http'
29
29
  ? srv.url
30
30
  : `${srv.command}${srv.args?.length ? ' ' + srv.args.join(' ') : ''}`;
31
- console.log(` ${chalk.cyan(key.padEnd(18))} ${chalk.dim(addr)} ${chalk.dim(`(${srv.type})`)}`);
31
+ const type = srv.type === 'http' ? 'remote' : 'stdio';
32
+ console.log(` ${chalk.cyan(key.padEnd(18))} ${chalk.dim(addr)} ${chalk.dim(`(${type})`)}`);
32
33
  }
33
34
  }
34
35
  console.log('');
35
36
  }
36
37
 
37
- async function addServer(config) {
38
+ async function addServer(mcpConfig) {
38
39
  const name = await text({
39
40
  message: 'Server key',
40
41
  placeholder: 'my-api',
41
42
  validate: (v) => {
42
43
  if (!v.trim()) return 'Key is required';
43
- if ((config.mcpServers || {})[v.trim()]) return `"${v.trim()}" already exists`;
44
+ if ((mcpConfig.mcpServers || {})[v.trim()]) return `"${v.trim()}" already exists`;
44
45
  if (DEFAULT_MCP_SERVERS[v.trim()]) return `"${v.trim()}" is a built-in — use "Toggle built-ins" to enable/disable it`;
45
46
  if (!/^[a-z0-9_-]+$/.test(v.trim())) return 'Use only lowercase letters, numbers, hyphens, underscores';
46
47
  },
@@ -50,15 +51,15 @@ async function addServer(config) {
50
51
  const type = await select({
51
52
  message: 'Server type',
52
53
  options: [
53
- { value: 'remote', label: 'Remote', hint: 'HTTP / SSE url' },
54
- { value: 'stdio', label: 'Local', hint: 'stdio process (node, python, etc.)' },
54
+ { value: 'http', label: 'Remote', hint: 'HTTP / SSE url' },
55
+ { value: 'stdio', label: 'Local', hint: 'stdio process (node, python, etc.)' },
55
56
  ],
56
57
  });
57
58
  if (isCancel(type)) return;
58
59
 
59
60
  const key = name.trim();
60
61
 
61
- if (type === 'remote') {
62
+ if (type === 'http') {
62
63
  const url = await text({
63
64
  message: 'URL',
64
65
  placeholder: 'https://my-mcp.com/mcp',
@@ -66,15 +67,34 @@ async function addServer(config) {
66
67
  });
67
68
  if (isCancel(url)) return;
68
69
 
70
+ const headersRaw = await text({
71
+ message: 'Headers (Key: Value, comma-separated, optional)',
72
+ placeholder: 'Authorization: Bearer TOKEN',
73
+ });
74
+ if (isCancel(headersRaw)) return;
75
+
69
76
  const description = await text({
70
77
  message: 'Description (optional)',
71
78
  placeholder: 'What does this server provide?',
72
79
  });
73
80
  if (isCancel(description)) return;
74
81
 
75
- config.mcpServers[key] = {
76
- type: 'remote',
82
+ const headers = {};
83
+ if (headersRaw.trim()) {
84
+ for (const pair of headersRaw.trim().split(',')) {
85
+ const colon = pair.indexOf(':');
86
+ if (colon > 0) {
87
+ const k = pair.slice(0, colon).trim();
88
+ const v = pair.slice(colon + 1).trim();
89
+ if (k) headers[k] = v;
90
+ }
91
+ }
92
+ }
93
+
94
+ mcpConfig.mcpServers[key] = {
95
+ type: 'http',
77
96
  url: url.trim(),
97
+ ...(Object.keys(headers).length ? { headers } : {}),
78
98
  ...(description.trim() ? { description: description.trim() } : {}),
79
99
  };
80
100
  } else {
@@ -111,19 +131,18 @@ async function addServer(config) {
111
131
  }
112
132
  }
113
133
 
114
- config.mcpServers[key] = {
115
- type: 'stdio',
134
+ mcpConfig.mcpServers[key] = {
116
135
  command: command.trim(),
117
136
  ...(args.length ? { args } : {}),
118
137
  ...(Object.keys(env).length ? { env } : {}),
119
138
  };
120
139
  }
121
140
 
122
- console.log(` ${chalk.green('✓')} Added ${chalk.cyan(key)}`);
141
+ console.log(` ${chalk.green('✓')} Added ${chalk.cyan(key)} → ${chalk.dim('.ai/mcp/mcp.json')}`);
123
142
  }
124
143
 
125
- async function removeServer(config) {
126
- const keys = Object.keys(config.mcpServers);
144
+ async function removeServer(mcpConfig) {
145
+ const keys = Object.keys(mcpConfig.mcpServers);
127
146
  if (keys.length === 0) {
128
147
  console.log(chalk.dim(' No custom servers to remove.'));
129
148
  return;
@@ -132,8 +151,8 @@ async function removeServer(config) {
132
151
  const toRemove = await multiselect({
133
152
  message: 'Select servers to remove',
134
153
  options: keys.map((k) => {
135
- const srv = config.mcpServers[k];
136
- const addr = srv.type === 'remote'
154
+ const srv = mcpConfig.mcpServers[k];
155
+ const addr = srv.type === 'http'
137
156
  ? srv.url
138
157
  : `${srv.command}${srv.args?.length ? ' ' + srv.args.join(' ') : ''}`;
139
158
  return { value: k, label: k, hint: addr };
@@ -142,45 +161,56 @@ async function removeServer(config) {
142
161
  });
143
162
  if (isCancel(toRemove)) return;
144
163
 
145
- for (const k of toRemove) {
146
- delete config.mcpServers[k];
147
- }
164
+ for (const k of toRemove) delete mcpConfig.mcpServers[k];
148
165
 
149
166
  if (toRemove.length) {
150
167
  console.log(` ${chalk.green('✓')} Removed: ${toRemove.map(k => chalk.cyan(k)).join(', ')}`);
151
168
  }
152
169
  }
153
170
 
154
- async function toggleDefaults(config) {
155
- const keys = Object.keys(DEFAULT_MCP_SERVERS);
156
- if (keys.length === 0) {
157
- console.log(chalk.dim(' No built-in servers configured.'));
171
+ async function toggleServers(mcpConfig, localConfig) {
172
+ const builtinKeys = Object.keys(DEFAULT_MCP_SERVERS);
173
+ const customKeys = Object.keys(mcpConfig.mcpServers || {});
174
+ const allKeys = [...builtinKeys, ...customKeys];
175
+
176
+ if (allKeys.length === 0) {
177
+ console.log(chalk.dim(' No servers to toggle.'));
158
178
  return;
159
179
  }
160
180
 
161
- const disabled = new Set(config.disableMcpServers || []);
181
+ const disabled = new Set(localConfig.disabledMcpServers || []);
162
182
 
163
- const enabled = await multiselect({
164
- message: 'Which built-in servers should be enabled?',
165
- options: keys.map((k) => ({
183
+ const options = [
184
+ ...builtinKeys.map((k) => ({
166
185
  value: k,
167
186
  label: k,
168
- hint: DEFAULT_MCP_SERVERS[k].url,
187
+ hint: `${DEFAULT_MCP_SERVERS[k].url} (built-in)`,
169
188
  })),
170
- initialValues: keys.filter((k) => !disabled.has(k)),
189
+ ...customKeys.map((k) => {
190
+ const srv = mcpConfig.mcpServers[k];
191
+ const addr = srv.type === 'http' ? srv.url : `${srv.command}${srv.args?.length ? ' ' + srv.args.join(' ') : ''}`;
192
+ return { value: k, label: k, hint: addr };
193
+ }),
194
+ ];
195
+
196
+ const enabled = await multiselect({
197
+ message: 'Which servers should be enabled locally?',
198
+ options,
199
+ initialValues: allKeys.filter((k) => !disabled.has(k)),
171
200
  required: false,
172
201
  });
173
202
  if (isCancel(enabled)) return;
174
203
 
175
204
  const enabledSet = new Set(enabled);
176
- config.disableMcpServers = keys.filter((k) => !enabledSet.has(k));
205
+ localConfig.disabledMcpServers = allKeys.filter((k) => !enabledSet.has(k));
206
+ console.log(` ${chalk.green('✓')} Updated → ${chalk.dim('.js-boost.json')}`);
177
207
  }
178
208
 
179
209
  export async function configureMcp(projectDir) {
180
- const configPath = path.join(projectDir, 'js-boost.config.json');
181
- const config = readConfig(projectDir);
182
- config.mcpServers = config.mcpServers || {};
183
- config.disableMcpServers = config.disableMcpServers || [];
210
+ const aiDir = path.join(projectDir, '.ai');
211
+ const mcpConfig = readMcpConfig(aiDir);
212
+ const localConfig = readLocalConfig(projectDir);
213
+ localConfig.disabledMcpServers = localConfig.disabledMcpServers || [];
184
214
 
185
215
  console.log('');
186
216
  console.log(chalk.bold.blue('⚡ js-boost') + chalk.dim(' — MCP server configuration'));
@@ -189,16 +219,16 @@ export async function configureMcp(projectDir) {
189
219
  let running = true;
190
220
 
191
221
  while (running) {
192
- displayServers(config);
222
+ displayServers(mcpConfig, localConfig);
193
223
 
194
- const hasCustom = Object.keys(config.mcpServers).length > 0;
195
- const hasBuiltins = Object.keys(DEFAULT_MCP_SERVERS).length > 0;
224
+ const hasCustom = Object.keys(mcpConfig.mcpServers).length > 0;
225
+ const hasAny = hasCustom || Object.keys(DEFAULT_MCP_SERVERS).length > 0;
196
226
 
197
227
  const options = [
198
- { value: 'add', label: 'Add a server' },
199
- ...(hasCustom ? [{ value: 'remove', label: 'Remove a server' }] : []),
200
- ...(hasBuiltins ? [{ value: 'toggle', label: 'Disable / enable built-in servers' }] : []),
201
- { value: 'done', label: 'Save and exit' },
228
+ { value: 'add', label: 'Add a server' },
229
+ ...(hasCustom ? [{ value: 'remove', label: 'Remove a server' }] : []),
230
+ ...(hasAny ? [{ value: 'toggle', label: 'Enable / disable servers locally' }] : []),
231
+ { value: 'done', label: 'Save and exit' },
202
232
  ];
203
233
 
204
234
  const action = await select({ message: 'What would you like to do?', options });
@@ -209,15 +239,18 @@ export async function configureMcp(projectDir) {
209
239
  }
210
240
 
211
241
  console.log('');
212
- if (action === 'add') await addServer(config);
213
- if (action === 'remove') await removeServer(config);
214
- if (action === 'toggle') await toggleDefaults(config);
242
+ if (action === 'add') await addServer(mcpConfig);
243
+ if (action === 'remove') await removeServer(mcpConfig);
244
+ if (action === 'toggle') await toggleServers(mcpConfig, localConfig);
215
245
  console.log('');
216
246
  }
217
247
 
218
- writeFile(configPath, JSON.stringify(config, null, 2));
248
+ writeMcpConfig(aiDir, mcpConfig);
249
+ writeLocalConfig(projectDir, localConfig);
219
250
  console.log('');
220
- console.log(chalk.green(' ✓ Saved to js-boost.config.json'));
221
- console.log(chalk.dim(' Run `js-boost generate` to apply changes to agent files.'));
251
+ console.log(chalk.green(' ✓ Saved'));
252
+ console.log(chalk.dim(' team config .ai/mcp/mcp.json'));
253
+ console.log(chalk.dim(' local config → .js-boost.json'));
254
+ console.log(chalk.dim(' Run `npx @1tool/js-boost generate` to apply changes.'));
222
255
  console.log('');
223
256
  }
package/src/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import path from 'path';
2
2
  import chalk from 'chalk';
3
- import { readGuidelines, readSkills, readConfig, writeFile } from './utils/reader.js';
3
+ import { readGuidelines, readSkills, readLocalConfig, writeLocalConfig, readMcpConfig, writeFile } from './utils/reader.js';
4
4
  import { buildMcpServers, generateMcpJson, generateJunieMcpJson } from './utils/mcp.js';
5
5
  import { AGENTS_MD_CONSUMERS, MCP_JSON_CONSUMERS } from './agents.js';
6
6
  import { generateAgentsMd } from './generators/agents.js';
@@ -11,29 +11,43 @@ import { generateKiroSteering } from './generators/kiro.js';
11
11
 
12
12
  export async function generate(projectDir, options = {}) {
13
13
  const aiDir = path.join(projectDir, '.ai');
14
- const config = readConfig(projectDir);
15
14
  const verbose = options.verbose ?? false;
16
15
 
17
- // Determine active agents — fall back to all agents if none configured
18
- const activeAgents = new Set(config.agents ?? Object.keys(
19
- { amp: 1, claude_code: 1, codex: 1, copilot: 1, cursor: 1, gemini: 1, junie: 1, kiro: 1, opencode: 1 }
20
- ));
21
-
22
- const has = (key) => activeAgents.has(key);
23
- const hasAny = (keys) => keys.some(k => activeAgents.has(k));
24
-
25
16
  const log = (msg) => console.log(msg);
26
17
  const info = (label, file) => log(` ${chalk.green('✓')} ${chalk.dim(label.padEnd(30))} ${chalk.cyan(file)}`);
27
18
  const skip = (label, reason) => verbose && log(` ${chalk.yellow('–')} ${chalk.dim(label.padEnd(30))} ${chalk.yellow(reason)}`);
28
19
 
20
+ // Resolve agents — flag (one-off) > local config > inline prompt
21
+ let localConfig = readLocalConfig(projectDir);
22
+ let activeAgentsList;
23
+ let isOneOff = false;
24
+
25
+ if (options.agents) {
26
+ activeAgentsList = options.agents.split(',').map(a => a.trim()).filter(Boolean);
27
+ isOneOff = true;
28
+ } else if (localConfig.agents?.length) {
29
+ activeAgentsList = localConfig.agents;
30
+ } else {
31
+ log('');
32
+ log(chalk.bold.blue('⚡ js-boost') + chalk.dim(' — first run setup'));
33
+ const { selectAgents } = await import('./init.js');
34
+ activeAgentsList = await selectAgents(projectDir, null);
35
+ localConfig.agents = activeAgentsList;
36
+ writeLocalConfig(projectDir, localConfig);
37
+ }
38
+
39
+ const activeAgents = new Set(activeAgentsList);
40
+ const has = (key) => activeAgents.has(key);
41
+ const hasAny = (keys) => keys.some(k => activeAgents.has(k));
42
+
29
43
  log('');
30
44
  log(chalk.bold.blue('⚡ js-boost') + chalk.dim(' — generating agent files'));
31
45
  log('');
32
46
 
33
- // 1. Read source files
47
+ // Read source files
34
48
  const guidelines = await readGuidelines(aiDir);
35
49
  const skills = await readSkills(aiDir);
36
- const mcpServers = buildMcpServers(config);
50
+ const mcpServers = buildMcpServers(readMcpConfig(aiDir), localConfig);
37
51
 
38
52
  if (guidelines.length === 0 && skills.length === 0) {
39
53
  log(chalk.yellow(' ⚠ No guidelines or skills found in .ai/'));
@@ -41,15 +55,15 @@ export async function generate(projectDir, options = {}) {
41
55
  log('');
42
56
  } else {
43
57
  log(chalk.dim(` Found ${guidelines.length} guideline(s), ${skills.length} skill(s), ${Object.keys(mcpServers).length} MCP server(s)`));
44
- log(chalk.dim(` Agents: ${[...activeAgents].join(', ')}`));
58
+ log(chalk.dim(` Agents: ${activeAgentsList.join(', ')}`));
45
59
  log('');
46
60
  }
47
61
 
48
62
  const generatedFiles = [];
49
63
 
50
- // 2. AGENTS.md — shared format for Codex, Copilot, Gemini, Amp, OpenCode
64
+ // AGENTS.md — Amp, Codex, Copilot, Gemini, OpenCode
51
65
  if (hasAny(AGENTS_MD_CONSUMERS)) {
52
- const agentsMd = generateAgentsMd(guidelines, skills, mcpServers, config);
66
+ const agentsMd = generateAgentsMd(guidelines, skills, mcpServers, {});
53
67
  writeFile(path.join(projectDir, 'AGENTS.md'), agentsMd);
54
68
  info('AGENTS.md', 'AGENTS.md');
55
69
  generatedFiles.push('AGENTS.md');
@@ -57,9 +71,9 @@ export async function generate(projectDir, options = {}) {
57
71
  skip('AGENTS.md', 'no AGENTS.md consumers selected');
58
72
  }
59
73
 
60
- // 3. CLAUDE.md — Claude Code
74
+ // CLAUDE.md — Claude Code
61
75
  if (has('claude_code')) {
62
- const claudeMd = generateClaudeMd(guidelines, skills, mcpServers, config);
76
+ const claudeMd = generateClaudeMd(guidelines, skills, mcpServers, {});
63
77
  writeFile(path.join(projectDir, 'CLAUDE.md'), claudeMd);
64
78
  info('Claude Code', 'CLAUDE.md');
65
79
  generatedFiles.push('CLAUDE.md');
@@ -67,7 +81,7 @@ export async function generate(projectDir, options = {}) {
67
81
  skip('Claude Code', 'not selected');
68
82
  }
69
83
 
70
- // 4. .mcp.json — Claude Code + Codex
84
+ // .mcp.json — Claude Code + Codex
71
85
  if (hasAny(MCP_JSON_CONSUMERS)) {
72
86
  const mcpJson = generateMcpJson(mcpServers);
73
87
  writeFile(path.join(projectDir, '.mcp.json'), mcpJson);
@@ -77,9 +91,9 @@ export async function generate(projectDir, options = {}) {
77
91
  skip('MCP', 'no MCP consumers selected');
78
92
  }
79
93
 
80
- // 5. .junie/ — JetBrains Junie
94
+ // .junie/ — JetBrains Junie
81
95
  if (has('junie')) {
82
- const junieGuidelines = generateJunieGuidelines(guidelines, skills, config);
96
+ const junieGuidelines = generateJunieGuidelines(guidelines, skills, {});
83
97
  writeFile(path.join(projectDir, '.junie', 'guidelines.md'), junieGuidelines);
84
98
  info('Junie guidelines', '.junie/guidelines.md');
85
99
  generatedFiles.push('.junie/guidelines.md');
@@ -92,14 +106,14 @@ export async function generate(projectDir, options = {}) {
92
106
  skip('Junie', 'not selected');
93
107
  }
94
108
 
95
- // 6. Cursor — .cursor/rules/ + legacy .cursorrules
109
+ // Cursor
96
110
  if (has('cursor')) {
97
- const cursorRules = generateCursorRules(guidelines, skills, config);
111
+ const cursorRules = generateCursorRules(guidelines, skills, {});
98
112
  writeFile(path.join(projectDir, '.cursor', 'rules', 'js-boost.mdc'), cursorRules);
99
113
  info('Cursor (modern)', '.cursor/rules/js-boost.mdc');
100
114
  generatedFiles.push('.cursor/rules/js-boost.mdc');
101
115
 
102
- const cursorLegacy = generateCursorRulesLegacy(guidelines, skills, config);
116
+ const cursorLegacy = generateCursorRulesLegacy(guidelines, skills, {});
103
117
  writeFile(path.join(projectDir, '.cursorrules'), cursorLegacy);
104
118
  info('Cursor (legacy)', '.cursorrules');
105
119
  generatedFiles.push('.cursorrules');
@@ -107,9 +121,9 @@ export async function generate(projectDir, options = {}) {
107
121
  skip('Cursor', 'not selected');
108
122
  }
109
123
 
110
- // 7. Kiro — .kiro/steering/guidelines.md
124
+ // Kiro
111
125
  if (has('kiro')) {
112
- const kiroSteering = generateKiroSteering(guidelines, skills, config);
126
+ const kiroSteering = generateKiroSteering(guidelines, skills, {});
113
127
  writeFile(path.join(projectDir, '.kiro', 'steering', 'guidelines.md'), kiroSteering);
114
128
  info('Kiro', '.kiro/steering/guidelines.md');
115
129
  generatedFiles.push('.kiro/steering/guidelines.md');
@@ -121,5 +135,12 @@ export async function generate(projectDir, options = {}) {
121
135
  log(chalk.green.bold(` ✓ Generated ${generatedFiles.length} files successfully`));
122
136
  log('');
123
137
 
138
+ // Write state back to local config (skip for one-off --agents flag)
139
+ if (!isOneOff) {
140
+ localConfig.guidelines = true;
141
+ localConfig.skills = skills.map(s => s.name);
142
+ writeLocalConfig(projectDir, localConfig);
143
+ }
144
+
124
145
  return generatedFiles;
125
- }
146
+ }
package/src/init.js CHANGED
@@ -2,7 +2,7 @@ import path from 'path';
2
2
  import fs from 'fs';
3
3
  import chalk from 'chalk';
4
4
  import { multiselect, isCancel, cancel } from '@clack/prompts';
5
- import { writeFile } from './utils/reader.js';
5
+ import { writeFile, writeMcpConfig, writeLocalConfig } from './utils/reader.js';
6
6
  import { AGENTS } from './agents.js';
7
7
  import { detectInstalledAgents } from './utils/detect.js';
8
8
 
@@ -39,6 +39,15 @@ TODO: describe the steps, patterns, or conventions the agent should follow.
39
39
  `
40
40
  };
41
41
 
42
+ function addToGitignore(projectDir, entry) {
43
+ const gitignorePath = path.join(projectDir, '.gitignore');
44
+ if (!fs.existsSync(gitignorePath)) return false;
45
+ const content = fs.readFileSync(gitignorePath, 'utf8');
46
+ if (content.split('\n').some(line => line.trim() === entry)) return false;
47
+ fs.appendFileSync(gitignorePath, `\n# js-boost local config\n${entry}\n`, 'utf8');
48
+ return true;
49
+ }
50
+
42
51
  export async function selectAgents(projectDir, currentAgents = null) {
43
52
  const detected = detectInstalledAgents(projectDir);
44
53
 
@@ -102,26 +111,33 @@ export async function init(projectDir, options = {}) {
102
111
  }
103
112
  }
104
113
 
105
- // Create js-boost.config.json with defaults (or read existing)
106
- const configPath = path.join(projectDir, 'js-boost.config.json');
107
- let existingConfig = {};
108
- if (fs.existsSync(configPath) && !force) {
109
- try { existingConfig = JSON.parse(fs.readFileSync(configPath, 'utf8')); } catch {}
114
+ // Create .ai/mcp/mcp.json
115
+ const mcpJsonPath = path.join(aiDir, 'mcp', 'mcp.json');
116
+ if (!fs.existsSync(mcpJsonPath) || force) {
117
+ writeMcpConfig(aiDir, { mcpServers: {} });
118
+ console.log(` ${chalk.green('')} ${chalk.cyan('.ai/mcp/mcp.json')}`);
110
119
  }
111
120
 
112
121
  // Agent selection
113
- const selectedAgents = await selectAgents(projectDir, existingConfig.agents);
122
+ const localConfigPath = path.join(projectDir, '.js-boost.json');
123
+ let existingLocal = {};
124
+ if (fs.existsSync(localConfigPath)) {
125
+ try { existingLocal = JSON.parse(fs.readFileSync(localConfigPath, 'utf8')); } catch {}
126
+ }
114
127
 
115
- const config = {
116
- projectName: existingConfig.projectName || path.basename(projectDir),
117
- projectDescription: existingConfig.projectDescription || '',
128
+ const selectedAgents = await selectAgents(projectDir, existingLocal.agents ?? null);
129
+
130
+ writeLocalConfig(projectDir, {
118
131
  agents: selectedAgents,
119
- mcpServers: existingConfig.mcpServers || {},
120
- disableMcpServers: existingConfig.disableMcpServers || [],
121
- };
132
+ disabledMcpServers: existingLocal.disabledMcpServers ?? [],
133
+ });
134
+ console.log(` ${chalk.green('✓')} ${chalk.cyan('.js-boost.json')}`);
122
135
 
123
- writeFile(configPath, JSON.stringify(config, null, 2));
124
- console.log(` ${chalk.green('✓')} ${chalk.cyan('js-boost.config.json')}`);
136
+ // Ensure .js-boost.json is gitignored
137
+ const added = addToGitignore(projectDir, '.js-boost.json');
138
+ if (added) {
139
+ console.log(` ${chalk.green('✓')} ${chalk.cyan('.js-boost.json')} added to ${chalk.cyan('.gitignore')}`);
140
+ }
125
141
 
126
142
  console.log('');
127
143
  console.log(chalk.green.bold(' ✓ .ai/ folder initialized'));
@@ -129,6 +145,6 @@ export async function init(projectDir, options = {}) {
129
145
  console.log(chalk.dim(' Next steps:'));
130
146
  console.log(chalk.dim(' 1. Edit .ai/guidelines/*.md with your project conventions'));
131
147
  console.log(chalk.dim(' 2. Add skills in .ai/skills/<name>/SKILL.md'));
132
- console.log(chalk.dim(' 3. Run: npx js-boost generate'));
148
+ console.log(chalk.dim(' 3. Run: npx @1tool/js-boost generate'));
133
149
  console.log('');
134
150
  }
package/src/utils/mcp.js CHANGED
@@ -1,53 +1,51 @@
1
- /**
2
- * Default MCP servers pre-configured for js-boost
3
- */
4
- export const DEFAULT_MCP_SERVERS = {
5
- };
1
+ export const DEFAULT_MCP_SERVERS = {};
6
2
 
7
3
  /**
8
- * Build the MCP servers object, merging defaults with user-defined servers
9
- * from js-boost.config.json
4
+ * Merge team servers (.ai/mcp/mcp.json) with per-developer disabled list (.js-boost.json)
10
5
  */
11
- export function buildMcpServers(config = {}) {
12
- const userServers = config.mcpServers || {};
13
- const disabledDefaults = config.disableMcpServers || [];
6
+ export function buildMcpServers(mcpConfig = {}, localConfig = {}) {
7
+ const userServers = mcpConfig.mcpServers || {};
8
+ const disabled = new Set(localConfig.disabledMcpServers || []);
14
9
 
15
10
  const servers = {};
16
11
 
17
- // Add default servers (unless disabled)
18
12
  for (const [key, server] of Object.entries(DEFAULT_MCP_SERVERS)) {
19
- if (!disabledDefaults.includes(key)) {
13
+ if (!disabled.has(key)) {
20
14
  servers[key] = server;
21
15
  }
22
16
  }
23
17
 
24
- // Merge user-defined servers (can override defaults)
25
18
  for (const [key, server] of Object.entries(userServers)) {
26
- servers[key] = server;
19
+ if (!disabled.has(key)) {
20
+ servers[key] = server;
21
+ }
27
22
  }
28
23
 
29
24
  return servers;
30
25
  }
31
26
 
32
27
  /**
33
- * Generate .mcp.json (used by Claude Code and Codex)
34
- * Supports both stdio and remote (HTTP/SSE) server types
28
+ * Generate .mcp.json (Claude Code + Codex)
29
+ * - stdio: detected by presence of `command` (no type field)
30
+ * - remote: type === 'http', wrapped in mcp-remote, headers passed as --header args
35
31
  */
36
32
  export function generateMcpJson(servers) {
37
33
  const mcpServers = {};
38
34
 
39
35
  for (const [key, server] of Object.entries(servers)) {
40
- if (server.type === 'stdio') {
36
+ if (server.type === 'http') {
37
+ const args = ['-y', 'mcp-remote', server.url];
38
+ if (server.headers) {
39
+ for (const [k, v] of Object.entries(server.headers)) {
40
+ args.push('--header', `${k}: ${v}`);
41
+ }
42
+ }
43
+ mcpServers[key] = { command: 'npx', args };
44
+ } else if (server.command) {
41
45
  mcpServers[key] = {
42
46
  command: server.command,
43
47
  args: server.args || [],
44
- ...(server.env ? { env: server.env } : {})
45
- };
46
- } else if (server.type === 'remote') {
47
- // Claude Code uses mcp-remote wrapper for HTTP/SSE servers
48
- mcpServers[key] = {
49
- command: 'npx',
50
- args: ['-y', 'mcp-remote', server.url]
48
+ ...(server.env ? { env: server.env } : {}),
51
49
  };
52
50
  }
53
51
  }
@@ -56,21 +54,19 @@ export function generateMcpJson(servers) {
56
54
  }
57
55
 
58
56
  /**
59
- * Generate .junie/mcp.json (Junie format supports URL directly)
57
+ * Generate .junie/mcp.json remote servers referenced by URL directly
60
58
  */
61
59
  export function generateJunieMcpJson(servers) {
62
60
  const mcpServers = {};
63
61
 
64
62
  for (const [key, server] of Object.entries(servers)) {
65
- if (server.type === 'stdio') {
63
+ if (server.type === 'http') {
64
+ mcpServers[key] = { url: server.url };
65
+ } else if (server.command) {
66
66
  mcpServers[key] = {
67
67
  command: server.command,
68
68
  args: server.args || [],
69
- ...(server.env ? { env: server.env } : {})
70
- };
71
- } else if (server.type === 'remote') {
72
- mcpServers[key] = {
73
- url: server.url
69
+ ...(server.env ? { env: server.env } : {}),
74
70
  };
75
71
  }
76
72
  }
@@ -88,8 +84,8 @@ export function buildMcpMarkdownSection(servers) {
88
84
  for (const [key, server] of Object.entries(servers)) {
89
85
  lines.push(`### ${key}`);
90
86
  if (server.description) lines.push(`> ${server.description}`);
91
- if (server.type === 'remote') lines.push(`- **URL:** \`${server.url}\``);
92
- if (server.type === 'stdio') lines.push(`- **Command:** \`${server.command} ${(server.args || []).join(' ')}\``);
87
+ if (server.type === 'http') lines.push(`- **URL:** \`${server.url}\``);
88
+ if (server.command) lines.push(`- **Command:** \`${server.command} ${(server.args || []).join(' ')}\``);
93
89
  lines.push('');
94
90
  }
95
91
 
@@ -41,7 +41,6 @@ export async function readSkills(aiDir) {
41
41
  const content = fs.readFileSync(fullPath, 'utf8').trim();
42
42
  const dir = path.dirname(file);
43
43
 
44
- // Parse YAML frontmatter: ---\nname: ...\ndescription: ...\n---
45
44
  let name = dir;
46
45
  let description = '';
47
46
  const fmMatch = content.match(/^---\s*\n([\s\S]*?)\n---/);
@@ -52,7 +51,6 @@ export async function readSkills(aiDir) {
52
51
  if (nameMatch) name = nameMatch[1].trim();
53
52
  if (descMatch) description = descMatch[1].trim();
54
53
  } else {
55
- // Fallback: try first heading
56
54
  const headingMatch = content.match(/^#+\s*(.+)$/m);
57
55
  if (headingMatch) name = headingMatch[1].trim();
58
56
  }
@@ -62,10 +60,11 @@ export async function readSkills(aiDir) {
62
60
  }
63
61
 
64
62
  /**
65
- * Read js-boost.config.json if it exists
63
+ * Read .js-boost.json per-developer local config (gitignored)
64
+ * Returns { agents, guidelines, skills, disabledMcpServers }
66
65
  */
67
- export function readConfig(projectDir) {
68
- const configPath = path.join(projectDir, 'js-boost.config.json');
66
+ export function readLocalConfig(projectDir) {
67
+ const configPath = path.join(projectDir, '.js-boost.json');
69
68
  if (!fs.existsSync(configPath)) return {};
70
69
  try {
71
70
  return JSON.parse(fs.readFileSync(configPath, 'utf8'));
@@ -74,6 +73,35 @@ export function readConfig(projectDir) {
74
73
  }
75
74
  }
76
75
 
76
+ /**
77
+ * Write .js-boost.json
78
+ */
79
+ export function writeLocalConfig(projectDir, config) {
80
+ writeFile(path.join(projectDir, '.js-boost.json'), JSON.stringify(config, null, 2));
81
+ }
82
+
83
+ /**
84
+ * Read .ai/mcp/mcp.json — team MCP server definitions
85
+ * Returns { mcpServers: {} }
86
+ */
87
+ export function readMcpConfig(aiDir) {
88
+ const mcpPath = path.join(aiDir, 'mcp', 'mcp.json');
89
+ if (!fs.existsSync(mcpPath)) return { mcpServers: {} };
90
+ try {
91
+ const parsed = JSON.parse(fs.readFileSync(mcpPath, 'utf8'));
92
+ return { mcpServers: parsed.mcpServers || {} };
93
+ } catch {
94
+ return { mcpServers: {} };
95
+ }
96
+ }
97
+
98
+ /**
99
+ * Write .ai/mcp/mcp.json
100
+ */
101
+ export function writeMcpConfig(aiDir, mcpConfig) {
102
+ writeFile(path.join(aiDir, 'mcp', 'mcp.json'), JSON.stringify(mcpConfig, null, 2));
103
+ }
104
+
77
105
  /**
78
106
  * Ensure a directory exists
79
107
  */
package/src/watch.js CHANGED
@@ -5,7 +5,7 @@ import { generate } from './index.js';
5
5
 
6
6
  export function watch(projectDir) {
7
7
  const aiDir = path.join(projectDir, '.ai');
8
- const configPath = path.join(projectDir, 'js-boost.config.json');
8
+ const configPath = path.join(projectDir, '.js-boost.json');
9
9
 
10
10
  console.log('');
11
11
  console.log(chalk.bold.blue('⚡ js-boost') + chalk.dim(' — watch mode'));