nova-terminal-assistant 0.1.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.

Potentially problematic release.


This version of nova-terminal-assistant might be problematic. Click here for more details.

Files changed (192) hide show
  1. package/README.md +358 -0
  2. package/bin/nova +38 -0
  3. package/bin/nova.js +12 -0
  4. package/package.json +67 -0
  5. package/src/cli/commands/SmartCompletion.ts +458 -0
  6. package/src/cli/index.ts +5 -0
  7. package/src/cli/startup/IFlowRepl.ts +212 -0
  8. package/src/cli/startup/InkBasedRepl.ts +1056 -0
  9. package/src/cli/startup/InteractiveRepl.ts +2833 -0
  10. package/src/cli/startup/NovaApp.ts +1861 -0
  11. package/src/cli/startup/index.ts +4 -0
  12. package/src/cli/startup/parseArgs.ts +293 -0
  13. package/src/cli/test-modules.ts +27 -0
  14. package/src/cli/ui/IFlowDropdown.ts +425 -0
  15. package/src/cli/ui/ModernReplUI.ts +276 -0
  16. package/src/cli/ui/SimpleSelector2.ts +215 -0
  17. package/src/cli/ui/components/ConfirmDialog.ts +176 -0
  18. package/src/cli/ui/components/ErrorPanel.ts +364 -0
  19. package/src/cli/ui/components/InkAppRunner.tsx +67 -0
  20. package/src/cli/ui/components/InkComponents.tsx +613 -0
  21. package/src/cli/ui/components/NovaInkApp.tsx +312 -0
  22. package/src/cli/ui/components/ProgressBar.ts +177 -0
  23. package/src/cli/ui/components/ProgressIndicator.ts +298 -0
  24. package/src/cli/ui/components/QuickActions.ts +396 -0
  25. package/src/cli/ui/components/SimpleErrorPanel.ts +231 -0
  26. package/src/cli/ui/components/StatusBar.ts +194 -0
  27. package/src/cli/ui/components/ThinkingBlockRenderer.ts +401 -0
  28. package/src/cli/ui/components/index.ts +27 -0
  29. package/src/cli/ui/ink-prototype.tsx +347 -0
  30. package/src/cli/utils/CliUI.ts +336 -0
  31. package/src/cli/utils/CompletionHelper.ts +388 -0
  32. package/src/cli/utils/EnhancedCompleter.test.ts +226 -0
  33. package/src/cli/utils/EnhancedCompleter.ts +513 -0
  34. package/src/cli/utils/ErrorEnhancer.ts +429 -0
  35. package/src/cli/utils/OutputFormatter.ts +193 -0
  36. package/src/cli/utils/index.ts +9 -0
  37. package/src/core/agents/AgentOrchestrator.ts +515 -0
  38. package/src/core/agents/index.ts +17 -0
  39. package/src/core/audit/AuditLogger.ts +509 -0
  40. package/src/core/audit/index.ts +11 -0
  41. package/src/core/auth/AuthManager.d.ts.map +1 -0
  42. package/src/core/auth/AuthManager.ts +138 -0
  43. package/src/core/auth/index.d.ts.map +1 -0
  44. package/src/core/auth/index.ts +2 -0
  45. package/src/core/config/ConfigManager.d.ts.map +1 -0
  46. package/src/core/config/ConfigManager.test.ts +183 -0
  47. package/src/core/config/ConfigManager.ts +1219 -0
  48. package/src/core/config/index.d.ts.map +1 -0
  49. package/src/core/config/index.ts +1 -0
  50. package/src/core/context/ContextBuilder.d.ts.map +1 -0
  51. package/src/core/context/ContextBuilder.ts +171 -0
  52. package/src/core/context/ContextCompressor.d.ts.map +1 -0
  53. package/src/core/context/ContextCompressor.ts +642 -0
  54. package/src/core/context/LayeredMemoryManager.ts +657 -0
  55. package/src/core/context/MemoryDiscovery.d.ts.map +1 -0
  56. package/src/core/context/MemoryDiscovery.ts +175 -0
  57. package/src/core/context/defaultSystemPrompt.d.ts.map +1 -0
  58. package/src/core/context/defaultSystemPrompt.ts +35 -0
  59. package/src/core/context/index.d.ts.map +1 -0
  60. package/src/core/context/index.ts +22 -0
  61. package/src/core/extensions/SkillGenerator.ts +421 -0
  62. package/src/core/extensions/SkillInstaller.d.ts.map +1 -0
  63. package/src/core/extensions/SkillInstaller.ts +257 -0
  64. package/src/core/extensions/SkillRegistry.d.ts.map +1 -0
  65. package/src/core/extensions/SkillRegistry.ts +361 -0
  66. package/src/core/extensions/SkillValidator.ts +525 -0
  67. package/src/core/extensions/index.ts +15 -0
  68. package/src/core/index.d.ts.map +1 -0
  69. package/src/core/index.ts +42 -0
  70. package/src/core/mcp/McpManager.d.ts.map +1 -0
  71. package/src/core/mcp/McpManager.ts +632 -0
  72. package/src/core/mcp/index.d.ts.map +1 -0
  73. package/src/core/mcp/index.ts +2 -0
  74. package/src/core/model/ModelClient.d.ts.map +1 -0
  75. package/src/core/model/ModelClient.ts +217 -0
  76. package/src/core/model/ModelConnectionTester.ts +363 -0
  77. package/src/core/model/ModelValidator.ts +348 -0
  78. package/src/core/model/index.d.ts.map +1 -0
  79. package/src/core/model/index.ts +6 -0
  80. package/src/core/model/providers/AnthropicProvider.d.ts.map +1 -0
  81. package/src/core/model/providers/AnthropicProvider.ts +279 -0
  82. package/src/core/model/providers/CodingPlanProvider.d.ts.map +1 -0
  83. package/src/core/model/providers/CodingPlanProvider.ts +210 -0
  84. package/src/core/model/providers/OllamaCloudProvider.d.ts.map +1 -0
  85. package/src/core/model/providers/OllamaCloudProvider.ts +405 -0
  86. package/src/core/model/providers/OllamaManager.d.ts.map +1 -0
  87. package/src/core/model/providers/OllamaManager.ts +201 -0
  88. package/src/core/model/providers/OllamaProvider.d.ts.map +1 -0
  89. package/src/core/model/providers/OllamaProvider.ts +73 -0
  90. package/src/core/model/providers/OpenAICompatibleProvider.d.ts.map +1 -0
  91. package/src/core/model/providers/OpenAICompatibleProvider.ts +327 -0
  92. package/src/core/model/providers/OpenAIProvider.d.ts.map +1 -0
  93. package/src/core/model/providers/OpenAIProvider.ts +29 -0
  94. package/src/core/model/providers/index.d.ts.map +1 -0
  95. package/src/core/model/providers/index.ts +12 -0
  96. package/src/core/model/types.d.ts.map +1 -0
  97. package/src/core/model/types.ts +77 -0
  98. package/src/core/security/ApprovalManager.d.ts.map +1 -0
  99. package/src/core/security/ApprovalManager.ts +174 -0
  100. package/src/core/security/FileFilter.d.ts.map +1 -0
  101. package/src/core/security/FileFilter.ts +141 -0
  102. package/src/core/security/HookExecutor.d.ts.map +1 -0
  103. package/src/core/security/HookExecutor.ts +178 -0
  104. package/src/core/security/SandboxExecutor.ts +447 -0
  105. package/src/core/security/index.d.ts.map +1 -0
  106. package/src/core/security/index.ts +8 -0
  107. package/src/core/session/AgentLoop.d.ts.map +1 -0
  108. package/src/core/session/AgentLoop.ts +501 -0
  109. package/src/core/session/SessionManager.d.ts.map +1 -0
  110. package/src/core/session/SessionManager.test.ts +183 -0
  111. package/src/core/session/SessionManager.ts +460 -0
  112. package/src/core/session/index.d.ts.map +1 -0
  113. package/src/core/session/index.ts +3 -0
  114. package/src/core/telemetry/Telemetry.d.ts.map +1 -0
  115. package/src/core/telemetry/Telemetry.ts +90 -0
  116. package/src/core/telemetry/TelemetryService.ts +531 -0
  117. package/src/core/telemetry/index.d.ts.map +1 -0
  118. package/src/core/telemetry/index.ts +12 -0
  119. package/src/core/testing/AutoFixer.ts +385 -0
  120. package/src/core/testing/ErrorAnalyzer.ts +499 -0
  121. package/src/core/testing/TestRunner.ts +265 -0
  122. package/src/core/testing/agent-cli-tests.ts +538 -0
  123. package/src/core/testing/index.ts +11 -0
  124. package/src/core/tools/ToolRegistry.d.ts.map +1 -0
  125. package/src/core/tools/ToolRegistry.test.ts +206 -0
  126. package/src/core/tools/ToolRegistry.ts +260 -0
  127. package/src/core/tools/impl/EditFileTool.d.ts.map +1 -0
  128. package/src/core/tools/impl/EditFileTool.ts +97 -0
  129. package/src/core/tools/impl/ListDirectoryTool.d.ts.map +1 -0
  130. package/src/core/tools/impl/ListDirectoryTool.ts +142 -0
  131. package/src/core/tools/impl/MemoryTool.d.ts.map +1 -0
  132. package/src/core/tools/impl/MemoryTool.ts +102 -0
  133. package/src/core/tools/impl/ReadFileTool.d.ts.map +1 -0
  134. package/src/core/tools/impl/ReadFileTool.ts +58 -0
  135. package/src/core/tools/impl/SearchContentTool.d.ts.map +1 -0
  136. package/src/core/tools/impl/SearchContentTool.ts +94 -0
  137. package/src/core/tools/impl/SearchFileTool.d.ts.map +1 -0
  138. package/src/core/tools/impl/SearchFileTool.ts +61 -0
  139. package/src/core/tools/impl/ShellTool.d.ts.map +1 -0
  140. package/src/core/tools/impl/ShellTool.ts +118 -0
  141. package/src/core/tools/impl/TaskTool.d.ts.map +1 -0
  142. package/src/core/tools/impl/TaskTool.ts +207 -0
  143. package/src/core/tools/impl/TodoTool.d.ts.map +1 -0
  144. package/src/core/tools/impl/TodoTool.ts +122 -0
  145. package/src/core/tools/impl/WebFetchTool.d.ts.map +1 -0
  146. package/src/core/tools/impl/WebFetchTool.ts +103 -0
  147. package/src/core/tools/impl/WebSearchTool.d.ts.map +1 -0
  148. package/src/core/tools/impl/WebSearchTool.ts +89 -0
  149. package/src/core/tools/impl/WriteFileTool.d.ts.map +1 -0
  150. package/src/core/tools/impl/WriteFileTool.ts +49 -0
  151. package/src/core/tools/impl/index.d.ts.map +1 -0
  152. package/src/core/tools/impl/index.ts +16 -0
  153. package/src/core/tools/index.d.ts.map +1 -0
  154. package/src/core/tools/index.ts +7 -0
  155. package/src/core/tools/schemas/execution.d.ts.map +1 -0
  156. package/src/core/tools/schemas/execution.ts +42 -0
  157. package/src/core/tools/schemas/file.d.ts.map +1 -0
  158. package/src/core/tools/schemas/file.ts +119 -0
  159. package/src/core/tools/schemas/index.d.ts.map +1 -0
  160. package/src/core/tools/schemas/index.ts +11 -0
  161. package/src/core/tools/schemas/memory.d.ts.map +1 -0
  162. package/src/core/tools/schemas/memory.ts +52 -0
  163. package/src/core/tools/schemas/orchestration.d.ts.map +1 -0
  164. package/src/core/tools/schemas/orchestration.ts +44 -0
  165. package/src/core/tools/schemas/search.d.ts.map +1 -0
  166. package/src/core/tools/schemas/search.ts +112 -0
  167. package/src/core/tools/schemas/todo.d.ts.map +1 -0
  168. package/src/core/tools/schemas/todo.ts +32 -0
  169. package/src/core/tools/schemas/web.d.ts.map +1 -0
  170. package/src/core/tools/schemas/web.ts +86 -0
  171. package/src/core/types/config.d.ts.map +1 -0
  172. package/src/core/types/config.ts +200 -0
  173. package/src/core/types/errors.d.ts.map +1 -0
  174. package/src/core/types/errors.ts +204 -0
  175. package/src/core/types/index.d.ts.map +1 -0
  176. package/src/core/types/index.ts +8 -0
  177. package/src/core/types/session.d.ts.map +1 -0
  178. package/src/core/types/session.ts +216 -0
  179. package/src/core/types/tools.d.ts.map +1 -0
  180. package/src/core/types/tools.ts +157 -0
  181. package/src/core/utils/CheckpointManager.d.ts.map +1 -0
  182. package/src/core/utils/CheckpointManager.ts +327 -0
  183. package/src/core/utils/Logger.d.ts.map +1 -0
  184. package/src/core/utils/Logger.ts +98 -0
  185. package/src/core/utils/RetryManager.ts +471 -0
  186. package/src/core/utils/TokenCounter.d.ts.map +1 -0
  187. package/src/core/utils/TokenCounter.ts +414 -0
  188. package/src/core/utils/VectorMemoryStore.ts +440 -0
  189. package/src/core/utils/helpers.d.ts.map +1 -0
  190. package/src/core/utils/helpers.ts +89 -0
  191. package/src/core/utils/index.d.ts.map +1 -0
  192. package/src/core/utils/index.ts +19 -0
@@ -0,0 +1,531 @@
1
+ // ============================================================================
2
+ // TelemetryService - Observability and metrics collection
3
+ // ============================================================================
4
+
5
+ import * as fs from 'node:fs';
6
+ import * as path from 'node:path';
7
+ import { createLogger } from '../utils/Logger.js';
8
+
9
+ const logger = createLogger('TelemetryService');
10
+
11
+ // ============================================================================
12
+ // Types
13
+ // ============================================================================
14
+
15
+ /**
16
+ * Metric types
17
+ */
18
+ export type MetricType = 'counter' | 'gauge' | 'histogram';
19
+
20
+ /**
21
+ * Metric value
22
+ */
23
+ export interface MetricValue {
24
+ name: string;
25
+ type: MetricType;
26
+ value: number;
27
+ timestamp: Date;
28
+ labels: Record<string, string>;
29
+ }
30
+
31
+ /**
32
+ * Metric definition
33
+ */
34
+ export interface MetricDefinition {
35
+ name: string;
36
+ type: MetricType;
37
+ description: string;
38
+ unit?: string;
39
+ labels?: string[];
40
+ }
41
+
42
+ /**
43
+ * Telemetry configuration
44
+ */
45
+ export interface TelemetryConfig {
46
+ enabled: boolean;
47
+ serviceName: string;
48
+ exportInterval: number; // milliseconds
49
+ storageDir: string;
50
+ exporters: ('file' | 'prometheus')[];
51
+ }
52
+
53
+ // ============================================================================
54
+ // Built-in Metrics
55
+ // ============================================================================
56
+
57
+ const BUILTIN_METRICS: MetricDefinition[] = [
58
+ { name: 'nova_requests_total', type: 'counter', description: 'Total number of requests' },
59
+ { name: 'nova_request_duration_ms', type: 'histogram', description: 'Request duration in milliseconds' },
60
+ { name: 'nova_tokens_input_total', type: 'counter', description: 'Total input tokens used' },
61
+ { name: 'nova_tokens_output_total', type: 'counter', description: 'Total output tokens used' },
62
+ { name: 'nova_tool_calls_total', type: 'counter', description: 'Total tool calls' },
63
+ { name: 'nova_tool_duration_ms', type: 'histogram', description: 'Tool execution duration' },
64
+ { name: 'nova_errors_total', type: 'counter', description: 'Total errors' },
65
+ { name: 'nova_context_compressions_total', type: 'counter', description: 'Context compression count' },
66
+ { name: 'nova_session_count', type: 'gauge', description: 'Active sessions' },
67
+ { name: 'nova_memory_usage_bytes', type: 'gauge', description: 'Memory usage in bytes' },
68
+ ];
69
+
70
+ // ============================================================================
71
+ // TelemetryService
72
+ // ============================================================================
73
+
74
+ /**
75
+ * Telemetry and observability service
76
+ */
77
+ export class TelemetryService {
78
+ private config: TelemetryConfig;
79
+ private metrics: Map<string, MetricValue[]> = new Map();
80
+ private counters: Map<string, number> = new Map();
81
+ private gauges: Map<string, number> = new Map();
82
+ private histograms: Map<string, number[]> = new Map();
83
+ private exportInterval: NodeJS.Timeout | null = null;
84
+
85
+ constructor(config: Partial<TelemetryConfig> = {}) {
86
+ this.config = {
87
+ enabled: config.enabled ?? true,
88
+ serviceName: config.serviceName ?? 'nova-cli',
89
+ exportInterval: config.exportInterval ?? 60000,
90
+ storageDir: config.storageDir ?? path.join(process.env.HOME ?? '~', '.nova', 'telemetry'),
91
+ exporters: config.exporters ?? ['file'],
92
+ };
93
+
94
+ // Initialize built-in metrics
95
+ for (const metric of BUILTIN_METRICS) {
96
+ this.registerMetric(metric);
97
+ }
98
+
99
+ // Ensure storage directory exists
100
+ if (!fs.existsSync(this.config.storageDir)) {
101
+ fs.mkdirSync(this.config.storageDir, { recursive: true });
102
+ }
103
+
104
+ // Start export interval
105
+ if (this.config.enabled) {
106
+ this.exportInterval = setInterval(() => this.export(), this.config.exportInterval);
107
+ }
108
+
109
+ // Export on exit
110
+ process.on('beforeExit', () => this.export());
111
+ }
112
+
113
+ // -----------------------------------------------------------------------
114
+ // Metric Registration
115
+ // -----------------------------------------------------------------------
116
+
117
+ /**
118
+ * Register a new metric
119
+ */
120
+ registerMetric(definition: MetricDefinition): void {
121
+ if (!this.metrics.has(definition.name)) {
122
+ this.metrics.set(definition.name, []);
123
+ }
124
+ logger.debug(`Registered metric: ${definition.name}`);
125
+ }
126
+
127
+ // -----------------------------------------------------------------------
128
+ // Recording Metrics
129
+ // -----------------------------------------------------------------------
130
+
131
+ /**
132
+ * Increment a counter
133
+ */
134
+ incrementCounter(name: string, value: number = 1, labels: Record<string, string> = {}): void {
135
+ if (!this.config.enabled) return;
136
+
137
+ const key = this.getMetricKey(name, labels);
138
+ const current = this.counters.get(key) ?? 0;
139
+ this.counters.set(key, current + value);
140
+
141
+ this.recordMetric(name, 'counter', current + value, labels);
142
+ }
143
+
144
+ /**
145
+ * Set a gauge value
146
+ */
147
+ setGauge(name: string, value: number, labels: Record<string, string> = {}): void {
148
+ if (!this.config.enabled) return;
149
+
150
+ const key = this.getMetricKey(name, labels);
151
+ this.gauges.set(key, value);
152
+
153
+ this.recordMetric(name, 'gauge', value, labels);
154
+ }
155
+
156
+ /**
157
+ * Record a histogram observation
158
+ */
159
+ observeHistogram(name: string, value: number, labels: Record<string, string> = {}): void {
160
+ if (!this.config.enabled) return;
161
+
162
+ const key = this.getMetricKey(name, labels);
163
+ const values = this.histograms.get(key) ?? [];
164
+ values.push(value);
165
+
166
+ // Keep only last 1000 observations
167
+ if (values.length > 1000) {
168
+ values.shift();
169
+ }
170
+
171
+ this.histograms.set(key, values);
172
+ this.recordMetric(name, 'histogram', value, labels);
173
+ }
174
+
175
+ /**
176
+ * Record a timing
177
+ */
178
+ recordTiming(name: string, durationMs: number, labels: Record<string, string> = {}): void {
179
+ this.observeHistogram(name, durationMs, labels);
180
+ }
181
+
182
+ /**
183
+ * Time an async function
184
+ */
185
+ async time<T>(name: string, fn: () => Promise<T>, labels: Record<string, string> = {}): Promise<T> {
186
+ const start = Date.now();
187
+ try {
188
+ const result = await fn();
189
+ this.recordTiming(name, Date.now() - start, labels);
190
+ return result;
191
+ } catch (error) {
192
+ this.recordTiming(name, Date.now() - start, { ...labels, error: 'true' });
193
+ throw error;
194
+ }
195
+ }
196
+
197
+ // -----------------------------------------------------------------------
198
+ // Convenience Methods
199
+ // -----------------------------------------------------------------------
200
+
201
+ /**
202
+ * Record a model API call
203
+ */
204
+ recordModelCall(
205
+ model: string,
206
+ provider: string,
207
+ inputTokens: number,
208
+ outputTokens: number,
209
+ durationMs: number
210
+ ): void {
211
+ const labels = { model, provider };
212
+
213
+ this.incrementCounter('nova_requests_total', 1, labels);
214
+ this.recordTiming('nova_request_duration_ms', durationMs, labels);
215
+ this.incrementCounter('nova_tokens_input_total', inputTokens, labels);
216
+ this.incrementCounter('nova_tokens_output_total', outputTokens, labels);
217
+ }
218
+
219
+ /**
220
+ * Record a tool call
221
+ */
222
+ recordToolCall(toolName: string, durationMs: number, success: boolean): void {
223
+ const labels = { tool: toolName, success: success.toString() };
224
+
225
+ this.incrementCounter('nova_tool_calls_total', 1, labels);
226
+ this.recordTiming('nova_tool_duration_ms', durationMs, labels);
227
+
228
+ if (!success) {
229
+ this.incrementCounter('nova_errors_total', 1, { type: 'tool', tool: toolName });
230
+ }
231
+ }
232
+
233
+ /**
234
+ * Record an error
235
+ */
236
+ recordError(type: string, message: string): void {
237
+ this.incrementCounter('nova_errors_total', 1, { type, message: message.slice(0, 100) });
238
+ }
239
+
240
+ /**
241
+ * Update session count
242
+ */
243
+ updateSessionCount(count: number): void {
244
+ this.setGauge('nova_session_count', count);
245
+ }
246
+
247
+ // -----------------------------------------------------------------------
248
+ // Export
249
+ // -----------------------------------------------------------------------
250
+
251
+ /**
252
+ * Export metrics to configured exporters
253
+ */
254
+ export(): void {
255
+ if (!this.config.enabled) return;
256
+
257
+ for (const exporter of this.config.exporters) {
258
+ switch (exporter) {
259
+ case 'file':
260
+ this.exportToFile();
261
+ break;
262
+ case 'prometheus':
263
+ this.exportToPrometheus();
264
+ break;
265
+ }
266
+ }
267
+ }
268
+
269
+ /**
270
+ * Export to file
271
+ */
272
+ private exportToFile(): void {
273
+ const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
274
+ const filename = `metrics-${timestamp}.json`;
275
+ const filepath = path.join(this.config.storageDir, filename);
276
+
277
+ const data = {
278
+ timestamp: new Date().toISOString(),
279
+ serviceName: this.config.serviceName,
280
+ counters: Object.fromEntries(this.counters),
281
+ gauges: Object.fromEntries(this.gauges),
282
+ histograms: Object.fromEntries(
283
+ Array.from(this.histograms.entries()).map(([k, v]) => [
284
+ k,
285
+ {
286
+ count: v.length,
287
+ sum: v.reduce((a, b) => a + b, 0),
288
+ min: Math.min(...v),
289
+ max: Math.max(...v),
290
+ avg: v.length > 0 ? v.reduce((a, b) => a + b, 0) / v.length : 0,
291
+ p50: this.percentile(v, 50),
292
+ p95: this.percentile(v, 95),
293
+ p99: this.percentile(v, 99),
294
+ },
295
+ ])
296
+ ),
297
+ };
298
+
299
+ try {
300
+ fs.writeFileSync(filepath, JSON.stringify(data, null, 2), 'utf-8');
301
+ logger.debug(`Exported metrics to ${filepath}`);
302
+
303
+ // Clean up old files (keep last 100)
304
+ this.cleanupOldFiles(100);
305
+ } catch (error) {
306
+ logger.error('Failed to export metrics', { error });
307
+ }
308
+ }
309
+
310
+ /**
311
+ * Export in Prometheus format
312
+ */
313
+ private exportToPrometheus(): void {
314
+ // Prometheus format text file for scraping
315
+ const lines: string[] = [];
316
+
317
+ // Counters
318
+ for (const [key, value] of this.counters) {
319
+ const [name, labelsStr] = this.parseMetricKey(key);
320
+ lines.push(`# TYPE ${name} counter`);
321
+ lines.push(`${name}${labelsStr} ${value}`);
322
+ }
323
+
324
+ // Gauges
325
+ for (const [key, value] of this.gauges) {
326
+ const [name, labelsStr] = this.parseMetricKey(key);
327
+ lines.push(`# TYPE ${name} gauge`);
328
+ lines.push(`${name}${labelsStr} ${value}`);
329
+ }
330
+
331
+ // Histograms
332
+ for (const [key, values] of this.histograms) {
333
+ const [name, labelsStr] = this.parseMetricKey(key);
334
+ lines.push(`# TYPE ${name} histogram`);
335
+
336
+ const sum = values.reduce((a, b) => a + b, 0);
337
+ const count = values.length;
338
+
339
+ lines.push(`${name}_sum${labelsStr} ${sum}`);
340
+ lines.push(`${name}_count${labelsStr} ${count}`);
341
+ }
342
+
343
+ const filepath = path.join(this.config.storageDir, 'prometheus.txt');
344
+ try {
345
+ fs.writeFileSync(filepath, lines.join('\n'), 'utf-8');
346
+ } catch (error) {
347
+ logger.error('Failed to export Prometheus metrics', { error });
348
+ }
349
+ }
350
+
351
+ // -----------------------------------------------------------------------
352
+ // Querying
353
+ // -----------------------------------------------------------------------
354
+
355
+ /**
356
+ * Get current metric values
357
+ */
358
+ getMetric(name: string): MetricValue[] {
359
+ return this.metrics.get(name) ?? [];
360
+ }
361
+
362
+ /**
363
+ * Get all metric values
364
+ */
365
+ getAllMetrics(): Map<string, MetricValue[]> {
366
+ return new Map(this.metrics);
367
+ }
368
+
369
+ /**
370
+ * Get summary statistics
371
+ */
372
+ getSummary(): {
373
+ totalRequests: number;
374
+ totalTokens: { input: number; output: number };
375
+ totalToolCalls: number;
376
+ totalErrors: number;
377
+ avgRequestDuration: number;
378
+ } {
379
+ const totalRequests = Array.from(this.counters.entries())
380
+ .filter(([k]) => k.startsWith('nova_requests_total'))
381
+ .reduce((sum, [, v]) => sum + v, 0);
382
+
383
+ const totalInputTokens = Array.from(this.counters.entries())
384
+ .filter(([k]) => k.startsWith('nova_tokens_input_total'))
385
+ .reduce((sum, [, v]) => sum + v, 0);
386
+
387
+ const totalOutputTokens = Array.from(this.counters.entries())
388
+ .filter(([k]) => k.startsWith('nova_tokens_output_total'))
389
+ .reduce((sum, [, v]) => sum + v, 0);
390
+
391
+ const totalToolCalls = Array.from(this.counters.entries())
392
+ .filter(([k]) => k.startsWith('nova_tool_calls_total'))
393
+ .reduce((sum, [, v]) => sum + v, 0);
394
+
395
+ const totalErrors = Array.from(this.counters.entries())
396
+ .filter(([k]) => k.startsWith('nova_errors_total'))
397
+ .reduce((sum, [, v]) => sum + v, 0);
398
+
399
+ // Calculate average request duration
400
+ const durations: number[] = [];
401
+ for (const [, values] of this.histograms) {
402
+ durations.push(...values);
403
+ }
404
+ const avgRequestDuration = durations.length > 0
405
+ ? durations.reduce((a, b) => a + b, 0) / durations.length
406
+ : 0;
407
+
408
+ return {
409
+ totalRequests,
410
+ totalTokens: { input: totalInputTokens, output: totalOutputTokens },
411
+ totalToolCalls,
412
+ totalErrors,
413
+ avgRequestDuration,
414
+ };
415
+ }
416
+
417
+ // -----------------------------------------------------------------------
418
+ // Utilities
419
+ // -----------------------------------------------------------------------
420
+
421
+ /**
422
+ * Get metric key with labels
423
+ */
424
+ private getMetricKey(name: string, labels: Record<string, string>): string {
425
+ if (Object.keys(labels).length === 0) return name;
426
+ const labelsStr = Object.entries(labels)
427
+ .map(([k, v]) => `${k}="${v}"`)
428
+ .join(',');
429
+ return `${name}{${labelsStr}}`;
430
+ }
431
+
432
+ /**
433
+ * Parse metric key to name and labels string
434
+ */
435
+ private parseMetricKey(key: string): [string, string] {
436
+ const match = key.match(/^([a-zA-Z_]+)(\{.*\})?$/);
437
+ if (!match || !match[1]) return [key, ''];
438
+ return [match[1], match[2] || ''];
439
+ }
440
+
441
+ /**
442
+ * Record a metric value
443
+ */
444
+ private recordMetric(
445
+ name: string,
446
+ type: MetricType,
447
+ value: number,
448
+ labels: Record<string, string>
449
+ ): void {
450
+ const values = this.metrics.get(name) ?? [];
451
+ values.push({
452
+ name,
453
+ type,
454
+ value,
455
+ timestamp: new Date(),
456
+ labels,
457
+ });
458
+
459
+ // Keep only last 1000 values
460
+ if (values.length > 1000) {
461
+ values.shift();
462
+ }
463
+
464
+ this.metrics.set(name, values);
465
+ }
466
+
467
+ /**
468
+ * Calculate percentile
469
+ */
470
+ private percentile(values: number[], p: number): number {
471
+ if (values.length === 0) return 0;
472
+ const sorted = [...values].sort((a, b) => a - b);
473
+ const idx = Math.ceil((p / 100) * sorted.length) - 1;
474
+ return sorted[Math.max(0, idx)] ?? 0;
475
+ }
476
+
477
+ /**
478
+ * Clean up old files
479
+ */
480
+ private cleanupOldFiles(keep: number): void {
481
+ try {
482
+ const files = fs.readdirSync(this.config.storageDir)
483
+ .filter(f => f.startsWith('metrics-') && f.endsWith('.json'))
484
+ .sort()
485
+ .reverse();
486
+
487
+ for (let i = keep; i < files.length; i++) {
488
+ const file = files[i];
489
+ if (file) {
490
+ fs.unlinkSync(path.join(this.config.storageDir, file));
491
+ }
492
+ }
493
+ } catch {
494
+ // Ignore cleanup errors
495
+ }
496
+ }
497
+
498
+ /**
499
+ * Shutdown telemetry
500
+ */
501
+ shutdown(): void {
502
+ if (this.exportInterval) {
503
+ clearInterval(this.exportInterval);
504
+ this.exportInterval = null;
505
+ }
506
+ this.export();
507
+ }
508
+ }
509
+
510
+ // ============================================================================
511
+ // Factory function
512
+ // ============================================================================
513
+
514
+ let defaultInstance: TelemetryService | null = null;
515
+
516
+ /**
517
+ * Get the default telemetry service instance
518
+ */
519
+ export function getTelemetry(): TelemetryService {
520
+ if (!defaultInstance) {
521
+ defaultInstance = new TelemetryService();
522
+ }
523
+ return defaultInstance;
524
+ }
525
+
526
+ /**
527
+ * Create a new telemetry service
528
+ */
529
+ export function createTelemetry(config: Partial<TelemetryConfig> = {}): TelemetryService {
530
+ return new TelemetryService(config);
531
+ }
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAC3C,YAAY,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC"}
@@ -0,0 +1,12 @@
1
+ export { Telemetry } from './Telemetry.js';
2
+ export type { TelemetryEvent } from './Telemetry.js';
3
+ export {
4
+ TelemetryService,
5
+ getTelemetry,
6
+ createTelemetry
7
+ } from './TelemetryService.js';
8
+ export type {
9
+ MetricType,
10
+ MetricValue,
11
+ MetricDefinition
12
+ } from './TelemetryService.js';