@girardmedia/bootspring 2.1.3 → 2.2.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.
Files changed (65) hide show
  1. package/bin/bootspring.js +157 -83
  2. package/claude-commands/agent.md +34 -0
  3. package/claude-commands/bs.md +31 -0
  4. package/claude-commands/build.md +25 -0
  5. package/claude-commands/skill.md +31 -0
  6. package/claude-commands/todo.md +25 -0
  7. package/dist/core/index.d.ts +5814 -0
  8. package/dist/core.js +5779 -0
  9. package/dist/index.js +93883 -0
  10. package/dist/mcp/index.d.ts +1 -0
  11. package/dist/mcp-server.js +2298 -0
  12. package/generators/api-docs.js +3 -3
  13. package/generators/decisions.js +14 -14
  14. package/generators/health.js +6 -6
  15. package/generators/sprint.js +4 -4
  16. package/generators/templates/build-planning.template.js +2 -2
  17. package/generators/visual-doc-generator.js +1 -1
  18. package/package.json +22 -68
  19. package/cli/agent.js +0 -799
  20. package/cli/auth.js +0 -896
  21. package/cli/billing.js +0 -320
  22. package/cli/build.js +0 -1442
  23. package/cli/dashboard.js +0 -123
  24. package/cli/init.js +0 -669
  25. package/cli/mcp.js +0 -240
  26. package/cli/orchestrator.js +0 -240
  27. package/cli/project.js +0 -825
  28. package/cli/quality.js +0 -281
  29. package/cli/skill.js +0 -503
  30. package/cli/switch.js +0 -453
  31. package/cli/todo.js +0 -629
  32. package/cli/update.js +0 -132
  33. package/core/api-client.d.ts +0 -69
  34. package/core/api-client.js +0 -1482
  35. package/core/auth.d.ts +0 -98
  36. package/core/auth.js +0 -737
  37. package/core/build-orchestrator.js +0 -508
  38. package/core/build-state.js +0 -612
  39. package/core/config.d.ts +0 -106
  40. package/core/config.js +0 -1328
  41. package/core/context-loader.js +0 -580
  42. package/core/context.d.ts +0 -61
  43. package/core/context.js +0 -327
  44. package/core/entitlements.d.ts +0 -70
  45. package/core/entitlements.js +0 -322
  46. package/core/index.d.ts +0 -53
  47. package/core/index.js +0 -62
  48. package/core/mcp-config.js +0 -115
  49. package/core/policies.d.ts +0 -43
  50. package/core/policies.js +0 -113
  51. package/core/policy-matrix.js +0 -303
  52. package/core/project-activity.js +0 -175
  53. package/core/redaction.d.ts +0 -5
  54. package/core/redaction.js +0 -63
  55. package/core/self-update.js +0 -259
  56. package/core/session.js +0 -353
  57. package/core/task-extractor.js +0 -1098
  58. package/core/telemetry.d.ts +0 -55
  59. package/core/telemetry.js +0 -617
  60. package/core/tier-enforcement.js +0 -928
  61. package/core/utils.d.ts +0 -90
  62. package/core/utils.js +0 -455
  63. package/core/validation.js +0 -572
  64. package/mcp/server.d.ts +0 -57
  65. package/mcp/server.js +0 -264
@@ -0,0 +1,2298 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __esm = (fn, res) => function __init() {
9
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
10
+ };
11
+ var __commonJS = (cb, mod) => function __require() {
12
+ return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
13
+ };
14
+ var __export = (target, all) => {
15
+ for (var name in all)
16
+ __defProp(target, name, { get: all[name], enumerable: true });
17
+ };
18
+ var __copyProps = (to, from, except, desc) => {
19
+ if (from && typeof from === "object" || typeof from === "function") {
20
+ for (let key of __getOwnPropNames(from))
21
+ if (!__hasOwnProp.call(to, key) && key !== except)
22
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
23
+ }
24
+ return to;
25
+ };
26
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
27
+ // If the importer is in node compatibility mode or this is not an ESM
28
+ // file that has been converted to a CommonJS file using a Babel-
29
+ // compatible transform (i.e. "__esModule" has not been set), then set
30
+ // "default" to the CommonJS "module.exports" for node compatibility.
31
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
32
+ mod
33
+ ));
34
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
35
+
36
+ // node_modules/tsup/assets/cjs_shims.js
37
+ var init_cjs_shims = __esm({
38
+ "node_modules/tsup/assets/cjs_shims.js"() {
39
+ "use strict";
40
+ }
41
+ });
42
+
43
+ // package.json
44
+ var require_package = __commonJS({
45
+ "package.json"(exports2, module2) {
46
+ module2.exports = {
47
+ name: "@girardmedia/bootspring",
48
+ version: "2.2.1",
49
+ description: "Thin client for Bootspring cloud MCP, hosted agents, and paywalled workflow intelligence",
50
+ keywords: [
51
+ "ai",
52
+ "development",
53
+ "scaffolding",
54
+ "mcp",
55
+ "claude",
56
+ "agents",
57
+ "context",
58
+ "workflow",
59
+ "devtools"
60
+ ],
61
+ author: "Bootspring",
62
+ license: "SEE LICENSE IN LICENSE",
63
+ repository: {
64
+ type: "git",
65
+ url: "https://github.com/bootspring/bootspring.git"
66
+ },
67
+ homepage: "https://bootspring.com",
68
+ bugs: {
69
+ url: "https://github.com/bootspring/bootspring/issues"
70
+ },
71
+ bin: {
72
+ bootspring: "./bin/bootspring.js"
73
+ },
74
+ main: "./dist/core.js",
75
+ types: "./dist/core/index.d.ts",
76
+ exports: {
77
+ ".": {
78
+ types: "./dist/core/index.d.ts",
79
+ default: "./dist/core.js"
80
+ },
81
+ "./mcp": {
82
+ types: "./dist/mcp/index.d.ts",
83
+ default: "./dist/mcp-server.js"
84
+ }
85
+ },
86
+ files: [
87
+ "bin/bootspring.js",
88
+ "dist/index.js",
89
+ "dist/core.js",
90
+ "dist/mcp-server.js",
91
+ "dist/core/index.d.ts",
92
+ "dist/mcp/index.d.ts",
93
+ "generators/",
94
+ "claude-commands/",
95
+ "scripts/postinstall.js"
96
+ ],
97
+ scripts: {
98
+ postinstall: "node scripts/postinstall.js",
99
+ start: "node bin/bootspring.js",
100
+ dashboard: "node bin/bootspring.js dashboard",
101
+ mcp: "node dist/mcp-server.js",
102
+ pretest: "npm run build",
103
+ test: "vitest run",
104
+ "test:launch-smoke": "vitest run __tests__/unit/cli-first-run-smoke.test.js __tests__/unit/health-fresh-start.test.ts __tests__/unit/session-project-scope.test.ts __tests__/unit/auth-cli-mixed-states.test.ts __tests__/unit/api-client-project-auth-fallback.test.js __tests__/unit/cli-help-surface.test.js __tests__/unit/cli-command-manifest.test.js",
105
+ "test:seed-ingestion": "vitest run __tests__/unit/seed-ingestion-regression.test.js",
106
+ "test:watch": "vitest",
107
+ "test:coverage": "vitest run --coverage",
108
+ lint: "eslint .",
109
+ "lint:fix": "eslint . --fix",
110
+ typecheck: "tsc --noEmit",
111
+ "typecheck:active": "tsc --noEmit -p tsconfig.active.json",
112
+ build: "tsup",
113
+ "build:watch": "tsup --watch",
114
+ dev: "tsx watch src/index.ts",
115
+ "verify:lint-budget": "node scripts/check-lint-budgets.js",
116
+ "verify:release-gates": "node scripts/release-gate-check.js",
117
+ "verify:shared-contracts": "node scripts/verify-shared-contracts.js",
118
+ "verify:thin-client-contract": "node scripts/verify-thin-client-contract.js",
119
+ "build:mcp-contract": "node scripts/export-mcp-contract.js",
120
+ "verify:mcp-contract": "node scripts/export-mcp-contract.js --check",
121
+ "planning:sync": "node scripts/sync-planning-queue.js",
122
+ "planning:sync:check": "node scripts/sync-planning-queue.js --check",
123
+ "planning:realign": "node scripts/sync-planning-queue.js --sync-runtime",
124
+ "verify:package": "node scripts/check-package-boundaries.js",
125
+ "db:sync": "node shared/db/sync.js",
126
+ "db:sync:check": "node shared/db/sync.js --check",
127
+ prepublishOnly: "npm run build && npm test && npm run lint --if-present && npm run verify:package && npm run verify:mcp-contract"
128
+ },
129
+ devDependencies: {
130
+ "@eslint/js": "^9.39.2",
131
+ "@swc/core": "^1.15.13",
132
+ "@types/node": "^25.3.1",
133
+ "@typescript-eslint/eslint-plugin": "^8.57.0",
134
+ "@typescript-eslint/parser": "^8.57.0",
135
+ "@vitest/coverage-v8": "^4.0.18",
136
+ eslint: "^9.39.2",
137
+ globals: "^17.3.0",
138
+ tsup: "^8.5.1",
139
+ tsx: "^4.21.0",
140
+ typescript: "^5.9.3",
141
+ vitest: "^4.0.18"
142
+ },
143
+ dependencies: {
144
+ "@modelcontextprotocol/sdk": "^1.0.0",
145
+ jsonwebtoken: "^9.0.3",
146
+ ws: "^8.18.0",
147
+ yaml: "^2.8.0",
148
+ zod: "^4.3.6"
149
+ },
150
+ engines: {
151
+ node: ">=18.0.0"
152
+ },
153
+ overrides: {
154
+ table: {
155
+ ajv: "^8.12.0"
156
+ },
157
+ minimatch: "^10.2.1",
158
+ hono: "4.12.4",
159
+ "@hono/node-server": "1.19.10",
160
+ "express-rate-limit": "^8.2.2"
161
+ }
162
+ };
163
+ }
164
+ });
165
+
166
+ // src/core/auth.ts
167
+ var auth_exports = {};
168
+ __export(auth_exports, {
169
+ BOOTSPRING_DIR: () => BOOTSPRING_DIR,
170
+ CONFIG_FILE: () => CONFIG_FILE,
171
+ CREDENTIALS_FILE: () => CREDENTIALS_FILE,
172
+ DEVICE_FILE: () => DEVICE_FILE,
173
+ clearCredentials: () => clearCredentials,
174
+ clearDeviceInfo: () => clearDeviceInfo,
175
+ clearProjectApiKey: () => clearProjectApiKey,
176
+ clearProjectScopedSession: () => clearProjectScopedSession,
177
+ ensureDir: () => ensureDir,
178
+ generateDeviceFingerprint: () => generateDeviceFingerprint,
179
+ getApiKey: () => getApiKey,
180
+ getConfig: () => getConfig,
181
+ getCredentials: () => getCredentials,
182
+ getCredentialsPath: () => getCredentialsPath,
183
+ getDeviceContext: () => getDeviceContext,
184
+ getDeviceId: () => getDeviceId,
185
+ getDeviceInfo: () => getDeviceInfo,
186
+ getLegacyProjectApiKey: () => getLegacyProjectApiKey,
187
+ getProjectScopedToken: () => getProjectScopedToken,
188
+ getRefreshToken: () => getRefreshToken,
189
+ getStoredApiKey: () => getStoredApiKey,
190
+ getTier: () => getTier,
191
+ getToken: () => getToken,
192
+ getUser: () => getUser,
193
+ isApiKeyAuth: () => isApiKeyAuth,
194
+ isAuthenticated: () => isAuthenticated,
195
+ login: () => login,
196
+ loginWithApiKey: () => loginWithApiKey,
197
+ logout: () => logout,
198
+ saveApiKeyToProject: () => saveApiKeyToProject,
199
+ saveConfig: () => saveConfig,
200
+ saveCredentials: () => saveCredentials,
201
+ saveProjectScopedSession: () => saveProjectScopedSession,
202
+ updateTokens: () => updateTokens
203
+ });
204
+ function getEncryptionKey() {
205
+ const machineId = os.hostname() + os.userInfo().username;
206
+ return crypto.createHash("sha256").update(machineId).digest();
207
+ }
208
+ function ensureDir() {
209
+ if (!fs.existsSync(BOOTSPRING_DIR)) {
210
+ fs.mkdirSync(BOOTSPRING_DIR, { recursive: true, mode: 448 });
211
+ }
212
+ }
213
+ function encrypt(data) {
214
+ try {
215
+ const key = getEncryptionKey();
216
+ const iv = crypto.randomBytes(16);
217
+ const cipher = crypto.createCipheriv("aes-256-cbc", key, iv);
218
+ let encrypted = cipher.update(JSON.stringify(data), "utf8", "hex");
219
+ encrypted += cipher.final("hex");
220
+ return { iv: iv.toString("hex"), data: encrypted };
221
+ } catch {
222
+ return data;
223
+ }
224
+ }
225
+ function decrypt(encrypted) {
226
+ try {
227
+ if ("iv" in encrypted && "data" in encrypted && typeof encrypted.iv === "string") {
228
+ const key = getEncryptionKey();
229
+ const iv = Buffer.from(encrypted.iv, "hex");
230
+ const decipher = crypto.createDecipheriv("aes-256-cbc", key, iv);
231
+ let decrypted = decipher.update(encrypted.data, "hex", "utf8");
232
+ decrypted += decipher.final("utf8");
233
+ return JSON.parse(decrypted);
234
+ }
235
+ return encrypted;
236
+ } catch {
237
+ return encrypted;
238
+ }
239
+ }
240
+ function getCredentials() {
241
+ try {
242
+ if (fs.existsSync(CREDENTIALS_FILE)) {
243
+ const raw = JSON.parse(fs.readFileSync(CREDENTIALS_FILE, "utf-8"));
244
+ return decrypt(raw);
245
+ }
246
+ } catch {
247
+ }
248
+ return null;
249
+ }
250
+ function saveCredentials(credentials) {
251
+ ensureDir();
252
+ const encrypted = encrypt(credentials);
253
+ fs.writeFileSync(
254
+ CREDENTIALS_FILE,
255
+ JSON.stringify(encrypted, null, 2),
256
+ { mode: 384 }
257
+ );
258
+ }
259
+ function clearCredentials() {
260
+ try {
261
+ if (fs.existsSync(CREDENTIALS_FILE)) {
262
+ fs.unlinkSync(CREDENTIALS_FILE);
263
+ }
264
+ } catch {
265
+ }
266
+ }
267
+ function getToken() {
268
+ const creds = getCredentials();
269
+ if (!creds) return null;
270
+ if (creds.expiresAt && new Date(creds.expiresAt) < /* @__PURE__ */ new Date()) {
271
+ return null;
272
+ }
273
+ return creds.token || null;
274
+ }
275
+ function findNearestProjectConfigPath() {
276
+ let dir = process.cwd();
277
+ for (let i = 0; i < 10; i++) {
278
+ const configPath = path.join(dir, ".bootspring.json");
279
+ if (fs.existsSync(configPath)) {
280
+ return configPath;
281
+ }
282
+ const parent = path.dirname(dir);
283
+ if (parent === dir) break;
284
+ dir = parent;
285
+ }
286
+ return null;
287
+ }
288
+ function readNearestProjectConfig() {
289
+ try {
290
+ const configPath = findNearestProjectConfigPath();
291
+ if (!configPath) {
292
+ return null;
293
+ }
294
+ const config = JSON.parse(fs.readFileSync(configPath, "utf-8"));
295
+ return { path: configPath, config };
296
+ } catch {
297
+ }
298
+ return null;
299
+ }
300
+ function writeNearestProjectConfig(pathname, config) {
301
+ try {
302
+ fs.writeFileSync(pathname, JSON.stringify(config, null, 2));
303
+ return true;
304
+ } catch {
305
+ return false;
306
+ }
307
+ }
308
+ function getProjectScopedSessionState() {
309
+ const projectConfig = readNearestProjectConfig();
310
+ if (!projectConfig || !projectConfig.config.projectAuth) {
311
+ return null;
312
+ }
313
+ const projectAuth = projectConfig.config.projectAuth;
314
+ if (typeof projectAuth.token !== "string" || projectAuth.token.length === 0 || typeof projectAuth.expiresAt !== "string" || projectAuth.expiresAt.length === 0) {
315
+ return null;
316
+ }
317
+ return {
318
+ token: projectAuth.token,
319
+ expiresAt: projectAuth.expiresAt,
320
+ issuedAt: typeof projectAuth.issuedAt === "string" ? projectAuth.issuedAt : (/* @__PURE__ */ new Date()).toISOString(),
321
+ source: typeof projectAuth.source === "string" ? projectAuth.source : void 0,
322
+ migratedFromLegacyApiKey: Boolean(projectAuth.migratedFromLegacyApiKey)
323
+ };
324
+ }
325
+ function getLegacyProjectApiKey() {
326
+ const projectConfig = readNearestProjectConfig();
327
+ if (!projectConfig) {
328
+ return null;
329
+ }
330
+ const legacyApiKey = projectConfig.config.apiKey;
331
+ if (typeof legacyApiKey === "string" && legacyApiKey.length > 0) {
332
+ return legacyApiKey;
333
+ }
334
+ return null;
335
+ }
336
+ function getProjectScopedToken() {
337
+ const projectAuth = getProjectScopedSessionState();
338
+ if (!projectAuth) {
339
+ return null;
340
+ }
341
+ const expiresAt = new Date(projectAuth.expiresAt).getTime();
342
+ if (!Number.isFinite(expiresAt)) {
343
+ return null;
344
+ }
345
+ if (Date.now() >= expiresAt - 6e4) {
346
+ return null;
347
+ }
348
+ return projectAuth.token;
349
+ }
350
+ function getStoredApiKey() {
351
+ const creds = getCredentials();
352
+ return creds?.apiKey || null;
353
+ }
354
+ function getApiKey() {
355
+ const envApiKey = process.env.BOOTSPRING_API_KEY;
356
+ if (envApiKey) {
357
+ return envApiKey;
358
+ }
359
+ if (getToken()) {
360
+ return null;
361
+ }
362
+ const projectScopedToken = getProjectScopedToken();
363
+ if (projectScopedToken) {
364
+ return projectScopedToken;
365
+ }
366
+ const storedApiKey = getStoredApiKey();
367
+ if (storedApiKey) {
368
+ return storedApiKey;
369
+ }
370
+ const legacyApiKey = getLegacyProjectApiKey();
371
+ if (legacyApiKey) {
372
+ if (!legacyProjectApiKeyWarned) {
373
+ legacyProjectApiKeyWarned = true;
374
+ console.warn(
375
+ "[bootspring] Using legacy .bootspring.json apiKey fallback. Run `bootspring auth login --api-key <key>` to migrate to short-lived project tokens."
376
+ );
377
+ }
378
+ return legacyApiKey;
379
+ }
380
+ return null;
381
+ }
382
+ function isApiKeyAuth() {
383
+ if (process.env.BOOTSPRING_API_KEY) {
384
+ return true;
385
+ }
386
+ if (getToken()) {
387
+ return false;
388
+ }
389
+ return Boolean(
390
+ getProjectScopedToken() || getStoredApiKey() || getLegacyProjectApiKey()
391
+ );
392
+ }
393
+ function getRefreshToken() {
394
+ const creds = getCredentials();
395
+ return creds?.refreshToken || null;
396
+ }
397
+ function isAuthenticated() {
398
+ return !!getToken() || !!getApiKey();
399
+ }
400
+ function getUser() {
401
+ const creds = getCredentials();
402
+ return creds?.user || null;
403
+ }
404
+ function getTier() {
405
+ const envTier = process.env.BOOTSPRING_USER_TIER;
406
+ if (envTier) {
407
+ return envTier.toLowerCase();
408
+ }
409
+ const user = getUser();
410
+ return user?.tier || "free";
411
+ }
412
+ function parseExpiry(expiry) {
413
+ const match = expiry.match(/^(\d+)([mhd])$/);
414
+ if (!match || !match[1] || !match[2]) return 15 * 60 * 1e3;
415
+ const value = parseInt(match[1]);
416
+ const unit = match[2];
417
+ switch (unit) {
418
+ case "m":
419
+ return value * 60 * 1e3;
420
+ case "h":
421
+ return value * 60 * 60 * 1e3;
422
+ case "d":
423
+ return value * 24 * 60 * 60 * 1e3;
424
+ default:
425
+ return 15 * 60 * 1e3;
426
+ }
427
+ }
428
+ function login(response) {
429
+ const expiresIn = response.expiresIn ?? "15m";
430
+ const expiresMs = parseExpiry(expiresIn);
431
+ const expiresAt = new Date(Date.now() + expiresMs).toISOString();
432
+ const credentials = {
433
+ token: response.token,
434
+ expiresAt
435
+ };
436
+ if (response.refreshToken !== void 0) {
437
+ credentials.refreshToken = response.refreshToken;
438
+ }
439
+ if (response.user !== void 0) {
440
+ credentials.user = response.user;
441
+ }
442
+ saveCredentials(credentials);
443
+ }
444
+ function saveProjectScopedSession(token, options = {}) {
445
+ try {
446
+ const projectConfig = readNearestProjectConfig();
447
+ if (!projectConfig || !token) {
448
+ return false;
449
+ }
450
+ const resolvedExpiresAt = (() => {
451
+ if (options.expiresAt) {
452
+ return options.expiresAt;
453
+ }
454
+ if (typeof options.expiresIn === "number" && Number.isFinite(options.expiresIn)) {
455
+ return new Date(Date.now() + options.expiresIn * 1e3).toISOString();
456
+ }
457
+ if (typeof options.expiresIn === "string") {
458
+ return new Date(Date.now() + parseExpiry(options.expiresIn)).toISOString();
459
+ }
460
+ return new Date(Date.now() + 15 * 60 * 1e3).toISOString();
461
+ })();
462
+ const nextConfig = {
463
+ ...projectConfig.config,
464
+ projectAuth: {
465
+ token,
466
+ expiresAt: resolvedExpiresAt,
467
+ issuedAt: (/* @__PURE__ */ new Date()).toISOString(),
468
+ source: options.source || "api-key-exchange",
469
+ migratedFromLegacyApiKey: Boolean(options.migratedFromLegacyApiKey)
470
+ }
471
+ };
472
+ if (options.migratedFromLegacyApiKey && Object.prototype.hasOwnProperty.call(nextConfig, "apiKey")) {
473
+ delete nextConfig.apiKey;
474
+ }
475
+ return writeNearestProjectConfig(projectConfig.path, nextConfig);
476
+ } catch {
477
+ }
478
+ return false;
479
+ }
480
+ function clearProjectScopedSession() {
481
+ try {
482
+ const projectConfig = readNearestProjectConfig();
483
+ if (!projectConfig || !projectConfig.config.projectAuth) {
484
+ return false;
485
+ }
486
+ const nextConfig = { ...projectConfig.config };
487
+ delete nextConfig.projectAuth;
488
+ return writeNearestProjectConfig(projectConfig.path, nextConfig);
489
+ } catch {
490
+ }
491
+ return false;
492
+ }
493
+ function clearProjectApiKey() {
494
+ try {
495
+ const projectConfig = readNearestProjectConfig();
496
+ if (!projectConfig) {
497
+ return false;
498
+ }
499
+ const config = projectConfig.config;
500
+ if (!Object.prototype.hasOwnProperty.call(config, "apiKey")) {
501
+ return false;
502
+ }
503
+ delete config.apiKey;
504
+ return writeNearestProjectConfig(projectConfig.path, config);
505
+ } catch {
506
+ }
507
+ return false;
508
+ }
509
+ function saveApiKeyToProject(apiKey) {
510
+ try {
511
+ const projectConfig = readNearestProjectConfig();
512
+ if (!projectConfig) {
513
+ return false;
514
+ }
515
+ const config = { ...projectConfig.config, apiKey };
516
+ return writeNearestProjectConfig(projectConfig.path, config);
517
+ } catch {
518
+ }
519
+ return false;
520
+ }
521
+ function loginWithApiKey(apiKey, user) {
522
+ const credentials = { apiKey };
523
+ if (user !== void 0) {
524
+ credentials.user = user;
525
+ }
526
+ saveCredentials(credentials);
527
+ clearProjectApiKey();
528
+ }
529
+ function updateTokens(response) {
530
+ const creds = getCredentials() || {};
531
+ const expiresIn = response.expiresIn ?? "15m";
532
+ const expiresMs = parseExpiry(expiresIn);
533
+ const expiresAt = new Date(Date.now() + expiresMs).toISOString();
534
+ const credentials = {
535
+ ...creds,
536
+ token: response.token,
537
+ expiresAt
538
+ };
539
+ if (response.refreshToken !== void 0) {
540
+ credentials.refreshToken = response.refreshToken;
541
+ }
542
+ saveCredentials(credentials);
543
+ }
544
+ function logout() {
545
+ clearCredentials();
546
+ clearProjectScopedSession();
547
+ clearProjectApiKey();
548
+ }
549
+ function getConfig() {
550
+ try {
551
+ if (fs.existsSync(CONFIG_FILE)) {
552
+ return JSON.parse(fs.readFileSync(CONFIG_FILE, "utf-8"));
553
+ }
554
+ } catch {
555
+ }
556
+ return {};
557
+ }
558
+ function saveConfig(config) {
559
+ ensureDir();
560
+ fs.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2));
561
+ }
562
+ function getCredentialsPath() {
563
+ return CREDENTIALS_FILE;
564
+ }
565
+ function generateDeviceFingerprint() {
566
+ const networkInterfaces2 = os.networkInterfaces();
567
+ const macAddresses = Object.values(networkInterfaces2).flat().filter(
568
+ (iface) => iface !== void 0 && !iface.internal && iface.mac !== "00:00:00:00:00:00"
569
+ ).map((iface) => iface.mac).sort().join(",");
570
+ const components = [
571
+ os.hostname(),
572
+ os.userInfo().username,
573
+ os.platform(),
574
+ os.arch(),
575
+ os.cpus()[0]?.model || "unknown-cpu",
576
+ os.homedir(),
577
+ macAddresses
578
+ ];
579
+ return crypto.createHash("sha256").update(components.join("|")).digest("hex");
580
+ }
581
+ function getDeviceInfo() {
582
+ ensureDir();
583
+ try {
584
+ if (fs.existsSync(DEVICE_FILE)) {
585
+ const stored = JSON.parse(fs.readFileSync(DEVICE_FILE, "utf-8"));
586
+ const currentFingerprint = generateDeviceFingerprint();
587
+ if (stored.fingerprint === currentFingerprint) {
588
+ return stored;
589
+ }
590
+ }
591
+ } catch {
592
+ }
593
+ const deviceInfo = {
594
+ deviceId: crypto.randomUUID(),
595
+ fingerprint: generateDeviceFingerprint(),
596
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
597
+ platform: os.platform(),
598
+ arch: os.arch(),
599
+ hostname: os.hostname()
600
+ };
601
+ fs.writeFileSync(DEVICE_FILE, JSON.stringify(deviceInfo, null, 2), { mode: 384 });
602
+ return deviceInfo;
603
+ }
604
+ function getDeviceId() {
605
+ return getDeviceInfo().deviceId;
606
+ }
607
+ function getDeviceContext() {
608
+ const info = getDeviceInfo();
609
+ let cliVersion = "unknown";
610
+ try {
611
+ const pkg = require_package();
612
+ cliVersion = pkg.version;
613
+ } catch {
614
+ }
615
+ return {
616
+ deviceId: info.deviceId,
617
+ platform: info.platform,
618
+ arch: info.arch,
619
+ hostname: info.hostname,
620
+ cliVersion,
621
+ nodeVersion: process.version,
622
+ timezone: Intl.DateTimeFormat().resolvedOptions().timeZone
623
+ };
624
+ }
625
+ function clearDeviceInfo() {
626
+ try {
627
+ if (fs.existsSync(DEVICE_FILE)) {
628
+ fs.unlinkSync(DEVICE_FILE);
629
+ }
630
+ } catch {
631
+ }
632
+ }
633
+ var fs, path, os, crypto, BOOTSPRING_DIR, CREDENTIALS_FILE, CONFIG_FILE, DEVICE_FILE, legacyProjectApiKeyWarned;
634
+ var init_auth = __esm({
635
+ "src/core/auth.ts"() {
636
+ "use strict";
637
+ init_cjs_shims();
638
+ fs = __toESM(require("fs"));
639
+ path = __toESM(require("path"));
640
+ os = __toESM(require("os"));
641
+ crypto = __toESM(require("crypto"));
642
+ BOOTSPRING_DIR = path.join(os.homedir(), ".bootspring");
643
+ CREDENTIALS_FILE = path.join(BOOTSPRING_DIR, "credentials.json");
644
+ CONFIG_FILE = path.join(BOOTSPRING_DIR, "config.json");
645
+ DEVICE_FILE = path.join(BOOTSPRING_DIR, "device.json");
646
+ legacyProjectApiKeyWarned = false;
647
+ }
648
+ });
649
+
650
+ // src/core/auth.js
651
+ var init_auth2 = __esm({
652
+ "src/core/auth.js"() {
653
+ "use strict";
654
+ init_cjs_shims();
655
+ init_auth();
656
+ }
657
+ });
658
+
659
+ // src/core/session.ts
660
+ function ensureDir2() {
661
+ if (!fs2.existsSync(BOOTSPRING_DIR2)) {
662
+ fs2.mkdirSync(BOOTSPRING_DIR2, { recursive: true, mode: 448 });
663
+ }
664
+ }
665
+ function getSession() {
666
+ try {
667
+ if (fs2.existsSync(SESSION_FILE)) {
668
+ return JSON.parse(fs2.readFileSync(SESSION_FILE, "utf-8"));
669
+ }
670
+ } catch {
671
+ }
672
+ return null;
673
+ }
674
+ function saveSession(session) {
675
+ ensureDir2();
676
+ const data = {
677
+ ...session,
678
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
679
+ };
680
+ fs2.writeFileSync(SESSION_FILE, JSON.stringify(data, null, 2));
681
+ }
682
+ function hasScopeMarker(dir) {
683
+ for (const marker of PROJECT_SCOPE_MARKERS) {
684
+ if (fs2.existsSync(path2.join(dir, marker))) {
685
+ return true;
686
+ }
687
+ }
688
+ return false;
689
+ }
690
+ function normalizeDirPath(dir) {
691
+ const resolved = path2.resolve(dir);
692
+ try {
693
+ return fs2.realpathSync.native(resolved);
694
+ } catch {
695
+ try {
696
+ return fs2.realpathSync(resolved);
697
+ } catch {
698
+ return resolved;
699
+ }
700
+ }
701
+ }
702
+ function findNearestScopeRoot(startDir) {
703
+ let dir = normalizeDirPath(startDir);
704
+ for (let i = 0; i < 10; i++) {
705
+ if (hasScopeMarker(dir)) {
706
+ return dir;
707
+ }
708
+ const parent = path2.dirname(dir);
709
+ if (parent === dir) {
710
+ break;
711
+ }
712
+ dir = parent;
713
+ }
714
+ return null;
715
+ }
716
+ function resolveProjectScope(scopeDir) {
717
+ const resolvedDir = normalizeDirPath(scopeDir);
718
+ const scopeRoot = findNearestScopeRoot(resolvedDir);
719
+ if (scopeRoot) {
720
+ return { dir: scopeRoot, mode: "tree" };
721
+ }
722
+ return { dir: resolvedDir, mode: "exact" };
723
+ }
724
+ function getScopeMode(sessionData, resolvedScopeDir) {
725
+ if (sessionData.projectScopeMode === "exact" || sessionData.projectScopeMode === "tree") {
726
+ return sessionData.projectScopeMode;
727
+ }
728
+ return hasScopeMarker(resolvedScopeDir) ? "tree" : "exact";
729
+ }
730
+ function setCurrentProject(project, scopeDir) {
731
+ const session = getSession() || {};
732
+ if (project) {
733
+ const scope = resolveProjectScope(scopeDir || process.cwd());
734
+ session.project = project;
735
+ session.projectScopeDir = scope.dir;
736
+ session.projectScopeMode = scope.mode;
737
+ } else {
738
+ delete session.project;
739
+ delete session.projectScopeDir;
740
+ delete session.projectScopeMode;
741
+ }
742
+ saveSession(session);
743
+ }
744
+ function addRecentProject(project) {
745
+ const session = getSession() || {};
746
+ const recent = session.recentProjects || [];
747
+ const filtered = recent.filter((p) => p.id !== project.id);
748
+ filtered.unshift({
749
+ id: project.id,
750
+ name: project.name,
751
+ slug: project.slug,
752
+ lastUsed: (/* @__PURE__ */ new Date()).toISOString()
753
+ });
754
+ session.recentProjects = filtered.slice(0, 10);
755
+ saveSession(session);
756
+ }
757
+ function findLocalConfig(startDir) {
758
+ let dir = startDir || process.cwd();
759
+ for (let i = 0; i < 10; i++) {
760
+ const jsonConfigPath = path2.join(dir, LOCAL_CONFIG_NAME);
761
+ if (fs2.existsSync(jsonConfigPath)) {
762
+ try {
763
+ const config = JSON.parse(fs2.readFileSync(jsonConfigPath, "utf-8"));
764
+ return {
765
+ ...config,
766
+ _path: jsonConfigPath,
767
+ _dir: dir
768
+ };
769
+ } catch {
770
+ }
771
+ }
772
+ const jsConfigPath = path2.join(dir, "bootspring.config.js");
773
+ if (fs2.existsSync(jsConfigPath)) {
774
+ try {
775
+ delete require.cache[require.resolve(jsConfigPath)];
776
+ const config = require(jsConfigPath);
777
+ if (config.projectId) {
778
+ const project = config.project;
779
+ return {
780
+ projectId: config.projectId,
781
+ projectName: project?.name || config.name || "Unknown",
782
+ projectSlug: project?.slug,
783
+ _path: jsConfigPath,
784
+ _dir: dir,
785
+ _fromJsConfig: true
786
+ };
787
+ }
788
+ } catch {
789
+ }
790
+ }
791
+ const parent = path2.dirname(dir);
792
+ if (parent === dir) break;
793
+ dir = parent;
794
+ }
795
+ return null;
796
+ }
797
+ function getEffectiveProject(startDir) {
798
+ const local = findLocalConfig(startDir);
799
+ if (local?.projectId) {
800
+ return {
801
+ id: local.projectId,
802
+ name: local.projectName || "Unknown",
803
+ slug: local.projectSlug,
804
+ source: "local",
805
+ _localConfig: local
806
+ };
807
+ }
808
+ const sessionData = getSession();
809
+ const sessionProject = sessionData?.project || null;
810
+ if (!sessionProject) {
811
+ return null;
812
+ }
813
+ if (!sessionData?.projectScopeDir) {
814
+ return null;
815
+ }
816
+ const resolvedScopeDir = normalizeDirPath(sessionData.projectScopeDir);
817
+ const scopeMode = getScopeMode(sessionData, resolvedScopeDir);
818
+ const currentDir = normalizeDirPath(startDir || process.cwd());
819
+ const isInScope = (() => {
820
+ if (scopeMode === "exact") {
821
+ return currentDir === resolvedScopeDir;
822
+ }
823
+ const relativeToScope = path2.relative(resolvedScopeDir, currentDir);
824
+ return relativeToScope === "" || !relativeToScope.startsWith("..") && !path2.isAbsolute(relativeToScope);
825
+ })();
826
+ if (isInScope) {
827
+ return {
828
+ ...sessionProject,
829
+ source: "session"
830
+ };
831
+ }
832
+ return null;
833
+ }
834
+ var fs2, path2, os2, BOOTSPRING_DIR2, SESSION_FILE, LOCAL_CONFIG_NAME, PROJECT_SCOPE_MARKERS;
835
+ var init_session = __esm({
836
+ "src/core/session.ts"() {
837
+ "use strict";
838
+ init_cjs_shims();
839
+ fs2 = __toESM(require("fs"));
840
+ path2 = __toESM(require("path"));
841
+ os2 = __toESM(require("os"));
842
+ BOOTSPRING_DIR2 = path2.join(os2.homedir(), ".bootspring");
843
+ SESSION_FILE = path2.join(BOOTSPRING_DIR2, "session.json");
844
+ LOCAL_CONFIG_NAME = ".bootspring.json";
845
+ PROJECT_SCOPE_MARKERS = [
846
+ LOCAL_CONFIG_NAME,
847
+ "bootspring.config.js",
848
+ ".git"
849
+ ];
850
+ }
851
+ });
852
+
853
+ // src/core/session.js
854
+ var init_session2 = __esm({
855
+ "src/core/session.js"() {
856
+ "use strict";
857
+ init_cjs_shims();
858
+ init_session();
859
+ }
860
+ });
861
+
862
+ // src/core/redaction.ts
863
+ function redactPatternMatches(value) {
864
+ return value.replace(/\b(?:bs|sk)_(?:live|test)_[A-Za-z0-9_-]{8,}\b/g, REDACTED).replace(/\beyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\b/g, REDACTED).replace(/\bBearer\s+[A-Za-z0-9._-]+\b/gi, `Bearer ${REDACTED}`).replace(/\bproj_[A-Za-z0-9_-]{6,}\b/g, `proj_${REDACTED}`).replace(/(["']?(?:authorization|x-api-key|apiKey|token|refreshToken|projectId)["']?\s*[:=]\s*["']?)([^"',\s}]+)/gi, `$1${REDACTED}`);
865
+ }
866
+ function redactSensitiveString(value) {
867
+ return redactPatternMatches(String(value || ""));
868
+ }
869
+ function redactSensitiveData(input, depth = 0) {
870
+ if (depth > 10) {
871
+ return input;
872
+ }
873
+ if (typeof input === "string") {
874
+ return redactSensitiveString(input);
875
+ }
876
+ if (Array.isArray(input)) {
877
+ return input.map((item) => redactSensitiveData(item, depth + 1));
878
+ }
879
+ if (!input || typeof input !== "object") {
880
+ return input;
881
+ }
882
+ const output = {};
883
+ for (const [key, value] of Object.entries(input)) {
884
+ if (SENSITIVE_KEY_PATTERN.test(key)) {
885
+ output[key] = REDACTED;
886
+ continue;
887
+ }
888
+ output[key] = redactSensitiveData(value, depth + 1);
889
+ }
890
+ return output;
891
+ }
892
+ var REDACTED, SENSITIVE_KEY_PATTERN;
893
+ var init_redaction = __esm({
894
+ "src/core/redaction.ts"() {
895
+ "use strict";
896
+ init_cjs_shims();
897
+ REDACTED = "[REDACTED]";
898
+ SENSITIVE_KEY_PATTERN = /(?:^|[_-])(api[_-]?key|token|refresh[_-]?token|authorization|x[_-]?api[_-]?key|project[_-]?id)$/i;
899
+ }
900
+ });
901
+
902
+ // src/core/redaction.js
903
+ var init_redaction2 = __esm({
904
+ "src/core/redaction.js"() {
905
+ "use strict";
906
+ init_cjs_shims();
907
+ init_redaction();
908
+ }
909
+ });
910
+
911
+ // src/core/api-client.ts
912
+ var api_client_exports = {};
913
+ __export(api_client_exports, {
914
+ API_BASE: () => API_BASE,
915
+ API_VERSION: () => API_VERSION,
916
+ addProjectMember: () => addProjectMember,
917
+ analyzeContext: () => analyzeContext,
918
+ callMcpTool: () => callMcpTool,
919
+ clearCache: () => clearCache,
920
+ createCheckout: () => createCheckout,
921
+ directRequest: () => directRequest,
922
+ downloadPreseedZip: () => downloadPreseedZip,
923
+ enrichCodebasePreseed: () => enrichCodebasePreseed,
924
+ ensureProjectScopedToken: () => ensureProjectScopedToken,
925
+ exchangeProjectScopedToken: () => exchangeProjectScopedToken,
926
+ findSimilarProjects: () => findSimilarProjects,
927
+ getActiveMcpConnectorMap: () => getActiveMcpConnectorMap,
928
+ getAgent: () => getAgent,
929
+ getAgentCapabilities: () => getAgentCapabilities,
930
+ getAgentContext: () => getAgentContext,
931
+ getInvoices: () => getInvoices,
932
+ getLintBudgets: () => getLintBudgets,
933
+ getMcpResource: () => getMcpResource,
934
+ getOrganization: () => getOrganization,
935
+ getPortalUrl: () => getPortalUrl,
936
+ getPreseedDocument: () => getPreseedDocument,
937
+ getPreseedWizard: () => getPreseedWizard,
938
+ getProjectActivity: () => getProjectActivity,
939
+ getProjectMembers: () => getProjectMembers,
940
+ getSkill: () => getSkill,
941
+ getSkillContent: () => getSkillContent,
942
+ getSubscription: () => getSubscription,
943
+ getSuggestions: () => getSuggestions,
944
+ getTemplate: () => getTemplate,
945
+ getUsage: () => getUsage,
946
+ getWorkflow: () => getWorkflow,
947
+ healthCheck: () => healthCheck,
948
+ inviteProjectMember: () => inviteProjectMember,
949
+ invokeAgent: () => invokeAgent,
950
+ listAgents: () => listAgents,
951
+ listMcpConnectors: () => listMcpConnectors,
952
+ listMcpResources: () => listMcpResources,
953
+ listMcpTools: () => listMcpTools,
954
+ listPreseedDocuments: () => listPreseedDocuments,
955
+ listProjectInvitations: () => listProjectInvitations,
956
+ listProjects: () => listProjects,
957
+ listQualityGates: () => listQualityGates,
958
+ listSkills: () => listSkills,
959
+ listTemplates: () => listTemplates,
960
+ listWorkflows: () => listWorkflows,
961
+ login: () => login2,
962
+ loginWithApiKey: () => loginWithApiKey2,
963
+ logout: () => logout2,
964
+ me: () => me,
965
+ pollDeviceToken: () => pollDeviceToken,
966
+ refreshToken: () => refreshToken,
967
+ register: () => register,
968
+ removeProjectMember: () => removeProjectMember,
969
+ request: () => request,
970
+ requestDeviceCode: () => requestDeviceCode,
971
+ requireAuth: () => requireAuth,
972
+ resolveEntitlements: () => resolveEntitlements,
973
+ respondToProjectInvitation: () => respondToProjectInvitation,
974
+ runQualityGate: () => runQualityGate,
975
+ searchSkills: () => searchSkills,
976
+ setMcpConnectorEnabled: () => setMcpConnectorEnabled,
977
+ startWorkflow: () => startWorkflow,
978
+ trackUsage: () => trackUsage,
979
+ transferProjectOwnership: () => transferProjectOwnership,
980
+ updateProjectMember: () => updateProjectMember,
981
+ uploadTelemetryBatch: () => uploadTelemetryBatch,
982
+ validateApiKey: () => validateApiKey
983
+ });
984
+ function getPackageVersion() {
985
+ if (packageVersion === null) {
986
+ try {
987
+ const pkg = require_package();
988
+ packageVersion = pkg.version;
989
+ } catch {
990
+ packageVersion = "unknown";
991
+ }
992
+ }
993
+ return packageVersion;
994
+ }
995
+ function formatHttpErrorBody(body, statusCode) {
996
+ const raw = String(body || "").trim();
997
+ if (!raw) {
998
+ return `API Error (${statusCode || "unknown"})`;
999
+ }
1000
+ if (/^\s*<!doctype html/i.test(raw) || /^\s*<html/i.test(raw)) {
1001
+ const titleMatch = raw.match(/<title[^>]*>([^<]+)<\/title>/i);
1002
+ const title = titleMatch && titleMatch[1] ? `: ${titleMatch[1].trim()}` : "";
1003
+ return `Bootspring API returned an HTML error page (HTTP ${statusCode || "unknown"}${title})`;
1004
+ }
1005
+ return redactSensitiveString(raw);
1006
+ }
1007
+ function parseExpiresInSeconds(raw) {
1008
+ if (typeof raw === "number" && Number.isFinite(raw) && raw > 0) {
1009
+ return Math.round(raw);
1010
+ }
1011
+ if (typeof raw === "string") {
1012
+ const trimmed = raw.trim().toLowerCase();
1013
+ if (!trimmed) return 15 * 60;
1014
+ if (/^\d+$/.test(trimmed)) {
1015
+ return parseInt(trimmed, 10);
1016
+ }
1017
+ const durationMatch = trimmed.match(/^(\d+)\s*([smhd])$/);
1018
+ if (durationMatch) {
1019
+ const value = parseInt(durationMatch[1] || "0", 10);
1020
+ const unit = durationMatch[2];
1021
+ if (unit === "s") return value;
1022
+ if (unit === "m") return value * 60;
1023
+ if (unit === "h") return value * 60 * 60;
1024
+ if (unit === "d") return value * 60 * 60 * 24;
1025
+ }
1026
+ }
1027
+ return 15 * 60;
1028
+ }
1029
+ function parseProjectScopedTokenPayload(raw) {
1030
+ const payload = raw && typeof raw === "object" && !Array.isArray(raw) ? raw : null;
1031
+ if (!payload) {
1032
+ return null;
1033
+ }
1034
+ const nestedData = payload.data;
1035
+ const source = nestedData && typeof nestedData === "object" && !Array.isArray(nestedData) ? nestedData : payload;
1036
+ const tokenCandidates = [
1037
+ source.token,
1038
+ source.sessionToken,
1039
+ source.session_token,
1040
+ source.accessToken,
1041
+ source.access_token
1042
+ ];
1043
+ const token = tokenCandidates.find((value) => typeof value === "string" && value.length > 0);
1044
+ if (!token) {
1045
+ return null;
1046
+ }
1047
+ const explicitExpiresAt = [source.expiresAt, source.expires_at].find((value) => typeof value === "string" && value.length > 0);
1048
+ const expiresInSeconds = parseExpiresInSeconds(
1049
+ [source.expiresIn, source.expires_in, source.ttl, source.ttlSeconds].find((value) => value !== void 0)
1050
+ );
1051
+ const expiresAt = explicitExpiresAt || new Date(Date.now() + expiresInSeconds * 1e3).toISOString();
1052
+ return {
1053
+ token,
1054
+ expiresAt,
1055
+ expiresInSeconds
1056
+ };
1057
+ }
1058
+ async function requestProjectScopedToken(path3, apiKey, projectId) {
1059
+ const url = new URL(path3, API_BASE);
1060
+ const isHttps = url.protocol === "https:";
1061
+ const httpModule = isHttps ? https : http;
1062
+ const deviceContext = getDeviceContext();
1063
+ return new Promise((resolve2, reject) => {
1064
+ const requestBody = JSON.stringify({
1065
+ apiKey,
1066
+ projectId,
1067
+ scope: "project",
1068
+ deviceFingerprint: deviceContext.deviceId,
1069
+ deviceName: `CLI - ${deviceContext.hostname}`
1070
+ });
1071
+ const req = httpModule.request(url, {
1072
+ method: "POST",
1073
+ headers: {
1074
+ "Content-Type": "application/json",
1075
+ "User-Agent": `bootspring-cli/${getPackageVersion()}`,
1076
+ "Content-Length": Buffer.byteLength(requestBody).toString()
1077
+ },
1078
+ timeout: 1e4
1079
+ }, (res) => {
1080
+ let body = "";
1081
+ res.on("data", (chunk) => {
1082
+ body += chunk;
1083
+ });
1084
+ res.on("end", () => {
1085
+ let parsed = null;
1086
+ try {
1087
+ parsed = body ? JSON.parse(body) : {};
1088
+ } catch {
1089
+ parsed = {};
1090
+ }
1091
+ if (res.statusCode && res.statusCode >= 400) {
1092
+ const payload = parsed && typeof parsed === "object" ? parsed : {};
1093
+ const error = new Error(
1094
+ redactSensitiveString(String(payload.message || payload.error || `Token exchange failed (${res.statusCode})`))
1095
+ );
1096
+ error.status = res.statusCode;
1097
+ error.code = typeof payload.code === "string" ? payload.code : void 0;
1098
+ error.details = redactSensitiveData(payload.details);
1099
+ reject(error);
1100
+ return;
1101
+ }
1102
+ const exchange = parseProjectScopedTokenPayload(parsed);
1103
+ if (!exchange) {
1104
+ const error = new Error("Token exchange response did not include a scoped token");
1105
+ error.status = res.statusCode;
1106
+ reject(error);
1107
+ return;
1108
+ }
1109
+ resolve2(exchange);
1110
+ });
1111
+ });
1112
+ req.on("error", reject);
1113
+ req.on("timeout", () => {
1114
+ req.destroy();
1115
+ reject(new Error("Token exchange request timeout"));
1116
+ });
1117
+ req.write(requestBody);
1118
+ req.end();
1119
+ });
1120
+ }
1121
+ async function exchangeProjectScopedToken(apiKey, projectId = null) {
1122
+ const paths = [
1123
+ "/api/keys/scoped-token",
1124
+ "/api/keys/exchange",
1125
+ "/api/keys/session"
1126
+ ];
1127
+ let lastError = null;
1128
+ for (const exchangePath of paths) {
1129
+ try {
1130
+ const exchange = await requestProjectScopedToken(exchangePath, apiKey, projectId);
1131
+ return {
1132
+ ...exchange,
1133
+ sourcePath: exchangePath
1134
+ };
1135
+ } catch (error) {
1136
+ lastError = error;
1137
+ const apiError = error;
1138
+ if (apiError.status && apiError.status >= 400 && apiError.status < 500 && apiError.status !== 404 && apiError.status !== 405) {
1139
+ break;
1140
+ }
1141
+ }
1142
+ }
1143
+ throw lastError || new Error("Failed to exchange scoped token");
1144
+ }
1145
+ async function ensureProjectScopedToken() {
1146
+ if (process.env.BOOTSPRING_API_KEY) {
1147
+ return process.env.BOOTSPRING_API_KEY;
1148
+ }
1149
+ const scopedToken = getProjectScopedToken();
1150
+ if (scopedToken) {
1151
+ return scopedToken;
1152
+ }
1153
+ const project = getEffectiveProject();
1154
+ const projectId = project?.id || null;
1155
+ const storedApiKey = getStoredApiKey();
1156
+ if (storedApiKey) {
1157
+ try {
1158
+ const exchanged = await exchangeProjectScopedToken(storedApiKey, projectId);
1159
+ saveProjectScopedSession(exchanged.token, {
1160
+ expiresAt: exchanged.expiresAt,
1161
+ source: exchanged.sourcePath
1162
+ });
1163
+ return exchanged.token;
1164
+ } catch {
1165
+ return storedApiKey;
1166
+ }
1167
+ }
1168
+ const legacyProjectApiKey = getLegacyProjectApiKey();
1169
+ if (legacyProjectApiKey) {
1170
+ try {
1171
+ const exchanged = await exchangeProjectScopedToken(legacyProjectApiKey, projectId);
1172
+ saveProjectScopedSession(exchanged.token, {
1173
+ expiresAt: exchanged.expiresAt,
1174
+ source: exchanged.sourcePath,
1175
+ migratedFromLegacyApiKey: true
1176
+ });
1177
+ clearProjectApiKey();
1178
+ return exchanged.token;
1179
+ } catch {
1180
+ return legacyProjectApiKey;
1181
+ }
1182
+ }
1183
+ return null;
1184
+ }
1185
+ async function resolveAuthHeaders() {
1186
+ const token = getToken();
1187
+ if (token) {
1188
+ return { Authorization: `Bearer ${token}` };
1189
+ }
1190
+ const scopedOrApiKey = await ensureProjectScopedToken();
1191
+ if (scopedOrApiKey) {
1192
+ return { "X-API-Key": scopedOrApiKey };
1193
+ }
1194
+ const fallbackApiKey = getApiKey();
1195
+ if (fallbackApiKey) {
1196
+ return { "X-API-Key": fallbackApiKey };
1197
+ }
1198
+ return {};
1199
+ }
1200
+ async function request(method, path3, data = null, options = {}) {
1201
+ const authHeaders = await resolveAuthHeaders();
1202
+ const url = new URL(`/api/${API_VERSION}${path3}`, API_BASE);
1203
+ const isHttps = url.protocol === "https:";
1204
+ const httpModule = isHttps ? https : http;
1205
+ const deviceId = getDeviceId();
1206
+ const project = getEffectiveProject();
1207
+ const projectId = project?.id || null;
1208
+ const headers = {
1209
+ "Content-Type": "application/json",
1210
+ "User-Agent": `bootspring-cli/${getPackageVersion()}`,
1211
+ "X-Device-Id": deviceId,
1212
+ ...projectId && { "X-Project-Id": projectId },
1213
+ ...authHeaders,
1214
+ ...options.headers
1215
+ };
1216
+ if (method === "GET" && !options.noCache) {
1217
+ const cacheKey = `${method}:${path3}`;
1218
+ const cached = cache.get(cacheKey);
1219
+ if (cached && Date.now() - cached.time < CACHE_TTL) {
1220
+ return cached.data;
1221
+ }
1222
+ }
1223
+ return new Promise((resolve2, reject) => {
1224
+ const req = httpModule.request(url, {
1225
+ method,
1226
+ headers,
1227
+ timeout: options.timeout || 3e4
1228
+ }, (res) => {
1229
+ let body = "";
1230
+ res.on("data", (chunk) => {
1231
+ body += chunk;
1232
+ });
1233
+ res.on("end", () => {
1234
+ try {
1235
+ const json = JSON.parse(body);
1236
+ if (res.statusCode && res.statusCode >= 400) {
1237
+ const error = new Error(
1238
+ redactSensitiveString(String(json.message || json.error || "API Error"))
1239
+ );
1240
+ error.status = res.statusCode;
1241
+ error.code = json.error || json.code;
1242
+ error.details = redactSensitiveData(json.details);
1243
+ reject(error);
1244
+ } else {
1245
+ if (method === "GET" && !options.noCache) {
1246
+ const cacheKey = `${method}:${path3}`;
1247
+ cache.set(cacheKey, { data: json, time: Date.now() });
1248
+ }
1249
+ resolve2(json);
1250
+ }
1251
+ } catch {
1252
+ if (res.statusCode && res.statusCode >= 400) {
1253
+ const error = new Error(formatHttpErrorBody(body, res.statusCode));
1254
+ error.status = res.statusCode;
1255
+ reject(error);
1256
+ } else {
1257
+ resolve2(body);
1258
+ }
1259
+ }
1260
+ });
1261
+ });
1262
+ req.on("error", (err) => {
1263
+ if (err.code === "ECONNREFUSED") {
1264
+ reject(new Error("Cannot connect to Bootspring API. Please check your internet connection."));
1265
+ } else {
1266
+ reject(new Error(redactSensitiveString(err.message || String(err))));
1267
+ }
1268
+ });
1269
+ req.on("timeout", () => {
1270
+ req.destroy();
1271
+ reject(new Error("Request timeout"));
1272
+ });
1273
+ if (data) {
1274
+ req.write(JSON.stringify(data));
1275
+ }
1276
+ req.end();
1277
+ });
1278
+ }
1279
+ async function directRequest(method, path3, data = null, options = {}) {
1280
+ const authHeaders = await resolveAuthHeaders();
1281
+ const url = new URL(`/api${path3}`, API_BASE);
1282
+ const isHttps = url.protocol === "https:";
1283
+ const httpModule = isHttps ? https : http;
1284
+ const deviceId = getDeviceId();
1285
+ const project = getEffectiveProject();
1286
+ const projectId = project?.id || null;
1287
+ const headers = {
1288
+ "Content-Type": "application/json",
1289
+ "User-Agent": `bootspring-cli/${getPackageVersion()}`,
1290
+ "X-Device-Id": deviceId,
1291
+ ...projectId && { "X-Project-Id": projectId },
1292
+ ...authHeaders,
1293
+ ...options.headers
1294
+ };
1295
+ return new Promise((resolve2, reject) => {
1296
+ const req = httpModule.request(url, {
1297
+ method,
1298
+ headers,
1299
+ timeout: options.timeout || 3e4
1300
+ }, (res) => {
1301
+ let body = "";
1302
+ res.on("data", (chunk) => {
1303
+ body += chunk;
1304
+ });
1305
+ res.on("end", () => {
1306
+ try {
1307
+ const json = JSON.parse(body);
1308
+ if (res.statusCode && res.statusCode >= 400) {
1309
+ const error = new Error(
1310
+ redactSensitiveString(String(json.message || json.error || "API Error"))
1311
+ );
1312
+ error.status = res.statusCode;
1313
+ error.code = json.error || json.code;
1314
+ error.details = redactSensitiveData(json.details);
1315
+ reject(error);
1316
+ } else {
1317
+ resolve2(json);
1318
+ }
1319
+ } catch {
1320
+ if (res.statusCode && res.statusCode >= 400) {
1321
+ const error = new Error(formatHttpErrorBody(body, res.statusCode));
1322
+ error.status = res.statusCode;
1323
+ reject(error);
1324
+ } else {
1325
+ resolve2(body);
1326
+ }
1327
+ }
1328
+ });
1329
+ });
1330
+ req.on("error", (err) => {
1331
+ if (err.code === "ECONNREFUSED") {
1332
+ reject(new Error("Cannot connect to Bootspring API. Please check your internet connection."));
1333
+ } else {
1334
+ reject(new Error(redactSensitiveString(err.message || String(err))));
1335
+ }
1336
+ });
1337
+ req.on("timeout", () => {
1338
+ req.destroy();
1339
+ reject(new Error("Request timeout"));
1340
+ });
1341
+ if (data) {
1342
+ req.write(JSON.stringify(data));
1343
+ }
1344
+ req.end();
1345
+ });
1346
+ }
1347
+ function clearCache() {
1348
+ cache.clear();
1349
+ }
1350
+ async function healthCheck() {
1351
+ try {
1352
+ const url = new URL("/health", API_BASE);
1353
+ const isHttps = url.protocol === "https:";
1354
+ const httpModule = isHttps ? https : http;
1355
+ return new Promise((resolve2) => {
1356
+ const req = httpModule.request(url, {
1357
+ method: "GET",
1358
+ timeout: 5e3
1359
+ }, (res) => {
1360
+ let body = "";
1361
+ res.on("data", (chunk) => {
1362
+ body += chunk;
1363
+ });
1364
+ res.on("end", () => {
1365
+ try {
1366
+ const json = JSON.parse(body);
1367
+ resolve2({ connected: true, version: json.version });
1368
+ } catch {
1369
+ resolve2({ connected: false, error: "Invalid response" });
1370
+ }
1371
+ });
1372
+ });
1373
+ req.on("error", (err) => {
1374
+ resolve2({ connected: false, error: redactSensitiveString(err.message) });
1375
+ });
1376
+ req.on("timeout", () => {
1377
+ req.destroy();
1378
+ resolve2({ connected: false, error: "Timeout" });
1379
+ });
1380
+ req.end();
1381
+ });
1382
+ } catch (error) {
1383
+ const err = error;
1384
+ return { connected: false, error: redactSensitiveString(err.message) };
1385
+ }
1386
+ }
1387
+ function requireAuth() {
1388
+ if (!isAuthenticated()) {
1389
+ const error = new Error("Authentication required. Run: bootspring auth login");
1390
+ error.code = "AUTH_REQUIRED";
1391
+ throw error;
1392
+ }
1393
+ }
1394
+ async function requestDeviceCode() {
1395
+ const deviceContext = getDeviceContext();
1396
+ const url = new URL("/api/v1/auth/device", API_BASE);
1397
+ const isHttps = url.protocol === "https:";
1398
+ const httpModule = isHttps ? https : http;
1399
+ return new Promise((resolve2, reject) => {
1400
+ const req = httpModule.request(url, {
1401
+ method: "POST",
1402
+ headers: {
1403
+ "Content-Type": "application/json",
1404
+ "User-Agent": `bootspring-cli/${getPackageVersion()}`
1405
+ },
1406
+ timeout: 1e4
1407
+ }, (res) => {
1408
+ let body = "";
1409
+ res.on("data", (chunk) => {
1410
+ body += chunk;
1411
+ });
1412
+ res.on("end", () => {
1413
+ try {
1414
+ const json = JSON.parse(body);
1415
+ if (res.statusCode && res.statusCode >= 400) {
1416
+ const error = new Error(
1417
+ redactSensitiveString(String(json.message || json.error || "Failed to get device code"))
1418
+ );
1419
+ error.status = res.statusCode;
1420
+ error.code = json.error;
1421
+ reject(error);
1422
+ } else {
1423
+ resolve2(json);
1424
+ }
1425
+ } catch {
1426
+ reject(new Error("Invalid response from API"));
1427
+ }
1428
+ });
1429
+ });
1430
+ req.on("error", reject);
1431
+ req.on("timeout", () => {
1432
+ req.destroy();
1433
+ reject(new Error("Request timeout"));
1434
+ });
1435
+ req.write(JSON.stringify({ device: deviceContext }));
1436
+ req.end();
1437
+ });
1438
+ }
1439
+ async function pollDeviceToken(deviceCode) {
1440
+ const url = new URL("/api/v1/auth/device/token", API_BASE);
1441
+ const isHttps = url.protocol === "https:";
1442
+ const httpModule = isHttps ? https : http;
1443
+ return new Promise((resolve2, reject) => {
1444
+ const req = httpModule.request(url, {
1445
+ method: "POST",
1446
+ headers: {
1447
+ "Content-Type": "application/json",
1448
+ "User-Agent": `bootspring-cli/${getPackageVersion()}`
1449
+ },
1450
+ timeout: 1e4
1451
+ }, (res) => {
1452
+ let body = "";
1453
+ res.on("data", (chunk) => {
1454
+ body += chunk;
1455
+ });
1456
+ res.on("end", () => {
1457
+ try {
1458
+ const json = JSON.parse(body);
1459
+ if (res.statusCode && res.statusCode >= 400) {
1460
+ const error = new Error(
1461
+ redactSensitiveString(String(json.message || json.error || "Authorization pending"))
1462
+ );
1463
+ error.status = res.statusCode;
1464
+ error.code = json.error;
1465
+ error.details = redactSensitiveData(json);
1466
+ reject(error);
1467
+ } else {
1468
+ resolve2(json);
1469
+ }
1470
+ } catch {
1471
+ reject(new Error("Invalid response from API"));
1472
+ }
1473
+ });
1474
+ });
1475
+ req.on("error", reject);
1476
+ req.on("timeout", () => {
1477
+ req.destroy();
1478
+ reject(new Error("Request timeout"));
1479
+ });
1480
+ req.write(JSON.stringify({ device_code: deviceCode }));
1481
+ req.end();
1482
+ });
1483
+ }
1484
+ async function login2(email, password) {
1485
+ const deviceContext = getDeviceContext();
1486
+ const response = await request("POST", "/auth/login", {
1487
+ email,
1488
+ password,
1489
+ device: deviceContext
1490
+ });
1491
+ login(response);
1492
+ return response;
1493
+ }
1494
+ async function loginWithApiKey2(apiKey, options = {}) {
1495
+ const url = new URL("/api/keys/validate", API_BASE);
1496
+ const isHttps = url.protocol === "https:";
1497
+ const httpModule = isHttps ? https : http;
1498
+ const deviceContext = getDeviceContext();
1499
+ return new Promise((resolve2, reject) => {
1500
+ const requestBody = JSON.stringify({
1501
+ apiKey,
1502
+ deviceFingerprint: deviceContext.deviceId,
1503
+ deviceName: `CLI - ${deviceContext.hostname}`
1504
+ });
1505
+ const req = httpModule.request(url, {
1506
+ method: "POST",
1507
+ headers: {
1508
+ "Content-Type": "application/json",
1509
+ "User-Agent": `bootspring-cli/${getPackageVersion()}`,
1510
+ "Content-Length": Buffer.byteLength(requestBody).toString()
1511
+ },
1512
+ timeout: 1e4
1513
+ }, (res) => {
1514
+ let body = "";
1515
+ res.on("data", (chunk) => {
1516
+ body += chunk;
1517
+ });
1518
+ res.on("end", async () => {
1519
+ try {
1520
+ const json = JSON.parse(body);
1521
+ if (res.statusCode && res.statusCode >= 400 || !json.valid) {
1522
+ const error = new Error(redactSensitiveString(json.error || "Invalid API key"));
1523
+ error.status = res.statusCode;
1524
+ error.code = json.error;
1525
+ reject(error);
1526
+ } else {
1527
+ const projectId = options.projectId || json.project?.id || null;
1528
+ const user = {
1529
+ tier: json.tier,
1530
+ scopes: json.scopes
1531
+ };
1532
+ loginWithApiKey(apiKey, user);
1533
+ if (json.project) {
1534
+ setCurrentProject(json.project);
1535
+ addRecentProject(json.project);
1536
+ }
1537
+ if (projectId && saveApiKeyToProject) {
1538
+ saveApiKeyToProject(apiKey);
1539
+ }
1540
+ try {
1541
+ const exchanged = await exchangeProjectScopedToken(apiKey, projectId);
1542
+ saveProjectScopedSession(exchanged.token, {
1543
+ expiresAt: exchanged.expiresAt,
1544
+ source: exchanged.sourcePath,
1545
+ migratedFromLegacyApiKey: true
1546
+ });
1547
+ } catch {
1548
+ }
1549
+ resolve2({
1550
+ ...json,
1551
+ user
1552
+ });
1553
+ }
1554
+ } catch {
1555
+ reject(new Error("Invalid response from API"));
1556
+ }
1557
+ });
1558
+ });
1559
+ req.on("error", reject);
1560
+ req.on("timeout", () => {
1561
+ req.destroy();
1562
+ reject(new Error("Request timeout"));
1563
+ });
1564
+ req.write(requestBody);
1565
+ req.end();
1566
+ });
1567
+ }
1568
+ async function register(email, password, name) {
1569
+ const deviceContext = getDeviceContext();
1570
+ const response = await request("POST", "/auth/register", {
1571
+ email,
1572
+ password,
1573
+ name,
1574
+ device: deviceContext
1575
+ });
1576
+ login(response);
1577
+ return response;
1578
+ }
1579
+ async function me() {
1580
+ requireAuth();
1581
+ return request("GET", "/auth/me");
1582
+ }
1583
+ async function validateApiKey(apiKey) {
1584
+ const url = new URL("/api/keys/validate", API_BASE);
1585
+ const isHttps = url.protocol === "https:";
1586
+ const httpModule = isHttps ? https : http;
1587
+ const deviceContext = getDeviceContext();
1588
+ return new Promise((resolve2, reject) => {
1589
+ const requestBody = JSON.stringify({
1590
+ apiKey,
1591
+ deviceFingerprint: deviceContext.deviceId,
1592
+ deviceName: `CLI - ${deviceContext.hostname}`
1593
+ });
1594
+ const req = httpModule.request(url, {
1595
+ method: "POST",
1596
+ headers: {
1597
+ "Content-Type": "application/json",
1598
+ "User-Agent": `bootspring-cli/${getPackageVersion()}`,
1599
+ "Content-Length": Buffer.byteLength(requestBody).toString()
1600
+ },
1601
+ timeout: 1e4
1602
+ }, (res) => {
1603
+ let body = "";
1604
+ res.on("data", (chunk) => {
1605
+ body += chunk;
1606
+ });
1607
+ res.on("end", () => {
1608
+ try {
1609
+ const json = JSON.parse(body);
1610
+ if (res.statusCode && res.statusCode >= 400 || !json.valid) {
1611
+ const error = new Error(redactSensitiveString(json.error || "Invalid API key"));
1612
+ error.status = res.statusCode;
1613
+ reject(error);
1614
+ } else {
1615
+ resolve2(json);
1616
+ }
1617
+ } catch {
1618
+ reject(new Error("Invalid response from API"));
1619
+ }
1620
+ });
1621
+ });
1622
+ req.on("error", reject);
1623
+ req.on("timeout", () => {
1624
+ req.destroy();
1625
+ reject(new Error("Request timeout"));
1626
+ });
1627
+ req.write(requestBody);
1628
+ req.end();
1629
+ });
1630
+ }
1631
+ async function refreshToken() {
1632
+ const refreshTokenValue = getRefreshToken();
1633
+ if (!refreshTokenValue) {
1634
+ throw new Error("No refresh token available");
1635
+ }
1636
+ const deviceContext = getDeviceContext();
1637
+ const response = await request("POST", "/auth/refresh", {
1638
+ refreshToken: refreshTokenValue,
1639
+ device: deviceContext
1640
+ });
1641
+ updateTokens(response);
1642
+ return response;
1643
+ }
1644
+ async function logout2() {
1645
+ if (isAuthenticated()) {
1646
+ try {
1647
+ const refreshTokenValue = getRefreshToken();
1648
+ await request("POST", "/auth/logout", { refreshToken: refreshTokenValue });
1649
+ } catch {
1650
+ }
1651
+ }
1652
+ logout();
1653
+ }
1654
+ async function listAgents() {
1655
+ requireAuth();
1656
+ return request("GET", "/agents");
1657
+ }
1658
+ async function getAgent(id) {
1659
+ requireAuth();
1660
+ return request("GET", `/agents/${encodeURIComponent(id)}`);
1661
+ }
1662
+ async function getAgentContext(id) {
1663
+ requireAuth();
1664
+ return request("GET", `/agents/${encodeURIComponent(id)}/context`);
1665
+ }
1666
+ async function invokeAgent(id, projectContext, task) {
1667
+ requireAuth();
1668
+ return request("POST", `/agents/${encodeURIComponent(id)}/invoke`, {
1669
+ projectContext,
1670
+ task
1671
+ });
1672
+ }
1673
+ async function getAgentCapabilities(id) {
1674
+ requireAuth();
1675
+ return request("GET", `/agents/${encodeURIComponent(id)}/capabilities`);
1676
+ }
1677
+ async function listSkills(options = {}) {
1678
+ requireAuth();
1679
+ const params = new URLSearchParams();
1680
+ if (options.category) params.append("category", options.category);
1681
+ if (options.search) params.append("search", options.search);
1682
+ const query = params.toString();
1683
+ return request("GET", `/skills${query ? "?" + query : ""}`);
1684
+ }
1685
+ async function getSkillContent(skillId) {
1686
+ requireAuth();
1687
+ return request("GET", `/skills/${skillId}`);
1688
+ }
1689
+ async function getSkill(category, name) {
1690
+ requireAuth();
1691
+ return request("GET", `/skills/${encodeURIComponent(category)}/${encodeURIComponent(name)}`);
1692
+ }
1693
+ async function searchSkills(query, options = {}) {
1694
+ requireAuth();
1695
+ const params = new URLSearchParams();
1696
+ params.append("search", query);
1697
+ if (options.category) params.append("category", options.category);
1698
+ return request("GET", `/skills?${params.toString()}`);
1699
+ }
1700
+ async function listTemplates(category) {
1701
+ requireAuth();
1702
+ return request("GET", `/templates/${encodeURIComponent(category)}`);
1703
+ }
1704
+ async function getTemplate(category, name) {
1705
+ requireAuth();
1706
+ return request("GET", `/templates/${encodeURIComponent(category)}/${encodeURIComponent(name)}`);
1707
+ }
1708
+ async function listWorkflows() {
1709
+ requireAuth();
1710
+ return request("GET", "/orchestrator/workflows");
1711
+ }
1712
+ async function getWorkflow(id) {
1713
+ requireAuth();
1714
+ return request("GET", `/orchestrator/workflows/${encodeURIComponent(id)}`);
1715
+ }
1716
+ async function analyzeContext(projectContext, currentTask, recentActions) {
1717
+ requireAuth();
1718
+ return request("POST", "/orchestrator/analyze", {
1719
+ projectContext,
1720
+ currentTask,
1721
+ recentActions
1722
+ });
1723
+ }
1724
+ async function startWorkflow(workflowId, projectContext, parameters) {
1725
+ requireAuth();
1726
+ return request("POST", "/orchestrator/start", {
1727
+ workflow: workflowId,
1728
+ projectContext,
1729
+ parameters
1730
+ });
1731
+ }
1732
+ async function getSuggestions(context, action) {
1733
+ requireAuth();
1734
+ return request("POST", "/orchestrator/suggest", { context, action });
1735
+ }
1736
+ async function listQualityGates() {
1737
+ requireAuth();
1738
+ return request("GET", "/quality/gates");
1739
+ }
1740
+ async function runQualityGate(gateId, projectContext, options) {
1741
+ requireAuth();
1742
+ return request("POST", "/quality/run", {
1743
+ gate: gateId,
1744
+ projectContext,
1745
+ options
1746
+ });
1747
+ }
1748
+ async function getLintBudgets() {
1749
+ requireAuth();
1750
+ return request("GET", "/quality/lint-budgets");
1751
+ }
1752
+ async function listMcpTools() {
1753
+ requireAuth();
1754
+ return request("GET", "/mcp/tools");
1755
+ }
1756
+ async function callMcpTool(tool, args) {
1757
+ requireAuth();
1758
+ return request("POST", "/mcp/tool", { tool, arguments: args });
1759
+ }
1760
+ async function listMcpResources() {
1761
+ requireAuth();
1762
+ return request("GET", "/mcp/resources");
1763
+ }
1764
+ async function getMcpResource(uri) {
1765
+ requireAuth();
1766
+ return request("GET", `/mcp/resources/${encodeURIComponent(uri)}`);
1767
+ }
1768
+ async function listMcpConnectors() {
1769
+ requireAuth();
1770
+ return request("GET", "/mcp/connectors", null, { noCache: true });
1771
+ }
1772
+ async function getActiveMcpConnectorMap() {
1773
+ requireAuth();
1774
+ return request("GET", "/mcp/connectors/active", null, { noCache: true });
1775
+ }
1776
+ async function setMcpConnectorEnabled(connectorId, enabled) {
1777
+ requireAuth();
1778
+ return request("PATCH", `/mcp/connectors/${encodeURIComponent(connectorId)}`, { enabled });
1779
+ }
1780
+ async function getSubscription() {
1781
+ requireAuth();
1782
+ return request("GET", "/billing/subscription");
1783
+ }
1784
+ async function createCheckout(plan) {
1785
+ requireAuth();
1786
+ return request("POST", "/billing/create-checkout", { plan });
1787
+ }
1788
+ async function getPortalUrl() {
1789
+ requireAuth();
1790
+ return request("POST", "/billing/portal");
1791
+ }
1792
+ async function getUsage() {
1793
+ requireAuth();
1794
+ return request("GET", "/billing/usage");
1795
+ }
1796
+ async function getInvoices() {
1797
+ requireAuth();
1798
+ return request("GET", "/billing/invoices");
1799
+ }
1800
+ async function resolveEntitlements() {
1801
+ requireAuth();
1802
+ return request("GET", "/entitlements/resolve", null, { noCache: false });
1803
+ }
1804
+ async function trackUsage(type, metadata = {}) {
1805
+ requireAuth();
1806
+ return request("POST", "/track", { type, metadata });
1807
+ }
1808
+ async function uploadTelemetryBatch(events, batchInfo = {}) {
1809
+ requireAuth();
1810
+ const batchId = batchInfo.id || crypto2.randomUUID();
1811
+ return request("POST", "/events/batch", {
1812
+ source: "bootspring",
1813
+ batch: {
1814
+ index: batchInfo.index || 0,
1815
+ total: batchInfo.total || 1,
1816
+ id: batchId
1817
+ },
1818
+ events
1819
+ }, {
1820
+ headers: {
1821
+ "X-Bootspring-Batch-Id": batchId
1822
+ }
1823
+ });
1824
+ }
1825
+ async function listProjects() {
1826
+ requireAuth();
1827
+ const url = new URL("/api/auth/device/projects", API_BASE);
1828
+ const isHttps = url.protocol === "https:";
1829
+ const httpModule = isHttps ? https : http;
1830
+ const authHeaders = await resolveAuthHeaders();
1831
+ return new Promise((resolve2, reject) => {
1832
+ const headers = {
1833
+ "Content-Type": "application/json",
1834
+ "User-Agent": `bootspring-cli/${getPackageVersion()}`,
1835
+ ...authHeaders
1836
+ };
1837
+ const req = httpModule.request(url, {
1838
+ method: "GET",
1839
+ headers,
1840
+ timeout: 1e4
1841
+ }, (res) => {
1842
+ let body = "";
1843
+ res.on("data", (chunk) => {
1844
+ body += chunk;
1845
+ });
1846
+ res.on("end", () => {
1847
+ try {
1848
+ const json = JSON.parse(body);
1849
+ if (res.statusCode && res.statusCode >= 400) {
1850
+ const error = new Error(
1851
+ redactSensitiveString(String(json.message || json.error || "Failed to list projects"))
1852
+ );
1853
+ error.status = res.statusCode;
1854
+ reject(error);
1855
+ } else {
1856
+ resolve2(json);
1857
+ }
1858
+ } catch {
1859
+ reject(new Error("Invalid response from API"));
1860
+ }
1861
+ });
1862
+ });
1863
+ req.on("error", reject);
1864
+ req.on("timeout", () => {
1865
+ req.destroy();
1866
+ reject(new Error("Request timeout"));
1867
+ });
1868
+ req.end();
1869
+ });
1870
+ }
1871
+ async function getProjectMembers(projectId) {
1872
+ requireAuth();
1873
+ return directRequest("GET", `/projects/${encodeURIComponent(projectId)}/members`);
1874
+ }
1875
+ async function addProjectMember(projectId, email, role = "member") {
1876
+ requireAuth();
1877
+ return directRequest("POST", `/projects/${encodeURIComponent(projectId)}/members`, {
1878
+ email,
1879
+ role
1880
+ });
1881
+ }
1882
+ async function updateProjectMember(projectId, userId, role) {
1883
+ requireAuth();
1884
+ return directRequest("PATCH", `/projects/${encodeURIComponent(projectId)}/members/${encodeURIComponent(userId)}`, {
1885
+ role
1886
+ });
1887
+ }
1888
+ async function removeProjectMember(projectId, userId) {
1889
+ requireAuth();
1890
+ await directRequest("DELETE", `/projects/${encodeURIComponent(projectId)}/members/${encodeURIComponent(userId)}`);
1891
+ }
1892
+ async function transferProjectOwnership(projectId, newOwnerId) {
1893
+ requireAuth();
1894
+ return directRequest("POST", `/projects/${encodeURIComponent(projectId)}/transfer`, {
1895
+ newOwnerId
1896
+ });
1897
+ }
1898
+ async function listProjectInvitations(projectId) {
1899
+ requireAuth();
1900
+ return directRequest(
1901
+ "GET",
1902
+ `/projects/${encodeURIComponent(projectId)}/invitations`
1903
+ );
1904
+ }
1905
+ async function inviteProjectMember(projectId, email, role = "member") {
1906
+ requireAuth();
1907
+ return directRequest(
1908
+ "POST",
1909
+ `/projects/${encodeURIComponent(projectId)}/invitations`,
1910
+ { email, role }
1911
+ );
1912
+ }
1913
+ async function respondToProjectInvitation(invitationId, decision) {
1914
+ requireAuth();
1915
+ return directRequest(
1916
+ "POST",
1917
+ `/projects/invitations/${encodeURIComponent(invitationId)}/${decision}`
1918
+ );
1919
+ }
1920
+ async function getProjectActivity(projectId, options = {}) {
1921
+ requireAuth();
1922
+ const limit = Number(options.limit);
1923
+ const effectiveLimit = Number.isFinite(limit) && limit > 0 ? Math.min(limit, 200) : 50;
1924
+ return directRequest(
1925
+ "GET",
1926
+ `/projects/${encodeURIComponent(projectId)}/activity?limit=${effectiveLimit}`
1927
+ );
1928
+ }
1929
+ async function findSimilarProjects(name, repoUrl) {
1930
+ requireAuth();
1931
+ const params = new URLSearchParams();
1932
+ if (name) params.set("name", name);
1933
+ if (repoUrl) params.set("repo", repoUrl);
1934
+ return directRequest("GET", `/projects/similar?${params.toString()}`);
1935
+ }
1936
+ async function listPreseedDocuments(projectId) {
1937
+ requireAuth();
1938
+ return directRequest("GET", `/projects/${encodeURIComponent(projectId)}/preseed/documents`);
1939
+ }
1940
+ async function getPreseedDocument(projectId, documentName) {
1941
+ requireAuth();
1942
+ return directRequest("GET", `/projects/${encodeURIComponent(projectId)}/preseed/documents?name=${encodeURIComponent(documentName)}`);
1943
+ }
1944
+ async function enrichCodebasePreseed(payload = {}, options = {}) {
1945
+ requireAuth();
1946
+ const contractVersion = typeof payload.contractVersion === "string" && payload.contractVersion.trim() ? payload.contractVersion.trim() : "preseed-enrichment.v1";
1947
+ const body = {
1948
+ ...payload,
1949
+ contractVersion
1950
+ };
1951
+ const requestedPaths = Array.isArray(options.paths) ? options.paths.filter((candidate) => typeof candidate === "string" && candidate.trim().length > 0) : [];
1952
+ const candidatePaths = [.../* @__PURE__ */ new Set([...requestedPaths, ...PRESEED_CODEBASE_ENRICHMENT_PATHS])];
1953
+ const requestOptions = {
1954
+ timeout: Number(options.timeout) > 0 ? Number(options.timeout) : 6e4,
1955
+ noCache: true,
1956
+ headers: {
1957
+ ...options.headers || {},
1958
+ "X-Bootspring-Contract": contractVersion
1959
+ }
1960
+ };
1961
+ let lastError = null;
1962
+ for (const endpoint of candidatePaths) {
1963
+ try {
1964
+ return await request("POST", endpoint, body, requestOptions);
1965
+ } catch (error) {
1966
+ lastError = error;
1967
+ const status = Number(error.status);
1968
+ if (status === 404 || status === 405 || status === 501) {
1969
+ continue;
1970
+ }
1971
+ throw error;
1972
+ }
1973
+ }
1974
+ const unavailable = new Error("Remote enrichment endpoint unavailable");
1975
+ unavailable.code = "ENRICHMENT_ENDPOINT_UNAVAILABLE";
1976
+ unavailable.details = { attemptedPaths: candidatePaths };
1977
+ if (lastError && typeof lastError.status === "number") {
1978
+ unavailable.status = lastError.status;
1979
+ }
1980
+ throw unavailable;
1981
+ }
1982
+ async function downloadPreseedZip(projectId) {
1983
+ requireAuth();
1984
+ const authHeaders = await resolveAuthHeaders();
1985
+ const url = new URL(`/api/projects/${encodeURIComponent(projectId)}/preseed/documents?format=zip`, API_BASE);
1986
+ const isHttps = url.protocol === "https:";
1987
+ const httpModule = isHttps ? https : http;
1988
+ return new Promise((resolve2, reject) => {
1989
+ const headers = {
1990
+ "User-Agent": `bootspring-cli/${getPackageVersion()}`,
1991
+ ...authHeaders
1992
+ };
1993
+ const req = httpModule.request(url, {
1994
+ method: "GET",
1995
+ headers,
1996
+ timeout: 6e4
1997
+ }, (res) => {
1998
+ if (res.statusCode && res.statusCode >= 400) {
1999
+ let body = "";
2000
+ res.on("data", (chunk) => {
2001
+ body += chunk;
2002
+ });
2003
+ res.on("end", () => {
2004
+ try {
2005
+ const json = JSON.parse(body);
2006
+ const error = new Error(
2007
+ redactSensitiveString(String(json.message || json.error || "Failed to download preseed documents"))
2008
+ );
2009
+ error.status = res.statusCode;
2010
+ reject(error);
2011
+ } catch {
2012
+ reject(new Error(redactSensitiveString(`HTTP ${res.statusCode}: ${body}`)));
2013
+ }
2014
+ });
2015
+ return;
2016
+ }
2017
+ const chunks = [];
2018
+ res.on("data", (chunk) => chunks.push(chunk));
2019
+ res.on("end", () => {
2020
+ resolve2(Buffer.concat(chunks));
2021
+ });
2022
+ });
2023
+ req.on("error", reject);
2024
+ req.on("timeout", () => {
2025
+ req.destroy();
2026
+ reject(new Error("Request timeout"));
2027
+ });
2028
+ req.end();
2029
+ });
2030
+ }
2031
+ async function getPreseedWizard(projectId) {
2032
+ requireAuth();
2033
+ return directRequest("GET", `/projects/${encodeURIComponent(projectId)}/preseed/wizard`);
2034
+ }
2035
+ async function getOrganization(orgId, options = {}) {
2036
+ requireAuth();
2037
+ try {
2038
+ return await request("GET", `/organizations/${encodeURIComponent(orgId)}`, null, options);
2039
+ } catch {
2040
+ return null;
2041
+ }
2042
+ }
2043
+ var https, http, crypto2, API_BASE, API_VERSION, cache, CACHE_TTL, PRESEED_CODEBASE_ENRICHMENT_PATHS, packageVersion;
2044
+ var init_api_client = __esm({
2045
+ "src/core/api-client.ts"() {
2046
+ "use strict";
2047
+ init_cjs_shims();
2048
+ https = __toESM(require("https"));
2049
+ http = __toESM(require("http"));
2050
+ crypto2 = __toESM(require("crypto"));
2051
+ init_auth2();
2052
+ init_session2();
2053
+ init_redaction2();
2054
+ API_BASE = process.env["BOOTSPRING_API_URL"] || "https://api.bootspring.com";
2055
+ API_VERSION = "v1";
2056
+ cache = /* @__PURE__ */ new Map();
2057
+ CACHE_TTL = 6e4;
2058
+ PRESEED_CODEBASE_ENRICHMENT_PATHS = [
2059
+ "/preseed/enrich/from-codebase",
2060
+ "/preseed/from-codebase/enrich",
2061
+ "/projects/preseed/enrich/from-codebase"
2062
+ ];
2063
+ packageVersion = null;
2064
+ }
2065
+ });
2066
+
2067
+ // src/mcp-proxy.ts
2068
+ init_cjs_shims();
2069
+ var { Server } = require("@modelcontextprotocol/sdk/server/index.js");
2070
+ var { StdioServerTransport } = require("@modelcontextprotocol/sdk/server/stdio.js");
2071
+ var {
2072
+ CallToolRequestSchema,
2073
+ ListResourcesRequestSchema,
2074
+ ListToolsRequestSchema,
2075
+ ReadResourceRequestSchema
2076
+ } = require("@modelcontextprotocol/sdk/types.js");
2077
+ var api = (init_api_client(), __toCommonJS(api_client_exports));
2078
+ var auth = (init_auth(), __toCommonJS(auth_exports));
2079
+ var VERSION = require_package().version;
2080
+ var FALLBACK_TOOLS = [
2081
+ {
2082
+ name: "bootspring_agent",
2083
+ description: "Invoke a hosted Bootspring agent.",
2084
+ inputSchema: {
2085
+ type: "object",
2086
+ properties: {
2087
+ agent: { type: "string" },
2088
+ task: { type: "string" }
2089
+ },
2090
+ required: ["agent"]
2091
+ }
2092
+ },
2093
+ {
2094
+ name: "bootspring_skill",
2095
+ description: "Fetch a hosted Bootspring skill.",
2096
+ inputSchema: {
2097
+ type: "object",
2098
+ properties: {
2099
+ skill: { type: "string" }
2100
+ },
2101
+ required: ["skill"]
2102
+ }
2103
+ },
2104
+ {
2105
+ name: "bootspring_context",
2106
+ description: "Get or analyze project context.",
2107
+ inputSchema: {
2108
+ type: "object",
2109
+ properties: {
2110
+ action: { type: "string", enum: ["get", "generate", "analyze"] }
2111
+ },
2112
+ required: ["action"]
2113
+ }
2114
+ },
2115
+ {
2116
+ name: "bootspring_orchestrator",
2117
+ description: "Use the hosted orchestrator.",
2118
+ inputSchema: {
2119
+ type: "object",
2120
+ properties: {
2121
+ action: { type: "string", enum: ["analyze", "start", "suggest"] },
2122
+ workflow: { type: "string" }
2123
+ },
2124
+ required: ["action"]
2125
+ }
2126
+ },
2127
+ {
2128
+ name: "bootspring_quality",
2129
+ description: "Fetch a hosted quality gate.",
2130
+ inputSchema: {
2131
+ type: "object",
2132
+ properties: {
2133
+ gate: { type: "string", enum: ["pre-commit", "pre-push", "pre-deploy"] }
2134
+ },
2135
+ required: ["gate"]
2136
+ }
2137
+ },
2138
+ {
2139
+ name: "bootspring_todo",
2140
+ description: "Manage local todo items.",
2141
+ inputSchema: {
2142
+ type: "object",
2143
+ properties: {
2144
+ action: { type: "string", enum: ["list", "add", "complete", "delete"] },
2145
+ text: { type: "string" },
2146
+ id: { type: "string" }
2147
+ },
2148
+ required: ["action"]
2149
+ }
2150
+ }
2151
+ ];
2152
+ var FALLBACK_RESOURCES = [
2153
+ {
2154
+ uri: "bootspring://context",
2155
+ name: "Project Context",
2156
+ description: "Current project context and configuration",
2157
+ mimeType: "application/json"
2158
+ },
2159
+ {
2160
+ uri: "bootspring://agents",
2161
+ name: "Available Agents",
2162
+ description: "Hosted Bootspring agents for your account",
2163
+ mimeType: "application/json"
2164
+ },
2165
+ {
2166
+ uri: "bootspring://skills",
2167
+ name: "Code Skills",
2168
+ description: "Hosted Bootspring skills and patterns",
2169
+ mimeType: "application/json"
2170
+ },
2171
+ {
2172
+ uri: "bootspring://workflows",
2173
+ name: "Workflows",
2174
+ description: "Hosted Bootspring workflows",
2175
+ mimeType: "application/json"
2176
+ }
2177
+ ];
2178
+ var TOOLS = FALLBACK_TOOLS;
2179
+ var RESOURCES = FALLBACK_RESOURCES;
2180
+ function formatTextContent(value) {
2181
+ return typeof value === "string" ? value : JSON.stringify(value, null, 2);
2182
+ }
2183
+ function createAuthError(message) {
2184
+ return {
2185
+ content: [{ type: "text", text: message }],
2186
+ isError: true
2187
+ };
2188
+ }
2189
+ async function resolveTools() {
2190
+ if (!auth.isAuthenticated()) {
2191
+ return FALLBACK_TOOLS;
2192
+ }
2193
+ try {
2194
+ const response = await api.listMcpTools();
2195
+ return Array.isArray(response) ? response : response.tools || FALLBACK_TOOLS;
2196
+ } catch {
2197
+ return FALLBACK_TOOLS;
2198
+ }
2199
+ }
2200
+ async function resolveResources() {
2201
+ if (!auth.isAuthenticated()) {
2202
+ return FALLBACK_RESOURCES;
2203
+ }
2204
+ try {
2205
+ const response = await api.listMcpResources();
2206
+ return Array.isArray(response) ? response : response.resources || FALLBACK_RESOURCES;
2207
+ } catch {
2208
+ return FALLBACK_RESOURCES;
2209
+ }
2210
+ }
2211
+ async function proxyToolCall(name, args) {
2212
+ if (!auth.isAuthenticated()) {
2213
+ return createAuthError("Authentication required. Run `bootspring auth login` first.");
2214
+ }
2215
+ try {
2216
+ const response = await api.callMcpTool(name, args || {});
2217
+ return {
2218
+ content: [{ type: "text", text: formatTextContent(response.result || response) }]
2219
+ };
2220
+ } catch (error) {
2221
+ const message = error.status === 403 ? "This MCP capability requires a paid Bootspring plan." : error.message;
2222
+ return {
2223
+ content: [{ type: "text", text: `Error: ${message}` }],
2224
+ isError: true
2225
+ };
2226
+ }
2227
+ }
2228
+ async function proxyResourceRead(uri) {
2229
+ if (!auth.isAuthenticated()) {
2230
+ return {
2231
+ contents: [{
2232
+ uri,
2233
+ mimeType: "text/plain",
2234
+ text: "Authentication required. Run `bootspring auth login` first."
2235
+ }]
2236
+ };
2237
+ }
2238
+ try {
2239
+ const response = await api.getMcpResource(uri);
2240
+ return {
2241
+ contents: [{
2242
+ uri: response.uri || uri,
2243
+ mimeType: response.mimeType || "application/json",
2244
+ text: formatTextContent(response.content || response)
2245
+ }]
2246
+ };
2247
+ } catch (error) {
2248
+ return {
2249
+ contents: [{
2250
+ uri,
2251
+ mimeType: "text/plain",
2252
+ text: `Error: ${error.message}`
2253
+ }]
2254
+ };
2255
+ }
2256
+ }
2257
+ var toolHandlers = Object.fromEntries(
2258
+ TOOLS.map((tool) => [tool.name, async (args = {}) => proxyToolCall(tool.name, args)])
2259
+ );
2260
+ var resourceHandlers = Object.fromEntries(
2261
+ RESOURCES.map((resource) => [resource.uri, async () => proxyResourceRead(resource.uri)])
2262
+ );
2263
+ async function main() {
2264
+ const server = new Server(
2265
+ { name: "bootspring", version: VERSION },
2266
+ { capabilities: { tools: {}, resources: {} } }
2267
+ );
2268
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({
2269
+ tools: await resolveTools()
2270
+ }));
2271
+ server.setRequestHandler(CallToolRequestSchema, async (request2) => {
2272
+ const { name, arguments: args } = request2.params;
2273
+ return proxyToolCall(name, args);
2274
+ });
2275
+ server.setRequestHandler(ListResourcesRequestSchema, async () => ({
2276
+ resources: await resolveResources()
2277
+ }));
2278
+ server.setRequestHandler(ReadResourceRequestSchema, async (request2) => proxyResourceRead(request2.params.uri));
2279
+ const transport = new StdioServerTransport();
2280
+ await server.connect(transport);
2281
+ console.error(`Bootspring MCP proxy v${VERSION} started`);
2282
+ }
2283
+ if (require.main === module) {
2284
+ main().catch((error) => {
2285
+ console.error("Failed to start MCP proxy:", error.message);
2286
+ process.exit(1);
2287
+ });
2288
+ }
2289
+ module.exports = {
2290
+ TOOLS,
2291
+ RESOURCES,
2292
+ toolHandlers,
2293
+ resourceHandlers,
2294
+ FALLBACK_TOOLS,
2295
+ FALLBACK_RESOURCES,
2296
+ main
2297
+ };
2298
+ //# sourceMappingURL=mcp-server.js.map