@1tool/js-boost 1.2.0 → 1.3.1

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
@@ -72,37 +72,43 @@ npx @1tool/js-boost agents
72
72
 
73
73
  ### `@1tool/js-boost mcp`
74
74
 
75
- 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.
76
76
 
77
77
  ```bash
78
78
  npx @1tool/js-boost mcp
79
79
  ```
80
80
 
81
- **Add a remote server:**
81
+ The wizard offers three actions:
82
+
83
+ **Add a remote server** — HTTP/SSE endpoint with optional auth headers:
82
84
  ```
83
- ✔ Server key: my-api
84
- ✔ Server type: Remote (HTTP / SSE url)
85
- ✔ URL: https://my-mcp.com/mcp
86
- 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
87
90
  ```
88
91
 
89
- **Add a local (stdio) server:**
92
+ **Add a local server** — stdio process with optional args and env vars:
90
93
  ```
91
- ✔ Server key: local-tools
92
- ✔ Server type: Local (stdio process)
93
- ✔ Command: node
94
- ✔ Arguments: ./mcp-server.js --port 3000
95
- ✔ 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
96
99
  ```
97
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
+
98
103
  ### `@1tool/js-boost generate`
99
104
 
100
- 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.
101
106
 
102
107
  ```bash
103
108
  npx @1tool/js-boost generate
104
- npx @1tool/js-boost gen # alias
105
- 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
106
112
  ```
107
113
 
108
114
  ### `@1tool/js-boost watch`
@@ -143,43 +149,71 @@ During `init` (and `agents`), installed agents are pre-selected automatically ba
143
149
 
144
150
  ## Configuration
145
151
 
146
- MCP server definitions live in `.ai/mcp/mcp.json` (managed by `js-boost mcp`):
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`.
147
155
 
148
156
  ```json
149
157
  {
150
- "servers": {
151
- "my-api": {
152
- "type": "remote",
158
+ "mcpServers": {
159
+ "my-remote": {
160
+ "type": "http",
153
161
  "url": "https://my-mcp.com/mcp",
154
- "description": "Internal API tools"
162
+ "headers": {
163
+ "Authorization": "Bearer YOUR_TOKEN_HERE"
164
+ }
155
165
  },
156
- "local-tools": {
157
- "type": "stdio",
166
+ "my-local": {
158
167
  "command": "node",
159
- "args": ["./mcp-server.js"],
160
- "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"]
161
176
  }
162
- },
163
- "disabled": []
177
+ }
164
178
  }
165
179
  ```
166
180
 
167
- Agent selection and project metadata live in `js-boost.config.json` (managed by `init` / `agents`):
181
+ Server types:
182
+
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`.
168
201
 
169
202
  ```json
170
203
  {
171
- "projectName": "my-app",
172
- "projectDescription": "",
173
- "agents": ["claude_code", "cursor", "codex"]
204
+ "agents": ["claude_code", "cursor", "codex"],
205
+ "guidelines": true,
206
+ "skills": ["example-skill"],
207
+ "disabledMcpServers": ["my-remote"]
174
208
  }
175
209
  ```
176
210
 
177
- MCP servers are written to the right format per agent:
178
-
179
- | Agent | Format | Remote servers |
180
- |---|---|---|
181
- | Claude Code, Codex | `.mcp.json` | wrapped in `mcp-remote` |
182
- | Junie | `.junie/mcp.json` | referenced by URL directly |
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 |
183
217
 
184
218
  ---
185
219
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@1tool/js-boost",
3
- "version": "1.2.0",
3
+ "version": "1.3.1",
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, readMcpConfig } = 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(readMcpConfig(aiDir));
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 { readMcpConfig, writeMcpConfig } from '../utils/reader.js';
5
+ import { readMcpConfig, writeMcpConfig, readLocalConfig, writeLocalConfig } from '../utils/reader.js';
6
6
 
7
- function displayServers(mcpConfig) {
8
- const userServers = mcpConfig.servers || {};
9
- const disabled = new Set(mcpConfig.disabled || []);
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,16 +19,17 @@ function displayServers(mcpConfig) {
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('');
@@ -40,7 +41,7 @@ async function addServer(mcpConfig) {
40
41
  placeholder: 'my-api',
41
42
  validate: (v) => {
42
43
  if (!v.trim()) return 'Key is required';
43
- if ((mcpConfig.servers || {})[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(mcpConfig) {
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(mcpConfig) {
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
- mcpConfig.servers[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(mcpConfig) {
111
131
  }
112
132
  }
113
133
 
114
- mcpConfig.servers[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
144
  async function removeServer(mcpConfig) {
126
- const keys = Object.keys(mcpConfig.servers);
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(mcpConfig) {
132
151
  const toRemove = await multiselect({
133
152
  message: 'Select servers to remove',
134
153
  options: keys.map((k) => {
135
- const srv = mcpConfig.servers[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,43 +161,56 @@ async function removeServer(mcpConfig) {
142
161
  });
143
162
  if (isCancel(toRemove)) return;
144
163
 
145
- for (const k of toRemove) {
146
- delete mcpConfig.servers[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(mcpConfig) {
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(mcpConfig.disabled || []);
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
- mcpConfig.disabled = 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
210
  const aiDir = path.join(projectDir, '.ai');
181
211
  const mcpConfig = readMcpConfig(aiDir);
212
+ const localConfig = readLocalConfig(projectDir);
213
+ localConfig.disabledMcpServers = localConfig.disabledMcpServers || [];
182
214
 
183
215
  console.log('');
184
216
  console.log(chalk.bold.blue('⚡ js-boost') + chalk.dim(' — MCP server configuration'));
@@ -187,16 +219,16 @@ export async function configureMcp(projectDir) {
187
219
  let running = true;
188
220
 
189
221
  while (running) {
190
- displayServers(mcpConfig);
222
+ displayServers(mcpConfig, localConfig);
191
223
 
192
- const hasCustom = Object.keys(mcpConfig.servers).length > 0;
193
- 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;
194
226
 
195
227
  const options = [
196
- { value: 'add', label: 'Add a server' },
197
- ...(hasCustom ? [{ value: 'remove', label: 'Remove a server' }] : []),
198
- ...(hasBuiltins ? [{ value: 'toggle', label: 'Disable / enable built-in servers' }] : []),
199
- { 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' },
200
232
  ];
201
233
 
202
234
  const action = await select({ message: 'What would you like to do?', options });
@@ -209,13 +241,16 @@ export async function configureMcp(projectDir) {
209
241
  console.log('');
210
242
  if (action === 'add') await addServer(mcpConfig);
211
243
  if (action === 'remove') await removeServer(mcpConfig);
212
- if (action === 'toggle') await toggleDefaults(mcpConfig);
244
+ if (action === 'toggle') await toggleServers(mcpConfig, localConfig);
213
245
  console.log('');
214
246
  }
215
247
 
216
248
  writeMcpConfig(aiDir, mcpConfig);
249
+ writeLocalConfig(projectDir, localConfig);
217
250
  console.log('');
218
- console.log(chalk.green(' ✓ Saved to .ai/mcp/mcp.json'));
219
- console.log(chalk.dim(' Run `@1tool/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.'));
220
255
  console.log('');
221
256
  }
@@ -1,27 +1,13 @@
1
1
  import { buildMcpMarkdownSection } from '../utils/mcp.js';
2
2
 
3
- /**
4
- * Generate AGENTS.md — the universal standard read by Codex, Cursor,
5
- * GitHub Copilot, Gemini CLI, and most other agents.
6
- *
7
- * This is the single source of truth. All other agent files reference this.
8
- */
9
- export function generateAgentsMd(guidelines, skills, mcpServers, config = {}) {
10
- const projectName = config.projectName || 'this project';
11
- const projectDescription = config.projectDescription || '';
3
+ export function generateAgentsMd(guidelines, skills, mcpServers) {
12
4
  const sections = [];
13
5
 
14
- // Header
15
- sections.push(`# AGENTS.md`);
16
- sections.push(`> Agent instructions for ${projectName}. Generated by [js-boost](https://github.com/your-org/js-boost) — do not edit manually.`);
17
- if (projectDescription) sections.push(`\n${projectDescription}`);
6
+ sections.push('# Project Guidelines');
18
7
  sections.push('');
19
-
20
- // Table of contents hint
21
8
  sections.push('---');
22
9
  sections.push('');
23
10
 
24
- // Guidelines — inline all content from .ai/guidelines/
25
11
  if (guidelines.length > 0) {
26
12
  sections.push('## Guidelines');
27
13
  sections.push('');
@@ -29,13 +15,11 @@ export function generateAgentsMd(guidelines, skills, mcpServers, config = {}) {
29
15
  sections.push('');
30
16
 
31
17
  for (const g of guidelines) {
32
- // Include the full content of each guideline file
33
18
  sections.push(g.content);
34
19
  sections.push('');
35
20
  }
36
21
  }
37
22
 
38
- // Skills — reference section with name + description
39
23
  if (skills.length > 0) {
40
24
  sections.push('## Agent Skills');
41
25
  sections.push('');
@@ -50,15 +34,9 @@ export function generateAgentsMd(guidelines, skills, mcpServers, config = {}) {
50
34
  }
51
35
  }
52
36
 
53
- // MCP section
54
37
  if (Object.keys(mcpServers).length > 0) {
55
38
  sections.push(buildMcpMarkdownSection(mcpServers));
56
39
  }
57
40
 
58
- // Footer
59
- sections.push('---');
60
- sections.push('');
61
- sections.push('*This file is auto-generated by `js-boost`. Run `npx js-boost generate` to regenerate.*');
62
-
63
41
  return sections.join('\n');
64
42
  }
@@ -7,13 +7,10 @@ import { buildMcpMarkdownSection } from '../utils/mcp.js';
7
7
  * guidelines content here (Claude benefits from having everything upfront),
8
8
  * plus Claude-specific directives for loading skills.
9
9
  */
10
- export function generateClaudeMd(guidelines, skills, mcpServers, config = {}) {
11
- const projectName = config.projectName || 'this project';
10
+ export function generateClaudeMd(guidelines, skills, mcpServers) {
12
11
  const sections = [];
13
12
 
14
- sections.push(`# CLAUDE.md — ${projectName}`);
15
- sections.push('');
16
- sections.push('> Generated by [js-boost](https://github.com/your-org/js-boost). Do not edit manually — edit `.ai/guidelines/` and `.ai/skills/` instead, then run `npx js-boost generate`.');
13
+ sections.push('# Project Guidelines');
17
14
  sections.push('');
18
15
  sections.push('---');
19
16
  sections.push('');
@@ -1,11 +1,4 @@
1
- /**
2
- * Generate .cursor/rules/js-boost.mdc for Cursor.
3
- *
4
- * Cursor reads .cursorrules (legacy) or .cursor/rules/*.mdc (v0.43+).
5
- * We generate the modern .cursor/rules/ format.
6
- * Each guideline becomes its own rule file for better organization.
7
- */
8
- export function generateCursorRules(guidelines, skills, config = {}) {
1
+ export function generateCursorRules(guidelines, skills) {
9
2
  const sections = [];
10
3
 
11
4
  sections.push('---');
@@ -14,8 +7,9 @@ export function generateCursorRules(guidelines, skills, config = {}) {
14
7
  sections.push('alwaysApply: true');
15
8
  sections.push('---');
16
9
  sections.push('');
10
+ sections.push('# Project Guidelines');
11
+ sections.push('');
17
12
 
18
- // Inline all guidelines
19
13
  if (guidelines.length > 0) {
20
14
  for (const g of guidelines) {
21
15
  sections.push(g.content);
@@ -23,7 +17,6 @@ export function generateCursorRules(guidelines, skills, config = {}) {
23
17
  }
24
18
  }
25
19
 
26
- // Skills reference
27
20
  if (skills.length > 0) {
28
21
  sections.push('## Available Skills');
29
22
  sections.push('');
@@ -34,25 +27,17 @@ export function generateCursorRules(guidelines, skills, config = {}) {
34
27
  sections.push('');
35
28
  }
36
29
 
37
- sections.push('---');
38
- sections.push('*Auto-generated by `js-boost`*');
39
-
40
30
  return sections.join('\n');
41
31
  }
42
32
 
43
- /**
44
- * Also generate legacy .cursorrules for older Cursor versions
45
- */
46
- export function generateCursorRulesLegacy(guidelines, skills, config = {}) {
33
+ export function generateCursorRulesLegacy(guidelines, skills) {
47
34
  const sections = [];
48
35
 
49
- sections.push('# Cursor Rules');
50
- sections.push('# Generated by js-boost — edit .ai/guidelines/ instead');
36
+ sections.push('# Project Guidelines');
51
37
  sections.push('');
52
38
 
53
39
  if (guidelines.length > 0) {
54
40
  for (const g of guidelines) {
55
- // Strip markdown headings for legacy format compatibility
56
41
  sections.push(g.content);
57
42
  sections.push('');
58
43
  }
@@ -60,8 +45,10 @@ export function generateCursorRulesLegacy(guidelines, skills, config = {}) {
60
45
 
61
46
  if (skills.length > 0) {
62
47
  sections.push('## Skills');
48
+ sections.push('');
63
49
  for (const skill of skills) {
64
- sections.push(`- ${skill.name}: .ai/skills/${skill.dir}/SKILL.md`);
50
+ sections.push(`- **${skill.name}**: .ai/skills/${skill.dir}/SKILL.md`);
51
+ if (skill.description) sections.push(` ${skill.description}`);
65
52
  }
66
53
  }
67
54
 
@@ -1,20 +1,9 @@
1
- /**
2
- * Generate .junie/guidelines.md for JetBrains Junie.
3
- *
4
- * Junie reads .junie/guidelines.md and uses it as persistent context
5
- * for every task. Skills need to be referenced explicitly since Junie
6
- * doesn't auto-load them from directories.
7
- */
8
- export function generateJunieGuidelines(guidelines, skills, config = {}) {
9
- const projectName = config.projectName || 'this project';
1
+ export function generateJunieGuidelines(guidelines, skills) {
10
2
  const sections = [];
11
3
 
12
- sections.push(`# Junie Guidelines — ${projectName}`);
13
- sections.push('');
14
- sections.push('> Generated by [js-boost](https://github.com/your-org/js-boost). Do not edit manually.');
4
+ sections.push('# Project Guidelines');
15
5
  sections.push('');
16
6
 
17
- // Inline all guidelines
18
7
  if (guidelines.length > 0) {
19
8
  for (const g of guidelines) {
20
9
  sections.push(g.content);
@@ -22,7 +11,6 @@ export function generateJunieGuidelines(guidelines, skills, config = {}) {
22
11
  }
23
12
  }
24
13
 
25
- // Skills reference
26
14
  if (skills.length > 0) {
27
15
  sections.push('## Available Skills');
28
16
  sections.push('');
@@ -35,8 +23,5 @@ export function generateJunieGuidelines(guidelines, skills, config = {}) {
35
23
  sections.push('');
36
24
  }
37
25
 
38
- sections.push('---');
39
- sections.push('*Auto-generated by `js-boost`. Source: `.ai/`*');
40
-
41
26
  return sections.join('\n');
42
27
  }
@@ -1,16 +1,11 @@
1
- /**
2
- * Generate .kiro/steering/guidelines.md
3
- * Kiro uses steering documents in .kiro/steering/ for project context.
4
- */
5
- export function generateKiroSteering(guidelines, skills, config) {
6
- const projectName = config.projectName || 'This project';
1
+ export function generateKiroSteering(guidelines, skills) {
7
2
  const lines = [];
8
3
 
9
4
  lines.push('---');
10
5
  lines.push('inclusion: always');
11
6
  lines.push('---');
12
7
  lines.push('');
13
- lines.push(`# ${projectName} — AI Guidelines`);
8
+ lines.push('# Project Guidelines');
14
9
  lines.push('');
15
10
 
16
11
  if (guidelines.length > 0) {
@@ -21,15 +16,16 @@ export function generateKiroSteering(guidelines, skills, config) {
21
16
  }
22
17
 
23
18
  if (skills.length > 0) {
24
- lines.push('## Skills');
19
+ lines.push('## Available Skills');
25
20
  lines.push('');
26
- lines.push('The following skills are available for this project:');
21
+ lines.push('The following skill files contain detailed patterns. Read the relevant SKILL.md before working on tasks in that domain:');
27
22
  lines.push('');
28
23
  for (const skill of skills) {
29
- lines.push(`- **${skill.name}**: ${skill.description || skill.dir}`);
24
+ lines.push(`- **${skill.name}**: \`.ai/skills/${skill.dir}/SKILL.md\``);
25
+ if (skill.description) lines.push(` ${skill.description}`);
30
26
  }
31
27
  lines.push('');
32
28
  }
33
29
 
34
30
  return lines.join('\n');
35
- }
31
+ }
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, readMcpConfig, 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(readMcpConfig(aiDir));
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, writeMcpConfig } 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,31 +111,33 @@ export async function init(projectDir, options = {}) {
102
111
  }
103
112
  }
104
113
 
105
- // Create .ai/mcp/mcp.json if it doesn't exist yet
114
+ // Create .ai/mcp/mcp.json
106
115
  const mcpJsonPath = path.join(aiDir, 'mcp', 'mcp.json');
107
116
  if (!fs.existsSync(mcpJsonPath) || force) {
108
- writeMcpConfig(aiDir, { servers: {}, disabled: [] });
117
+ writeMcpConfig(aiDir, { mcpServers: {} });
109
118
  console.log(` ${chalk.green('✓')} ${chalk.cyan('.ai/mcp/mcp.json')}`);
110
119
  }
111
120
 
112
- // Create js-boost.config.json with defaults (or read existing)
113
- const configPath = path.join(projectDir, 'js-boost.config.json');
114
- let existingConfig = {};
115
- if (fs.existsSync(configPath) && !force) {
116
- try { existingConfig = JSON.parse(fs.readFileSync(configPath, 'utf8')); } catch {}
121
+ // Agent selection
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 {}
117
126
  }
118
127
 
119
- // Agent selection
120
- const selectedAgents = await selectAgents(projectDir, existingConfig.agents);
128
+ const selectedAgents = await selectAgents(projectDir, existingLocal.agents ?? null);
121
129
 
122
- const config = {
123
- projectName: existingConfig.projectName || path.basename(projectDir),
124
- projectDescription: existingConfig.projectDescription || '',
130
+ writeLocalConfig(projectDir, {
125
131
  agents: selectedAgents,
126
- };
132
+ disabledMcpServers: existingLocal.disabledMcpServers ?? [],
133
+ });
134
+ console.log(` ${chalk.green('✓')} ${chalk.cyan('.js-boost.json')}`);
127
135
 
128
- writeFile(configPath, JSON.stringify(config, null, 2));
129
- 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
+ }
130
141
 
131
142
  console.log('');
132
143
  console.log(chalk.green.bold(' ✓ .ai/ folder initialized'));
@@ -134,6 +145,6 @@ export async function init(projectDir, options = {}) {
134
145
  console.log(chalk.dim(' Next steps:'));
135
146
  console.log(chalk.dim(' 1. Edit .ai/guidelines/*.md with your project conventions'));
136
147
  console.log(chalk.dim(' 2. Add skills in .ai/skills/<name>/SKILL.md'));
137
- console.log(chalk.dim(' 3. Run: npx js-boost generate'));
148
+ console.log(chalk.dim(' 3. Run: npx @1tool/js-boost generate'));
138
149
  console.log('');
139
150
  }
package/src/utils/mcp.js CHANGED
@@ -1,53 +1,50 @@
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(mcpConfig = {}) {
12
- const userServers = mcpConfig.servers || {};
13
- const disabledDefaults = mcpConfig.disabled || [];
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') {
41
37
  mcpServers[key] = {
42
- command: server.command,
43
- args: server.args || [],
44
- ...(server.env ? { env: server.env } : {})
38
+ type: 'http',
39
+ url: server.url,
40
+ ...(server.headers ? { headers: server.headers } : {}),
45
41
  };
46
- } else if (server.type === 'remote') {
47
- // Claude Code uses mcp-remote wrapper for HTTP/SSE servers
42
+ } else if (server.command) {
48
43
  mcpServers[key] = {
49
- command: 'npx',
50
- args: ['-y', 'mcp-remote', server.url]
44
+ type: 'stdio',
45
+ command: server.command,
46
+ args: server.args || [],
47
+ env: server.env || {},
51
48
  };
52
49
  }
53
50
  }
@@ -56,21 +53,19 @@ export function generateMcpJson(servers) {
56
53
  }
57
54
 
58
55
  /**
59
- * Generate .junie/mcp.json (Junie format supports URL directly)
56
+ * Generate .junie/mcp.json remote servers referenced by URL directly
60
57
  */
61
58
  export function generateJunieMcpJson(servers) {
62
59
  const mcpServers = {};
63
60
 
64
61
  for (const [key, server] of Object.entries(servers)) {
65
- if (server.type === 'stdio') {
62
+ if (server.type === 'http') {
63
+ mcpServers[key] = { url: server.url };
64
+ } else if (server.command) {
66
65
  mcpServers[key] = {
67
66
  command: server.command,
68
67
  args: server.args || [],
69
- ...(server.env ? { env: server.env } : {})
70
- };
71
- } else if (server.type === 'remote') {
72
- mcpServers[key] = {
73
- url: server.url
68
+ ...(server.env ? { env: server.env } : {}),
74
69
  };
75
70
  }
76
71
  }
@@ -88,8 +83,8 @@ export function buildMcpMarkdownSection(servers) {
88
83
  for (const [key, server] of Object.entries(servers)) {
89
84
  lines.push(`### ${key}`);
90
85
  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(' ')}\``);
86
+ if (server.type === 'http') lines.push(`- **URL:** \`${server.url}\``);
87
+ if (server.command) lines.push(`- **Command:** \`${server.command} ${(server.args || []).join(' ')}\``);
93
88
  lines.push('');
94
89
  }
95
90
 
@@ -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'));
@@ -75,20 +74,24 @@ export function readConfig(projectDir) {
75
74
  }
76
75
 
77
76
  /**
78
- * Read .ai/mcp/mcp.json — MCP server definitions
79
- * Returns { servers: {}, disabled: [] }
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: {} }
80
86
  */
81
87
  export function readMcpConfig(aiDir) {
82
88
  const mcpPath = path.join(aiDir, 'mcp', 'mcp.json');
83
- if (!fs.existsSync(mcpPath)) return { servers: {}, disabled: [] };
89
+ if (!fs.existsSync(mcpPath)) return { mcpServers: {} };
84
90
  try {
85
91
  const parsed = JSON.parse(fs.readFileSync(mcpPath, 'utf8'));
86
- return {
87
- servers: parsed.servers || {},
88
- disabled: parsed.disabled || [],
89
- };
92
+ return { mcpServers: parsed.mcpServers || {} };
90
93
  } catch {
91
- return { servers: {}, disabled: [] };
94
+ return { mcpServers: {} };
92
95
  }
93
96
  }
94
97
 
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'));