@rulecatch/ai-pooler 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js ADDED
@@ -0,0 +1,1240 @@
1
+ import { createDecipheriv, pbkdf2Sync, randomBytes, createCipheriv, createHash } from 'crypto';
2
+ import { execSync } from 'child_process';
3
+
4
+ /**
5
+ * Rulecatch AI - MCP Development Analytics Types
6
+ *
7
+ * Event types and interfaces for tracking AI-assisted development metrics.
8
+ */
9
+ /**
10
+ * Pricing table for cost calculation
11
+ * Source: Official pricing pages as of January 2026
12
+ */
13
+ const MODEL_PRICING = {
14
+ // Claude 4 series (January 2026 pricing)
15
+ 'claude-opus-4-5': {
16
+ inputPer1M: 15.0,
17
+ outputPer1M: 75.0,
18
+ cacheReadPer1M: 1.5,
19
+ cacheWritePer1M: 18.75,
20
+ thinkingPer1M: 75.0,
21
+ },
22
+ 'claude-sonnet-4': {
23
+ inputPer1M: 3.0,
24
+ outputPer1M: 15.0,
25
+ cacheReadPer1M: 0.3,
26
+ cacheWritePer1M: 3.75,
27
+ thinkingPer1M: 15.0,
28
+ },
29
+ // Claude 3.5 series
30
+ 'claude-3.5-sonnet': {
31
+ inputPer1M: 3.0,
32
+ outputPer1M: 15.0,
33
+ cacheReadPer1M: 0.3,
34
+ cacheWritePer1M: 3.75,
35
+ },
36
+ 'claude-3.5-haiku': {
37
+ inputPer1M: 0.8,
38
+ outputPer1M: 4.0,
39
+ cacheReadPer1M: 0.08,
40
+ cacheWritePer1M: 1.0,
41
+ },
42
+ // Claude 3 series
43
+ 'claude-3-opus': {
44
+ inputPer1M: 15.0,
45
+ outputPer1M: 75.0,
46
+ cacheReadPer1M: 1.5,
47
+ cacheWritePer1M: 18.75,
48
+ },
49
+ 'claude-3-sonnet': { inputPer1M: 3.0, outputPer1M: 15.0 },
50
+ 'claude-3-haiku': { inputPer1M: 0.25, outputPer1M: 1.25 },
51
+ // GPT series
52
+ 'gpt-4': { inputPer1M: 30.0, outputPer1M: 60.0 },
53
+ 'gpt-4-turbo': { inputPer1M: 10.0, outputPer1M: 30.0 },
54
+ 'gpt-4o': { inputPer1M: 2.5, outputPer1M: 10.0 },
55
+ 'gpt-4o-mini': { inputPer1M: 0.15, outputPer1M: 0.6 },
56
+ o1: { inputPer1M: 15.0, outputPer1M: 60.0 },
57
+ 'o1-mini': { inputPer1M: 3.0, outputPer1M: 12.0 },
58
+ 'o1-pro': { inputPer1M: 150.0, outputPer1M: 600.0 },
59
+ // Gemini
60
+ 'gemini-2.0-flash': { inputPer1M: 0.1, outputPer1M: 0.4 },
61
+ 'gemini-1.5-pro': { inputPer1M: 1.25, outputPer1M: 5.0 },
62
+ 'gemini-1.5-flash': { inputPer1M: 0.075, outputPer1M: 0.3 },
63
+ // Fallback
64
+ unknown: { inputPer1M: 5.0, outputPer1M: 15.0 },
65
+ };
66
+ /**
67
+ * Calculate cost from token usage with caching support
68
+ */
69
+ function calculateCost(model, inputTokens, outputTokens, options) {
70
+ const pricing = MODEL_PRICING[model] || MODEL_PRICING.unknown;
71
+ // Base input/output costs
72
+ let inputCost = (inputTokens / 1_000_000) * pricing.inputPer1M;
73
+ const outputCost = (outputTokens / 1_000_000) * pricing.outputPer1M;
74
+ let totalCost = inputCost + outputCost;
75
+ // Cache costs (reduces input cost)
76
+ if (options?.cacheReadTokens && pricing.cacheReadPer1M) {
77
+ const cacheReadCost = (options.cacheReadTokens / 1_000_000) * pricing.cacheReadPer1M;
78
+ // Cache reads replace regular input cost for those tokens
79
+ inputCost -= (options.cacheReadTokens / 1_000_000) * pricing.inputPer1M;
80
+ totalCost = inputCost + outputCost + cacheReadCost;
81
+ }
82
+ if (options?.cacheWriteTokens && pricing.cacheWritePer1M) {
83
+ const cacheWriteCost = (options.cacheWriteTokens / 1_000_000) * pricing.cacheWritePer1M;
84
+ totalCost += cacheWriteCost;
85
+ }
86
+ // Thinking tokens (additional cost)
87
+ if (options?.thinkingTokens && pricing.thinkingPer1M) {
88
+ const thinkingCost = (options.thinkingTokens / 1_000_000) * pricing.thinkingPer1M;
89
+ totalCost += thinkingCost;
90
+ }
91
+ return Math.round(totalCost * 10000) / 10000; // 4 decimal places
92
+ }
93
+ /**
94
+ * Estimate tokens from character count
95
+ * Rough approximation: ~4 chars per token for English
96
+ */
97
+ function estimateTokens(charCount) {
98
+ return Math.ceil(charCount / 4);
99
+ }
100
+ /**
101
+ * Format duration in human-readable form
102
+ */
103
+ function formatDuration(seconds) {
104
+ const hours = Math.floor(seconds / 3600);
105
+ const minutes = Math.floor((seconds % 3600) / 60);
106
+ const secs = Math.floor(seconds % 60);
107
+ if (hours > 0) {
108
+ return `${hours}h ${minutes}m ${secs}s`;
109
+ }
110
+ if (minutes > 0) {
111
+ return `${minutes}m ${secs}s`;
112
+ }
113
+ return `${secs}s`;
114
+ }
115
+ /**
116
+ * Format cost in USD
117
+ */
118
+ function formatCost(cost) {
119
+ if (cost < 0.01) {
120
+ return `$${cost.toFixed(4)}`;
121
+ }
122
+ if (cost < 1) {
123
+ return `$${cost.toFixed(3)}`;
124
+ }
125
+ return `$${cost.toFixed(2)}`;
126
+ }
127
+
128
+ /**
129
+ * Client-side encryption for PII (Zero-Knowledge Architecture)
130
+ *
131
+ * All PII (emails, usernames, file paths) is encrypted on the client
132
+ * before being sent to Rulecatch API. We never see plaintext PII.
133
+ *
134
+ * The user's privacy key is derived from their password or a separate
135
+ * key they set during setup. Without this key, the data is unreadable.
136
+ */
137
+ const ALGORITHM = 'aes-256-gcm';
138
+ const KEY_LENGTH = 32;
139
+ const IV_LENGTH = 16;
140
+ const PBKDF2_ITERATIONS = 100000;
141
+ /**
142
+ * Derive an encryption key from a password/passphrase
143
+ * Uses PBKDF2 with 100k iterations for brute-force resistance
144
+ */
145
+ function deriveKey(password, salt) {
146
+ return pbkdf2Sync(password, salt, PBKDF2_ITERATIONS, KEY_LENGTH, 'sha256');
147
+ }
148
+ /**
149
+ * Generate a random salt for new users
150
+ */
151
+ function generateSalt() {
152
+ return randomBytes(32).toString('base64');
153
+ }
154
+ /**
155
+ * Encrypt a PII field (email, username, file path, etc.)
156
+ * Returns ciphertext + IV + auth tag for storage
157
+ */
158
+ function encryptPII(plaintext, key) {
159
+ if (!plaintext) {
160
+ return { ciphertext: '', iv: '', tag: '' };
161
+ }
162
+ const iv = randomBytes(IV_LENGTH);
163
+ const cipher = createCipheriv(ALGORITHM, key, iv);
164
+ const encrypted = Buffer.concat([
165
+ cipher.update(plaintext, 'utf8'),
166
+ cipher.final(),
167
+ ]);
168
+ const tag = cipher.getAuthTag();
169
+ return {
170
+ ciphertext: encrypted.toString('base64'),
171
+ iv: iv.toString('base64'),
172
+ tag: tag.toString('base64'),
173
+ };
174
+ }
175
+ /**
176
+ * Decrypt a PII field
177
+ * Used client-side in the dashboard to show actual values
178
+ */
179
+ function decryptPII(encrypted, key) {
180
+ if (!encrypted.ciphertext) {
181
+ return '';
182
+ }
183
+ const decipher = createDecipheriv(ALGORITHM, key, Buffer.from(encrypted.iv, 'base64'));
184
+ decipher.setAuthTag(Buffer.from(encrypted.tag, 'base64'));
185
+ const decrypted = Buffer.concat([
186
+ decipher.update(Buffer.from(encrypted.ciphertext, 'base64')),
187
+ decipher.final(),
188
+ ]);
189
+ return decrypted.toString('utf8');
190
+ }
191
+ /**
192
+ * Create a one-way hash for indexing/grouping
193
+ * Cannot be reversed, but same input = same hash (for deduplication)
194
+ *
195
+ * We use a truncated hash (16 chars) to prevent rainbow table attacks
196
+ * while still allowing grouping by the same identifier.
197
+ */
198
+ function hashForIndex(plaintext, salt) {
199
+ if (!plaintext) {
200
+ return '';
201
+ }
202
+ return createHash('sha256')
203
+ .update(plaintext + salt)
204
+ .digest('hex')
205
+ .slice(0, 16);
206
+ }
207
+ /**
208
+ * Encrypt all PII fields in an event payload
209
+ * Non-PII fields are passed through unchanged
210
+ */
211
+ function encryptEventPII(event, key, salt) {
212
+ const piiFields = [
213
+ 'accountEmail',
214
+ 'gitEmail',
215
+ 'gitUsername',
216
+ 'filePath',
217
+ 'cwd',
218
+ 'projectId',
219
+ ];
220
+ const arrayPiiFields = ['filesModified'];
221
+ const result = { ...event };
222
+ // Encrypt single PII fields
223
+ for (const field of piiFields) {
224
+ const value = event[field];
225
+ if (typeof value === 'string' && value) {
226
+ const encrypted = encryptPII(value, key);
227
+ result[`${field}_encrypted`] = encrypted.ciphertext;
228
+ result[`${field}_iv`] = encrypted.iv;
229
+ result[`${field}_tag`] = encrypted.tag;
230
+ result[`${field}_hash`] = hashForIndex(value, salt);
231
+ delete result[field]; // Remove plaintext
232
+ }
233
+ }
234
+ // Encrypt array PII fields
235
+ for (const field of arrayPiiFields) {
236
+ const value = event[field];
237
+ if (Array.isArray(value)) {
238
+ const encryptedArray = value.map((item) => {
239
+ if (typeof item === 'string') {
240
+ const encrypted = encryptPII(item, key);
241
+ return {
242
+ encrypted: encrypted.ciphertext,
243
+ iv: encrypted.iv,
244
+ tag: encrypted.tag,
245
+ hash: hashForIndex(item, salt),
246
+ };
247
+ }
248
+ return item;
249
+ });
250
+ result[`${field}_encrypted`] = encryptedArray;
251
+ result[`${field}_hashes`] = value.map((item) => typeof item === 'string' ? hashForIndex(item, salt) : '');
252
+ delete result[field]; // Remove plaintext
253
+ }
254
+ }
255
+ return result;
256
+ }
257
+ /**
258
+ * Decrypt all PII fields in an event payload
259
+ * Used in the dashboard to display actual values
260
+ */
261
+ function decryptEventPII(event, key) {
262
+ const piiFields = [
263
+ 'accountEmail',
264
+ 'gitEmail',
265
+ 'gitUsername',
266
+ 'filePath',
267
+ 'cwd',
268
+ 'projectId',
269
+ ];
270
+ const arrayPiiFields = ['filesModified'];
271
+ const result = { ...event };
272
+ // Decrypt single PII fields
273
+ for (const field of piiFields) {
274
+ const ciphertext = event[`${field}_encrypted`];
275
+ const iv = event[`${field}_iv`];
276
+ const tag = event[`${field}_tag`];
277
+ if (typeof ciphertext === 'string' &&
278
+ typeof iv === 'string' &&
279
+ typeof tag === 'string') {
280
+ try {
281
+ result[field] = decryptPII({ ciphertext, iv, tag }, key);
282
+ }
283
+ catch {
284
+ result[field] = '[decryption failed]';
285
+ }
286
+ // Clean up encrypted fields
287
+ delete result[`${field}_encrypted`];
288
+ delete result[`${field}_iv`];
289
+ delete result[`${field}_tag`];
290
+ delete result[`${field}_hash`];
291
+ }
292
+ }
293
+ // Decrypt array PII fields
294
+ for (const field of arrayPiiFields) {
295
+ const encryptedArray = event[`${field}_encrypted`];
296
+ if (Array.isArray(encryptedArray)) {
297
+ result[field] = encryptedArray.map((item) => {
298
+ if (item &&
299
+ typeof item === 'object' &&
300
+ 'encrypted' in item &&
301
+ 'iv' in item &&
302
+ 'tag' in item) {
303
+ try {
304
+ return decryptPII({
305
+ ciphertext: item.encrypted,
306
+ iv: item.iv,
307
+ tag: item.tag,
308
+ }, key);
309
+ }
310
+ catch {
311
+ return '[decryption failed]';
312
+ }
313
+ }
314
+ return item;
315
+ });
316
+ delete result[`${field}_encrypted`];
317
+ delete result[`${field}_hashes`];
318
+ }
319
+ }
320
+ return result;
321
+ }
322
+ /**
323
+ * Verify a privacy key is correct by attempting to decrypt a test value
324
+ */
325
+ function verifyPrivacyKey(testCiphertext, expectedPlaintext, key) {
326
+ try {
327
+ const decrypted = decryptPII(testCiphertext, key);
328
+ return decrypted === expectedPlaintext;
329
+ }
330
+ catch {
331
+ return false;
332
+ }
333
+ }
334
+
335
+ /**
336
+ * Git Context Collection
337
+ *
338
+ * Automatically collects git information from the current repository.
339
+ * This provides context about the development environment to track
340
+ * which project/branch/commit is being worked on.
341
+ */
342
+ /**
343
+ * Execute a git command and return the output
344
+ */
345
+ function execGit(args, cwd) {
346
+ try {
347
+ const result = execSync(`git ${args}`, {
348
+ encoding: 'utf-8',
349
+ cwd: cwd || process.cwd(),
350
+ stdio: ['pipe', 'pipe', 'pipe'],
351
+ });
352
+ return result.trim();
353
+ }
354
+ catch {
355
+ return null;
356
+ }
357
+ }
358
+ /**
359
+ * Check if we're in a git repository
360
+ */
361
+ function isGitRepo(cwd) {
362
+ return execGit('rev-parse --is-inside-work-tree', cwd) === 'true';
363
+ }
364
+ /**
365
+ * Get the root directory of the git repository
366
+ */
367
+ function getGitRoot(cwd) {
368
+ return execGit('rev-parse --show-toplevel', cwd);
369
+ }
370
+ /**
371
+ * Collect git context from the current directory
372
+ */
373
+ function collectGitContext(cwd) {
374
+ const context = {};
375
+ if (!isGitRepo(cwd)) {
376
+ return context;
377
+ }
378
+ // Root directory
379
+ context.rootDir = getGitRoot(cwd) || undefined;
380
+ // User info
381
+ context.username = execGit('config user.name', cwd) || undefined;
382
+ context.email = execGit('config user.email', cwd) || undefined;
383
+ // Repository URL
384
+ const remoteUrl = execGit('config --get remote.origin.url', cwd);
385
+ if (remoteUrl) {
386
+ context.repoUrl = remoteUrl;
387
+ context.repoName = extractRepoName(remoteUrl);
388
+ }
389
+ // Branch
390
+ context.branch = execGit('rev-parse --abbrev-ref HEAD', cwd) || undefined;
391
+ // Commit info
392
+ context.commit = execGit('rev-parse --short HEAD', cwd) || undefined;
393
+ context.commitFull = execGit('rev-parse HEAD', cwd) || undefined;
394
+ context.commitMessage = execGit('log -1 --format=%s', cwd) || undefined;
395
+ context.commitAuthor = execGit('log -1 --format=%an', cwd) || undefined;
396
+ context.commitTimestamp = execGit('log -1 --format=%cI', cwd) || undefined;
397
+ // Dirty state
398
+ const status = execGit('status --porcelain', cwd);
399
+ if (status !== null) {
400
+ const files = status.split('\n').filter(line => line.trim().length > 0);
401
+ context.isDirty = files.length > 0;
402
+ context.uncommittedFiles = files.length;
403
+ }
404
+ return context;
405
+ }
406
+ /**
407
+ * Extract repository name from git URL
408
+ */
409
+ function extractRepoName(url) {
410
+ // Handle SSH URLs: git@github.com:user/repo.git
411
+ const sshMatch = url.match(/[:/]([^/]+\/[^/]+?)(?:\.git)?$/);
412
+ if (sshMatch) {
413
+ return sshMatch[1];
414
+ }
415
+ // Handle HTTPS URLs: https://github.com/user/repo.git
416
+ const httpsMatch = url.match(/\/([^/]+\/[^/]+?)(?:\.git)?$/);
417
+ if (httpsMatch) {
418
+ return httpsMatch[1];
419
+ }
420
+ return undefined;
421
+ }
422
+ /**
423
+ * Watch for git changes (branch switches, commits, etc.)
424
+ * Returns a cleanup function to stop watching
425
+ */
426
+ function watchGitChanges(callback, options) {
427
+ const cwd = options?.cwd;
428
+ const pollInterval = options?.pollInterval || 5000;
429
+ let lastCommit;
430
+ let lastBranch;
431
+ const check = () => {
432
+ if (!isGitRepo(cwd))
433
+ return;
434
+ const currentCommit = execGit('rev-parse HEAD', cwd) || undefined;
435
+ const currentBranch = execGit('rev-parse --abbrev-ref HEAD', cwd) || undefined;
436
+ // Only trigger callback if something changed
437
+ if (currentCommit !== lastCommit || currentBranch !== lastBranch) {
438
+ lastCommit = currentCommit;
439
+ lastBranch = currentBranch;
440
+ callback(collectGitContext(cwd));
441
+ }
442
+ };
443
+ // Initial check
444
+ check();
445
+ // Poll for changes
446
+ const intervalId = setInterval(check, pollInterval);
447
+ // Return cleanup function
448
+ return () => {
449
+ clearInterval(intervalId);
450
+ };
451
+ }
452
+ /**
453
+ * Get a summary string for the current git context
454
+ */
455
+ function getGitSummary(context) {
456
+ const parts = [];
457
+ if (context.repoName) {
458
+ parts.push(context.repoName);
459
+ }
460
+ if (context.branch) {
461
+ parts.push(`@${context.branch}`);
462
+ }
463
+ if (context.commit) {
464
+ parts.push(`(${context.commit}${context.isDirty ? '*' : ''})`);
465
+ }
466
+ return parts.join(' ') || 'unknown';
467
+ }
468
+
469
+ /**
470
+ * @rulecatch/ai-pooler - AI Development Analytics Pooler
471
+ *
472
+ * Production-grade tracking for AI-assisted development including:
473
+ * - Token usage with prompt caching and thinking tokens
474
+ * - Active time tracking (coding vs idle)
475
+ * - Per-file and per-language cost breakdown
476
+ * - Conversation turns and user interventions
477
+ * - Response latency metrics
478
+ */
479
+ /**
480
+ * Event buffer for batching
481
+ */
482
+ const eventBuffer = [];
483
+ let flushTimeout = null;
484
+ /**
485
+ * Current git context
486
+ */
487
+ let gitContext = {};
488
+ let stopGitWatcher = null;
489
+ /**
490
+ * Current session state with enhanced tracking
491
+ */
492
+ let currentSession = null;
493
+ /**
494
+ * Configuration
495
+ */
496
+ let config = null;
497
+ /**
498
+ * Initialize the Rulecatch AI MCP server
499
+ */
500
+ function init(options) {
501
+ config = {
502
+ endpoint: 'https://api.rulecatch.ai/ai/ingest',
503
+ batchSize: 10,
504
+ flushInterval: 30000,
505
+ includeFilePaths: true,
506
+ trackActiveTime: true,
507
+ trackGitContext: true,
508
+ idleTimeout: 5 * 60 * 1000, // 5 minutes
509
+ debug: false,
510
+ ...options,
511
+ };
512
+ // Collect initial git context
513
+ if (config.trackGitContext !== false) {
514
+ gitContext = collectGitContext(config.cwd);
515
+ if (config.debug) {
516
+ console.log('[rulecatch-ai] Git context:', getGitSummary(gitContext));
517
+ }
518
+ // Watch for git changes (branch switches, commits)
519
+ stopGitWatcher = watchGitChanges((newContext) => {
520
+ gitContext = newContext;
521
+ if (config?.debug) {
522
+ console.log('[rulecatch-ai] Git context updated:', getGitSummary(newContext));
523
+ }
524
+ }, { cwd: config.cwd, pollInterval: 10000 });
525
+ }
526
+ if (config.debug) {
527
+ console.log('[rulecatch-ai] Initialized with config:', {
528
+ projectId: config.projectId,
529
+ endpoint: config.endpoint,
530
+ git: getGitSummary(gitContext),
531
+ });
532
+ }
533
+ // Start session automatically
534
+ startSession();
535
+ }
536
+ /**
537
+ * Start a new development session
538
+ */
539
+ function startSession() {
540
+ const sessionId = generateSessionId();
541
+ const now = new Date().toISOString();
542
+ currentSession = {
543
+ id: sessionId,
544
+ startTime: now,
545
+ events: [],
546
+ turnNumber: 0,
547
+ lastActivityTime: Date.now(),
548
+ activeTimeMs: 0,
549
+ fileCosts: new Map(),
550
+ languageCosts: new Map(),
551
+ fileChanges: new Map(),
552
+ toolCalls: new Map(),
553
+ responseTimes: [],
554
+ timeToFirstTokens: [],
555
+ contextUsages: [],
556
+ ruleViolations: new Map(),
557
+ violationsByCategory: new Map(),
558
+ violationsBySeverity: new Map(),
559
+ violationsBySource: new Map(),
560
+ correctionsCount: 0,
561
+ };
562
+ track({
563
+ type: 'session_start',
564
+ timestamp: now,
565
+ sessionId,
566
+ projectId: config?.projectId || 'unknown',
567
+ });
568
+ return sessionId;
569
+ }
570
+ /**
571
+ * End the current session
572
+ */
573
+ function endSession() {
574
+ if (!currentSession)
575
+ return null;
576
+ const now = new Date().toISOString();
577
+ // Update active time for final segment
578
+ updateActiveTime();
579
+ track({
580
+ type: 'session_end',
581
+ timestamp: now,
582
+ sessionId: currentSession.id,
583
+ projectId: config?.projectId || 'unknown',
584
+ });
585
+ const metrics = calculateSessionMetrics(currentSession.events);
586
+ flush(); // Ensure all events are sent
587
+ // Stop git watcher
588
+ if (stopGitWatcher) {
589
+ stopGitWatcher();
590
+ stopGitWatcher = null;
591
+ }
592
+ currentSession = null;
593
+ return metrics;
594
+ }
595
+ /**
596
+ * Update active time tracking
597
+ */
598
+ function updateActiveTime() {
599
+ if (!currentSession || !config?.trackActiveTime)
600
+ return;
601
+ const now = Date.now();
602
+ const idleTimeout = config.idleTimeout || 5 * 60 * 1000;
603
+ const timeSinceLastActivity = now - currentSession.lastActivityTime;
604
+ if (timeSinceLastActivity < idleTimeout) {
605
+ currentSession.activeTimeMs += timeSinceLastActivity;
606
+ }
607
+ currentSession.lastActivityTime = now;
608
+ }
609
+ /**
610
+ * Track an AI request/response with full metrics
611
+ */
612
+ function trackAIRequest(params) {
613
+ updateActiveTime();
614
+ const cost = calculateCost(params.model, params.inputTokens, params.outputTokens, {
615
+ cacheReadTokens: params.cacheReadTokens,
616
+ cacheWriteTokens: params.cacheWriteTokens,
617
+ thinkingTokens: params.thinkingTokens,
618
+ });
619
+ // Track per-file costs
620
+ if (currentSession && params.filesContext) {
621
+ const costPerFile = cost / params.filesContext.length;
622
+ for (const file of params.filesContext) {
623
+ const current = currentSession.fileCosts.get(file) || 0;
624
+ currentSession.fileCosts.set(file, current + costPerFile);
625
+ }
626
+ }
627
+ // Track per-language costs
628
+ if (currentSession && params.language) {
629
+ const current = currentSession.languageCosts.get(params.language) || 0;
630
+ currentSession.languageCosts.set(params.language, current + cost);
631
+ }
632
+ // Track response times
633
+ if (currentSession) {
634
+ if (params.totalResponseTime) {
635
+ currentSession.responseTimes.push(params.totalResponseTime);
636
+ }
637
+ if (params.timeToFirstToken) {
638
+ currentSession.timeToFirstTokens.push(params.timeToFirstToken);
639
+ }
640
+ if (params.contextUsage !== undefined) {
641
+ currentSession.contextUsages.push(params.contextUsage);
642
+ }
643
+ }
644
+ track({
645
+ type: 'ai_request',
646
+ timestamp: new Date().toISOString(),
647
+ sessionId: currentSession?.id || 'unknown',
648
+ projectId: config?.projectId || 'unknown',
649
+ model: params.model,
650
+ inputTokens: params.inputTokens,
651
+ outputTokens: params.outputTokens,
652
+ cacheReadTokens: params.cacheReadTokens,
653
+ cacheWriteTokens: params.cacheWriteTokens,
654
+ thinkingTokens: params.thinkingTokens,
655
+ promptLength: params.promptLength,
656
+ responseLength: params.responseLength,
657
+ contextUsage: params.contextUsage,
658
+ timeToFirstToken: params.timeToFirstToken,
659
+ totalResponseTime: params.totalResponseTime,
660
+ estimatedCost: cost,
661
+ promptCached: (params.cacheReadTokens || 0) > 0,
662
+ filesModified: params.filesContext,
663
+ language: params.language,
664
+ taskContext: params.taskContext,
665
+ metadata: params.metadata,
666
+ });
667
+ }
668
+ /**
669
+ * Track a conversation turn (user message + AI response cycle)
670
+ */
671
+ function trackConversationTurn(params) {
672
+ updateActiveTime();
673
+ if (currentSession) {
674
+ currentSession.turnNumber++;
675
+ }
676
+ track({
677
+ type: 'conversation_turn',
678
+ timestamp: new Date().toISOString(),
679
+ sessionId: currentSession?.id || 'unknown',
680
+ projectId: config?.projectId || 'unknown',
681
+ turnNumber: currentSession?.turnNumber || 1,
682
+ userIntervention: params.userIntervention,
683
+ taskContext: params.taskContext,
684
+ metadata: params.metadata,
685
+ });
686
+ }
687
+ /**
688
+ * Track a tool call with enhanced metrics
689
+ */
690
+ function trackToolCall(params) {
691
+ updateActiveTime();
692
+ // Track tool success/failure by name
693
+ if (currentSession) {
694
+ const stats = currentSession.toolCalls.get(params.toolName) || { success: 0, fail: 0 };
695
+ if (params.success) {
696
+ stats.success++;
697
+ }
698
+ else {
699
+ stats.fail++;
700
+ }
701
+ currentSession.toolCalls.set(params.toolName, stats);
702
+ // Track file changes
703
+ if (params.filesModified) {
704
+ for (const file of params.filesModified) {
705
+ const changes = (params.linesAdded || 0) + (params.linesRemoved || 0);
706
+ const current = currentSession.fileChanges.get(file) || 0;
707
+ currentSession.fileChanges.set(file, current + changes);
708
+ }
709
+ }
710
+ }
711
+ track({
712
+ type: 'tool_call',
713
+ timestamp: new Date().toISOString(),
714
+ sessionId: currentSession?.id || 'unknown',
715
+ projectId: config?.projectId || 'unknown',
716
+ toolName: params.toolName,
717
+ toolSuccess: params.success,
718
+ toolDuration: params.duration,
719
+ toolInputSize: params.inputSize,
720
+ toolOutputSize: params.outputSize,
721
+ linesAdded: params.linesAdded,
722
+ linesRemoved: params.linesRemoved,
723
+ filesModified: config?.includeFilePaths ? params.filesModified : undefined,
724
+ language: params.language,
725
+ fileOperation: params.fileOperation,
726
+ retryAttempt: params.retryAttempt,
727
+ metadata: params.metadata,
728
+ });
729
+ }
730
+ /**
731
+ * Track file operation (read/write/edit)
732
+ */
733
+ function trackFileOperation(params) {
734
+ updateActiveTime();
735
+ if (currentSession) {
736
+ const changes = (params.linesAdded || 0) + (params.linesRemoved || 0);
737
+ const current = currentSession.fileChanges.get(params.file) || 0;
738
+ currentSession.fileChanges.set(params.file, current + changes);
739
+ }
740
+ track({
741
+ type: 'file_operation',
742
+ timestamp: new Date().toISOString(),
743
+ sessionId: currentSession?.id || 'unknown',
744
+ projectId: config?.projectId || 'unknown',
745
+ fileOperation: params.operation,
746
+ filesModified: [params.file],
747
+ linesAdded: params.linesAdded,
748
+ linesRemoved: params.linesRemoved,
749
+ language: params.language,
750
+ toolDuration: params.duration,
751
+ });
752
+ }
753
+ /**
754
+ * Track code acceptance/rejection with context
755
+ */
756
+ function trackCodeDecision(params) {
757
+ updateActiveTime();
758
+ track({
759
+ type: params.accepted ? 'code_accept' : 'code_reject',
760
+ timestamp: new Date().toISOString(),
761
+ sessionId: currentSession?.id || 'unknown',
762
+ projectId: config?.projectId || 'unknown',
763
+ linesAdded: params.linesAdded,
764
+ linesRemoved: params.linesRemoved,
765
+ filesModified: config?.includeFilePaths ? params.filesModified : undefined,
766
+ language: params.language,
767
+ userIntervention: params.userIntervention,
768
+ taskContext: params.taskContext,
769
+ });
770
+ }
771
+ /**
772
+ * Track an error event
773
+ */
774
+ function trackError(params) {
775
+ track({
776
+ type: 'error',
777
+ timestamp: new Date().toISOString(),
778
+ sessionId: currentSession?.id || 'unknown',
779
+ projectId: config?.projectId || 'unknown',
780
+ errorMessage: params.message,
781
+ errorRecovered: params.recovered,
782
+ retryAttempt: params.retryAttempt,
783
+ toolName: params.toolName,
784
+ metadata: params.metadata,
785
+ });
786
+ }
787
+ /**
788
+ * Track a rule violation (coding standard violation, pattern anti-pattern, etc.)
789
+ *
790
+ * Use this to track when AI-generated code violates established
791
+ * project rules like those in CLAUDE.md, eslint configs, or architectural patterns.
792
+ *
793
+ * @example
794
+ * trackRuleDeviation({
795
+ * ruleName: 'use-bulkwrite',
796
+ * ruleSource: 'CLAUDE.md',
797
+ * category: 'db_pattern',
798
+ * severity: 'warning',
799
+ * description: 'MongoDB writes should use bulkWrite instead of individual operations',
800
+ * violatingCode: 'await collection.updateOne(...)',
801
+ * suggestedFix: 'Use collection.bulkWrite([{ updateOne: {...} }])',
802
+ * file: 'src/api/ingest.ts',
803
+ * line: 238,
804
+ * corrected: true,
805
+ * confidence: 0.95,
806
+ * });
807
+ */
808
+ function trackRuleDeviation(params) {
809
+ const category = params.category || 'custom';
810
+ const severity = params.severity || 'warning';
811
+ const source = params.ruleSource || 'unknown';
812
+ // Update session tracking
813
+ if (currentSession) {
814
+ // Track by rule name
815
+ const existing = currentSession.ruleViolations.get(params.ruleName);
816
+ if (existing) {
817
+ existing.count++;
818
+ }
819
+ else {
820
+ currentSession.ruleViolations.set(params.ruleName, { count: 1, severity });
821
+ }
822
+ // Track by category
823
+ const categoryCount = currentSession.violationsByCategory.get(category) || 0;
824
+ currentSession.violationsByCategory.set(category, categoryCount + 1);
825
+ // Track by severity
826
+ const severityCount = currentSession.violationsBySeverity.get(severity) || 0;
827
+ currentSession.violationsBySeverity.set(severity, severityCount + 1);
828
+ // Track by source
829
+ const sourceCount = currentSession.violationsBySource.get(source) || 0;
830
+ currentSession.violationsBySource.set(source, sourceCount + 1);
831
+ // Track corrections
832
+ if (params.corrected) {
833
+ currentSession.correctionsCount++;
834
+ }
835
+ }
836
+ track({
837
+ type: 'rule_deviation',
838
+ timestamp: new Date().toISOString(),
839
+ sessionId: currentSession?.id || 'unknown',
840
+ projectId: config?.projectId || 'unknown',
841
+ ruleName: params.ruleName,
842
+ ruleSource: source,
843
+ ruleCategory: category,
844
+ ruleSeverity: severity,
845
+ ruleDescription: params.description,
846
+ violatingCode: params.violatingCode,
847
+ suggestedFix: params.suggestedFix,
848
+ violationFile: params.file,
849
+ violationLine: params.line,
850
+ corrected: params.corrected,
851
+ violationConfidence: params.confidence,
852
+ metadata: params.metadata,
853
+ });
854
+ }
855
+ /**
856
+ * Track a generic event
857
+ */
858
+ function track(event) {
859
+ // Add version info from config (customer-defined via env vars)
860
+ if (config?.version) {
861
+ event.version = config.version;
862
+ }
863
+ if (config?.commit) {
864
+ event.configCommit = config.commit;
865
+ }
866
+ // Automatically add git context to all events
867
+ if (config?.trackGitContext !== false && gitContext) {
868
+ event.gitUsername = gitContext.username;
869
+ event.gitEmail = gitContext.email;
870
+ event.gitRepo = gitContext.repoName;
871
+ event.gitBranch = gitContext.branch;
872
+ event.gitCommit = gitContext.commit;
873
+ event.gitDirty = gitContext.isDirty;
874
+ }
875
+ if (currentSession) {
876
+ currentSession.events.push(event);
877
+ }
878
+ eventBuffer.push(event);
879
+ if (config?.debug) {
880
+ console.log('[rulecatch-ai] Event:', event.type, event);
881
+ }
882
+ // Check if we should flush
883
+ if (eventBuffer.length >= (config?.batchSize || 10)) {
884
+ flush();
885
+ }
886
+ else if (!flushTimeout && config?.flushInterval) {
887
+ flushTimeout = setTimeout(flush, config.flushInterval);
888
+ }
889
+ }
890
+ /**
891
+ * Flush events to Rulecatch API
892
+ */
893
+ async function flush() {
894
+ if (flushTimeout) {
895
+ clearTimeout(flushTimeout);
896
+ flushTimeout = null;
897
+ }
898
+ if (eventBuffer.length === 0)
899
+ return;
900
+ const events = [...eventBuffer];
901
+ eventBuffer.length = 0;
902
+ if (!config?.licenseKey || !config?.endpoint) {
903
+ if (config?.debug) {
904
+ console.log('[rulecatch-ai] No license key or endpoint configured, skipping flush');
905
+ }
906
+ return;
907
+ }
908
+ try {
909
+ const response = await fetch(config.endpoint, {
910
+ method: 'POST',
911
+ headers: {
912
+ 'Content-Type': 'application/json',
913
+ Authorization: `Bearer ${config.licenseKey}`,
914
+ },
915
+ body: JSON.stringify({
916
+ projectId: config.projectId,
917
+ events,
918
+ gitContext: config.trackGitContext !== false ? {
919
+ username: gitContext.username,
920
+ email: gitContext.email,
921
+ repo: gitContext.repoName,
922
+ branch: gitContext.branch,
923
+ commit: gitContext.commit,
924
+ isDirty: gitContext.isDirty,
925
+ } : undefined,
926
+ }),
927
+ });
928
+ if (!response.ok) {
929
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
930
+ }
931
+ if (config.debug) {
932
+ console.log(`[rulecatch-ai] Flushed ${events.length} events`);
933
+ }
934
+ }
935
+ catch (error) {
936
+ if (config?.debug) {
937
+ console.error('[rulecatch-ai] Failed to flush events:', error);
938
+ }
939
+ // Re-add events to buffer for retry
940
+ eventBuffer.unshift(...events);
941
+ }
942
+ }
943
+ /**
944
+ * Calculate comprehensive session metrics from events
945
+ */
946
+ function calculateSessionMetrics(events) {
947
+ const sessionStart = events.find((e) => e.type === 'session_start');
948
+ const sessionEnd = events.find((e) => e.type === 'session_end');
949
+ const startTime = sessionStart?.timestamp || new Date().toISOString();
950
+ const endTime = sessionEnd?.timestamp;
951
+ const duration = endTime
952
+ ? (new Date(endTime).getTime() - new Date(startTime).getTime()) / 1000
953
+ : 0;
954
+ const activeDuration = currentSession
955
+ ? currentSession.activeTimeMs / 1000
956
+ : duration;
957
+ const aiRequests = events.filter((e) => e.type === 'ai_request');
958
+ const toolCalls = events.filter((e) => e.type === 'tool_call');
959
+ const codeAccepts = events.filter((e) => e.type === 'code_accept');
960
+ const codeRejects = events.filter((e) => e.type === 'code_reject');
961
+ const turns = events.filter((e) => e.type === 'conversation_turn');
962
+ const errors = events.filter((e) => e.type === 'error');
963
+ const interventions = events.filter((e) => e.userIntervention);
964
+ // Token metrics
965
+ const totalInputTokens = aiRequests.reduce((sum, e) => sum + (e.inputTokens || 0), 0);
966
+ const totalOutputTokens = aiRequests.reduce((sum, e) => sum + (e.outputTokens || 0), 0);
967
+ const totalThinkingTokens = aiRequests.reduce((sum, e) => sum + (e.thinkingTokens || 0), 0);
968
+ const totalCacheReadTokens = aiRequests.reduce((sum, e) => sum + (e.cacheReadTokens || 0), 0);
969
+ const totalCost = aiRequests.reduce((sum, e) => sum + (e.estimatedCost || 0), 0);
970
+ // Cache hit rate
971
+ const cachedRequests = aiRequests.filter((e) => e.promptCached);
972
+ const cacheHitRate = aiRequests.length > 0 ? cachedRequests.length / aiRequests.length : 0;
973
+ // Context usage
974
+ const contextUsages = aiRequests.map((e) => e.contextUsage).filter((c) => c !== undefined);
975
+ const avgContextUsage = contextUsages.length > 0
976
+ ? contextUsages.reduce((a, b) => a + b, 0) / contextUsages.length
977
+ : 0;
978
+ // Response times
979
+ const responseTimes = currentSession?.responseTimes || [];
980
+ const avgResponseTime = responseTimes.length > 0
981
+ ? responseTimes.reduce((a, b) => a + b, 0) / responseTimes.length
982
+ : 0;
983
+ const timeToFirstTokens = currentSession?.timeToFirstTokens || [];
984
+ const avgTimeToFirstToken = timeToFirstTokens.length > 0
985
+ ? timeToFirstTokens.reduce((a, b) => a + b, 0) / timeToFirstTokens.length
986
+ : 0;
987
+ // Tool metrics
988
+ const successfulTools = toolCalls.filter((e) => e.toolSuccess);
989
+ const toolSuccessRate = toolCalls.length > 0 ? successfulTools.length / toolCalls.length : 1;
990
+ const toolCallsByName = {};
991
+ const toolFailuresByName = {};
992
+ if (currentSession) {
993
+ for (const [name, stats] of currentSession.toolCalls) {
994
+ toolCallsByName[name] = stats.success + stats.fail;
995
+ if (stats.fail > 0) {
996
+ toolFailuresByName[name] = stats.fail;
997
+ }
998
+ }
999
+ }
1000
+ // Code metrics
1001
+ const allLinesAdded = events.reduce((sum, e) => sum + (e.linesAdded || 0), 0);
1002
+ const allLinesRemoved = events.reduce((sum, e) => sum + (e.linesRemoved || 0), 0);
1003
+ const allFiles = new Set();
1004
+ events.forEach((e) => e.filesModified?.forEach((f) => allFiles.add(f)));
1005
+ // Top modified files
1006
+ const topModifiedFiles = [];
1007
+ if (currentSession) {
1008
+ const sorted = [...currentSession.fileChanges.entries()]
1009
+ .sort((a, b) => b[1] - a[1])
1010
+ .slice(0, 10);
1011
+ for (const [file, changes] of sorted) {
1012
+ topModifiedFiles.push({ file, changes });
1013
+ }
1014
+ }
1015
+ // Cost breakdowns
1016
+ const costByFile = {};
1017
+ const costByLanguage = {};
1018
+ if (currentSession) {
1019
+ for (const [file, cost] of currentSession.fileCosts) {
1020
+ costByFile[file] = Math.round(cost * 10000) / 10000;
1021
+ }
1022
+ for (const [lang, cost] of currentSession.languageCosts) {
1023
+ costByLanguage[lang] = Math.round(cost * 10000) / 10000;
1024
+ }
1025
+ }
1026
+ // Acceptance and intervention rates
1027
+ const codeDecisions = codeAccepts.length + codeRejects.length;
1028
+ const codeAcceptanceRate = codeDecisions > 0 ? codeAccepts.length / codeDecisions : 1;
1029
+ const userInterventionRate = turns.length > 0 ? interventions.length / turns.length : 0;
1030
+ // Model usage
1031
+ const modelCounts = {};
1032
+ aiRequests.forEach((e) => {
1033
+ const model = e.model || 'unknown';
1034
+ modelCounts[model] = (modelCounts[model] || 0) + 1;
1035
+ });
1036
+ const primaryModel = Object.entries(modelCounts).sort((a, b) => b[1] - a[1])[0]?.[0] || 'unknown';
1037
+ const languages = [...new Set(events.map((e) => e.language).filter(Boolean))];
1038
+ // Productivity metrics
1039
+ const activeMinutes = activeDuration / 60;
1040
+ const tokensPerMinute = activeMinutes > 0 ? (totalInputTokens + totalOutputTokens) / activeMinutes : 0;
1041
+ const activeHours = activeDuration / 3600;
1042
+ const linesPerHour = activeHours > 0 ? allLinesAdded / activeHours : 0;
1043
+ const costPerLine = allLinesAdded > 0 ? totalCost / allLinesAdded : 0;
1044
+ // Error metrics
1045
+ const recoveredErrors = errors.filter((e) => e.errorRecovered);
1046
+ const errorRecoveryRate = errors.length > 0 ? recoveredErrors.length / errors.length : 1;
1047
+ // Rule violation metrics
1048
+ const ruleViolationEvents = events.filter((e) => e.type === 'rule_deviation');
1049
+ const totalRuleViolations = ruleViolationEvents.length;
1050
+ const violationsByCategory = {
1051
+ coding_standard: 0,
1052
+ db_pattern: 0,
1053
+ security: 0,
1054
+ performance: 0,
1055
+ architecture: 0,
1056
+ documentation: 0,
1057
+ testing: 0,
1058
+ custom: 0,
1059
+ };
1060
+ const violationsBySeverity = {
1061
+ info: 0,
1062
+ warning: 0,
1063
+ error: 0,
1064
+ };
1065
+ const violationsBySource = {};
1066
+ const ruleViolationCounts = {};
1067
+ const fileViolationCounts = {};
1068
+ let correctedCount = 0;
1069
+ // Process violation events
1070
+ ruleViolationEvents.forEach((d) => {
1071
+ if (d.ruleCategory) {
1072
+ violationsByCategory[d.ruleCategory] = (violationsByCategory[d.ruleCategory] || 0) + 1;
1073
+ }
1074
+ if (d.ruleSeverity) {
1075
+ violationsBySeverity[d.ruleSeverity] = (violationsBySeverity[d.ruleSeverity] || 0) + 1;
1076
+ }
1077
+ if (d.ruleSource) {
1078
+ violationsBySource[d.ruleSource] = (violationsBySource[d.ruleSource] || 0) + 1;
1079
+ }
1080
+ if (d.ruleName) {
1081
+ if (!ruleViolationCounts[d.ruleName]) {
1082
+ ruleViolationCounts[d.ruleName] = { count: 0, severity: d.ruleSeverity || 'warning' };
1083
+ }
1084
+ ruleViolationCounts[d.ruleName].count++;
1085
+ }
1086
+ if (d.violationFile) {
1087
+ fileViolationCounts[d.violationFile] = (fileViolationCounts[d.violationFile] || 0) + 1;
1088
+ }
1089
+ if (d.corrected) {
1090
+ correctedCount++;
1091
+ }
1092
+ });
1093
+ // Use session data if available (more accurate)
1094
+ if (currentSession) {
1095
+ for (const [category, count] of currentSession.violationsByCategory) {
1096
+ violationsByCategory[category] = count;
1097
+ }
1098
+ for (const [severity, count] of currentSession.violationsBySeverity) {
1099
+ violationsBySeverity[severity] = count;
1100
+ }
1101
+ for (const [source, count] of currentSession.violationsBySource) {
1102
+ violationsBySource[source] = count;
1103
+ }
1104
+ correctedCount = currentSession.correctionsCount;
1105
+ }
1106
+ // Top rule violations
1107
+ const topRuleViolations = Object.entries(ruleViolationCounts)
1108
+ .map(([rule, { count, severity }]) => ({ rule, count, severity }))
1109
+ .sort((a, b) => b.count - a.count)
1110
+ .slice(0, 10);
1111
+ // Files with most violations
1112
+ const filesWithMostViolations = Object.entries(fileViolationCounts)
1113
+ .map(([file, count]) => ({ file, count }))
1114
+ .sort((a, b) => b.count - a.count)
1115
+ .slice(0, 10);
1116
+ const violationCorrectionRate = totalRuleViolations > 0
1117
+ ? correctedCount / totalRuleViolations
1118
+ : 1;
1119
+ return {
1120
+ sessionId: currentSession?.id || 'unknown',
1121
+ startTime,
1122
+ endTime,
1123
+ duration,
1124
+ activeDuration,
1125
+ totalInputTokens,
1126
+ totalOutputTokens,
1127
+ totalThinkingTokens,
1128
+ totalCacheReadTokens,
1129
+ cacheHitRate,
1130
+ avgContextUsage,
1131
+ totalCost,
1132
+ costByFile,
1133
+ costByLanguage,
1134
+ requestCount: aiRequests.length,
1135
+ conversationTurns: turns.length,
1136
+ avgResponseTime,
1137
+ avgTimeToFirstToken,
1138
+ toolCallCount: toolCalls.length,
1139
+ toolSuccessRate,
1140
+ toolCallsByName,
1141
+ toolFailuresByName,
1142
+ totalLinesAdded: allLinesAdded,
1143
+ totalLinesRemoved: allLinesRemoved,
1144
+ netLinesChanged: allLinesAdded - allLinesRemoved,
1145
+ uniqueFilesModified: allFiles.size,
1146
+ topModifiedFiles,
1147
+ codeAcceptanceRate,
1148
+ userInterventionRate,
1149
+ primaryModel,
1150
+ modelUsage: modelCounts,
1151
+ languages,
1152
+ tokensPerMinute: Math.round(tokensPerMinute),
1153
+ linesPerHour: Math.round(linesPerHour * 10) / 10,
1154
+ costPerLine: Math.round(costPerLine * 10000) / 10000,
1155
+ errorCount: errors.length,
1156
+ errorRecoveryRate,
1157
+ // Rule violation metrics
1158
+ totalRuleViolations,
1159
+ violationsByCategory,
1160
+ violationsBySeverity,
1161
+ violationsBySource,
1162
+ topRuleViolations,
1163
+ violationCorrectionRate,
1164
+ filesWithMostViolations,
1165
+ };
1166
+ }
1167
+ /**
1168
+ * Generate a unique session ID
1169
+ */
1170
+ function generateSessionId() {
1171
+ const timestamp = Date.now().toString(36);
1172
+ const random = Math.random().toString(36).substring(2, 10);
1173
+ return `ai_${timestamp}_${random}`;
1174
+ }
1175
+ /**
1176
+ * Get current session metrics
1177
+ */
1178
+ function getSessionMetrics() {
1179
+ if (!currentSession)
1180
+ return null;
1181
+ updateActiveTime();
1182
+ return calculateSessionMetrics(currentSession.events);
1183
+ }
1184
+ /**
1185
+ * Check if a session is active
1186
+ */
1187
+ function isSessionActive() {
1188
+ return currentSession !== null;
1189
+ }
1190
+ /**
1191
+ * Get current session ID
1192
+ */
1193
+ function getSessionId() {
1194
+ return currentSession?.id || null;
1195
+ }
1196
+ /**
1197
+ * Pause session (marks idle period)
1198
+ */
1199
+ function pauseSession() {
1200
+ if (!currentSession)
1201
+ return;
1202
+ updateActiveTime();
1203
+ track({
1204
+ type: 'session_pause',
1205
+ timestamp: new Date().toISOString(),
1206
+ sessionId: currentSession.id,
1207
+ projectId: config?.projectId || 'unknown',
1208
+ activeTime: currentSession.activeTimeMs,
1209
+ });
1210
+ }
1211
+ /**
1212
+ * Resume session
1213
+ */
1214
+ function resumeSession() {
1215
+ if (!currentSession)
1216
+ return;
1217
+ currentSession.lastActivityTime = Date.now();
1218
+ track({
1219
+ type: 'session_resume',
1220
+ timestamp: new Date().toISOString(),
1221
+ sessionId: currentSession.id,
1222
+ projectId: config?.projectId || 'unknown',
1223
+ });
1224
+ }
1225
+ /**
1226
+ * Get current git context
1227
+ */
1228
+ function getGitContext() {
1229
+ return { ...gitContext };
1230
+ }
1231
+ /**
1232
+ * Manually refresh git context
1233
+ */
1234
+ function refreshGitContext() {
1235
+ gitContext = collectGitContext(config?.cwd);
1236
+ return { ...gitContext };
1237
+ }
1238
+
1239
+ export { MODEL_PRICING, calculateCost, collectGitContext, decryptEventPII, decryptPII, deriveKey, encryptEventPII, encryptPII, endSession, estimateTokens, flush, formatCost, formatDuration, generateSalt, getGitContext, getGitRoot, getGitSummary, getSessionId, getSessionMetrics, hashForIndex, init, isGitRepo, isSessionActive, pauseSession, refreshGitContext, resumeSession, startSession, track, trackAIRequest, trackCodeDecision, trackConversationTurn, trackError, trackFileOperation, trackRuleDeviation, trackToolCall, verifyPrivacyKey, watchGitChanges };
1240
+ //# sourceMappingURL=index.js.map