@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
@@ -1,55 +0,0 @@
1
- /**
2
- * Bootspring Telemetry Types
3
- * @module core/telemetry
4
- */
5
-
6
- export interface TelemetryEvent {
7
- event: string;
8
- timestamp: number;
9
- data?: Record<string, unknown>;
10
- sessionId?: string;
11
- }
12
-
13
- export interface UploadResult {
14
- success: boolean;
15
- uploaded: number;
16
- errors?: string[];
17
- }
18
-
19
- /** Maximum events to store locally */
20
- export const MAX_EVENTS_LIMIT: number;
21
-
22
- /**
23
- * Emit a telemetry event
24
- * @param event - Event name
25
- * @param data - Event data
26
- */
27
- export function emit(event: string, data?: Record<string, unknown>): void;
28
-
29
- /**
30
- * List stored telemetry events
31
- * @param limit - Maximum events to return
32
- * @returns Array of events
33
- */
34
- export function list(limit?: number): TelemetryEvent[];
35
-
36
- /**
37
- * Upload telemetry events to server
38
- * @param options - Upload options
39
- * @returns Upload result
40
- */
41
- export function upload(options?: {
42
- batchSize?: number;
43
- clearOnSuccess?: boolean;
44
- }): Promise<UploadResult>;
45
-
46
- /**
47
- * Clear stored telemetry events
48
- */
49
- export function clear(): void;
50
-
51
- /**
52
- * Get telemetry session ID
53
- * @returns Current session ID
54
- */
55
- export function getSessionId(): string;
package/core/telemetry.js DELETED
@@ -1,617 +0,0 @@
1
- /**
2
- * Bootspring Telemetry
3
- * Lightweight JSONL event emitter for product instrumentation.
4
- */
5
-
6
- const fs = require('fs');
7
- const path = require('path');
8
- const crypto = require('crypto');
9
- const { redactErrorMessage, redactSensitiveData } = require('./redaction');
10
-
11
- const MAX_EVENTS_LIMIT = 10000;
12
- const ASSISTANTS = ['claude', 'codex', 'gemini'];
13
- const ASSISTANT_SETUP_EVENT = 'assistant_setup';
14
- const ASSISTANT_FIRST_SUCCESS_EVENT = 'assistant_first_success';
15
- const ASSISTANT_RETURN_EVENT = 'assistant_return';
16
- const BILLING_UPGRADE_STARTED_EVENT = 'billing_upgrade_started';
17
- const UPGRADE_PROMPT_EVENT = 'premium_prompted';
18
- const UPGRADE_COMPLETED_EVENT = 'premium_unlocked';
19
- const ASSISTANT_STATUS_ALLOWLIST = new Set(['installed', 'updated', 'skipped', 'failed', 'created', 'unchanged']);
20
- const ASSISTANT_SOURCE_ALLOWLIST = new Set(['mcp', 'cli', 'setup', 'dashboard', 'api', 'unknown']);
21
-
22
- function normalizeAssistantId(value) {
23
- const normalized = String(value || '').trim().toLowerCase();
24
- if (!normalized) return null;
25
- const compact = normalized.replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '');
26
- if (compact === 'claude' || compact === 'claude-code') return 'claude';
27
- if (compact === 'codex' || compact === 'openai-codex') return 'codex';
28
- if (compact === 'gemini' || compact === 'gemini-cli') return 'gemini';
29
- return null;
30
- }
31
-
32
- function inferAssistantFromEnvironment(env = process.env) {
33
- const explicit = normalizeAssistantId(env.BOOTSPRING_ASSISTANT);
34
- if (explicit) return explicit;
35
- if (env.CLAUDE_CODE || env.CLAUDECODE) return 'claude';
36
- if (env.CODEX_SANDBOX || env.CODEX_ENV || env.OPENAI_CODEX) return 'codex';
37
- if (env.GEMINI_CLI || env.GOOGLE_GEMINI_CLI) return 'gemini';
38
- return null;
39
- }
40
-
41
- function normalizeCardinalityValue(value, fallback = 'unknown') {
42
- const normalized = String(value || '')
43
- .trim()
44
- .toLowerCase()
45
- .replace(/[^a-z0-9_-]+/g, '-')
46
- .replace(/^-+|-+$/g, '');
47
- return normalized || fallback;
48
- }
49
-
50
- function normalizeAssistantStatus(value) {
51
- const normalized = normalizeCardinalityValue(value, 'updated');
52
- return ASSISTANT_STATUS_ALLOWLIST.has(normalized) ? normalized : 'other';
53
- }
54
-
55
- function normalizeAssistantSource(value) {
56
- const normalized = normalizeCardinalityValue(value, 'unknown');
57
- return ASSISTANT_SOURCE_ALLOWLIST.has(normalized) ? normalized : 'other';
58
- }
59
-
60
- function getAssistantRecords(assistant, options = {}) {
61
- const normalized = normalizeAssistantId(assistant);
62
- if (!normalized) return [];
63
- return listEvents({
64
- projectRoot: options.projectRoot
65
- }).filter(record => normalizeAssistantId(record?.payload?.assistant) === normalized);
66
- }
67
-
68
- function returnWindow(daysSinceFirst) {
69
- if (daysSinceFirst >= 7) return 'd7_plus';
70
- if (daysSinceFirst >= 2) return 'd2_6';
71
- if (daysSinceFirst >= 1) return 'd1';
72
- return 'd0';
73
- }
74
-
75
- function eventTimeMs(record) {
76
- const occurredAt = String(record?.payload?.occurredAt || '').trim();
77
- const source = occurredAt || String(record?.timestamp || '').trim();
78
- const ts = new Date(source).getTime();
79
- return Number.isFinite(ts) ? ts : null;
80
- }
81
-
82
- function eventTimeIso(record) {
83
- const occurredAt = String(record?.payload?.occurredAt || '').trim();
84
- if (occurredAt) return occurredAt;
85
- const fallback = String(record?.timestamp || '').trim();
86
- return fallback || null;
87
- }
88
-
89
- function getTelemetryDir(projectRoot = process.cwd()) {
90
- return path.join(projectRoot, '.bootspring', 'telemetry');
91
- }
92
-
93
- function getTelemetryFile(projectRoot = process.cwd()) {
94
- return path.join(getTelemetryDir(projectRoot), 'events.jsonl');
95
- }
96
-
97
- function ensureTelemetryDir(projectRoot = process.cwd()) {
98
- const dir = getTelemetryDir(projectRoot);
99
- fs.mkdirSync(dir, { recursive: true });
100
- return dir;
101
- }
102
-
103
- function emitEvent(event, payload = {}, options = {}) {
104
- const projectRoot = options.projectRoot || process.cwd();
105
- ensureTelemetryDir(projectRoot);
106
- const file = getTelemetryFile(projectRoot);
107
- const eventTime = options.now ? new Date(options.now) : new Date();
108
- const record = {
109
- timestamp: eventTime.toISOString(),
110
- event: String(event || '').trim(),
111
- payload: redactSensitiveData(payload || {})
112
- };
113
- fs.appendFileSync(file, `${JSON.stringify(record)}\n`, 'utf-8');
114
- return record;
115
- }
116
-
117
- function trackAssistantSetup(assistant, payload = {}, options = {}) {
118
- const normalized = normalizeAssistantId(assistant);
119
- if (!normalized) {
120
- return null;
121
- }
122
-
123
- return emitEvent(ASSISTANT_SETUP_EVENT, {
124
- assistant: normalized,
125
- status: normalizeAssistantStatus(payload.status)
126
- }, options);
127
- }
128
-
129
- function trackAssistantUsageSuccess(assistant, payload = {}, options = {}) {
130
- const normalized = normalizeAssistantId(assistant);
131
- if (!normalized) {
132
- return {
133
- assistant: null,
134
- firstSuccessTracked: false,
135
- returnTracked: false,
136
- daysSinceFirst: null
137
- };
138
- }
139
-
140
- const projectRoot = options.projectRoot || process.cwd();
141
- const now = options.now ? new Date(options.now) : new Date();
142
- const records = getAssistantRecords(normalized, { projectRoot });
143
- const firstSuccess = records.find(record => record.event === ASSISTANT_FIRST_SUCCESS_EVENT);
144
-
145
- if (!firstSuccess) {
146
- emitEvent(ASSISTANT_FIRST_SUCCESS_EVENT, {
147
- assistant: normalized,
148
- source: normalizeAssistantSource(payload.source || 'mcp')
149
- }, { projectRoot, now });
150
- return {
151
- assistant: normalized,
152
- firstSuccessTracked: true,
153
- returnTracked: false,
154
- daysSinceFirst: 0
155
- };
156
- }
157
-
158
- const firstSuccessAt = eventTimeMs(firstSuccess);
159
- const daysSinceFirst = Number.isFinite(firstSuccessAt)
160
- ? Math.floor((now.getTime() - firstSuccessAt) / (24 * 60 * 60 * 1000))
161
- : 0;
162
-
163
- if (daysSinceFirst >= 1) {
164
- const window = returnWindow(daysSinceFirst);
165
- const alreadyTrackedWindow = records.some(record => {
166
- if (record.event !== ASSISTANT_RETURN_EVENT) return false;
167
- return String(record?.payload?.window || '') === window;
168
- });
169
-
170
- if (!alreadyTrackedWindow) {
171
- emitEvent(ASSISTANT_RETURN_EVENT, {
172
- assistant: normalized,
173
- source: normalizeAssistantSource(payload.source || 'mcp'),
174
- window
175
- }, { projectRoot, now });
176
- return {
177
- assistant: normalized,
178
- firstSuccessTracked: false,
179
- returnTracked: true,
180
- daysSinceFirst
181
- };
182
- }
183
- }
184
-
185
- return {
186
- assistant: normalized,
187
- firstSuccessTracked: false,
188
- returnTracked: false,
189
- daysSinceFirst
190
- };
191
- }
192
-
193
- function dayDelta(fromIso, toIso) {
194
- const from = new Date(String(fromIso || '')).getTime();
195
- const to = new Date(String(toIso || '')).getTime();
196
- if (!Number.isFinite(from) || !Number.isFinite(to)) {
197
- return null;
198
- }
199
- return Math.floor((to - from) / (24 * 60 * 60 * 1000));
200
- }
201
-
202
- function summarizeAssistantFunnel(records, assistant) {
203
- const filtered = records.filter(record => normalizeAssistantId(record?.payload?.assistant) === assistant);
204
- const setupEvent = filtered.find(record => record.event === ASSISTANT_SETUP_EVENT);
205
- const firstSuccessEvent = filtered.find(record => record.event === ASSISTANT_FIRST_SUCCESS_EVENT);
206
- const returns = filtered.filter(record => record.event === ASSISTANT_RETURN_EVENT);
207
-
208
- const d1 = firstSuccessEvent
209
- ? returns.some(record => {
210
- const delta = dayDelta(eventTimeIso(firstSuccessEvent), eventTimeIso(record));
211
- return delta !== null && delta >= 1;
212
- })
213
- : false;
214
- const d7 = firstSuccessEvent
215
- ? returns.some(record => {
216
- const delta = dayDelta(eventTimeIso(firstSuccessEvent), eventTimeIso(record));
217
- return delta !== null && delta >= 7;
218
- })
219
- : false;
220
-
221
- return {
222
- setup: !!setupEvent,
223
- firstSuccess: !!firstSuccessEvent,
224
- returned: returns.length > 0,
225
- d1,
226
- d7,
227
- setupAt: eventTimeIso(setupEvent),
228
- firstSuccessAt: eventTimeIso(firstSuccessEvent),
229
- lastReturnAt: returns.length > 0 ? eventTimeIso(returns[returns.length - 1]) : null
230
- };
231
- }
232
-
233
- function getAssistantActivationFunnel(options = {}) {
234
- const projectRoot = options.projectRoot || process.cwd();
235
- const assistantFilter = normalizeAssistantId(options.assistant);
236
- const records = listEvents({
237
- projectRoot,
238
- from: options.from,
239
- to: options.to
240
- });
241
- const assistantIds = assistantFilter ? [assistantFilter] : ASSISTANTS;
242
-
243
- const assistants = {};
244
- for (const assistant of assistantIds) {
245
- assistants[assistant] = summarizeAssistantFunnel(records, assistant);
246
- }
247
-
248
- const totals = Object.values(assistants).reduce((acc, entry) => {
249
- if (entry.setup) acc.setup += 1;
250
- if (entry.firstSuccess) acc.firstSuccess += 1;
251
- if (entry.returned) acc.return += 1;
252
- if (entry.d1) acc.d1 += 1;
253
- if (entry.d7) acc.d7 += 1;
254
- return acc;
255
- }, { setup: 0, firstSuccess: 0, return: 0, d1: 0, d7: 0 });
256
-
257
- return {
258
- generatedAt: new Date().toISOString(),
259
- query: {
260
- assistant: assistantFilter,
261
- from: options.from || null,
262
- to: options.to || null
263
- },
264
- totals,
265
- assistants
266
- };
267
- }
268
-
269
- function safeRate(numerator, denominator) {
270
- if (!denominator) return 0;
271
- return Number((numerator / denominator).toFixed(4));
272
- }
273
-
274
- function normalizeDimension(value, fallback) {
275
- const normalized = String(value || '')
276
- .trim()
277
- .toLowerCase()
278
- .replace(/[^a-z0-9_-]+/g, '-')
279
- .replace(/^-+|-+$/g, '');
280
- return normalized || fallback;
281
- }
282
-
283
- function getUpgradeEventContext(payload = {}) {
284
- const feature = payload.feature || payload.skillId || payload.workflow || payload.agent || 'unknown';
285
- return {
286
- capability: normalizeDimension(payload.capability, 'unknown'),
287
- featureType: normalizeDimension(payload.featureType, 'general'),
288
- feature: normalizeDimension(feature, 'unknown'),
289
- placement: normalizeDimension(payload.placement, 'inline'),
290
- variant: normalizeDimension(payload.variant, 'control')
291
- };
292
- }
293
-
294
- /**
295
- * Summarize upgrade conversion funnel (prompt shown -> upgrade started -> premium unlocked).
296
- * @param {{ projectRoot?: string }} options
297
- * @returns {{ generatedAt: string, totals: { prompted: number, started: number, completed: number, converted: number, startRate: number, conversionRate: number }, capabilities: Record<string, object> }}
298
- */
299
- function getUpgradeConversionFunnel(options = {}) {
300
- const projectRoot = options.projectRoot || process.cwd();
301
- const records = listEvents({ projectRoot });
302
- const capabilities = {};
303
-
304
- for (const record of records) {
305
- if (record.event !== UPGRADE_PROMPT_EVENT
306
- && record.event !== BILLING_UPGRADE_STARTED_EVENT
307
- && record.event !== UPGRADE_COMPLETED_EVENT) {
308
- continue;
309
- }
310
-
311
- const context = getUpgradeEventContext(record.payload);
312
- const key = `${context.capability}:${context.featureType}:${context.feature}`;
313
- if (!capabilities[key]) {
314
- capabilities[key] = {
315
- capability: context.capability,
316
- featureType: context.featureType,
317
- feature: context.feature,
318
- prompted: 0,
319
- started: 0,
320
- completed: 0,
321
- converted: 0,
322
- conversionRate: 0,
323
- placements: {},
324
- variants: {}
325
- };
326
- }
327
-
328
- const bucket = capabilities[key];
329
- if (!bucket) continue;
330
-
331
- if (record.event === UPGRADE_PROMPT_EVENT) {
332
- bucket.prompted += 1;
333
- bucket.placements[context.placement] = (bucket.placements[context.placement] || 0) + 1;
334
- bucket.variants[context.variant] = (bucket.variants[context.variant] || 0) + 1;
335
- continue;
336
- }
337
-
338
- if (record.event === BILLING_UPGRADE_STARTED_EVENT) {
339
- bucket.started += 1;
340
- continue;
341
- }
342
-
343
- bucket.completed += 1;
344
- }
345
-
346
- const totals = Object.values(capabilities).reduce((acc, bucket) => {
347
- bucket.converted = Math.min(bucket.prompted, bucket.completed);
348
- bucket.conversionRate = safeRate(bucket.converted, bucket.prompted);
349
-
350
- acc.prompted += bucket.prompted;
351
- acc.started += bucket.started;
352
- acc.completed += bucket.completed;
353
- acc.converted += bucket.converted;
354
- return acc;
355
- }, {
356
- prompted: 0,
357
- started: 0,
358
- completed: 0,
359
- converted: 0
360
- });
361
-
362
- return {
363
- generatedAt: new Date().toISOString(),
364
- totals: {
365
- ...totals,
366
- startRate: safeRate(totals.started, totals.prompted),
367
- conversionRate: safeRate(totals.converted, totals.prompted)
368
- },
369
- capabilities
370
- };
371
- }
372
-
373
- function parseEventLine(line) {
374
- try {
375
- return JSON.parse(line);
376
- } catch {
377
- return null;
378
- }
379
- }
380
-
381
- function listEvents(options = {}) {
382
- const projectRoot = options.projectRoot || process.cwd();
383
- const file = getTelemetryFile(projectRoot);
384
- if (!fs.existsSync(file)) return [];
385
-
386
- const eventFilter = String(options.event || '').trim();
387
- const from = options.from ? new Date(options.from).getTime() : null;
388
- const to = options.to ? new Date(options.to).getTime() : null;
389
- const limit = Number(options.limit);
390
-
391
- const lines = fs.readFileSync(file, 'utf-8').split('\n').filter(Boolean);
392
- let records = lines
393
- .map(parseEventLine)
394
- .filter(Boolean)
395
- .filter(record => {
396
- if (eventFilter && record.event !== eventFilter) return false;
397
- const ts = new Date(record.timestamp).getTime();
398
- if (Number.isFinite(from) && ts < from) return false;
399
- if (Number.isFinite(to) && ts > to) return false;
400
- return true;
401
- });
402
-
403
- const effectiveLimit = Number.isFinite(limit) && limit > 0
404
- ? Math.min(limit, MAX_EVENTS_LIMIT)
405
- : MAX_EVENTS_LIMIT;
406
- records = records.slice(-effectiveLimit);
407
- return records;
408
- }
409
-
410
- function clearEvents(options = {}) {
411
- const projectRoot = options.projectRoot || process.cwd();
412
- const file = getTelemetryFile(projectRoot);
413
- const records = listEvents({ projectRoot });
414
- if (fs.existsSync(file)) {
415
- fs.writeFileSync(file, '', 'utf-8');
416
- }
417
- return {
418
- cleared: records.length,
419
- file
420
- };
421
- }
422
-
423
- function getStatus(options = {}) {
424
- const projectRoot = options.projectRoot || process.cwd();
425
- const file = getTelemetryFile(projectRoot);
426
- const records = listEvents({ projectRoot });
427
- return {
428
- file,
429
- exists: fs.existsSync(file),
430
- count: records.length,
431
- lastEventAt: records.length > 0 ? records[records.length - 1].timestamp : null
432
- };
433
- }
434
-
435
- function sleep(ms) {
436
- return new Promise(resolve => setTimeout(resolve, ms));
437
- }
438
-
439
- function chunkArray(items, size) {
440
- const chunks = [];
441
- for (let i = 0; i < items.length; i += size) {
442
- chunks.push(items.slice(i, i + size));
443
- }
444
- return chunks;
445
- }
446
-
447
- async function postBatchWithRetry(endpoint, body, headers, options = {}) {
448
- const maxRetries = Number(options.maxRetries);
449
- const retryDelayMs = Number(options.retryDelayMs);
450
- const retries = Number.isFinite(maxRetries) && maxRetries >= 0 ? maxRetries : 2;
451
- const delayBase = Number.isFinite(retryDelayMs) && retryDelayMs > 0 ? retryDelayMs : 300;
452
-
453
- let attempt = 0;
454
- while (true) {
455
- try {
456
- const response = await fetch(endpoint, {
457
- method: 'POST',
458
- headers,
459
- body: JSON.stringify(body)
460
- });
461
- if (!response.ok) {
462
- throw new Error(`HTTP ${response.status}`);
463
- }
464
- return { success: true, attempts: attempt + 1 };
465
- } catch (error) {
466
- if (attempt >= retries) {
467
- return {
468
- success: false,
469
- attempts: attempt + 1,
470
- error: redactErrorMessage(error)
471
- };
472
- }
473
- attempt += 1;
474
- const delay = delayBase * (2 ** (attempt - 1));
475
- await sleep(delay);
476
- }
477
- }
478
- }
479
-
480
- async function uploadEvents(options = {}) {
481
- const projectRoot = options.projectRoot || process.cwd();
482
- const API_BASE = process.env.BOOTSPRING_API_URL || 'https://www.bootspring.com';
483
- const defaultEndpoint = `${API_BASE}/api/v1/events/batch`;
484
- const endpoint = options.endpoint || process.env.BOOTSPRING_TELEMETRY_ENDPOINT || defaultEndpoint;
485
-
486
- const token = options.token || process.env.BOOTSPRING_TELEMETRY_TOKEN;
487
- const event = options.event;
488
- const limit = Number(options.limit) || undefined;
489
- const batchSizeOption = Number(options.batchSize || process.env.BOOTSPRING_TELEMETRY_BATCH_SIZE);
490
- const batchSize = Number.isFinite(batchSizeOption) && batchSizeOption > 0 ? batchSizeOption : 100;
491
- const clearOnSuccess = options.clearOnSuccess === true;
492
- const records = listEvents({ projectRoot, event, limit });
493
-
494
- if (records.length === 0) {
495
- return {
496
- uploaded: 0,
497
- remaining: 0,
498
- endpoint
499
- };
500
- }
501
-
502
- // Try to get API key and project context from auth/session modules
503
- let apiKey = null;
504
- let projectId = null;
505
- try {
506
- const auth = require('./auth');
507
- const session = require('./session');
508
- apiKey = auth.getApiKey();
509
- const project = session.getEffectiveProject();
510
- projectId = project?.id || null;
511
- } catch {
512
- // Modules not available, continue without
513
- }
514
-
515
- const headers = {
516
- 'content-type': 'application/json',
517
- accept: 'application/json',
518
- 'user-agent': `bootspring-cli/${require('../package.json').version}`
519
- };
520
-
521
- // Use API key if available, otherwise fall back to token
522
- if (apiKey) {
523
- headers['x-api-key'] = apiKey;
524
- } else if (token) {
525
- headers.authorization = `Bearer ${token}`;
526
- }
527
-
528
- // Add project context if available
529
- if (projectId) {
530
- headers['x-project-id'] = projectId;
531
- }
532
-
533
- const batches = chunkArray(records, batchSize);
534
- let uploaded = 0;
535
- let totalAttempts = 0;
536
- const failedBatches = [];
537
-
538
- for (let i = 0; i < batches.length; i++) {
539
- const events = batches[i];
540
- const batchId = crypto.createHash('sha1')
541
- .update(JSON.stringify(events))
542
- .digest('hex');
543
- const result = await postBatchWithRetry(
544
- endpoint,
545
- {
546
- source: 'bootspring',
547
- batch: {
548
- index: i,
549
- total: batches.length,
550
- id: batchId
551
- },
552
- events
553
- },
554
- {
555
- ...headers,
556
- 'x-bootspring-batch-id': batchId
557
- },
558
- options
559
- );
560
-
561
- totalAttempts += result.attempts || 1;
562
- if (!result.success) {
563
- failedBatches.push({
564
- index: i,
565
- count: events.length,
566
- error: redactErrorMessage(result.error || 'upload_failed')
567
- });
568
- continue;
569
- }
570
- uploaded += events.length;
571
- }
572
-
573
- if (failedBatches.length > 0) {
574
- throw new Error(`Telemetry upload failed for ${failedBatches.length}/${batches.length} batches`);
575
- }
576
-
577
- if (clearOnSuccess) {
578
- clearEvents({ projectRoot });
579
- }
580
-
581
- const remaining = clearOnSuccess ? 0 : listEvents({ projectRoot }).length;
582
- return {
583
- uploaded,
584
- attempted: records.length,
585
- batches: batches.length,
586
- attempts: totalAttempts,
587
- remaining,
588
- endpoint,
589
- failedBatches
590
- };
591
- }
592
-
593
- module.exports = {
594
- MAX_EVENTS_LIMIT,
595
- ASSISTANTS,
596
- ASSISTANT_SETUP_EVENT,
597
- ASSISTANT_FIRST_SUCCESS_EVENT,
598
- ASSISTANT_RETURN_EVENT,
599
- BILLING_UPGRADE_STARTED_EVENT,
600
- UPGRADE_PROMPT_EVENT,
601
- UPGRADE_COMPLETED_EVENT,
602
- getTelemetryDir,
603
- getTelemetryFile,
604
- ensureTelemetryDir,
605
- emitEvent,
606
- track: emitEvent, // Alias for compatibility
607
- normalizeAssistantId,
608
- inferAssistantFromEnvironment,
609
- trackAssistantSetup,
610
- trackAssistantUsageSuccess,
611
- getAssistantActivationFunnel,
612
- getUpgradeConversionFunnel,
613
- listEvents,
614
- clearEvents,
615
- getStatus,
616
- uploadEvents
617
- };