@girardmedia/bootspring 2.5.6 → 2.5.8

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@girardmedia/bootspring",
3
- "version": "2.5.6",
3
+ "version": "2.5.8",
4
4
  "description": "Thin client for Bootspring cloud MCP, hosted agents, and paywalled workflow intelligence",
5
5
  "keywords": [
6
6
  "ai",
@@ -6,11 +6,15 @@ const path = require('path');
6
6
 
7
7
  const c = {
8
8
  reset: '\x1b[0m',
9
+ bold: '\x1b[1m',
9
10
  green: '\x1b[32m',
11
+ yellow: '\x1b[33m',
10
12
  cyan: '\x1b[36m',
11
13
  dim: '\x1b[2m'
12
14
  };
13
15
 
16
+ const HOME = os.homedir();
17
+
14
18
  function resolveCommandsSource() {
15
19
  const fromEnv = process.env.BOOTSPRING_COMMANDS_SOURCE;
16
20
  const candidates = [
@@ -108,11 +112,127 @@ function installBootspringSkill(skillsDir) {
108
112
  }
109
113
  }
110
114
 
115
+ // ── MCP Server Registration ───────────────────────────────────────────
116
+ //
117
+ // Each assistant has its own MCP config file format. We register the
118
+ // `bootspring` MCP server so that bootspring_build, bootspring_context,
119
+ // bootspring_quality, etc. are exposed as tools in every project — no
120
+ // per-project .mcp.json required.
121
+ //
122
+ // All registrations are idempotent (skip if already configured) and
123
+ // failure-tolerant (silently skip on permission/IO errors so a broken
124
+ // assistant config never blocks `npm install`).
125
+
126
+ function registerClaudeMcp() {
127
+ // Claude Code v2.x reads user-scope MCP servers from ~/.claude.json
128
+ // (the file at home root, NOT inside the ~/.claude/ directory).
129
+ const configPath = path.join(HOME, '.claude.json');
130
+ try {
131
+ let config = {};
132
+ if (fs.existsSync(configPath)) {
133
+ const raw = fs.readFileSync(configPath, 'utf8');
134
+ config = raw.trim() ? JSON.parse(raw) : {};
135
+ }
136
+ if (!config.mcpServers || typeof config.mcpServers !== 'object') {
137
+ config.mcpServers = {};
138
+ }
139
+ // Skip if ANY bootspring entry exists — never clobber user customizations.
140
+ if (config.mcpServers.bootspring) {
141
+ return { name: 'Claude Code', status: 'unchanged', path: configPath };
142
+ }
143
+ config.mcpServers.bootspring = {
144
+ type: 'stdio',
145
+ command: 'bootspring',
146
+ args: ['mcp'],
147
+ env: { BOOTSPRING_ASSISTANT: 'claude' }
148
+ };
149
+ fs.writeFileSync(configPath, `${JSON.stringify(config, null, 2)}\n`, 'utf8');
150
+ return { name: 'Claude Code', status: 'updated', path: configPath };
151
+ } catch (err) {
152
+ return { name: 'Claude Code', status: 'skipped', path: configPath, reason: err.message };
153
+ }
154
+ }
155
+
156
+ function registerCodexMcp() {
157
+ // Codex reads MCP servers from ~/.codex/config.toml under [mcp_servers.<name>]
158
+ const configPath = path.join(HOME, '.codex', 'config.toml');
159
+ try {
160
+ const dir = path.dirname(configPath);
161
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
162
+
163
+ const existing = fs.existsSync(configPath) ? fs.readFileSync(configPath, 'utf8') : '';
164
+ if (existing.includes('[mcp_servers.bootspring]')) {
165
+ return { name: 'Codex', status: 'unchanged', path: configPath };
166
+ }
167
+ const block = '[mcp_servers.bootspring]\ncommand = "bootspring"\nargs = ["mcp"]\nenv = { BOOTSPRING_ASSISTANT = "codex" }\n';
168
+ const content = existing.length > 0 ? `${existing.trimEnd()}\n\n${block}` : block;
169
+ fs.writeFileSync(configPath, content, 'utf8');
170
+ return { name: 'Codex', status: 'updated', path: configPath };
171
+ } catch (err) {
172
+ return { name: 'Codex', status: 'skipped', path: configPath, reason: err.message };
173
+ }
174
+ }
175
+
176
+ function registerGeminiMcp() {
177
+ // Gemini CLI reads MCP servers from ~/.gemini/settings.json under "mcpServers"
178
+ const configPath = path.join(HOME, '.gemini', 'settings.json');
179
+ try {
180
+ const dir = path.dirname(configPath);
181
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
182
+
183
+ let settings = {};
184
+ if (fs.existsSync(configPath)) {
185
+ const raw = fs.readFileSync(configPath, 'utf8');
186
+ settings = raw.trim() ? JSON.parse(raw) : {};
187
+ }
188
+ if (!settings.mcpServers || typeof settings.mcpServers !== 'object') {
189
+ settings.mcpServers = {};
190
+ }
191
+ // Skip if ANY bootspring entry exists — never clobber user customizations.
192
+ if (settings.mcpServers.bootspring) {
193
+ return { name: 'Gemini CLI', status: 'unchanged', path: configPath };
194
+ }
195
+ settings.mcpServers.bootspring = {
196
+ command: 'bootspring',
197
+ args: ['mcp'],
198
+ env: { BOOTSPRING_ASSISTANT: 'gemini' }
199
+ };
200
+ fs.writeFileSync(configPath, `${JSON.stringify(settings, null, 2)}\n`, 'utf8');
201
+ return { name: 'Gemini CLI', status: 'updated', path: configPath };
202
+ } catch (err) {
203
+ return { name: 'Gemini CLI', status: 'skipped', path: configPath, reason: err.message };
204
+ }
205
+ }
206
+
207
+ function registerAllMcp() {
208
+ return [registerClaudeMcp(), registerCodexMcp(), registerGeminiMcp()];
209
+ }
210
+
211
+ function isFirstInstall() {
212
+ // Heuristic: a first install is one where Claude Code config does NOT yet
213
+ // have a bootspring entry. Updates to an existing install will already
214
+ // have one and shouldn't see the welcome screen.
215
+ try {
216
+ const claudeConfig = path.join(HOME, '.claude.json');
217
+ if (!fs.existsSync(claudeConfig)) return true;
218
+ const raw = fs.readFileSync(claudeConfig, 'utf8');
219
+ if (!raw.trim()) return true;
220
+ const parsed = JSON.parse(raw);
221
+ return !parsed?.mcpServers?.bootspring;
222
+ } catch {
223
+ return false;
224
+ }
225
+ }
226
+
111
227
  function main() {
112
228
  if (process.env.CI || process.env.BOOTSPRING_SKIP_POSTINSTALL) {
113
229
  return;
114
230
  }
115
231
 
232
+ // Capture this BEFORE we run any registration so the welcome message
233
+ // only fires the first time bootspring is installed on this machine.
234
+ const firstInstall = isFirstInstall();
235
+
116
236
  let totalCommandsInstalled = 0;
117
237
  let totalSkillsInstalled = 0;
118
238
  const installedTo = [];
@@ -128,16 +248,53 @@ function main() {
128
248
  }
129
249
  }
130
250
 
131
- if (totalCommandsInstalled > 0 || totalSkillsInstalled > 0) {
251
+ // Register the Bootspring MCP server with each assistant so its tools
252
+ // (bootspring_build, bootspring_context, bootspring_quality, etc.) are
253
+ // available everywhere — no per-project .mcp.json needed.
254
+ const mcpResults = registerAllMcp();
255
+ const mcpUpdated = mcpResults.filter((r) => r.status === 'updated');
256
+ const mcpUnchanged = mcpResults.filter((r) => r.status === 'unchanged');
257
+ const mcpSkipped = mcpResults.filter((r) => r.status === 'skipped');
258
+
259
+ const didAnything =
260
+ totalCommandsInstalled > 0 ||
261
+ totalSkillsInstalled > 0 ||
262
+ mcpUpdated.length > 0;
263
+
264
+ if (didAnything) {
132
265
  console.log(`
133
266
  ${c.cyan}⚡ Bootspring${c.reset} assistant integrations installed!
134
267
 
135
268
  ${c.dim}Installed:${c.reset}
136
269
  ${c.green}${totalCommandsInstalled}${c.reset} slash command templates
137
270
  ${c.green}${totalSkillsInstalled}${c.reset} Bootspring base skills
271
+ ${c.green}${mcpUpdated.length}${c.reset} MCP server registrations${mcpUnchanged.length > 0 ? ` ${c.dim}(${mcpUnchanged.length} already configured)${c.reset}` : ''}
272
+ `);
273
+
274
+ if (mcpUpdated.length > 0) {
275
+ console.log(`${c.dim}MCP registered for:${c.reset} ${mcpUpdated.map((r) => r.name).join(', ')}`);
276
+ }
277
+ if (installedTo.length > 0) {
278
+ console.log(`${c.dim}Commands & skills installed to:${c.reset} ${installedTo.join(', ')}`);
279
+ }
280
+ if (mcpSkipped.length > 0) {
281
+ console.log(`${c.yellow}Note:${c.reset} ${c.dim}MCP registration skipped for ${mcpSkipped.map((r) => r.name).join(', ')} — run \`bootspring setup\` to retry.${c.reset}`);
282
+ }
283
+
284
+ if (firstInstall) {
285
+ console.log(`
286
+ ${c.bold}Next steps:${c.reset}
287
+ ${c.cyan}1.${c.reset} Restart your assistant (Claude Code / Codex / Gemini) so MCP loads
288
+ ${c.cyan}2.${c.reset} ${c.green}bootspring auth login${c.reset} ${c.dim}# authenticate (opens browser)${c.reset}
289
+ ${c.cyan}3.${c.reset} ${c.green}cd${c.reset} into a project, then ${c.green}bootspring go${c.reset} ${c.dim}# one-shot setup${c.reset}
138
290
 
139
- ${c.dim}Installed to: ${installedTo.join(', ')}${c.reset}
291
+ ${c.dim}Run ${c.reset}${c.green}bootspring${c.reset}${c.dim} with no args to see the welcome screen.${c.reset}
140
292
  `);
293
+ } else {
294
+ console.log(`
295
+ ${c.dim}Restart your assistant (Claude Code, Codex, or Gemini CLI) to load the bootspring MCP tools.${c.reset}
296
+ `);
297
+ }
141
298
  }
142
299
  }
143
300