@dependabit/action 0.1.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 (92) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/LICENSE +21 -0
  3. package/README.md +225 -0
  4. package/action.yml +85 -0
  5. package/dist/actions/check.d.ts +33 -0
  6. package/dist/actions/check.d.ts.map +1 -0
  7. package/dist/actions/check.js +162 -0
  8. package/dist/actions/check.js.map +1 -0
  9. package/dist/actions/generate.d.ts +9 -0
  10. package/dist/actions/generate.d.ts.map +1 -0
  11. package/dist/actions/generate.js +152 -0
  12. package/dist/actions/generate.js.map +1 -0
  13. package/dist/actions/update.d.ts +9 -0
  14. package/dist/actions/update.d.ts.map +1 -0
  15. package/dist/actions/update.js +246 -0
  16. package/dist/actions/update.js.map +1 -0
  17. package/dist/actions/validate.d.ts +33 -0
  18. package/dist/actions/validate.d.ts.map +1 -0
  19. package/dist/actions/validate.js +226 -0
  20. package/dist/actions/validate.js.map +1 -0
  21. package/dist/index.d.ts +8 -0
  22. package/dist/index.d.ts.map +1 -0
  23. package/dist/index.js +35 -0
  24. package/dist/index.js.map +1 -0
  25. package/dist/logger.d.ts +114 -0
  26. package/dist/logger.d.ts.map +1 -0
  27. package/dist/logger.js +154 -0
  28. package/dist/logger.js.map +1 -0
  29. package/dist/utils/agent-config.d.ts +31 -0
  30. package/dist/utils/agent-config.d.ts.map +1 -0
  31. package/dist/utils/agent-config.js +42 -0
  32. package/dist/utils/agent-config.js.map +1 -0
  33. package/dist/utils/agent-router.d.ts +33 -0
  34. package/dist/utils/agent-router.d.ts.map +1 -0
  35. package/dist/utils/agent-router.js +57 -0
  36. package/dist/utils/agent-router.js.map +1 -0
  37. package/dist/utils/errors.d.ts +51 -0
  38. package/dist/utils/errors.d.ts.map +1 -0
  39. package/dist/utils/errors.js +219 -0
  40. package/dist/utils/errors.js.map +1 -0
  41. package/dist/utils/inputs.d.ts +35 -0
  42. package/dist/utils/inputs.d.ts.map +1 -0
  43. package/dist/utils/inputs.js +47 -0
  44. package/dist/utils/inputs.js.map +1 -0
  45. package/dist/utils/metrics.d.ts +66 -0
  46. package/dist/utils/metrics.d.ts.map +1 -0
  47. package/dist/utils/metrics.js +116 -0
  48. package/dist/utils/metrics.js.map +1 -0
  49. package/dist/utils/outputs.d.ts +43 -0
  50. package/dist/utils/outputs.d.ts.map +1 -0
  51. package/dist/utils/outputs.js +146 -0
  52. package/dist/utils/outputs.js.map +1 -0
  53. package/dist/utils/performance.d.ts +100 -0
  54. package/dist/utils/performance.d.ts.map +1 -0
  55. package/dist/utils/performance.js +185 -0
  56. package/dist/utils/performance.js.map +1 -0
  57. package/dist/utils/reporter.d.ts +43 -0
  58. package/dist/utils/reporter.d.ts.map +1 -0
  59. package/dist/utils/reporter.js +122 -0
  60. package/dist/utils/reporter.js.map +1 -0
  61. package/dist/utils/secrets.d.ts +45 -0
  62. package/dist/utils/secrets.d.ts.map +1 -0
  63. package/dist/utils/secrets.js +94 -0
  64. package/dist/utils/secrets.js.map +1 -0
  65. package/package.json +45 -0
  66. package/src/actions/check.ts +223 -0
  67. package/src/actions/generate.ts +181 -0
  68. package/src/actions/update.ts +284 -0
  69. package/src/actions/validate.ts +292 -0
  70. package/src/index.ts +43 -0
  71. package/src/logger.test.ts +200 -0
  72. package/src/logger.ts +210 -0
  73. package/src/utils/agent-config.ts +61 -0
  74. package/src/utils/agent-router.ts +67 -0
  75. package/src/utils/errors.ts +251 -0
  76. package/src/utils/inputs.ts +75 -0
  77. package/src/utils/metrics.ts +169 -0
  78. package/src/utils/outputs.ts +202 -0
  79. package/src/utils/performance.ts +248 -0
  80. package/src/utils/reporter.ts +169 -0
  81. package/src/utils/secrets.ts +124 -0
  82. package/test/actions/check.test.ts +216 -0
  83. package/test/actions/generate.test.ts +82 -0
  84. package/test/actions/update.test.ts +70 -0
  85. package/test/actions/validate.test.ts +257 -0
  86. package/test/utils/agent-config.test.ts +112 -0
  87. package/test/utils/agent-router.test.ts +129 -0
  88. package/test/utils/metrics.test.ts +221 -0
  89. package/test/utils/reporter.test.ts +196 -0
  90. package/test/utils/secrets.test.ts +217 -0
  91. package/tsconfig.json +15 -0
  92. package/tsconfig.tsbuildinfo +1 -0
@@ -0,0 +1,169 @@
1
+ /**
2
+ * Metrics calculator for false positive rate tracking
3
+ * Implements 30-day rolling window analysis and trend detection
4
+ */
5
+
6
+ export interface MetricsConfig {
7
+ windowDays?: number;
8
+ threshold?: number;
9
+ }
10
+
11
+ export interface FeedbackSummary {
12
+ truePositives: number;
13
+ falsePositives: number;
14
+ total: number;
15
+ }
16
+
17
+ export interface DataPoint {
18
+ date: string;
19
+ falsePositiveRate: number;
20
+ }
21
+
22
+ export interface Trend {
23
+ direction: 'improving' | 'worsening' | 'stable' | 'no-data';
24
+ slope: number;
25
+ }
26
+
27
+ export interface ThresholdCheck {
28
+ passed: boolean;
29
+ rate: number;
30
+ threshold: number;
31
+ }
32
+
33
+ export interface MetricsReport {
34
+ currentRate: number;
35
+ rollingAverage: number;
36
+ trend: Trend;
37
+ thresholdCheck: ThresholdCheck;
38
+ totalFeedback: number;
39
+ }
40
+
41
+ /**
42
+ * Calculator for false positive metrics with rolling window analysis
43
+ */
44
+ export class MetricsCalculator {
45
+ private windowDays: number;
46
+ private threshold: number;
47
+
48
+ constructor(config: MetricsConfig = {}) {
49
+ this.windowDays = config.windowDays || 30;
50
+ this.threshold = config.threshold || 0.1; // 10% default threshold
51
+ }
52
+
53
+ /**
54
+ * Calculate false positive rate from feedback data
55
+ */
56
+ calculateFalsePositiveRate(feedback: FeedbackSummary): number {
57
+ if (feedback.total === 0) {
58
+ return 0;
59
+ }
60
+ return feedback.falsePositives / feedback.total;
61
+ }
62
+
63
+ /**
64
+ * Calculate rolling average over the time window
65
+ */
66
+ calculateRollingAverage(
67
+ dataPoints: DataPoint[],
68
+ windowDays?: number,
69
+ referenceDate?: Date
70
+ ): number {
71
+ const days = windowDays || this.windowDays;
72
+ const endDate = referenceDate || new Date();
73
+ const startDate = new Date(endDate);
74
+ startDate.setDate(startDate.getDate() - days);
75
+
76
+ // Filter data points within the window
77
+ const windowData = dataPoints.filter((point) => {
78
+ const pointDate = new Date(point.date);
79
+ return pointDate >= startDate && pointDate <= endDate;
80
+ });
81
+
82
+ if (windowData.length === 0) {
83
+ return 0;
84
+ }
85
+
86
+ const sum = windowData.reduce((acc, point) => acc + point.falsePositiveRate, 0);
87
+ return sum / windowData.length;
88
+ }
89
+
90
+ /**
91
+ * Detect trend direction using linear regression
92
+ */
93
+ getTrend(dataPoints: DataPoint[]): Trend {
94
+ if (dataPoints.length < 2) {
95
+ return { direction: 'no-data', slope: 0 };
96
+ }
97
+
98
+ // Simple linear regression
99
+ const n = dataPoints.length;
100
+ let sumX = 0;
101
+ let sumY = 0;
102
+ let sumXY = 0;
103
+ let sumX2 = 0;
104
+
105
+ dataPoints.forEach((point, index) => {
106
+ const x = index;
107
+ const y = point.falsePositiveRate;
108
+ sumX += x;
109
+ sumY += y;
110
+ sumXY += x * y;
111
+ sumX2 += x * x;
112
+ });
113
+
114
+ const denominator = n * sumX2 - sumX * sumX;
115
+ if (denominator === 0) {
116
+ return { direction: 'no-data', slope: 0 };
117
+ }
118
+
119
+ const slope = (n * sumXY - sumX * sumY) / denominator;
120
+ // Determine direction based on slope
121
+ let direction: Trend['direction'];
122
+ if (Math.abs(slope) < 0.001) {
123
+ direction = 'stable';
124
+ } else if (slope < 0) {
125
+ direction = 'improving';
126
+ } else {
127
+ direction = 'worsening';
128
+ }
129
+
130
+ return { direction, slope };
131
+ }
132
+
133
+ /**
134
+ * Check if rate is within acceptable threshold
135
+ */
136
+ checkThreshold(rate: number, threshold?: number): ThresholdCheck {
137
+ const targetThreshold = threshold !== undefined ? threshold : this.threshold;
138
+ return {
139
+ passed: rate <= targetThreshold,
140
+ rate,
141
+ threshold: targetThreshold
142
+ };
143
+ }
144
+
145
+ /**
146
+ * Generate comprehensive metrics report
147
+ */
148
+ generateReport(currentFeedback: FeedbackSummary, history: DataPoint[]): MetricsReport {
149
+ const currentRate = this.calculateFalsePositiveRate(currentFeedback);
150
+ const rollingAverage = this.calculateRollingAverage(history);
151
+ const trend = this.getTrend(history);
152
+ const thresholdCheck = this.checkThreshold(rollingAverage);
153
+
154
+ return {
155
+ currentRate,
156
+ rollingAverage,
157
+ trend,
158
+ thresholdCheck,
159
+ totalFeedback: currentFeedback.total
160
+ };
161
+ }
162
+
163
+ /**
164
+ * Format rate as percentage string
165
+ */
166
+ formatRate(rate: number): string {
167
+ return `${(rate * 100).toFixed(1)}%`;
168
+ }
169
+ }
@@ -0,0 +1,202 @@
1
+ /**
2
+ * Action Output Formatting
3
+ * Handles setting GitHub Action outputs and creating summaries
4
+ */
5
+
6
+ import * as core from '@actions/core';
7
+ import type { DependencyManifest } from '@dependabit/manifest';
8
+
9
+ /**
10
+ * Set outputs for the generate action
11
+ */
12
+ export function setGenerateOutputs(
13
+ manifest: DependencyManifest,
14
+ manifestPath: string,
15
+ statistics: {
16
+ filesScanned: number;
17
+ llmCalls: number;
18
+ totalTokens: number;
19
+ totalLatencyMs: number;
20
+ }
21
+ ): void {
22
+ core.setOutput('manifest_path', manifestPath);
23
+ core.setOutput('dependency_count', manifest.dependencies.length);
24
+ core.setOutput('files_scanned', statistics.filesScanned);
25
+ core.setOutput('llm_calls', statistics.llmCalls);
26
+ core.setOutput('total_tokens', statistics.totalTokens);
27
+ core.setOutput('average_confidence', manifest.statistics.averageConfidence);
28
+ }
29
+
30
+ /**
31
+ * Create a summary report for the generate action
32
+ */
33
+ export async function createGenerateSummary(
34
+ manifest: DependencyManifest,
35
+ statistics: {
36
+ filesScanned: number;
37
+ llmCalls: number;
38
+ totalTokens: number;
39
+ totalLatencyMs: number;
40
+ }
41
+ ): Promise<void> {
42
+ await core.summary
43
+ .addHeading('🔍 Dependency Detection Complete')
44
+ .addRaw('\n')
45
+ .addTable([
46
+ [
47
+ { data: 'Metric', header: true },
48
+ { data: 'Value', header: true }
49
+ ],
50
+ ['Dependencies Found', manifest.dependencies.length.toString()],
51
+ ['Files Scanned', statistics.filesScanned.toString()],
52
+ ['LLM Calls', statistics.llmCalls.toString()],
53
+ ['Total Tokens Used', statistics.totalTokens.toString()],
54
+ ['Average Confidence', `${(manifest.statistics.averageConfidence * 100).toFixed(1)}%`],
55
+ ['Analysis Time', `${(statistics.totalLatencyMs / 1000).toFixed(2)}s`]
56
+ ])
57
+ .addRaw('\n')
58
+ .addHeading('📊 Dependencies by Type', 3)
59
+ .addList(Object.entries(manifest.statistics.byType).map(([type, count]) => `${type}: ${count}`))
60
+ .addRaw('\n')
61
+ .addHeading('🔧 Dependencies by Access Method', 3)
62
+ .addList(
63
+ Object.entries(manifest.statistics.byAccessMethod).map(
64
+ ([method, count]) => `${method}: ${count}`
65
+ )
66
+ )
67
+ .write();
68
+ }
69
+
70
+ /**
71
+ * Create a summary for detected dependencies
72
+ */
73
+ export async function createDependencyListSummary(
74
+ dependencies: Array<{ name: string; url: string; type: string; confidence: number }>
75
+ ): Promise<void> {
76
+ if (dependencies.length === 0) {
77
+ await core.summary
78
+ .addHeading('No Dependencies Found')
79
+ .addRaw('No external informational dependencies were detected in this repository.')
80
+ .write();
81
+ return;
82
+ }
83
+
84
+ await core.summary
85
+ .addHeading('🔗 Detected Dependencies', 3)
86
+ .addTable([
87
+ [
88
+ { data: 'Name', header: true },
89
+ { data: 'Type', header: true },
90
+ { data: 'Confidence', header: true },
91
+ { data: 'URL', header: true }
92
+ ],
93
+ ...dependencies
94
+ .slice(0, 20)
95
+ .map((dep) => [
96
+ dep.name,
97
+ dep.type,
98
+ `${(dep.confidence * 100).toFixed(0)}%`,
99
+ `[Link](${dep.url})`
100
+ ])
101
+ ])
102
+ .write();
103
+
104
+ if (dependencies.length > 20) {
105
+ await core.summary.addRaw(`\n_...and ${dependencies.length - 20} more dependencies_\n`).write();
106
+ }
107
+ }
108
+
109
+ /**
110
+ * Set outputs for the update action
111
+ */
112
+ function countDependencyDiffs(
113
+ existingManifest: DependencyManifest,
114
+ updatedManifest: DependencyManifest
115
+ ): { added: number; removed: number } {
116
+ const existingSet = new Set(existingManifest.dependencies.map((dep) => JSON.stringify(dep)));
117
+ const updatedSet = new Set(updatedManifest.dependencies.map((dep) => JSON.stringify(dep)));
118
+
119
+ let added = 0;
120
+ for (const dep of updatedSet) {
121
+ if (!existingSet.has(dep)) {
122
+ added++;
123
+ }
124
+ }
125
+
126
+ let removed = 0;
127
+ for (const dep of existingSet) {
128
+ if (!updatedSet.has(dep)) {
129
+ removed++;
130
+ }
131
+ }
132
+
133
+ return { added, removed };
134
+ }
135
+
136
+ export function setUpdateOutputs(
137
+ updatedManifest: DependencyManifest,
138
+ existingManifest: DependencyManifest,
139
+ filesChanged: number
140
+ ): void {
141
+ const { added, removed } = countDependencyDiffs(existingManifest, updatedManifest);
142
+ const changesDetected = added > 0 || removed > 0;
143
+
144
+ core.setOutput('changes_detected', changesDetected);
145
+ core.setOutput('dependencies_added', added);
146
+ core.setOutput('dependencies_removed', removed);
147
+ core.setOutput('total_dependencies', updatedManifest.dependencies.length);
148
+ core.setOutput('files_analyzed', filesChanged);
149
+ }
150
+
151
+ /**
152
+ * Create a summary report for the update action
153
+ */
154
+ export async function createUpdateSummary(
155
+ existingManifest: DependencyManifest,
156
+ updatedManifest: DependencyManifest,
157
+ stats: {
158
+ commitsAnalyzed: number;
159
+ filesChanged: number;
160
+ urlsAdded: number;
161
+ urlsRemoved: number;
162
+ }
163
+ ): Promise<void> {
164
+ const dependenciesAdded =
165
+ updatedManifest.dependencies.length - existingManifest.dependencies.length;
166
+ const changesDetected = dependenciesAdded !== 0;
167
+
168
+ await core.summary
169
+ .addHeading('🔄 Manifest Update Complete')
170
+ .addRaw('\n')
171
+ .addTable([
172
+ [
173
+ { data: 'Metric', header: true },
174
+ { data: 'Value', header: true }
175
+ ],
176
+ ['Commits Analyzed', stats.commitsAnalyzed.toString()],
177
+ ['Files Changed', stats.filesChanged.toString()],
178
+ ['URLs Added', stats.urlsAdded.toString()],
179
+ ['URLs Removed', stats.urlsRemoved.toString()],
180
+ ['Dependencies Before', existingManifest.dependencies.length.toString()],
181
+ ['Dependencies After', updatedManifest.dependencies.length.toString()],
182
+ [
183
+ 'Net Change',
184
+ dependenciesAdded >= 0 ? `+${dependenciesAdded}` : dependenciesAdded.toString()
185
+ ],
186
+ ['Changes Detected', changesDetected ? '✅ Yes' : '❌ No']
187
+ ])
188
+ .write();
189
+
190
+ if (dependenciesAdded > 0) {
191
+ const newDeps = updatedManifest.dependencies
192
+ .filter((dep) => !existingManifest.dependencies.some((existing) => existing.url === dep.url))
193
+ .slice(0, 10);
194
+
195
+ if (newDeps.length > 0) {
196
+ await core.summary
197
+ .addHeading('🆕 New Dependencies', 3)
198
+ .addList(newDeps.map((dep) => `**${dep.name}**: ${dep.url} (${dep.type})`))
199
+ .write();
200
+ }
201
+ }
202
+ }
@@ -0,0 +1,248 @@
1
+ /**
2
+ * Performance metrics tracking for operations
3
+ * Tracks operation durations, API quotas, and resource usage
4
+ */
5
+
6
+ export interface OperationMetrics {
7
+ operationId?: string | undefined;
8
+ name: string;
9
+ startTime: number;
10
+ endTime?: number | undefined;
11
+ duration?: number | undefined;
12
+ status: 'running' | 'completed' | 'failed';
13
+ metadata?: Record<string, unknown> | undefined;
14
+ }
15
+
16
+ export interface APIQuotaMetrics {
17
+ limit: number;
18
+ remaining: number;
19
+ used: number;
20
+ resetAt: Date;
21
+ percentUsed: number;
22
+ }
23
+
24
+ export interface PerformanceReport {
25
+ totalOperations: number;
26
+ completedOperations: number;
27
+ failedOperations: number;
28
+ averageDuration: number;
29
+ totalDuration: number;
30
+ apiQuota?: APIQuotaMetrics | undefined;
31
+ operations: OperationMetrics[];
32
+ }
33
+
34
+ /**
35
+ * Performance metrics tracker
36
+ */
37
+ export class PerformanceTracker {
38
+ private operations: Map<string, OperationMetrics>;
39
+ private completedOperations: OperationMetrics[];
40
+ private apiQuota?: APIQuotaMetrics | undefined;
41
+
42
+ constructor() {
43
+ this.operations = new Map();
44
+ this.completedOperations = [];
45
+ this.apiQuota = undefined;
46
+ }
47
+
48
+ /**
49
+ * Start tracking an operation
50
+ */
51
+ startOperation(name: string, metadata?: Record<string, unknown>): string {
52
+ const operationId = `${name}-${Date.now()}-${Math.random().toString(36).slice(2, 11)}`;
53
+
54
+ const metrics: OperationMetrics = {
55
+ operationId,
56
+ name,
57
+ startTime: Date.now(),
58
+ status: 'running',
59
+ metadata
60
+ };
61
+
62
+ this.operations.set(operationId, metrics);
63
+ return operationId;
64
+ }
65
+
66
+ /**
67
+ * End tracking an operation
68
+ */
69
+ endOperation(operationId: string, status: 'completed' | 'failed' = 'completed'): void {
70
+ const metrics = this.operations.get(operationId);
71
+ if (!metrics) {
72
+ return;
73
+ }
74
+
75
+ metrics.endTime = Date.now();
76
+ metrics.duration = metrics.endTime - metrics.startTime;
77
+ metrics.status = status;
78
+
79
+ this.completedOperations.push(metrics);
80
+ this.operations.delete(operationId);
81
+ }
82
+
83
+ /**
84
+ * Update API quota metrics
85
+ */
86
+ updateAPIQuota(quota: { limit: number; remaining: number; used: number; reset: number }): void {
87
+ this.apiQuota = {
88
+ limit: quota.limit,
89
+ remaining: quota.remaining,
90
+ used: quota.used,
91
+ resetAt: new Date(quota.reset * 1000),
92
+ percentUsed: (quota.used / quota.limit) * 100
93
+ };
94
+ }
95
+
96
+ /**
97
+ * Get current API quota status
98
+ */
99
+ getAPIQuota(): APIQuotaMetrics | undefined {
100
+ return this.apiQuota;
101
+ }
102
+
103
+ /**
104
+ * Get metrics for a specific operation
105
+ */
106
+ getOperation(operationId: string): OperationMetrics | undefined {
107
+ return (
108
+ this.operations.get(operationId) ||
109
+ this.completedOperations.find((op) => op.operationId === operationId)
110
+ );
111
+ }
112
+
113
+ /**
114
+ * Get all running operations
115
+ */
116
+ getRunningOperations(): OperationMetrics[] {
117
+ return Array.from(this.operations.values());
118
+ }
119
+
120
+ /**
121
+ * Get all completed operations
122
+ */
123
+ getCompletedOperations(): OperationMetrics[] {
124
+ return this.completedOperations;
125
+ }
126
+
127
+ /**
128
+ * Generate performance report
129
+ */
130
+ generateReport(): PerformanceReport {
131
+ const allOperations = this.completedOperations;
132
+ const completed = allOperations.filter((op) => op.status === 'completed');
133
+ const failed = allOperations.filter((op) => op.status === 'failed');
134
+
135
+ const durations = completed.filter((op) => op.duration !== undefined).map((op) => op.duration!);
136
+
137
+ const totalDuration = durations.reduce((sum, d) => sum + d, 0);
138
+ const averageDuration = durations.length > 0 ? totalDuration / durations.length : 0;
139
+
140
+ return {
141
+ totalOperations: allOperations.length,
142
+ completedOperations: completed.length,
143
+ failedOperations: failed.length,
144
+ averageDuration,
145
+ totalDuration,
146
+ apiQuota: this.apiQuota,
147
+ operations: allOperations
148
+ };
149
+ }
150
+
151
+ /**
152
+ * Get operations by name
153
+ */
154
+ getOperationsByName(name: string): OperationMetrics[] {
155
+ return this.completedOperations.filter((op) => op.name === name);
156
+ }
157
+
158
+ /**
159
+ * Calculate percentile duration for an operation
160
+ */
161
+ getOperationPercentile(name: string, percentile: number): number {
162
+ const operations = this.getOperationsByName(name);
163
+ const durations = operations
164
+ .filter((op) => op.duration !== undefined)
165
+ .map((op) => op.duration!)
166
+ .sort((a, b) => a - b);
167
+
168
+ if (durations.length === 0) {
169
+ return 0;
170
+ }
171
+
172
+ const index = Math.ceil((percentile / 100) * durations.length) - 1;
173
+ const value = durations[Math.max(0, index)];
174
+ return value !== undefined ? value : 0;
175
+ }
176
+
177
+ /**
178
+ * Clear all metrics
179
+ */
180
+ clear(): void {
181
+ this.operations.clear();
182
+ this.completedOperations = [];
183
+ this.apiQuota = undefined;
184
+ }
185
+
186
+ /**
187
+ * Format metrics as human-readable string
188
+ */
189
+ formatReport(report?: PerformanceReport): string {
190
+ const r = report || this.generateReport();
191
+ const lines = [
192
+ 'Performance Report',
193
+ '==================',
194
+ `Total Operations: ${r.totalOperations}`,
195
+ `Completed: ${r.completedOperations} | Failed: ${r.failedOperations}`,
196
+ `Average Duration: ${r.averageDuration.toFixed(2)}ms`,
197
+ `Total Duration: ${r.totalDuration.toFixed(2)}ms`
198
+ ];
199
+
200
+ if (r.apiQuota) {
201
+ lines.push('');
202
+ lines.push('API Quota:');
203
+ lines.push(
204
+ ` Used: ${r.apiQuota.used}/${r.apiQuota.limit} (${r.apiQuota.percentUsed.toFixed(1)}%)`
205
+ );
206
+ lines.push(` Remaining: ${r.apiQuota.remaining}`);
207
+ lines.push(` Resets at: ${r.apiQuota.resetAt.toISOString()}`);
208
+ }
209
+
210
+ return lines.join('\n');
211
+ }
212
+ }
213
+
214
+ /**
215
+ * Global performance tracker instance
216
+ */
217
+ let globalTracker: PerformanceTracker | undefined;
218
+
219
+ /**
220
+ * Get or create global performance tracker
221
+ */
222
+ export function getPerformanceTracker(): PerformanceTracker {
223
+ if (!globalTracker) {
224
+ globalTracker = new PerformanceTracker();
225
+ }
226
+ return globalTracker;
227
+ }
228
+
229
+ /**
230
+ * Track an async operation
231
+ */
232
+ export async function trackOperation<T>(
233
+ name: string,
234
+ fn: () => Promise<T>,
235
+ metadata?: Record<string, unknown>
236
+ ): Promise<T> {
237
+ const tracker = getPerformanceTracker();
238
+ const operationId = tracker.startOperation(name, metadata);
239
+
240
+ try {
241
+ const result = await fn();
242
+ tracker.endOperation(operationId, 'completed');
243
+ return result;
244
+ } catch (error) {
245
+ tracker.endOperation(operationId, 'failed');
246
+ throw error;
247
+ }
248
+ }