neuro-cli 4.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.
Files changed (175) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +510 -0
  3. package/dist/advisor/advisor.d.ts +50 -0
  4. package/dist/advisor/advisor.js +178 -0
  5. package/dist/agents/base.d.ts +62 -0
  6. package/dist/agents/base.js +215 -0
  7. package/dist/agents/orchestrator.d.ts +46 -0
  8. package/dist/agents/orchestrator.js +192 -0
  9. package/dist/agents/team.d.ts +51 -0
  10. package/dist/agents/team.js +210 -0
  11. package/dist/api/models.d.ts +23 -0
  12. package/dist/api/models.js +514 -0
  13. package/dist/api/ollama.d.ts +153 -0
  14. package/dist/api/ollama.js +751 -0
  15. package/dist/api/openrouter.d.ts +55 -0
  16. package/dist/api/openrouter.js +223 -0
  17. package/dist/commands/commands.d.ts +43 -0
  18. package/dist/commands/commands.js +308 -0
  19. package/dist/config/config.d.ts +8 -0
  20. package/dist/config/config.js +311 -0
  21. package/dist/context/compaction.d.ts +54 -0
  22. package/dist/context/compaction.js +251 -0
  23. package/dist/context/custom-agents.d.ts +107 -0
  24. package/dist/context/custom-agents.js +397 -0
  25. package/dist/context/custom-tools.d.ts +120 -0
  26. package/dist/context/custom-tools.js +564 -0
  27. package/dist/context/git-checkpoint.d.ts +52 -0
  28. package/dist/context/git-checkpoint.js +240 -0
  29. package/dist/context/neuro-md.d.ts +48 -0
  30. package/dist/context/neuro-md.js +202 -0
  31. package/dist/context/neuroignore.d.ts +102 -0
  32. package/dist/context/neuroignore.js +441 -0
  33. package/dist/context/repo-map.d.ts +38 -0
  34. package/dist/context/repo-map.js +220 -0
  35. package/dist/context/skill-standard.d.ts +262 -0
  36. package/dist/context/skill-standard.js +1156 -0
  37. package/dist/context/skill-system.d.ts +75 -0
  38. package/dist/context/skill-system.js +578 -0
  39. package/dist/context/tree-sitter.d.ts +99 -0
  40. package/dist/context/tree-sitter.js +1956 -0
  41. package/dist/core/acp.d.ts +325 -0
  42. package/dist/core/acp.js +1498 -0
  43. package/dist/core/api-server.d.ts +143 -0
  44. package/dist/core/api-server.js +550 -0
  45. package/dist/core/approval.d.ts +81 -0
  46. package/dist/core/approval.js +432 -0
  47. package/dist/core/auto-compact.d.ts +127 -0
  48. package/dist/core/auto-compact.js +436 -0
  49. package/dist/core/auto-mode.d.ts +232 -0
  50. package/dist/core/auto-mode.js +831 -0
  51. package/dist/core/background-session.d.ts +166 -0
  52. package/dist/core/background-session.js +696 -0
  53. package/dist/core/cicd.d.ts +257 -0
  54. package/dist/core/cicd.js +1443 -0
  55. package/dist/core/cloud-sync.d.ts +156 -0
  56. package/dist/core/cloud-sync.js +582 -0
  57. package/dist/core/code-review.d.ts +132 -0
  58. package/dist/core/code-review.js +1191 -0
  59. package/dist/core/completion.d.ts +49 -0
  60. package/dist/core/completion.js +384 -0
  61. package/dist/core/context.d.ts +38 -0
  62. package/dist/core/context.js +144 -0
  63. package/dist/core/diff-preview.d.ts +35 -0
  64. package/dist/core/diff-preview.js +173 -0
  65. package/dist/core/doom-loop.d.ts +51 -0
  66. package/dist/core/doom-loop.js +179 -0
  67. package/dist/core/engine.d.ts +183 -0
  68. package/dist/core/engine.js +942 -0
  69. package/dist/core/extended-thinking.d.ts +103 -0
  70. package/dist/core/extended-thinking.js +269 -0
  71. package/dist/core/fallback.d.ts +54 -0
  72. package/dist/core/fallback.js +104 -0
  73. package/dist/core/git-worktree.d.ts +139 -0
  74. package/dist/core/git-worktree.js +614 -0
  75. package/dist/core/headless.d.ts +30 -0
  76. package/dist/core/headless.js +101 -0
  77. package/dist/core/i18n.d.ts +83 -0
  78. package/dist/core/i18n.js +739 -0
  79. package/dist/core/linting.d.ts +129 -0
  80. package/dist/core/linting.js +699 -0
  81. package/dist/core/model-router.d.ts +109 -0
  82. package/dist/core/model-router.js +581 -0
  83. package/dist/core/multi-model.d.ts +243 -0
  84. package/dist/core/multi-model.js +1099 -0
  85. package/dist/core/multi-session.d.ts +144 -0
  86. package/dist/core/multi-session.js +442 -0
  87. package/dist/core/multimodal.d.ts +125 -0
  88. package/dist/core/multimodal.js +286 -0
  89. package/dist/core/observability.d.ts +93 -0
  90. package/dist/core/observability.js +737 -0
  91. package/dist/core/os-sandbox.d.ts +122 -0
  92. package/dist/core/os-sandbox.js +1193 -0
  93. package/dist/core/outcome-grading.d.ts +228 -0
  94. package/dist/core/outcome-grading.js +1123 -0
  95. package/dist/core/output-styles.d.ts +57 -0
  96. package/dist/core/output-styles.js +382 -0
  97. package/dist/core/parallel-agents.d.ts +183 -0
  98. package/dist/core/parallel-agents.js +563 -0
  99. package/dist/core/plugin-bundle.d.ts +236 -0
  100. package/dist/core/plugin-bundle.js +887 -0
  101. package/dist/core/plugin-sdk.d.ts +139 -0
  102. package/dist/core/plugin-sdk.js +273 -0
  103. package/dist/core/prompt-cache.d.ts +163 -0
  104. package/dist/core/prompt-cache.js +599 -0
  105. package/dist/core/sandbox.d.ts +127 -0
  106. package/dist/core/sandbox.js +369 -0
  107. package/dist/core/scheduled-tasks.d.ts +151 -0
  108. package/dist/core/scheduled-tasks.js +502 -0
  109. package/dist/core/security-scanner.d.ts +160 -0
  110. package/dist/core/security-scanner.js +1494 -0
  111. package/dist/core/session.d.ts +83 -0
  112. package/dist/core/session.js +269 -0
  113. package/dist/core/shell-completion.d.ts +51 -0
  114. package/dist/core/shell-completion.js +674 -0
  115. package/dist/core/smart-monitor.d.ts +146 -0
  116. package/dist/core/smart-monitor.js +1199 -0
  117. package/dist/core/spec-driven.d.ts +233 -0
  118. package/dist/core/spec-driven.js +1485 -0
  119. package/dist/core/spending-warnings.d.ts +123 -0
  120. package/dist/core/spending-warnings.js +456 -0
  121. package/dist/core/sub-agent.d.ts +298 -0
  122. package/dist/core/sub-agent.js +1023 -0
  123. package/dist/core/telemetry.d.ts +157 -0
  124. package/dist/core/telemetry.js +412 -0
  125. package/dist/core/terminal-ux.d.ts +134 -0
  126. package/dist/core/terminal-ux.js +649 -0
  127. package/dist/core/testing.d.ts +146 -0
  128. package/dist/core/testing.js +1200 -0
  129. package/dist/core/types.d.ts +439 -0
  130. package/dist/core/types.js +6 -0
  131. package/dist/core/undo-redo.d.ts +112 -0
  132. package/dist/core/undo-redo.js +290 -0
  133. package/dist/core/updater.d.ts +159 -0
  134. package/dist/core/updater.js +608 -0
  135. package/dist/core/vim-mode.d.ts +151 -0
  136. package/dist/core/vim-mode.js +771 -0
  137. package/dist/core/voice.d.ts +137 -0
  138. package/dist/core/voice.js +538 -0
  139. package/dist/core/web-dashboard.d.ts +109 -0
  140. package/dist/core/web-dashboard.js +484 -0
  141. package/dist/hooks/hooks.d.ts +74 -0
  142. package/dist/hooks/hooks.js +160 -0
  143. package/dist/hooks/llm-evaluator.d.ts +165 -0
  144. package/dist/hooks/llm-evaluator.js +560 -0
  145. package/dist/index.d.ts +3 -0
  146. package/dist/index.js +1186 -0
  147. package/dist/lsp/lsp-manager.d.ts +63 -0
  148. package/dist/lsp/lsp-manager.js +351 -0
  149. package/dist/mcp/client.d.ts +133 -0
  150. package/dist/mcp/client.js +684 -0
  151. package/dist/mcp/mcp-apps.d.ts +70 -0
  152. package/dist/mcp/mcp-apps.js +1007 -0
  153. package/dist/tools/bash.d.ts +5 -0
  154. package/dist/tools/bash.js +195 -0
  155. package/dist/tools/browser.d.ts +92 -0
  156. package/dist/tools/browser.js +1570 -0
  157. package/dist/tools/extended.d.ts +6 -0
  158. package/dist/tools/extended.js +191 -0
  159. package/dist/tools/file.d.ts +10 -0
  160. package/dist/tools/file.js +382 -0
  161. package/dist/tools/github.d.ts +389 -0
  162. package/dist/tools/github.js +759 -0
  163. package/dist/tools/index.d.ts +9 -0
  164. package/dist/tools/index.js +40 -0
  165. package/dist/tools/memory.d.ts +6 -0
  166. package/dist/tools/memory.js +197 -0
  167. package/dist/tools/registry.d.ts +29 -0
  168. package/dist/tools/registry.js +64 -0
  169. package/dist/tools/web.d.ts +6 -0
  170. package/dist/tools/web.js +150 -0
  171. package/dist/ui/renderer.d.ts +97 -0
  172. package/dist/ui/renderer.js +279 -0
  173. package/dist/ui/theme.d.ts +27 -0
  174. package/dist/ui/theme.js +106 -0
  175. package/package.json +83 -0
@@ -0,0 +1,737 @@
1
+ // ============================================================
2
+ // NeuroCLI - GAP-36: OpenTelemetry / Observability Integration
3
+ // Zero-dependency OTLP JSON over HTTP export, custom spans,
4
+ // metrics, session tracing, and console pretty-printer.
5
+ // Compatible with Datadog, Jaeger, Dynatrace, Grafana.
6
+ // ============================================================
7
+ import chalk from 'chalk';
8
+ // -----------------------------------------------------------
9
+ // OTLP constants
10
+ // -----------------------------------------------------------
11
+ const SPAN_KIND_MAP = {
12
+ internal: 1,
13
+ server: 2,
14
+ client: 3,
15
+ };
16
+ const STATUS_CODE_MAP = {
17
+ ok: 1,
18
+ error: 2,
19
+ };
20
+ const DEFAULT_HISTOGRAM_BUCKETS = [0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10];
21
+ // -----------------------------------------------------------
22
+ // ID generation (crypto-quality random hex)
23
+ // -----------------------------------------------------------
24
+ function generateTraceId() {
25
+ const bytes = new Uint8Array(16);
26
+ globalThis.crypto.getRandomValues(bytes);
27
+ return Array.from(bytes, b => b.toString(16).padStart(2, '0')).join('');
28
+ }
29
+ function generateSpanId() {
30
+ const bytes = new Uint8Array(8);
31
+ globalThis.crypto.getRandomValues(bytes);
32
+ return Array.from(bytes, b => b.toString(16).padStart(2, '0')).join('');
33
+ }
34
+ // -----------------------------------------------------------
35
+ // OTLP JSON encoders
36
+ // -----------------------------------------------------------
37
+ function encodeAttributeValue(value) {
38
+ if (typeof value === 'string') {
39
+ return { stringValue: value };
40
+ }
41
+ if (typeof value === 'number') {
42
+ return Number.isInteger(value)
43
+ ? { intValue: value }
44
+ : { doubleValue: value };
45
+ }
46
+ if (typeof value === 'boolean') {
47
+ return { boolValue: value };
48
+ }
49
+ return { stringValue: String(value) };
50
+ }
51
+ function encodeAttributes(attrs) {
52
+ return Object.entries(attrs).map(([key, value]) => ({
53
+ key,
54
+ value: encodeAttributeValue(value),
55
+ }));
56
+ }
57
+ function encodeSpanToOTLP(span, serviceName) {
58
+ return {
59
+ traceId: span.traceId,
60
+ spanId: span.spanId,
61
+ parentSpanId: span.parentSpanId ?? '',
62
+ name: span.name,
63
+ kind: SPAN_KIND_MAP[span.kind] ?? 1,
64
+ startTimeUnixNano: String(span.startTime * 1_000_000),
65
+ endTimeUnixNano: String(span.endTime * 1_000_000),
66
+ attributes: encodeAttributes({ 'service.name': serviceName, ...span.attributes }),
67
+ events: span.events.map(evt => ({
68
+ name: evt.name,
69
+ timeUnixNano: String(evt.timestamp * 1_000_000),
70
+ attributes: encodeAttributes(evt.attributes),
71
+ })),
72
+ status: {
73
+ code: STATUS_CODE_MAP[span.status.code] ?? 0,
74
+ message: span.status.message ?? '',
75
+ },
76
+ };
77
+ }
78
+ function buildTracesPayload(spans, serviceName) {
79
+ return {
80
+ resourceSpans: [{
81
+ resource: {
82
+ attributes: [{ key: 'service.name', value: { stringValue: serviceName } }],
83
+ },
84
+ scopeSpans: [{
85
+ scope: { name: serviceName, version: '1.0.0' },
86
+ spans: spans.map(s => encodeSpanToOTLP(s, serviceName)),
87
+ }],
88
+ }],
89
+ };
90
+ }
91
+ function buildMetricsPayload(metrics, serviceName) {
92
+ const grouped = new Map();
93
+ for (const m of metrics) {
94
+ const key = `${m.name}:${m.type}`;
95
+ const list = grouped.get(key) ?? [];
96
+ list.push(m);
97
+ grouped.set(key, list);
98
+ }
99
+ const otlpMetrics = [];
100
+ for (const [, group] of grouped) {
101
+ const first = group[0];
102
+ const dataPoints = group.map(m => ({
103
+ asDouble: m.value,
104
+ timeUnixNano: String(m.timestamp * 1_000_000),
105
+ attributes: encodeAttributes(m.attributes),
106
+ }));
107
+ let metricData;
108
+ if (first.type === 'counter') {
109
+ metricData = { sum: { dataPoints, isMonotonic: true } };
110
+ }
111
+ else if (first.type === 'gauge') {
112
+ metricData = { gauge: { dataPoints } };
113
+ }
114
+ else {
115
+ const values = group.map(m => m.value);
116
+ const sorted = [...values].sort((a, b) => a - b);
117
+ const count = sorted.length;
118
+ const sum = sorted.reduce((a, b) => a + b, 0);
119
+ const min = sorted[0] ?? 0;
120
+ const max = sorted[count - 1] ?? 0;
121
+ const bucketCounts = new Array(DEFAULT_HISTOGRAM_BUCKETS.length + 1).fill(0);
122
+ for (const v of sorted) {
123
+ let placed = false;
124
+ for (let i = 0; i < DEFAULT_HISTOGRAM_BUCKETS.length; i++) {
125
+ if (v <= DEFAULT_HISTOGRAM_BUCKETS[i]) {
126
+ bucketCounts[i]++;
127
+ placed = true;
128
+ break;
129
+ }
130
+ }
131
+ if (!placed)
132
+ bucketCounts[DEFAULT_HISTOGRAM_BUCKETS.length]++;
133
+ }
134
+ metricData = {
135
+ histogram: {
136
+ dataPoints: [{
137
+ asDouble: sum,
138
+ timeUnixNano: String(first.timestamp * 1_000_000),
139
+ attributes: encodeAttributes(first.attributes),
140
+ count,
141
+ sum,
142
+ min,
143
+ max,
144
+ explicitBounds: DEFAULT_HISTOGRAM_BUCKETS,
145
+ bucketCounts,
146
+ }],
147
+ },
148
+ };
149
+ }
150
+ otlpMetrics.push({ name: first.name, ...metricData });
151
+ }
152
+ return {
153
+ resourceMetrics: [{
154
+ resource: {
155
+ attributes: [{ key: 'service.name', value: { stringValue: serviceName } }],
156
+ },
157
+ scopeMetrics: [{
158
+ scope: { name: serviceName, version: '1.0.0' },
159
+ metrics: otlpMetrics,
160
+ }],
161
+ }],
162
+ };
163
+ }
164
+ // -----------------------------------------------------------
165
+ // Ring buffer (fixed-capacity, overwrites oldest)
166
+ // -----------------------------------------------------------
167
+ class RingBuffer {
168
+ buffer;
169
+ head = 0;
170
+ tail = 0;
171
+ size = 0;
172
+ capacity;
173
+ constructor(capacity) {
174
+ this.capacity = capacity;
175
+ this.buffer = new Array(capacity);
176
+ }
177
+ push(item) {
178
+ this.buffer[this.tail] = item;
179
+ this.tail = (this.tail + 1) % this.capacity;
180
+ if (this.size === this.capacity) {
181
+ this.head = (this.head + 1) % this.capacity;
182
+ }
183
+ else {
184
+ this.size++;
185
+ }
186
+ }
187
+ items() {
188
+ const result = [];
189
+ for (let i = 0; i < this.size; i++) {
190
+ const idx = (this.head + i) % this.capacity;
191
+ const item = this.buffer[idx];
192
+ if (item !== undefined)
193
+ result.push(item);
194
+ }
195
+ return result;
196
+ }
197
+ clear() {
198
+ this.buffer = new Array(this.capacity);
199
+ this.head = 0;
200
+ this.tail = 0;
201
+ this.size = 0;
202
+ }
203
+ get length() {
204
+ return this.size;
205
+ }
206
+ }
207
+ // -----------------------------------------------------------
208
+ // Console pretty-printer
209
+ // -----------------------------------------------------------
210
+ function formatDuration(ms) {
211
+ if (ms < 1)
212
+ return `${(ms * 1000).toFixed(0)}μs`;
213
+ if (ms < 1000)
214
+ return `${ms.toFixed(1)}ms`;
215
+ return `${(ms / 1000).toFixed(2)}s`;
216
+ }
217
+ function formatTimestamp(unixMs) {
218
+ return new Date(unixMs).toISOString();
219
+ }
220
+ function consolePrintSpan(span, depth) {
221
+ const indent = ' '.repeat(depth);
222
+ const duration = span.endTime - span.startTime;
223
+ const statusIcon = span.status.code === 'ok'
224
+ ? chalk.green('✓')
225
+ : chalk.red('✗');
226
+ const kindBadge = {
227
+ internal: chalk.gray('INT'),
228
+ client: chalk.blue('CLI'),
229
+ server: chalk.magenta('SRV'),
230
+ }[span.kind] ?? chalk.gray('???');
231
+ const lines = [];
232
+ lines.push(`${indent}${statusIcon} ${chalk.bold(span.name)} ${kindBadge} ${chalk.gray(formatDuration(duration))}`);
233
+ if (span.status.code === 'error' && span.status.message) {
234
+ lines.push(`${indent} ${chalk.red('error:')} ${span.status.message}`);
235
+ }
236
+ const attrEntries = Object.entries(span.attributes);
237
+ if (attrEntries.length > 0) {
238
+ const attrStr = attrEntries
239
+ .slice(0, 5)
240
+ .map(([k, v]) => `${chalk.cyan(k)}=${chalk.yellow(String(v))}`)
241
+ .join(' ');
242
+ const overflow = attrEntries.length > 5 ? ` ${chalk.gray(`+${attrEntries.length - 5} more`)}` : '';
243
+ lines.push(`${indent} ${attrStr}${overflow}`);
244
+ }
245
+ for (const evt of span.events) {
246
+ lines.push(`${indent} ${chalk.gray('@')} ${chalk.white(evt.name)} ${chalk.gray(formatTimestamp(evt.timestamp))}`);
247
+ }
248
+ return lines.join('\n');
249
+ }
250
+ function consolePrintSpanTree(spans) {
251
+ if (spans.length === 0)
252
+ return chalk.gray('No spans recorded.');
253
+ const spanMap = new Map(spans.map(s => [s.spanId, s]));
254
+ const children = new Map();
255
+ const roots = [];
256
+ for (const span of spans) {
257
+ if (!span.parentSpanId || !spanMap.has(span.parentSpanId)) {
258
+ roots.push(span);
259
+ }
260
+ else {
261
+ const list = children.get(span.parentSpanId) ?? [];
262
+ list.push(span);
263
+ children.set(span.parentSpanId, list);
264
+ }
265
+ }
266
+ const lines = [];
267
+ lines.push('');
268
+ lines.push(chalk.bold('━━━ NeuroCLI Trace ━━━'));
269
+ function render(span, depth) {
270
+ lines.push(consolePrintSpan(span, depth));
271
+ const kids = children.get(span.spanId) ?? [];
272
+ kids.sort((a, b) => a.startTime - b.startTime);
273
+ for (const child of kids) {
274
+ render(child, depth + 1);
275
+ }
276
+ }
277
+ roots.sort((a, b) => a.startTime - b.startTime);
278
+ for (const root of roots) {
279
+ render(root, 0);
280
+ }
281
+ lines.push(chalk.bold('━━━━━━━━━━━━━━━━━━━━'));
282
+ lines.push('');
283
+ return lines.join('\n');
284
+ }
285
+ // -----------------------------------------------------------
286
+ // HTTP export helper
287
+ // -----------------------------------------------------------
288
+ async function exportOTLPJson(url, payload, headers) {
289
+ try {
290
+ const response = await fetch(url, {
291
+ method: 'POST',
292
+ headers: {
293
+ 'Content-Type': 'application/json',
294
+ ...headers,
295
+ },
296
+ body: JSON.stringify(payload),
297
+ });
298
+ return response.status >= 200 && response.status < 300;
299
+ }
300
+ catch {
301
+ return false;
302
+ }
303
+ }
304
+ // -----------------------------------------------------------
305
+ // ObservabilityManager
306
+ // -----------------------------------------------------------
307
+ export class ObservabilityManager {
308
+ config;
309
+ pendingSpans = [];
310
+ pendingMetrics = [];
311
+ spanBuffer;
312
+ sessionTraceId;
313
+ sessionRootSpanId;
314
+ exportTimer;
315
+ activeSpans = new Map();
316
+ histogramBuckets = new Map();
317
+ counters = new Map();
318
+ gauges = new Map();
319
+ constructor(config) {
320
+ this.config = config;
321
+ this.spanBuffer = new RingBuffer(1000);
322
+ this.sessionTraceId = generateTraceId();
323
+ }
324
+ // ----------------------------------------------------------
325
+ // Lifecycle
326
+ // ----------------------------------------------------------
327
+ start() {
328
+ if (!this.config.enabled)
329
+ return;
330
+ if (this.config.exportInterval > 0) {
331
+ this.exportTimer = setInterval(() => {
332
+ this.flush().catch(() => { });
333
+ }, this.config.exportInterval);
334
+ if (this.exportTimer.unref) {
335
+ this.exportTimer.unref();
336
+ }
337
+ }
338
+ }
339
+ async stop() {
340
+ if (this.exportTimer !== undefined) {
341
+ clearInterval(this.exportTimer);
342
+ this.exportTimer = undefined;
343
+ }
344
+ await this.flush();
345
+ }
346
+ // ----------------------------------------------------------
347
+ // Sampling
348
+ // ----------------------------------------------------------
349
+ shouldSample() {
350
+ if (this.config.sampleRate >= 1)
351
+ return true;
352
+ if (this.config.sampleRate <= 0)
353
+ return false;
354
+ return Math.random() < this.config.sampleRate;
355
+ }
356
+ // ----------------------------------------------------------
357
+ // Tracing
358
+ // ----------------------------------------------------------
359
+ startSpan(name, parent, attributes) {
360
+ if (!this.config.enabled) {
361
+ return this.createNoopSpan();
362
+ }
363
+ const traceId = parent?.traceId ?? this.sessionTraceId;
364
+ const spanId = generateSpanId();
365
+ const span = {
366
+ traceId,
367
+ spanId,
368
+ parentSpanId: parent?.spanId ?? this.sessionRootSpanId,
369
+ name,
370
+ kind: 'internal',
371
+ startTime: Date.now(),
372
+ endTime: 0,
373
+ attributes: attributes ?? {},
374
+ events: [],
375
+ status: { code: 'ok' },
376
+ };
377
+ this.activeSpans.set(spanId, span);
378
+ return span;
379
+ }
380
+ endSpan(span, status) {
381
+ if (!this.config.enabled)
382
+ return;
383
+ span.endTime = Date.now();
384
+ if (status) {
385
+ span.status = status;
386
+ }
387
+ this.activeSpans.delete(span.spanId);
388
+ if (!this.shouldSample())
389
+ return;
390
+ this.pendingSpans.push(span);
391
+ this.spanBuffer.push(span);
392
+ if (this.config.consoleExporter) {
393
+ this.printSpanToConsole(span);
394
+ }
395
+ if (this.pendingSpans.length >= this.config.maxBatchSize) {
396
+ this.flush().catch(() => { });
397
+ }
398
+ }
399
+ addEvent(span, name, attributes) {
400
+ if (!this.config.enabled)
401
+ return;
402
+ span.events.push({
403
+ name,
404
+ timestamp: Date.now(),
405
+ attributes: attributes ?? {},
406
+ });
407
+ }
408
+ // ----------------------------------------------------------
409
+ // Metrics
410
+ // ----------------------------------------------------------
411
+ incrementCounter(name, value = 1, attributes = {}) {
412
+ if (!this.config.enabled)
413
+ return;
414
+ const key = `${name}:${JSON.stringify(attributes)}`;
415
+ const current = this.counters.get(key) ?? 0;
416
+ this.counters.set(key, current + value);
417
+ const metric = {
418
+ name,
419
+ type: 'counter',
420
+ value: current + value,
421
+ attributes,
422
+ timestamp: Date.now(),
423
+ };
424
+ this.pendingMetrics.push(metric);
425
+ if (this.pendingMetrics.length >= this.config.maxBatchSize) {
426
+ this.flush().catch(() => { });
427
+ }
428
+ }
429
+ recordGauge(name, value, attributes = {}) {
430
+ if (!this.config.enabled)
431
+ return;
432
+ const key = `${name}:${JSON.stringify(attributes)}`;
433
+ this.gauges.set(key, value);
434
+ const metric = {
435
+ name,
436
+ type: 'gauge',
437
+ value,
438
+ attributes,
439
+ timestamp: Date.now(),
440
+ };
441
+ this.pendingMetrics.push(metric);
442
+ if (this.pendingMetrics.length >= this.config.maxBatchSize) {
443
+ this.flush().catch(() => { });
444
+ }
445
+ }
446
+ recordHistogram(name, value, attributes = {}) {
447
+ if (!this.config.enabled)
448
+ return;
449
+ const key = `${name}:${JSON.stringify(attributes)}`;
450
+ let bucket = this.histogramBuckets.get(key);
451
+ if (!bucket) {
452
+ const bounds = [...DEFAULT_HISTOGRAM_BUCKETS];
453
+ bucket = { bounds, counts: new Array(bounds.length + 1).fill(0), sum: 0, count: 0, min: Infinity, max: -Infinity };
454
+ this.histogramBuckets.set(key, bucket);
455
+ }
456
+ bucket.sum += value;
457
+ bucket.count++;
458
+ if (value < bucket.min)
459
+ bucket.min = value;
460
+ if (value > bucket.max)
461
+ bucket.max = value;
462
+ let placed = false;
463
+ for (let i = 0; i < bucket.bounds.length; i++) {
464
+ if (value <= bucket.bounds[i]) {
465
+ bucket.counts[i]++;
466
+ placed = true;
467
+ break;
468
+ }
469
+ }
470
+ if (!placed) {
471
+ bucket.counts[bucket.bounds.length]++;
472
+ }
473
+ const metric = {
474
+ name,
475
+ type: 'histogram',
476
+ value,
477
+ attributes,
478
+ timestamp: Date.now(),
479
+ };
480
+ this.pendingMetrics.push(metric);
481
+ if (this.pendingMetrics.length >= this.config.maxBatchSize) {
482
+ this.flush().catch(() => { });
483
+ }
484
+ }
485
+ // ----------------------------------------------------------
486
+ // Convenience tracing methods
487
+ // ----------------------------------------------------------
488
+ traceLLMRequest(model, promptTokens, completionTokens, duration, cost) {
489
+ const span = this.startSpan('llm.request', undefined, {
490
+ 'llm.model': model,
491
+ 'llm.prompt_tokens': promptTokens,
492
+ 'llm.completion_tokens': completionTokens,
493
+ 'llm.total_tokens': promptTokens + completionTokens,
494
+ 'llm.cost_usd': cost,
495
+ 'llm.duration_ms': duration,
496
+ });
497
+ span.kind = 'client';
498
+ span.endTime = span.startTime + duration;
499
+ span.status = { code: 'ok' };
500
+ this.activeSpans.delete(span.spanId);
501
+ if (this.shouldSample()) {
502
+ this.pendingSpans.push(span);
503
+ this.spanBuffer.push(span);
504
+ }
505
+ this.incrementCounter('llm.requests.total', 1, { model });
506
+ this.incrementCounter('llm.tokens.prompt', promptTokens, { model });
507
+ this.incrementCounter('llm.tokens.completion', completionTokens, { model });
508
+ this.recordGauge('llm.cost', cost, { model });
509
+ this.recordHistogram('llm.latency', duration, { model });
510
+ if (this.config.consoleExporter) {
511
+ this.printSpanToConsole(span);
512
+ }
513
+ return span;
514
+ }
515
+ traceToolCall(tool, args, duration, success) {
516
+ const span = this.startSpan('tool.call', undefined, {
517
+ 'tool.name': tool,
518
+ 'tool.duration_ms': duration,
519
+ 'tool.success': success,
520
+ 'tool.args_preview': typeof args === 'string' ? args.slice(0, 200) : JSON.stringify(args).slice(0, 200),
521
+ });
522
+ span.kind = 'client';
523
+ span.endTime = span.startTime + duration;
524
+ span.status = success ? { code: 'ok' } : { code: 'error', message: `Tool '${tool}' failed` };
525
+ this.activeSpans.delete(span.spanId);
526
+ if (this.shouldSample()) {
527
+ this.pendingSpans.push(span);
528
+ this.spanBuffer.push(span);
529
+ }
530
+ this.incrementCounter('tool.calls.total', 1, { tool, success: String(success) });
531
+ this.incrementCounter(success ? 'tool.calls.success' : 'tool.calls.error', 1, { tool });
532
+ this.recordHistogram('tool.duration', duration, { tool });
533
+ if (this.config.consoleExporter) {
534
+ this.printSpanToConsole(span);
535
+ }
536
+ return span;
537
+ }
538
+ traceAgentAction(agent, action, duration) {
539
+ const span = this.startSpan('agent.action', undefined, {
540
+ 'agent.name': agent,
541
+ 'agent.action': action,
542
+ 'agent.duration_ms': duration,
543
+ });
544
+ span.kind = 'internal';
545
+ span.endTime = span.startTime + duration;
546
+ span.status = { code: 'ok' };
547
+ this.activeSpans.delete(span.spanId);
548
+ if (this.shouldSample()) {
549
+ this.pendingSpans.push(span);
550
+ this.spanBuffer.push(span);
551
+ }
552
+ this.incrementCounter('agent.actions.total', 1, { agent, action });
553
+ this.recordHistogram('agent.action.duration', duration, { agent, action });
554
+ if (this.config.consoleExporter) {
555
+ this.printSpanToConsole(span);
556
+ }
557
+ return span;
558
+ }
559
+ // ----------------------------------------------------------
560
+ // Export
561
+ // ----------------------------------------------------------
562
+ async flush() {
563
+ if (!this.config.enabled)
564
+ return;
565
+ const spansToExport = this.pendingSpans.splice(0);
566
+ const metricsToExport = this.pendingMetrics.splice(0);
567
+ if (spansToExport.length === 0 && metricsToExport.length === 0)
568
+ return;
569
+ const tracesUrl = this.config.endpoint.replace(/\/$/, '');
570
+ const metricsUrl = tracesUrl.replace(/\/v1\/traces$/, '/v1/metrics');
571
+ const exportPromises = [];
572
+ if (spansToExport.length > 0) {
573
+ const tracesPayload = buildTracesPayload(spansToExport, this.config.serviceName);
574
+ exportPromises.push(exportOTLPJson(tracesUrl, tracesPayload, this.config.headers));
575
+ }
576
+ if (metricsToExport.length > 0) {
577
+ const metricsPayload = buildMetricsPayload(metricsToExport, this.config.serviceName);
578
+ exportPromises.push(exportOTLPJson(metricsUrl, metricsPayload, this.config.headers));
579
+ }
580
+ await Promise.allSettled(exportPromises);
581
+ }
582
+ getPendingCount() {
583
+ return {
584
+ spans: this.pendingSpans.length,
585
+ metrics: this.pendingMetrics.length,
586
+ };
587
+ }
588
+ // ----------------------------------------------------------
589
+ // Session tracing
590
+ // ----------------------------------------------------------
591
+ startSession(sessionName) {
592
+ this.sessionTraceId = generateTraceId();
593
+ const rootSpan = this.startSpan(sessionName ?? 'session', undefined, {
594
+ 'session.id': this.sessionTraceId,
595
+ });
596
+ rootSpan.kind = 'server';
597
+ this.sessionRootSpanId = rootSpan.spanId;
598
+ return rootSpan;
599
+ }
600
+ endSession(rootSpan) {
601
+ this.endSpan(rootSpan, { code: 'ok' });
602
+ this.sessionRootSpanId = undefined;
603
+ }
604
+ getSessionTrace() {
605
+ return this.spanBuffer.items().filter(s => s.traceId === this.sessionTraceId);
606
+ }
607
+ printTrace() {
608
+ const spans = this.getSessionTrace();
609
+ return consolePrintSpanTree(spans);
610
+ }
611
+ // ----------------------------------------------------------
612
+ // Configuration
613
+ // ----------------------------------------------------------
614
+ getConfig() {
615
+ return { ...this.config };
616
+ }
617
+ updateConfig(partial) {
618
+ Object.assign(this.config, partial);
619
+ }
620
+ getSessionTraceId() {
621
+ return this.sessionTraceId;
622
+ }
623
+ setSessionTraceId(traceId) {
624
+ this.sessionTraceId = traceId;
625
+ }
626
+ getActiveSpanCount() {
627
+ return this.activeSpans.size;
628
+ }
629
+ // ----------------------------------------------------------
630
+ // Metric summaries
631
+ // ----------------------------------------------------------
632
+ getCounterValue(name, attributes = {}) {
633
+ const key = `${name}:${JSON.stringify(attributes)}`;
634
+ return this.counters.get(key) ?? 0;
635
+ }
636
+ getGaugeValue(name, attributes = {}) {
637
+ const key = `${name}:${JSON.stringify(attributes)}`;
638
+ return this.gauges.get(key);
639
+ }
640
+ getHistogramSummary(name, attributes = {}) {
641
+ const key = `${name}:${JSON.stringify(attributes)}`;
642
+ const bucket = this.histogramBuckets.get(key);
643
+ if (!bucket)
644
+ return undefined;
645
+ return {
646
+ count: bucket.count,
647
+ sum: bucket.sum,
648
+ min: bucket.min === Infinity ? 0 : bucket.min,
649
+ max: bucket.max === -Infinity ? 0 : bucket.max,
650
+ avg: bucket.count > 0 ? bucket.sum / bucket.count : 0,
651
+ };
652
+ }
653
+ // ----------------------------------------------------------
654
+ // Private helpers
655
+ // ----------------------------------------------------------
656
+ createNoopSpan() {
657
+ return {
658
+ traceId: '0'.repeat(32),
659
+ spanId: '0'.repeat(16),
660
+ name: 'noop',
661
+ kind: 'internal',
662
+ startTime: 0,
663
+ endTime: 0,
664
+ attributes: {},
665
+ events: [],
666
+ status: { code: 'ok' },
667
+ };
668
+ }
669
+ printSpanToConsole(span) {
670
+ const duration = span.endTime > 0 ? span.endTime - span.startTime : 0;
671
+ const statusIcon = span.status.code === 'ok' ? chalk.green('✓') : chalk.red('✗');
672
+ const kindLabel = {
673
+ internal: chalk.gray('INT'),
674
+ client: chalk.blue('CLI'),
675
+ server: chalk.magenta('SRV'),
676
+ }[span.kind] ?? chalk.gray('???');
677
+ const parts = [
678
+ statusIcon,
679
+ chalk.bold(span.name),
680
+ kindLabel,
681
+ chalk.gray(formatDuration(duration)),
682
+ ];
683
+ if (span.status.code === 'error' && span.status.message) {
684
+ parts.push(chalk.red(`(${span.status.message})`));
685
+ }
686
+ const attrEntries = Object.entries(span.attributes);
687
+ if (attrEntries.length > 0) {
688
+ const attrStr = attrEntries
689
+ .slice(0, 3)
690
+ .map(([k, v]) => `${chalk.cyan(k)}=${chalk.yellow(String(v))}`)
691
+ .join(' ');
692
+ const overflow = attrEntries.length > 3 ? chalk.gray(` +${attrEntries.length - 3}`) : '';
693
+ parts.push(attrStr + overflow);
694
+ }
695
+ try {
696
+ console.log(parts.join(' '));
697
+ }
698
+ catch {
699
+ // Silently fail if console is not available (e.g., during tests)
700
+ }
701
+ }
702
+ }
703
+ // -----------------------------------------------------------
704
+ // Default config factory
705
+ // -----------------------------------------------------------
706
+ export function defaultObservabilityConfig() {
707
+ return {
708
+ enabled: false,
709
+ serviceName: 'neuro-cli',
710
+ endpoint: 'http://localhost:4318/v1/traces',
711
+ headers: {},
712
+ exportInterval: 5000,
713
+ maxBatchSize: 512,
714
+ consoleExporter: true,
715
+ sampleRate: 1.0,
716
+ };
717
+ }
718
+ // -----------------------------------------------------------
719
+ // Singleton accessor (optional convenience)
720
+ // -----------------------------------------------------------
721
+ let globalInstance = null;
722
+ export function initObservability(config) {
723
+ const full = { ...defaultObservabilityConfig(), ...config };
724
+ globalInstance = new ObservabilityManager(full);
725
+ globalInstance.start();
726
+ return globalInstance;
727
+ }
728
+ export function getObservability() {
729
+ return globalInstance;
730
+ }
731
+ export function resetObservability() {
732
+ if (globalInstance) {
733
+ globalInstance.stop().catch(() => { });
734
+ globalInstance = null;
735
+ }
736
+ }
737
+ //# sourceMappingURL=observability.js.map