@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 +79 -36
- package/package.json +1 -1
- package/src/cli.js +32 -30
- package/src/configure/mcp.js +85 -52
- package/src/index.js +47 -26
- package/src/init.js +32 -16
- package/src/utils/mcp.js +29 -33
- package/src/utils/reader.js +33 -5
- package/src/watch.js +1 -1
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
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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
|
|
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
|
-
|
|
81
|
+
The wizard offers three actions:
|
|
82
|
+
|
|
83
|
+
**Add a remote server** — HTTP/SSE endpoint with optional auth headers:
|
|
80
84
|
```
|
|
81
|
-
✔ Server key
|
|
82
|
-
✔ Server type
|
|
83
|
-
✔ URL
|
|
84
|
-
✔
|
|
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
|
|
92
|
+
**Add a local server** — stdio process with optional args and env vars:
|
|
88
93
|
```
|
|
89
|
-
✔ Server key
|
|
90
|
-
✔ Server type
|
|
91
|
-
✔ Command
|
|
92
|
-
✔ Arguments
|
|
93
|
-
✔ Environment variables
|
|
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.
|
|
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
|
|
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
|
-
|
|
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-
|
|
153
|
-
"type": "
|
|
159
|
+
"my-remote": {
|
|
160
|
+
"type": "http",
|
|
154
161
|
"url": "https://my-mcp.com/mcp",
|
|
155
|
-
"
|
|
162
|
+
"headers": {
|
|
163
|
+
"Authorization": "Bearer YOUR_TOKEN_HERE"
|
|
164
|
+
}
|
|
156
165
|
},
|
|
157
|
-
"local
|
|
158
|
-
"type": "stdio",
|
|
166
|
+
"my-local": {
|
|
159
167
|
"command": "node",
|
|
160
|
-
"args": ["./mcp-server.js"],
|
|
161
|
-
"env": {
|
|
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
|
-
|
|
181
|
+
Server types:
|
|
169
182
|
|
|
170
|
-
|
|
|
171
|
-
|
|
172
|
-
|
|
|
173
|
-
|
|
|
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
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
|
|
11
|
-
.version('1.
|
|
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
|
|
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
|
|
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('
|
|
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 {
|
|
59
|
-
const
|
|
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,
|
|
63
|
-
|
|
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(' ✓
|
|
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
|
|
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
|
|
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,
|
|
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(
|
|
97
|
+
const mcpServers = buildMcpServers(readMcpConfig(aiDir), localConfig);
|
|
100
98
|
|
|
101
99
|
const allAgentKeys = Object.keys(AGENTS);
|
|
102
|
-
const activeAgents = new Set(
|
|
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
|
-
|
|
117
|
-
console.log(` ${icon} ${label} ${hint}`);
|
|
114
|
+
console.log(` ${icon} ${label} ${chalk.dim(`(${agent.hint})`)}`);
|
|
118
115
|
}
|
|
119
|
-
if (
|
|
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
|
|
139
|
-
console.log(` ${chalk.green('•')} ${chalk.cyan(key)} — ${chalk.dim(
|
|
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',
|
|
146
|
-
has('claude_code') && ['CLAUDE.md',
|
|
147
|
-
hasAny(MCP_JSON_CONSUMERS) && ['.mcp.json',
|
|
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',
|
|
152
|
+
has('kiro') && ['.kiro/steering/guidelines.md', 'Kiro'],
|
|
151
153
|
].filter(Boolean);
|
|
152
154
|
|
|
153
155
|
for (const [file, label] of willGenerate) {
|
package/src/configure/mcp.js
CHANGED
|
@@ -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 {
|
|
5
|
+
import { readMcpConfig, writeMcpConfig, readLocalConfig, writeLocalConfig } from '../utils/reader.js';
|
|
6
6
|
|
|
7
|
-
function displayServers(
|
|
8
|
-
const userServers =
|
|
9
|
-
const disabled = new Set(
|
|
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 === '
|
|
28
|
+
const addr = srv.type === 'http'
|
|
29
29
|
? srv.url
|
|
30
30
|
: `${srv.command}${srv.args?.length ? ' ' + srv.args.join(' ') : ''}`;
|
|
31
|
-
|
|
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(
|
|
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 ((
|
|
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: '
|
|
54
|
-
{ value: 'stdio',
|
|
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 === '
|
|
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
|
-
|
|
76
|
-
|
|
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
|
-
|
|
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(
|
|
126
|
-
const keys = Object.keys(
|
|
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 =
|
|
136
|
-
const addr = srv.type === '
|
|
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
|
|
155
|
-
const
|
|
156
|
-
|
|
157
|
-
|
|
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(
|
|
181
|
+
const disabled = new Set(localConfig.disabledMcpServers || []);
|
|
162
182
|
|
|
163
|
-
const
|
|
164
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
181
|
-
const
|
|
182
|
-
|
|
183
|
-
|
|
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(
|
|
222
|
+
displayServers(mcpConfig, localConfig);
|
|
193
223
|
|
|
194
|
-
const hasCustom = Object.keys(
|
|
195
|
-
const
|
|
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',
|
|
199
|
-
...(hasCustom
|
|
200
|
-
...(
|
|
201
|
-
{ value: 'done',
|
|
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(
|
|
213
|
-
if (action === 'remove') await removeServer(
|
|
214
|
-
if (action === 'toggle') await
|
|
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
|
-
|
|
248
|
+
writeMcpConfig(aiDir, mcpConfig);
|
|
249
|
+
writeLocalConfig(projectDir, localConfig);
|
|
219
250
|
console.log('');
|
|
220
|
-
console.log(chalk.green(' ✓ Saved
|
|
221
|
-
console.log(chalk.dim('
|
|
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,
|
|
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
|
-
//
|
|
47
|
+
// Read source files
|
|
34
48
|
const guidelines = await readGuidelines(aiDir);
|
|
35
49
|
const skills = await readSkills(aiDir);
|
|
36
|
-
const mcpServers = buildMcpServers(
|
|
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: ${
|
|
58
|
+
log(chalk.dim(` Agents: ${activeAgentsList.join(', ')}`));
|
|
45
59
|
log('');
|
|
46
60
|
}
|
|
47
61
|
|
|
48
62
|
const generatedFiles = [];
|
|
49
63
|
|
|
50
|
-
//
|
|
64
|
+
// AGENTS.md — Amp, Codex, Copilot, Gemini, OpenCode
|
|
51
65
|
if (hasAny(AGENTS_MD_CONSUMERS)) {
|
|
52
|
-
const agentsMd = generateAgentsMd(guidelines, skills, mcpServers,
|
|
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
|
-
//
|
|
74
|
+
// CLAUDE.md — Claude Code
|
|
61
75
|
if (has('claude_code')) {
|
|
62
|
-
const claudeMd = generateClaudeMd(guidelines, skills, mcpServers,
|
|
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
|
-
//
|
|
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
|
-
//
|
|
94
|
+
// .junie/ — JetBrains Junie
|
|
81
95
|
if (has('junie')) {
|
|
82
|
-
const junieGuidelines = generateJunieGuidelines(guidelines, skills,
|
|
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
|
-
//
|
|
109
|
+
// Cursor
|
|
96
110
|
if (has('cursor')) {
|
|
97
|
-
const cursorRules = generateCursorRules(guidelines, skills,
|
|
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,
|
|
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
|
-
//
|
|
124
|
+
// Kiro
|
|
111
125
|
if (has('kiro')) {
|
|
112
|
-
const kiroSteering = generateKiroSteering(guidelines, skills,
|
|
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
|
|
106
|
-
const
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
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
|
|
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
|
|
116
|
-
|
|
117
|
-
|
|
128
|
+
const selectedAgents = await selectAgents(projectDir, existingLocal.agents ?? null);
|
|
129
|
+
|
|
130
|
+
writeLocalConfig(projectDir, {
|
|
118
131
|
agents: selectedAgents,
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
};
|
|
132
|
+
disabledMcpServers: existingLocal.disabledMcpServers ?? [],
|
|
133
|
+
});
|
|
134
|
+
console.log(` ${chalk.green('✓')} ${chalk.cyan('.js-boost.json')}`);
|
|
122
135
|
|
|
123
|
-
|
|
124
|
-
|
|
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
|
-
*
|
|
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(
|
|
12
|
-
const userServers =
|
|
13
|
-
const
|
|
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 (!
|
|
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
|
-
|
|
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 (
|
|
34
|
-
*
|
|
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 === '
|
|
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
|
|
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 === '
|
|
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 === '
|
|
92
|
-
if (server.
|
|
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
|
|
package/src/utils/reader.js
CHANGED
|
@@ -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.
|
|
63
|
+
* Read .js-boost.json — per-developer local config (gitignored)
|
|
64
|
+
* Returns { agents, guidelines, skills, disabledMcpServers }
|
|
66
65
|
*/
|
|
67
|
-
export function
|
|
68
|
-
const configPath = path.join(projectDir, 'js-boost.
|
|
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.
|
|
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'));
|