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