@girardmedia/bootspring 2.0.21 → 2.0.23

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 (159) hide show
  1. package/bin/bootspring.js +5 -0
  2. package/cli/org.js +474 -0
  3. package/cli/preseed/index.js +16 -0
  4. package/cli/preseed/interactive.js +143 -0
  5. package/cli/preseed/templates.js +227 -0
  6. package/cli/preseed.js +9 -301
  7. package/cli/seed/builders/ai-context-builder.js +85 -0
  8. package/cli/seed/builders/index.js +13 -0
  9. package/cli/seed/builders/seed-builder.js +272 -0
  10. package/cli/seed/extractors/content-extractors.js +383 -0
  11. package/cli/seed/extractors/index.js +47 -0
  12. package/cli/seed/extractors/metadata-extractors.js +167 -0
  13. package/cli/seed/extractors/section-extractor.js +54 -0
  14. package/cli/seed/extractors/stack-extractors.js +228 -0
  15. package/cli/seed/index.js +18 -0
  16. package/cli/seed/utils/folder-structure.js +84 -0
  17. package/cli/seed/utils/index.js +11 -0
  18. package/cli/seed.js +23 -1074
  19. package/core/api-client.js +77 -0
  20. package/core/entitlements.js +36 -0
  21. package/core/organizations.js +223 -0
  22. package/core/policies.js +51 -6
  23. package/core/policy-matrix.js +303 -0
  24. package/core/project-context.js +1 -0
  25. package/dist/cli/index.d.ts +3 -0
  26. package/dist/cli/index.js +3220 -0
  27. package/dist/cli/index.js.map +1 -0
  28. package/dist/context-McpJQa_2.d.ts +5710 -0
  29. package/dist/core/index.d.ts +635 -0
  30. package/dist/core/index.js +2593 -0
  31. package/dist/core/index.js.map +1 -0
  32. package/dist/index-QqbeEiDm.d.ts +857 -0
  33. package/dist/index-UiYCgwiH.d.ts +174 -0
  34. package/dist/index.d.ts +453 -0
  35. package/dist/index.js +44228 -0
  36. package/dist/index.js.map +1 -0
  37. package/dist/mcp/index.d.ts +1 -0
  38. package/dist/mcp/index.js +41173 -0
  39. package/dist/mcp/index.js.map +1 -0
  40. package/generators/index.ts +82 -0
  41. package/intelligence/orchestrator/config/failure-signatures.js +48 -0
  42. package/intelligence/orchestrator/config/index.js +23 -0
  43. package/intelligence/orchestrator/config/pack-lifecycle.js +262 -0
  44. package/intelligence/orchestrator/config/phases.js +111 -0
  45. package/intelligence/orchestrator/config/remediation.js +150 -0
  46. package/intelligence/orchestrator/config/workflows.js +168 -0
  47. package/intelligence/orchestrator/core/index.js +16 -0
  48. package/intelligence/orchestrator/core/state-manager.js +88 -0
  49. package/intelligence/orchestrator/core/telemetry.js +24 -0
  50. package/intelligence/orchestrator/index.js +17 -0
  51. package/intelligence/orchestrator.js +17 -512
  52. package/mcp/contracts/mcp-contract.v1.json +1 -1
  53. package/package.json +16 -3
  54. package/src/cli/agent.ts +703 -0
  55. package/src/cli/analyze.ts +640 -0
  56. package/src/cli/audit.ts +707 -0
  57. package/src/cli/auth.ts +930 -0
  58. package/src/cli/billing.ts +364 -0
  59. package/src/cli/build.ts +1089 -0
  60. package/src/cli/business.ts +508 -0
  61. package/src/cli/checkpoint-utils.ts +236 -0
  62. package/src/cli/checkpoint.ts +757 -0
  63. package/src/cli/cloud-sync.ts +534 -0
  64. package/src/cli/content.ts +273 -0
  65. package/src/cli/context.ts +667 -0
  66. package/src/cli/dashboard.ts +133 -0
  67. package/src/cli/deploy.ts +704 -0
  68. package/src/cli/doctor.ts +480 -0
  69. package/src/cli/fundraise.ts +494 -0
  70. package/src/cli/generate.ts +346 -0
  71. package/src/cli/github-cmd.ts +566 -0
  72. package/src/cli/health.ts +599 -0
  73. package/src/cli/index.ts +113 -0
  74. package/src/cli/init.ts +838 -0
  75. package/src/cli/legal.ts +495 -0
  76. package/src/cli/log.ts +316 -0
  77. package/src/cli/loop.ts +1660 -0
  78. package/src/cli/manager.ts +878 -0
  79. package/src/cli/mcp.ts +275 -0
  80. package/src/cli/memory.ts +346 -0
  81. package/src/cli/metrics.ts +590 -0
  82. package/src/cli/monitor.ts +960 -0
  83. package/src/cli/mvp.ts +662 -0
  84. package/src/cli/onboard.ts +663 -0
  85. package/src/cli/orchestrator.ts +622 -0
  86. package/src/cli/plugin.ts +483 -0
  87. package/src/cli/prd.ts +671 -0
  88. package/src/cli/preseed-start.ts +1633 -0
  89. package/src/cli/preseed.ts +2434 -0
  90. package/src/cli/project.ts +526 -0
  91. package/src/cli/quality.ts +885 -0
  92. package/src/cli/security.ts +1079 -0
  93. package/src/cli/seed.ts +1224 -0
  94. package/src/cli/skill.ts +537 -0
  95. package/src/cli/suggest.ts +1225 -0
  96. package/src/cli/switch.ts +518 -0
  97. package/src/cli/task.ts +780 -0
  98. package/src/cli/telemetry.ts +172 -0
  99. package/src/cli/todo.ts +627 -0
  100. package/src/cli/types.ts +15 -0
  101. package/src/cli/update.ts +334 -0
  102. package/src/cli/visualize.ts +609 -0
  103. package/src/cli/watch.ts +895 -0
  104. package/src/cli/workspace.ts +709 -0
  105. package/src/core/action-recorder.ts +673 -0
  106. package/src/core/analyze-workflow.ts +1453 -0
  107. package/src/core/api-client.ts +1120 -0
  108. package/src/core/audit-workflow.ts +1681 -0
  109. package/src/core/auth.ts +471 -0
  110. package/src/core/build-orchestrator.ts +509 -0
  111. package/src/core/build-state.ts +621 -0
  112. package/src/core/checkpoint-engine.ts +482 -0
  113. package/src/core/config.ts +1285 -0
  114. package/src/core/context-loader.ts +694 -0
  115. package/src/core/context.ts +410 -0
  116. package/src/core/deploy-workflow.ts +1085 -0
  117. package/src/core/entitlements.ts +322 -0
  118. package/src/core/github-sync.ts +720 -0
  119. package/src/core/index.ts +981 -0
  120. package/src/core/ingest.ts +1186 -0
  121. package/src/core/metrics-engine.ts +886 -0
  122. package/src/core/mvp.ts +847 -0
  123. package/src/core/onboard-workflow.ts +1293 -0
  124. package/src/core/policies.ts +81 -0
  125. package/src/core/preseed-workflow.ts +1163 -0
  126. package/src/core/preseed.ts +1826 -0
  127. package/src/core/project-context.ts +380 -0
  128. package/src/core/project-state.ts +699 -0
  129. package/src/core/r2-sync.ts +691 -0
  130. package/src/core/scaffold.ts +1715 -0
  131. package/src/core/session.ts +286 -0
  132. package/src/core/task-extractor.ts +799 -0
  133. package/src/core/telemetry.ts +371 -0
  134. package/src/core/tier-enforcement.ts +737 -0
  135. package/src/core/utils.ts +437 -0
  136. package/src/index.ts +29 -0
  137. package/src/intelligence/agent-collab.ts +2376 -0
  138. package/src/intelligence/auto-suggest.ts +713 -0
  139. package/src/intelligence/content-gen.ts +1351 -0
  140. package/src/intelligence/cross-project.ts +1692 -0
  141. package/src/intelligence/git-memory.ts +529 -0
  142. package/src/intelligence/index.ts +318 -0
  143. package/src/intelligence/orchestrator.ts +534 -0
  144. package/src/intelligence/prd.ts +466 -0
  145. package/src/intelligence/recommendations.ts +982 -0
  146. package/src/intelligence/workflow-composer.ts +1472 -0
  147. package/src/mcp/capabilities.ts +233 -0
  148. package/src/mcp/index.ts +37 -0
  149. package/src/mcp/registry.ts +1268 -0
  150. package/src/mcp/response-formatter.ts +797 -0
  151. package/src/mcp/server.ts +240 -0
  152. package/src/types/agent.ts +69 -0
  153. package/src/types/config.ts +86 -0
  154. package/src/types/context.ts +77 -0
  155. package/src/types/index.ts +53 -0
  156. package/src/types/mcp.ts +91 -0
  157. package/src/types/skills.ts +47 -0
  158. package/src/types/workflow.ts +155 -0
  159. package/generators/index.js +0 -18
@@ -0,0 +1,1120 @@
1
+ /**
2
+ * Bootspring API Client
3
+ *
4
+ * Handles all communication with api.bootspring.com
5
+ * This is the thin-client version that requires API for all operations.
6
+ *
7
+ * @package bootspring
8
+ * @module core/api-client
9
+ */
10
+
11
+ import * as https from 'https';
12
+ import * as http from 'http';
13
+ import * as crypto from 'crypto';
14
+ import * as auth from './auth';
15
+ import type { LoginResponse as AuthLoginResponse } from './auth';
16
+ import * as session from './session';
17
+
18
+ // ============================================================================
19
+ // Types
20
+ // ============================================================================
21
+
22
+ export interface ApiRequestOptions {
23
+ headers?: Record<string, string> | undefined;
24
+ timeout?: number | undefined;
25
+ noCache?: boolean | undefined;
26
+ }
27
+
28
+ export interface ApiError extends Error {
29
+ status?: number | undefined;
30
+ code?: string | undefined;
31
+ details?: unknown | undefined;
32
+ }
33
+
34
+ export interface HealthCheckResult {
35
+ connected: boolean;
36
+ version?: string | undefined;
37
+ error?: string | undefined;
38
+ }
39
+
40
+ export interface DeviceCodeResponse {
41
+ device_code: string;
42
+ user_code: string;
43
+ verification_uri: string;
44
+ expires_in: number;
45
+ interval: number;
46
+ }
47
+
48
+ export interface DeviceTokenResponse {
49
+ access_token: string;
50
+ refresh_token: string;
51
+ token_type: string;
52
+ expires_in: number;
53
+ user: UserData;
54
+ }
55
+
56
+ export interface UserData {
57
+ id: string;
58
+ email: string;
59
+ name?: string | undefined;
60
+ tier: string;
61
+ [key: string]: unknown;
62
+ }
63
+
64
+ // Use AuthLoginResponse from auth.ts module
65
+
66
+ export interface ApiKeyValidationResponse {
67
+ valid: boolean;
68
+ tier: string;
69
+ scopes: string[];
70
+ project?: ProjectData | undefined;
71
+ device?: unknown | undefined;
72
+ usage?: unknown | undefined;
73
+ error?: string | undefined;
74
+ }
75
+
76
+ export interface ProjectData {
77
+ id: string;
78
+ name: string;
79
+ [key: string]: unknown;
80
+ }
81
+
82
+ export interface Agent {
83
+ id: string;
84
+ name: string;
85
+ description?: string | undefined;
86
+ tier?: string | undefined;
87
+ [key: string]: unknown;
88
+ }
89
+
90
+ export interface AgentContext {
91
+ id: string;
92
+ name: string;
93
+ description: string;
94
+ context: string;
95
+ tier: string;
96
+ }
97
+
98
+ export interface Skill {
99
+ id: string;
100
+ name: string;
101
+ category: string;
102
+ tier?: string | undefined;
103
+ [key: string]: unknown;
104
+ }
105
+
106
+ export interface SkillListResponse {
107
+ skills: Skill[];
108
+ categories: string[];
109
+ userTier: string;
110
+ }
111
+
112
+ export interface SkillContent {
113
+ id: string;
114
+ name: string;
115
+ content: string;
116
+ tier: string;
117
+ }
118
+
119
+ export interface Template {
120
+ name: string;
121
+ description?: string | undefined;
122
+ [key: string]: unknown;
123
+ }
124
+
125
+ export interface TemplateContent {
126
+ name: string;
127
+ content: string;
128
+ variables?: string[] | undefined;
129
+ }
130
+
131
+ export interface Workflow {
132
+ id: string;
133
+ name: string;
134
+ description?: string | undefined;
135
+ [key: string]: unknown;
136
+ }
137
+
138
+ export interface AnalyzeContextRequest {
139
+ projectContext: unknown;
140
+ currentTask?: string | undefined;
141
+ recentActions?: unknown[] | undefined;
142
+ }
143
+
144
+ export interface McpTool {
145
+ name: string;
146
+ description?: string | undefined;
147
+ [key: string]: unknown;
148
+ }
149
+
150
+ export interface McpResource {
151
+ uri: string;
152
+ name?: string | undefined;
153
+ [key: string]: unknown;
154
+ }
155
+
156
+ export interface Subscription {
157
+ plan: string;
158
+ status: string;
159
+ [key: string]: unknown;
160
+ }
161
+
162
+ export interface Entitlements {
163
+ tier: string;
164
+ features: string[];
165
+ limits: Record<string, number>;
166
+ [key: string]: unknown;
167
+ }
168
+
169
+ export interface ProjectMember {
170
+ userId: string;
171
+ email: string;
172
+ role: string;
173
+ [key: string]: unknown;
174
+ }
175
+
176
+ export interface PreseedDocument {
177
+ name: string;
178
+ content?: string | undefined;
179
+ format?: string | undefined;
180
+ [key: string]: unknown;
181
+ }
182
+
183
+ export interface PreseedDocumentsResponse {
184
+ documents: PreseedDocument[];
185
+ count: number;
186
+ storage?: unknown | undefined;
187
+ }
188
+
189
+ export interface PreseedWizardResponse {
190
+ wizardData: unknown;
191
+ progress: unknown;
192
+ }
193
+
194
+ export interface SkillSearchOptions {
195
+ category?: string | undefined;
196
+ search?: string | undefined;
197
+ }
198
+
199
+ export interface TelemetryBatchInfo {
200
+ id?: string | undefined;
201
+ index?: number | undefined;
202
+ total?: number | undefined;
203
+ }
204
+
205
+ export interface CacheEntry {
206
+ data: unknown;
207
+ time: number;
208
+ }
209
+
210
+ // ============================================================================
211
+ // Constants & Cache
212
+ // ============================================================================
213
+
214
+ export const API_BASE = process.env['BOOTSPRING_API_URL'] || 'https://api.bootspring.com';
215
+ export const API_VERSION = 'v1'; // Note: auth routes don't use version prefix
216
+
217
+ // Cache for API responses
218
+ const cache = new Map<string, CacheEntry>();
219
+ const CACHE_TTL = 60000; // 1 minute
220
+
221
+ // Lazy-loaded package.json
222
+ let packageVersion: string | null = null;
223
+
224
+ function getPackageVersion(): string {
225
+ if (packageVersion === null) {
226
+ try {
227
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
228
+ const pkg = require('../../package.json') as { version: string };
229
+ packageVersion = pkg.version;
230
+ } catch {
231
+ packageVersion = 'unknown';
232
+ }
233
+ }
234
+ return packageVersion;
235
+ }
236
+
237
+ // ============================================================================
238
+ // Core Request Functions
239
+ // ============================================================================
240
+
241
+ /**
242
+ * Make an API request (with version prefix)
243
+ */
244
+ export async function request<T = unknown>(
245
+ method: string,
246
+ path: string,
247
+ data: unknown = null,
248
+ options: ApiRequestOptions = {}
249
+ ): Promise<T> {
250
+ const token = auth.getToken();
251
+ const apiKey = auth.getApiKey();
252
+ const url = new URL(`/api/${API_VERSION}${path}`, API_BASE);
253
+ const isHttps = url.protocol === 'https:';
254
+ const httpModule = isHttps ? https : http;
255
+
256
+ // Get device ID for request tracking
257
+ const deviceId = auth.getDeviceId();
258
+
259
+ // Get project context for tracking
260
+ const project = session.getEffectiveProject();
261
+ const projectId = project?.id || null;
262
+
263
+ const headers: Record<string, string> = {
264
+ 'Content-Type': 'application/json',
265
+ 'User-Agent': `bootspring-cli/${getPackageVersion()}`,
266
+ 'X-Device-Id': deviceId,
267
+ ...(projectId && { 'X-Project-Id': projectId }),
268
+ ...options.headers
269
+ };
270
+
271
+ // Use API key if available, otherwise use JWT token
272
+ if (apiKey) {
273
+ headers['X-API-Key'] = apiKey;
274
+ } else if (token) {
275
+ headers['Authorization'] = `Bearer ${token}`;
276
+ }
277
+
278
+ // Check cache for GET requests
279
+ if (method === 'GET' && !options.noCache) {
280
+ const cacheKey = `${method}:${path}`;
281
+ const cached = cache.get(cacheKey);
282
+ if (cached && Date.now() - cached.time < CACHE_TTL) {
283
+ return cached.data as T;
284
+ }
285
+ }
286
+
287
+ return new Promise<T>((resolve, reject) => {
288
+ const req = httpModule.request(url, {
289
+ method,
290
+ headers,
291
+ timeout: options.timeout || 30000
292
+ }, (res) => {
293
+ let body = '';
294
+ res.on('data', (chunk: Buffer | string) => { body += chunk; });
295
+ res.on('end', () => {
296
+ try {
297
+ const json = JSON.parse(body) as T & { message?: string; error?: string; code?: string; details?: unknown };
298
+
299
+ if (res.statusCode && res.statusCode >= 400) {
300
+ const error: ApiError = new Error(json.message || json.error || 'API Error');
301
+ error.status = res.statusCode;
302
+ error.code = json.error || json.code;
303
+ error.details = json.details;
304
+ reject(error);
305
+ } else {
306
+ // Cache successful GET responses
307
+ if (method === 'GET' && !options.noCache) {
308
+ const cacheKey = `${method}:${path}`;
309
+ cache.set(cacheKey, { data: json, time: Date.now() });
310
+ }
311
+ resolve(json);
312
+ }
313
+ } catch {
314
+ if (res.statusCode && res.statusCode >= 400) {
315
+ const error: ApiError = new Error(body || 'API Error');
316
+ error.status = res.statusCode;
317
+ reject(error);
318
+ } else {
319
+ resolve(body as unknown as T);
320
+ }
321
+ }
322
+ });
323
+ });
324
+
325
+ req.on('error', (err: NodeJS.ErrnoException) => {
326
+ if (err.code === 'ECONNREFUSED') {
327
+ reject(new Error('Cannot connect to Bootspring API. Please check your internet connection.'));
328
+ } else {
329
+ reject(err);
330
+ }
331
+ });
332
+
333
+ req.on('timeout', () => {
334
+ req.destroy();
335
+ reject(new Error('Request timeout'));
336
+ });
337
+
338
+ if (data) {
339
+ req.write(JSON.stringify(data));
340
+ }
341
+ req.end();
342
+ });
343
+ }
344
+
345
+ /**
346
+ * Make a direct API request (without version prefix, for /api/projects etc.)
347
+ */
348
+ export async function directRequest<T = unknown>(
349
+ method: string,
350
+ path: string,
351
+ data: unknown = null,
352
+ options: ApiRequestOptions = {}
353
+ ): Promise<T> {
354
+ const token = auth.getToken();
355
+ const apiKey = auth.getApiKey();
356
+ const url = new URL(`/api${path}`, API_BASE);
357
+ const isHttps = url.protocol === 'https:';
358
+ const httpModule = isHttps ? https : http;
359
+
360
+ const deviceId = auth.getDeviceId();
361
+ const project = session.getEffectiveProject();
362
+ const projectId = project?.id || null;
363
+
364
+ const headers: Record<string, string> = {
365
+ 'Content-Type': 'application/json',
366
+ 'User-Agent': `bootspring-cli/${getPackageVersion()}`,
367
+ 'X-Device-Id': deviceId,
368
+ ...(projectId && { 'X-Project-Id': projectId }),
369
+ ...options.headers
370
+ };
371
+
372
+ if (apiKey) {
373
+ headers['X-API-Key'] = apiKey;
374
+ } else if (token) {
375
+ headers['Authorization'] = `Bearer ${token}`;
376
+ }
377
+
378
+ return new Promise<T>((resolve, reject) => {
379
+ const req = httpModule.request(url, {
380
+ method,
381
+ headers,
382
+ timeout: options.timeout || 30000
383
+ }, (res) => {
384
+ let body = '';
385
+ res.on('data', (chunk: Buffer | string) => { body += chunk; });
386
+ res.on('end', () => {
387
+ try {
388
+ const json = JSON.parse(body) as T & { message?: string; error?: string; code?: string; details?: unknown };
389
+ if (res.statusCode && res.statusCode >= 400) {
390
+ const error: ApiError = new Error(json.message || json.error || 'API Error');
391
+ error.status = res.statusCode;
392
+ error.code = json.error || json.code;
393
+ error.details = json.details;
394
+ reject(error);
395
+ } else {
396
+ resolve(json);
397
+ }
398
+ } catch {
399
+ if (res.statusCode && res.statusCode >= 400) {
400
+ const error: ApiError = new Error(body || 'API Error');
401
+ error.status = res.statusCode;
402
+ reject(error);
403
+ } else {
404
+ resolve(body as unknown as T);
405
+ }
406
+ }
407
+ });
408
+ });
409
+
410
+ req.on('error', (err: NodeJS.ErrnoException) => {
411
+ if (err.code === 'ECONNREFUSED') {
412
+ reject(new Error('Cannot connect to Bootspring API. Please check your internet connection.'));
413
+ } else {
414
+ reject(err);
415
+ }
416
+ });
417
+
418
+ req.on('timeout', () => {
419
+ req.destroy();
420
+ reject(new Error('Request timeout'));
421
+ });
422
+
423
+ if (data) {
424
+ req.write(JSON.stringify(data));
425
+ }
426
+ req.end();
427
+ });
428
+ }
429
+
430
+ /**
431
+ * Clear the cache
432
+ */
433
+ export function clearCache(): void {
434
+ cache.clear();
435
+ }
436
+
437
+ /**
438
+ * Check if API is reachable
439
+ */
440
+ export async function healthCheck(): Promise<HealthCheckResult> {
441
+ try {
442
+ // Health endpoint is at /health, not under /api/v1
443
+ const url = new URL('/health', API_BASE);
444
+ const isHttps = url.protocol === 'https:';
445
+ const httpModule = isHttps ? https : http;
446
+
447
+ return new Promise<HealthCheckResult>((resolve) => {
448
+ const req = httpModule.request(url, {
449
+ method: 'GET',
450
+ timeout: 5000
451
+ }, (res) => {
452
+ let body = '';
453
+ res.on('data', (chunk: Buffer | string) => { body += chunk; });
454
+ res.on('end', () => {
455
+ try {
456
+ const json = JSON.parse(body) as { version?: string };
457
+ resolve({ connected: true, version: json.version });
458
+ } catch {
459
+ resolve({ connected: false, error: 'Invalid response' });
460
+ }
461
+ });
462
+ });
463
+
464
+ req.on('error', (err: Error) => {
465
+ resolve({ connected: false, error: err.message });
466
+ });
467
+
468
+ req.on('timeout', () => {
469
+ req.destroy();
470
+ resolve({ connected: false, error: 'Timeout' });
471
+ });
472
+
473
+ req.end();
474
+ });
475
+ } catch (error) {
476
+ const err = error as Error;
477
+ return { connected: false, error: err.message };
478
+ }
479
+ }
480
+
481
+ /**
482
+ * Require authentication - throws if not logged in
483
+ */
484
+ export function requireAuth(): void {
485
+ if (!auth.isAuthenticated()) {
486
+ const error: ApiError = new Error('Authentication required. Run: bootspring auth login');
487
+ error.code = 'AUTH_REQUIRED';
488
+ throw error;
489
+ }
490
+ }
491
+
492
+ // ============================================================================
493
+ // API Methods
494
+ // ============================================================================
495
+
496
+ // Device Authorization Flow
497
+ export async function requestDeviceCode(): Promise<DeviceCodeResponse> {
498
+ const deviceContext = auth.getDeviceContext();
499
+ const url = new URL('/api/v1/auth/device', API_BASE);
500
+ const isHttps = url.protocol === 'https:';
501
+ const httpModule = isHttps ? https : http;
502
+
503
+ return new Promise<DeviceCodeResponse>((resolve, reject) => {
504
+ const req = httpModule.request(url, {
505
+ method: 'POST',
506
+ headers: {
507
+ 'Content-Type': 'application/json',
508
+ 'User-Agent': `bootspring-cli/${getPackageVersion()}`,
509
+ },
510
+ timeout: 10000
511
+ }, (res) => {
512
+ let body = '';
513
+ res.on('data', (chunk: Buffer | string) => { body += chunk; });
514
+ res.on('end', () => {
515
+ try {
516
+ const json = JSON.parse(body) as DeviceCodeResponse & { message?: string; error?: string };
517
+ if (res.statusCode && res.statusCode >= 400) {
518
+ const error: ApiError = new Error(json.message || json.error || 'Failed to get device code');
519
+ error.status = res.statusCode;
520
+ error.code = json.error;
521
+ reject(error);
522
+ } else {
523
+ resolve(json);
524
+ }
525
+ } catch {
526
+ reject(new Error('Invalid response from API'));
527
+ }
528
+ });
529
+ });
530
+ req.on('error', reject);
531
+ req.on('timeout', () => {
532
+ req.destroy();
533
+ reject(new Error('Request timeout'));
534
+ });
535
+ req.write(JSON.stringify({ device: deviceContext }));
536
+ req.end();
537
+ });
538
+ }
539
+
540
+ export async function pollDeviceToken(deviceCode: string): Promise<DeviceTokenResponse> {
541
+ const url = new URL('/api/v1/auth/device/token', API_BASE);
542
+ const isHttps = url.protocol === 'https:';
543
+ const httpModule = isHttps ? https : http;
544
+
545
+ return new Promise<DeviceTokenResponse>((resolve, reject) => {
546
+ const req = httpModule.request(url, {
547
+ method: 'POST',
548
+ headers: {
549
+ 'Content-Type': 'application/json',
550
+ 'User-Agent': `bootspring-cli/${getPackageVersion()}`,
551
+ },
552
+ timeout: 10000
553
+ }, (res) => {
554
+ let body = '';
555
+ res.on('data', (chunk: Buffer | string) => { body += chunk; });
556
+ res.on('end', () => {
557
+ try {
558
+ const json = JSON.parse(body) as DeviceTokenResponse & { message?: string; error?: string };
559
+ if (res.statusCode && res.statusCode >= 400) {
560
+ const error: ApiError = new Error(json.message || json.error || 'Authorization pending');
561
+ error.status = res.statusCode;
562
+ error.code = json.error;
563
+ error.details = json;
564
+ reject(error);
565
+ } else {
566
+ resolve(json);
567
+ }
568
+ } catch {
569
+ reject(new Error('Invalid response from API'));
570
+ }
571
+ });
572
+ });
573
+ req.on('error', reject);
574
+ req.on('timeout', () => {
575
+ req.destroy();
576
+ reject(new Error('Request timeout'));
577
+ });
578
+ req.write(JSON.stringify({ device_code: deviceCode }));
579
+ req.end();
580
+ });
581
+ }
582
+
583
+ // Auth
584
+ export async function login(email: string, password: string): Promise<AuthLoginResponse> {
585
+ const deviceContext = auth.getDeviceContext();
586
+ const response = await request<AuthLoginResponse>('POST', '/auth/login', {
587
+ email,
588
+ password,
589
+ device: deviceContext
590
+ });
591
+ auth.login(response);
592
+ return response;
593
+ }
594
+
595
+ export async function loginWithApiKey(apiKey: string): Promise<ApiKeyValidationResponse> {
596
+ // Validate API key by calling /api/keys/validate
597
+ const url = new URL('/api/keys/validate', API_BASE);
598
+ const isHttps = url.protocol === 'https:';
599
+ const httpModule = isHttps ? https : http;
600
+ const deviceContext = auth.getDeviceContext();
601
+
602
+ return new Promise<ApiKeyValidationResponse>((resolve, reject) => {
603
+ const requestBody = JSON.stringify({
604
+ apiKey,
605
+ deviceFingerprint: deviceContext.deviceId,
606
+ deviceName: `CLI - ${deviceContext.hostname}`
607
+ });
608
+
609
+ const req = httpModule.request(url, {
610
+ method: 'POST',
611
+ headers: {
612
+ 'Content-Type': 'application/json',
613
+ 'User-Agent': `bootspring-cli/${getPackageVersion()}`,
614
+ 'Content-Length': Buffer.byteLength(requestBody).toString()
615
+ },
616
+ timeout: 10000
617
+ }, (res) => {
618
+ let body = '';
619
+ res.on('data', (chunk: Buffer | string) => { body += chunk; });
620
+ res.on('end', () => {
621
+ try {
622
+ const json = JSON.parse(body) as ApiKeyValidationResponse;
623
+ if ((res.statusCode && res.statusCode >= 400) || !json.valid) {
624
+ const error: ApiError = new Error(json.error || 'Invalid API key');
625
+ error.status = res.statusCode;
626
+ error.code = json.error;
627
+ reject(error);
628
+ } else {
629
+ // Build user info from response
630
+ const user = {
631
+ tier: json.tier,
632
+ scopes: json.scopes
633
+ };
634
+
635
+ // Save API key and user info
636
+ auth.loginWithApiKey(apiKey, user);
637
+
638
+ // If key has a project, auto-set project context
639
+ if (json.project) {
640
+ session.setCurrentProject(json.project);
641
+ session.addRecentProject(json.project);
642
+ }
643
+
644
+ resolve(json);
645
+ }
646
+ } catch {
647
+ reject(new Error('Invalid response from API'));
648
+ }
649
+ });
650
+ });
651
+
652
+ req.on('error', reject);
653
+ req.on('timeout', () => {
654
+ req.destroy();
655
+ reject(new Error('Request timeout'));
656
+ });
657
+ req.write(requestBody);
658
+ req.end();
659
+ });
660
+ }
661
+
662
+ export async function register(email: string, password: string, name: string): Promise<AuthLoginResponse> {
663
+ const deviceContext = auth.getDeviceContext();
664
+ const response = await request<AuthLoginResponse>('POST', '/auth/register', {
665
+ email,
666
+ password,
667
+ name,
668
+ device: deviceContext
669
+ });
670
+ auth.login(response);
671
+ return response;
672
+ }
673
+
674
+ export async function me(): Promise<UserData> {
675
+ requireAuth();
676
+ return request<UserData>('GET', '/auth/me');
677
+ }
678
+
679
+ export async function validateApiKey(apiKey: string): Promise<ApiKeyValidationResponse> {
680
+ const url = new URL('/api/keys/validate', API_BASE);
681
+ const isHttps = url.protocol === 'https:';
682
+ const httpModule = isHttps ? https : http;
683
+ const deviceContext = auth.getDeviceContext();
684
+
685
+ return new Promise<ApiKeyValidationResponse>((resolve, reject) => {
686
+ const requestBody = JSON.stringify({
687
+ apiKey,
688
+ deviceFingerprint: deviceContext.deviceId,
689
+ deviceName: `CLI - ${deviceContext.hostname}`
690
+ });
691
+
692
+ const req = httpModule.request(url, {
693
+ method: 'POST',
694
+ headers: {
695
+ 'Content-Type': 'application/json',
696
+ 'User-Agent': `bootspring-cli/${getPackageVersion()}`,
697
+ 'Content-Length': Buffer.byteLength(requestBody).toString()
698
+ },
699
+ timeout: 10000
700
+ }, (res) => {
701
+ let body = '';
702
+ res.on('data', (chunk: Buffer | string) => { body += chunk; });
703
+ res.on('end', () => {
704
+ try {
705
+ const json = JSON.parse(body) as ApiKeyValidationResponse;
706
+ if ((res.statusCode && res.statusCode >= 400) || !json.valid) {
707
+ const error: ApiError = new Error(json.error || 'Invalid API key');
708
+ error.status = res.statusCode;
709
+ reject(error);
710
+ } else {
711
+ resolve(json);
712
+ }
713
+ } catch {
714
+ reject(new Error('Invalid response from API'));
715
+ }
716
+ });
717
+ });
718
+
719
+ req.on('error', reject);
720
+ req.on('timeout', () => {
721
+ req.destroy();
722
+ reject(new Error('Request timeout'));
723
+ });
724
+ req.write(requestBody);
725
+ req.end();
726
+ });
727
+ }
728
+
729
+ export async function refreshToken(): Promise<AuthLoginResponse> {
730
+ const refreshTokenValue = auth.getRefreshToken();
731
+ if (!refreshTokenValue) {
732
+ throw new Error('No refresh token available');
733
+ }
734
+ const deviceContext = auth.getDeviceContext();
735
+ const response = await request<AuthLoginResponse>('POST', '/auth/refresh', {
736
+ refreshToken: refreshTokenValue,
737
+ device: deviceContext
738
+ });
739
+ auth.updateTokens(response);
740
+ return response;
741
+ }
742
+
743
+ export async function logout(): Promise<void> {
744
+ if (auth.isAuthenticated()) {
745
+ try {
746
+ const refreshTokenValue = auth.getRefreshToken();
747
+ await request('POST', '/auth/logout', { refreshToken: refreshTokenValue });
748
+ } catch {
749
+ // Ignore logout API errors
750
+ }
751
+ }
752
+ auth.logout();
753
+ }
754
+
755
+ // Agents
756
+ export async function listAgents(): Promise<{ agents: Agent[] }> {
757
+ requireAuth();
758
+ return request<{ agents: Agent[] }>('GET', '/agents');
759
+ }
760
+
761
+ export async function getAgent(id: string): Promise<Agent> {
762
+ requireAuth();
763
+ return request<Agent>('GET', `/agents/${encodeURIComponent(id)}`);
764
+ }
765
+
766
+ export async function getAgentContext(id: string): Promise<AgentContext> {
767
+ requireAuth();
768
+ return request<AgentContext>('GET', `/agents/${encodeURIComponent(id)}/context`);
769
+ }
770
+
771
+ export async function invokeAgent(id: string, projectContext: unknown, task: string): Promise<unknown> {
772
+ requireAuth();
773
+ return request('POST', `/agents/${encodeURIComponent(id)}/invoke`, {
774
+ projectContext,
775
+ task
776
+ });
777
+ }
778
+
779
+ export async function getAgentCapabilities(id: string): Promise<unknown> {
780
+ requireAuth();
781
+ return request('GET', `/agents/${encodeURIComponent(id)}/capabilities`);
782
+ }
783
+
784
+ // Skills
785
+ export async function listSkills(options: SkillSearchOptions = {}): Promise<SkillListResponse> {
786
+ requireAuth();
787
+ const params = new URLSearchParams();
788
+ if (options.category) params.append('category', options.category);
789
+ if (options.search) params.append('search', options.search);
790
+
791
+ const query = params.toString();
792
+ return request<SkillListResponse>('GET', `/skills${query ? '?' + query : ''}`);
793
+ }
794
+
795
+ export async function getSkillContent(skillId: string): Promise<SkillContent> {
796
+ requireAuth();
797
+ return request<SkillContent>('GET', `/skills/${skillId}`);
798
+ }
799
+
800
+ /**
801
+ * @deprecated Use getSkillContent instead
802
+ */
803
+ export async function getSkill(category: string, name: string): Promise<SkillContent> {
804
+ requireAuth();
805
+ return request<SkillContent>('GET', `/skills/${encodeURIComponent(category)}/${encodeURIComponent(name)}`);
806
+ }
807
+
808
+ export async function searchSkills(query: string, options: SkillSearchOptions = {}): Promise<SkillListResponse> {
809
+ requireAuth();
810
+ const params = new URLSearchParams();
811
+ params.append('search', query);
812
+ if (options.category) params.append('category', options.category);
813
+ return request<SkillListResponse>('GET', `/skills?${params.toString()}`);
814
+ }
815
+
816
+ // Templates
817
+ export async function listTemplates(category: string): Promise<{ templates: Template[] }> {
818
+ requireAuth();
819
+ return request<{ templates: Template[] }>('GET', `/templates/${encodeURIComponent(category)}`);
820
+ }
821
+
822
+ export async function getTemplate(category: string, name: string): Promise<TemplateContent> {
823
+ requireAuth();
824
+ return request<TemplateContent>('GET', `/templates/${encodeURIComponent(category)}/${encodeURIComponent(name)}`);
825
+ }
826
+
827
+ // Orchestrator
828
+ export async function listWorkflows(): Promise<{ workflows: Workflow[] }> {
829
+ requireAuth();
830
+ return request<{ workflows: Workflow[] }>('GET', '/orchestrator/workflows');
831
+ }
832
+
833
+ export async function getWorkflow(id: string): Promise<Workflow> {
834
+ requireAuth();
835
+ return request<Workflow>('GET', `/orchestrator/workflows/${encodeURIComponent(id)}`);
836
+ }
837
+
838
+ export async function analyzeContext(projectContext: unknown, currentTask?: string, recentActions?: unknown[]): Promise<unknown> {
839
+ requireAuth();
840
+ return request('POST', '/orchestrator/analyze', {
841
+ projectContext,
842
+ currentTask,
843
+ recentActions
844
+ });
845
+ }
846
+
847
+ export async function startWorkflow(workflowId: string, projectContext: unknown, parameters?: unknown): Promise<unknown> {
848
+ requireAuth();
849
+ return request('POST', '/orchestrator/start', {
850
+ workflow: workflowId,
851
+ projectContext,
852
+ parameters
853
+ });
854
+ }
855
+
856
+ export async function getSuggestions(context: unknown, action?: string): Promise<unknown> {
857
+ requireAuth();
858
+ return request('POST', '/orchestrator/suggest', { context, action });
859
+ }
860
+
861
+ // Quality
862
+ export async function listQualityGates(): Promise<unknown> {
863
+ requireAuth();
864
+ return request('GET', '/quality/gates');
865
+ }
866
+
867
+ export async function runQualityGate(gateId: string, projectContext: unknown, options?: unknown): Promise<unknown> {
868
+ requireAuth();
869
+ return request('POST', '/quality/run', {
870
+ gate: gateId,
871
+ projectContext,
872
+ options
873
+ });
874
+ }
875
+
876
+ export async function getLintBudgets(): Promise<unknown> {
877
+ requireAuth();
878
+ return request('GET', '/quality/lint-budgets');
879
+ }
880
+
881
+ // MCP
882
+ export async function listMcpTools(): Promise<{ tools: McpTool[] }> {
883
+ requireAuth();
884
+ return request<{ tools: McpTool[] }>('GET', '/mcp/tools');
885
+ }
886
+
887
+ export async function callMcpTool(tool: string, args: unknown): Promise<unknown> {
888
+ requireAuth();
889
+ return request('POST', '/mcp/tool', { tool, arguments: args });
890
+ }
891
+
892
+ export async function listMcpResources(): Promise<{ resources: McpResource[] }> {
893
+ requireAuth();
894
+ return request<{ resources: McpResource[] }>('GET', '/mcp/resources');
895
+ }
896
+
897
+ export async function getMcpResource(uri: string): Promise<McpResource> {
898
+ requireAuth();
899
+ return request<McpResource>('GET', `/mcp/resources/${encodeURIComponent(uri)}`);
900
+ }
901
+
902
+ // Billing
903
+ export async function getSubscription(): Promise<Subscription> {
904
+ requireAuth();
905
+ return request<Subscription>('GET', '/billing/subscription');
906
+ }
907
+
908
+ export async function createCheckout(plan: string): Promise<{ url: string }> {
909
+ requireAuth();
910
+ return request<{ url: string }>('POST', '/billing/create-checkout', { plan });
911
+ }
912
+
913
+ export async function getPortalUrl(): Promise<{ url: string }> {
914
+ requireAuth();
915
+ return request<{ url: string }>('POST', '/billing/portal');
916
+ }
917
+
918
+ export async function getUsage(): Promise<unknown> {
919
+ requireAuth();
920
+ return request('GET', '/billing/usage');
921
+ }
922
+
923
+ export async function getInvoices(): Promise<unknown> {
924
+ requireAuth();
925
+ return request('GET', '/billing/invoices');
926
+ }
927
+
928
+ // Entitlements (v1)
929
+ export async function resolveEntitlements(): Promise<Entitlements> {
930
+ requireAuth();
931
+ return request<Entitlements>('GET', '/entitlements/resolve', null, { noCache: false });
932
+ }
933
+
934
+ // Usage Tracking (v1)
935
+ export async function trackUsage(type: string, metadata: Record<string, unknown> = {}): Promise<unknown> {
936
+ requireAuth();
937
+ return request('POST', '/track', { type, metadata });
938
+ }
939
+
940
+ // Telemetry Batch Upload (v1)
941
+ export async function uploadTelemetryBatch(events: unknown[], batchInfo: TelemetryBatchInfo = {}): Promise<unknown> {
942
+ requireAuth();
943
+ const batchId = batchInfo.id || crypto.randomUUID();
944
+ return request('POST', '/events/batch', {
945
+ source: 'bootspring',
946
+ batch: {
947
+ index: batchInfo.index || 0,
948
+ total: batchInfo.total || 1,
949
+ id: batchId
950
+ },
951
+ events
952
+ }, {
953
+ headers: {
954
+ 'X-Bootspring-Batch-Id': batchId
955
+ }
956
+ });
957
+ }
958
+
959
+ // Projects
960
+ export async function listProjects(): Promise<{ projects: ProjectData[] }> {
961
+ requireAuth();
962
+ const url = new URL('/api/auth/device/projects', API_BASE);
963
+ const isHttps = url.protocol === 'https:';
964
+ const httpModule = isHttps ? https : http;
965
+ const apiKey = auth.getApiKey();
966
+ const token = auth.getToken();
967
+
968
+ return new Promise<{ projects: ProjectData[] }>((resolve, reject) => {
969
+ const headers: Record<string, string> = {
970
+ 'Content-Type': 'application/json',
971
+ 'User-Agent': `bootspring-cli/${getPackageVersion()}`,
972
+ };
973
+ if (apiKey) {
974
+ headers['X-API-Key'] = apiKey;
975
+ } else if (token) {
976
+ headers['Authorization'] = `Bearer ${token}`;
977
+ }
978
+
979
+ const req = httpModule.request(url, {
980
+ method: 'GET',
981
+ headers,
982
+ timeout: 10000
983
+ }, (res) => {
984
+ let body = '';
985
+ res.on('data', (chunk: Buffer | string) => { body += chunk; });
986
+ res.on('end', () => {
987
+ try {
988
+ const json = JSON.parse(body) as { projects: ProjectData[]; message?: string; error?: string };
989
+ if (res.statusCode && res.statusCode >= 400) {
990
+ const error: ApiError = new Error(json.message || json.error || 'Failed to list projects');
991
+ error.status = res.statusCode;
992
+ reject(error);
993
+ } else {
994
+ resolve(json);
995
+ }
996
+ } catch {
997
+ reject(new Error('Invalid response from API'));
998
+ }
999
+ });
1000
+ });
1001
+ req.on('error', reject);
1002
+ req.on('timeout', () => {
1003
+ req.destroy();
1004
+ reject(new Error('Request timeout'));
1005
+ });
1006
+ req.end();
1007
+ });
1008
+ }
1009
+
1010
+ // Project Members Management
1011
+ export async function getProjectMembers(projectId: string): Promise<{ members: ProjectMember[] }> {
1012
+ requireAuth();
1013
+ return directRequest<{ members: ProjectMember[] }>('GET', `/projects/${encodeURIComponent(projectId)}/members`);
1014
+ }
1015
+
1016
+ export async function addProjectMember(projectId: string, email: string, role: string = 'member'): Promise<ProjectMember> {
1017
+ requireAuth();
1018
+ return directRequest<ProjectMember>('POST', `/projects/${encodeURIComponent(projectId)}/members`, {
1019
+ email,
1020
+ role
1021
+ });
1022
+ }
1023
+
1024
+ export async function updateProjectMember(projectId: string, userId: string, role: string): Promise<ProjectMember> {
1025
+ requireAuth();
1026
+ return directRequest<ProjectMember>('PATCH', `/projects/${encodeURIComponent(projectId)}/members/${encodeURIComponent(userId)}`, {
1027
+ role
1028
+ });
1029
+ }
1030
+
1031
+ export async function removeProjectMember(projectId: string, userId: string): Promise<void> {
1032
+ requireAuth();
1033
+ await directRequest('DELETE', `/projects/${encodeURIComponent(projectId)}/members/${encodeURIComponent(userId)}`);
1034
+ }
1035
+
1036
+ export async function transferProjectOwnership(projectId: string, newOwnerId: string): Promise<unknown> {
1037
+ requireAuth();
1038
+ return directRequest('POST', `/projects/${encodeURIComponent(projectId)}/transfer`, {
1039
+ newOwnerId
1040
+ });
1041
+ }
1042
+
1043
+ export async function findSimilarProjects(name?: string, repoUrl?: string): Promise<{ projects: ProjectData[] }> {
1044
+ requireAuth();
1045
+ const params = new URLSearchParams();
1046
+ if (name) params.set('name', name);
1047
+ if (repoUrl) params.set('repo', repoUrl);
1048
+ return directRequest<{ projects: ProjectData[] }>('GET', `/projects/similar?${params.toString()}`);
1049
+ }
1050
+
1051
+ // Preseed Documents
1052
+ export async function listPreseedDocuments(projectId: string): Promise<PreseedDocumentsResponse> {
1053
+ requireAuth();
1054
+ return directRequest<PreseedDocumentsResponse>('GET', `/projects/${encodeURIComponent(projectId)}/preseed/documents`);
1055
+ }
1056
+
1057
+ export async function getPreseedDocument(projectId: string, documentName: string): Promise<PreseedDocument> {
1058
+ requireAuth();
1059
+ return directRequest<PreseedDocument>('GET', `/projects/${encodeURIComponent(projectId)}/preseed/documents?name=${encodeURIComponent(documentName)}`);
1060
+ }
1061
+
1062
+ export async function downloadPreseedZip(projectId: string): Promise<Buffer> {
1063
+ requireAuth();
1064
+ const apiKey = auth.getApiKey();
1065
+ const token = auth.getToken();
1066
+ const url = new URL(`/api/projects/${encodeURIComponent(projectId)}/preseed/documents?format=zip`, API_BASE);
1067
+ const isHttps = url.protocol === 'https:';
1068
+ const httpModule = isHttps ? https : http;
1069
+
1070
+ return new Promise<Buffer>((resolve, reject) => {
1071
+ const headers: Record<string, string> = {
1072
+ 'User-Agent': `bootspring-cli/${getPackageVersion()}`,
1073
+ };
1074
+ if (apiKey) {
1075
+ headers['X-API-Key'] = apiKey;
1076
+ } else if (token) {
1077
+ headers['Authorization'] = `Bearer ${token}`;
1078
+ }
1079
+
1080
+ const req = httpModule.request(url, {
1081
+ method: 'GET',
1082
+ headers,
1083
+ timeout: 60000
1084
+ }, (res) => {
1085
+ if (res.statusCode && res.statusCode >= 400) {
1086
+ let body = '';
1087
+ res.on('data', (chunk: Buffer | string) => { body += chunk; });
1088
+ res.on('end', () => {
1089
+ try {
1090
+ const json = JSON.parse(body) as { message?: string; error?: string };
1091
+ const error: ApiError = new Error(json.message || json.error || 'Failed to download preseed documents');
1092
+ error.status = res.statusCode;
1093
+ reject(error);
1094
+ } catch {
1095
+ reject(new Error(`HTTP ${res.statusCode}: ${body}`));
1096
+ }
1097
+ });
1098
+ return;
1099
+ }
1100
+
1101
+ const chunks: Buffer[] = [];
1102
+ res.on('data', (chunk: Buffer) => chunks.push(chunk));
1103
+ res.on('end', () => {
1104
+ resolve(Buffer.concat(chunks));
1105
+ });
1106
+ });
1107
+
1108
+ req.on('error', reject);
1109
+ req.on('timeout', () => {
1110
+ req.destroy();
1111
+ reject(new Error('Request timeout'));
1112
+ });
1113
+ req.end();
1114
+ });
1115
+ }
1116
+
1117
+ export async function getPreseedWizard(projectId: string): Promise<PreseedWizardResponse> {
1118
+ requireAuth();
1119
+ return directRequest<PreseedWizardResponse>('GET', `/projects/${encodeURIComponent(projectId)}/preseed/wizard`);
1120
+ }