@newrelic/preflight 0.0.1-pre.1 → 1.0.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.
- package/LICENSE +183 -0
- package/README.md +498 -0
- package/dist/alerts/alert-log.d.ts +24 -0
- package/dist/alerts/alert-log.d.ts.map +1 -0
- package/dist/alerts/alert-log.js +159 -0
- package/dist/alerts/alert-log.js.map +1 -0
- package/dist/alerts/alert-snapshot-collector.d.ts +168 -0
- package/dist/alerts/alert-snapshot-collector.d.ts.map +1 -0
- package/dist/alerts/alert-snapshot-collector.js +243 -0
- package/dist/alerts/alert-snapshot-collector.js.map +1 -0
- package/dist/alerts/local-alert-engine.d.ts +86 -0
- package/dist/alerts/local-alert-engine.d.ts.map +1 -0
- package/dist/alerts/local-alert-engine.js +466 -0
- package/dist/alerts/local-alert-engine.js.map +1 -0
- package/dist/alerts/local-alert-rule.d.ts +439 -0
- package/dist/alerts/local-alert-rule.d.ts.map +1 -0
- package/dist/alerts/local-alert-rule.js +139 -0
- package/dist/alerts/local-alert-rule.js.map +1 -0
- package/dist/alerts/os-notifier.d.ts +39 -0
- package/dist/alerts/os-notifier.d.ts.map +1 -0
- package/dist/alerts/os-notifier.js +170 -0
- package/dist/alerts/os-notifier.js.map +1 -0
- package/dist/alerts/types.d.ts +35 -0
- package/dist/alerts/types.d.ts.map +1 -0
- package/dist/alerts/types.js +8 -0
- package/dist/alerts/types.js.map +1 -0
- package/dist/config.d.ts +169 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +868 -0
- package/dist/config.js.map +1 -0
- package/dist/dashboard/dashboard-server.d.ts +38 -0
- package/dist/dashboard/dashboard-server.d.ts.map +1 -0
- package/dist/dashboard/dashboard-server.js +207 -0
- package/dist/dashboard/dashboard-server.js.map +1 -0
- package/dist/dashboard/index.d.ts +3 -0
- package/dist/dashboard/index.d.ts.map +1 -0
- package/dist/dashboard/index.js +2 -0
- package/dist/dashboard/index.js.map +1 -0
- package/dist/dashboard/live-event-bus.d.ts +99 -0
- package/dist/dashboard/live-event-bus.d.ts.map +1 -0
- package/dist/dashboard/live-event-bus.js +56 -0
- package/dist/dashboard/live-event-bus.js.map +1 -0
- package/dist/dashboard/routes/api-handler.d.ts +122 -0
- package/dist/dashboard/routes/api-handler.d.ts.map +1 -0
- package/dist/dashboard/routes/api-handler.js +1414 -0
- package/dist/dashboard/routes/api-handler.js.map +1 -0
- package/dist/dashboard/routes/replay-analyzer.d.ts +15 -0
- package/dist/dashboard/routes/replay-analyzer.d.ts.map +1 -0
- package/dist/dashboard/routes/replay-analyzer.js +227 -0
- package/dist/dashboard/routes/replay-analyzer.js.map +1 -0
- package/dist/dashboard/routes/sse-handler.d.ts +4 -0
- package/dist/dashboard/routes/sse-handler.d.ts.map +1 -0
- package/dist/dashboard/routes/sse-handler.js +122 -0
- package/dist/dashboard/routes/sse-handler.js.map +1 -0
- package/dist/dashboard/routes/static-handler.d.ts +3 -0
- package/dist/dashboard/routes/static-handler.d.ts.map +1 -0
- package/dist/dashboard/routes/static-handler.js +123 -0
- package/dist/dashboard/routes/static-handler.js.map +1 -0
- package/dist/data/alerts/conditions/01-daily-cost-spike.json +16 -0
- package/dist/data/alerts/conditions/02-low-efficiency-score.json +16 -0
- package/dist/data/alerts/conditions/03-stuck-loop-rate.json +16 -0
- package/dist/data/alerts/conditions/04-anti-pattern-rate.json +16 -0
- package/dist/data/alerts/conditions/05-session-cost-budget.json +16 -0
- package/dist/data/alerts/conditions-personal/01-personal-daily-cost.json +16 -0
- package/dist/data/alerts/conditions-personal/02-personal-session-cost.json +16 -0
- package/dist/data/alerts/conditions-personal/03-personal-low-efficiency.json +16 -0
- package/dist/data/alerts/conditions-personal/04-personal-anti-pattern-rate.json +16 -0
- package/dist/data/alerts/conditions-personal/05-personal-stuck-loop.json +16 -0
- package/dist/data/alerts/policy.json +4 -0
- package/dist/data/dashboards/ai-coding-assistant-manager-view.json +103 -0
- package/dist/data/dashboards/ai-coding-assistant-overview.json +239 -0
- package/dist/data/dashboards/ai-coding-assistant-personal.json +442 -0
- package/dist/data/dashboards/ai-coding-assistant-platform-comparison.json +320 -0
- package/dist/data/dashboards/ai-coding-assistant-security.json +275 -0
- package/dist/data/dashboards/ai-coding-assistant-session-detail.json +296 -0
- package/dist/data/dashboards/ai-coding-assistant-team-view.json +345 -0
- package/dist/deploy/data-paths.d.ts +22 -0
- package/dist/deploy/data-paths.d.ts.map +1 -0
- package/dist/deploy/data-paths.js +69 -0
- package/dist/deploy/data-paths.js.map +1 -0
- package/dist/deploy/deploy-alerts.d.ts +58 -0
- package/dist/deploy/deploy-alerts.d.ts.map +1 -0
- package/dist/deploy/deploy-alerts.js +371 -0
- package/dist/deploy/deploy-alerts.js.map +1 -0
- package/dist/deploy/deploy-dashboards.d.ts +92 -0
- package/dist/deploy/deploy-dashboards.d.ts.map +1 -0
- package/dist/deploy/deploy-dashboards.js +282 -0
- package/dist/deploy/deploy-dashboards.js.map +1 -0
- package/dist/digest/digest-formatter.d.ts +3 -0
- package/dist/digest/digest-formatter.d.ts.map +1 -0
- package/dist/digest/digest-formatter.js +37 -0
- package/dist/digest/digest-formatter.js.map +1 -0
- package/dist/digest/digest-sender.d.ts +2 -0
- package/dist/digest/digest-sender.d.ts.map +1 -0
- package/dist/digest/digest-sender.js +29 -0
- package/dist/digest/digest-sender.js.map +1 -0
- package/dist/hooks/bash-classifier.d.ts +26 -0
- package/dist/hooks/bash-classifier.d.ts.map +1 -0
- package/dist/hooks/bash-classifier.js +409 -0
- package/dist/hooks/bash-classifier.js.map +1 -0
- package/dist/hooks/collector-script.d.ts +47 -0
- package/dist/hooks/collector-script.d.ts.map +1 -0
- package/dist/hooks/collector-script.js +662 -0
- package/dist/hooks/collector-script.js.map +1 -0
- package/dist/hooks/event-processor.d.ts +65 -0
- package/dist/hooks/event-processor.d.ts.map +1 -0
- package/dist/hooks/event-processor.js +342 -0
- package/dist/hooks/event-processor.js.map +1 -0
- package/dist/hooks/index.d.ts +7 -0
- package/dist/hooks/index.d.ts.map +1 -0
- package/dist/hooks/index.js +5 -0
- package/dist/hooks/index.js.map +1 -0
- package/dist/hooks/session-resolver.d.ts +66 -0
- package/dist/hooks/session-resolver.d.ts.map +1 -0
- package/dist/hooks/session-resolver.js +196 -0
- package/dist/hooks/session-resolver.js.map +1 -0
- package/dist/hooks/tool-parsers.d.ts +19 -0
- package/dist/hooks/tool-parsers.d.ts.map +1 -0
- package/dist/hooks/tool-parsers.js +260 -0
- package/dist/hooks/tool-parsers.js.map +1 -0
- package/dist/index.d.ts +107 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1505 -0
- package/dist/index.js.map +1 -0
- package/dist/install/cli.d.ts +11 -0
- package/dist/install/cli.d.ts.map +1 -0
- package/dist/install/cli.js +365 -0
- package/dist/install/cli.js.map +1 -0
- package/dist/install/index.d.ts +4 -0
- package/dist/install/index.d.ts.map +1 -0
- package/dist/install/index.js +3 -0
- package/dist/install/index.js.map +1 -0
- package/dist/install/install-helper.d.ts +35 -0
- package/dist/install/install-helper.d.ts.map +1 -0
- package/dist/install/install-helper.js +227 -0
- package/dist/install/install-helper.js.map +1 -0
- package/dist/install/key-validator.d.ts +19 -0
- package/dist/install/key-validator.d.ts.map +1 -0
- package/dist/install/key-validator.js +122 -0
- package/dist/install/key-validator.js.map +1 -0
- package/dist/install/migrate.d.ts +12 -0
- package/dist/install/migrate.d.ts.map +1 -0
- package/dist/install/migrate.js +115 -0
- package/dist/install/migrate.js.map +1 -0
- package/dist/install/schedule.d.ts +11 -0
- package/dist/install/schedule.d.ts.map +1 -0
- package/dist/install/schedule.js +114 -0
- package/dist/install/schedule.js.map +1 -0
- package/dist/install/setup-wizard.d.ts +40 -0
- package/dist/install/setup-wizard.d.ts.map +1 -0
- package/dist/install/setup-wizard.js +489 -0
- package/dist/install/setup-wizard.js.map +1 -0
- package/dist/lib/date.d.ts +54 -0
- package/dist/lib/date.d.ts.map +1 -0
- package/dist/lib/date.js +85 -0
- package/dist/lib/date.js.map +1 -0
- package/dist/metrics/anti-patterns.d.ts +62 -0
- package/dist/metrics/anti-patterns.d.ts.map +1 -0
- package/dist/metrics/anti-patterns.js +301 -0
- package/dist/metrics/anti-patterns.js.map +1 -0
- package/dist/metrics/api-failure-tracker.d.ts +82 -0
- package/dist/metrics/api-failure-tracker.d.ts.map +1 -0
- package/dist/metrics/api-failure-tracker.js +202 -0
- package/dist/metrics/api-failure-tracker.js.map +1 -0
- package/dist/metrics/budget-tracker.d.ts +60 -0
- package/dist/metrics/budget-tracker.d.ts.map +1 -0
- package/dist/metrics/budget-tracker.js +130 -0
- package/dist/metrics/budget-tracker.js.map +1 -0
- package/dist/metrics/claudemd-tracker.d.ts +108 -0
- package/dist/metrics/claudemd-tracker.d.ts.map +1 -0
- package/dist/metrics/claudemd-tracker.js +337 -0
- package/dist/metrics/claudemd-tracker.js.map +1 -0
- package/dist/metrics/collaboration-profile.d.ts +65 -0
- package/dist/metrics/collaboration-profile.d.ts.map +1 -0
- package/dist/metrics/collaboration-profile.js +231 -0
- package/dist/metrics/collaboration-profile.js.map +1 -0
- package/dist/metrics/context-composition-tracker.d.ts +74 -0
- package/dist/metrics/context-composition-tracker.d.ts.map +1 -0
- package/dist/metrics/context-composition-tracker.js +202 -0
- package/dist/metrics/context-composition-tracker.js.map +1 -0
- package/dist/metrics/context-tracker.d.ts +78 -0
- package/dist/metrics/context-tracker.d.ts.map +1 -0
- package/dist/metrics/context-tracker.js +222 -0
- package/dist/metrics/context-tracker.js.map +1 -0
- package/dist/metrics/context-window-tracker.d.ts +18 -0
- package/dist/metrics/context-window-tracker.d.ts.map +1 -0
- package/dist/metrics/context-window-tracker.js +35 -0
- package/dist/metrics/context-window-tracker.js.map +1 -0
- package/dist/metrics/cost-forecast.d.ts +36 -0
- package/dist/metrics/cost-forecast.d.ts.map +1 -0
- package/dist/metrics/cost-forecast.js +91 -0
- package/dist/metrics/cost-forecast.js.map +1 -0
- package/dist/metrics/cost-per-outcome.d.ts +102 -0
- package/dist/metrics/cost-per-outcome.d.ts.map +1 -0
- package/dist/metrics/cost-per-outcome.js +266 -0
- package/dist/metrics/cost-per-outcome.js.map +1 -0
- package/dist/metrics/cost-tracker.d.ts +78 -0
- package/dist/metrics/cost-tracker.d.ts.map +1 -0
- package/dist/metrics/cost-tracker.js +169 -0
- package/dist/metrics/cost-tracker.js.map +1 -0
- package/dist/metrics/decision-tracker.d.ts +49 -0
- package/dist/metrics/decision-tracker.d.ts.map +1 -0
- package/dist/metrics/decision-tracker.js +161 -0
- package/dist/metrics/decision-tracker.js.map +1 -0
- package/dist/metrics/efficiency-score.d.ts +80 -0
- package/dist/metrics/efficiency-score.d.ts.map +1 -0
- package/dist/metrics/efficiency-score.js +219 -0
- package/dist/metrics/efficiency-score.js.map +1 -0
- package/dist/metrics/git-efficiency-tracker.d.ts +165 -0
- package/dist/metrics/git-efficiency-tracker.d.ts.map +1 -0
- package/dist/metrics/git-efficiency-tracker.js +1056 -0
- package/dist/metrics/git-efficiency-tracker.js.map +1 -0
- package/dist/metrics/index.d.ts +26 -0
- package/dist/metrics/index.d.ts.map +1 -0
- package/dist/metrics/index.js +14 -0
- package/dist/metrics/index.js.map +1 -0
- package/dist/metrics/instruction-drift-tracker.d.ts +69 -0
- package/dist/metrics/instruction-drift-tracker.d.ts.map +1 -0
- package/dist/metrics/instruction-drift-tracker.js +213 -0
- package/dist/metrics/instruction-drift-tracker.js.map +1 -0
- package/dist/metrics/latency-decomposition.d.ts +50 -0
- package/dist/metrics/latency-decomposition.d.ts.map +1 -0
- package/dist/metrics/latency-decomposition.js +112 -0
- package/dist/metrics/latency-decomposition.js.map +1 -0
- package/dist/metrics/latency-tracker.d.ts +33 -0
- package/dist/metrics/latency-tracker.d.ts.map +1 -0
- package/dist/metrics/latency-tracker.js +93 -0
- package/dist/metrics/latency-tracker.js.map +1 -0
- package/dist/metrics/live-session-registry.d.ts +29 -0
- package/dist/metrics/live-session-registry.d.ts.map +1 -0
- package/dist/metrics/live-session-registry.js +103 -0
- package/dist/metrics/live-session-registry.js.map +1 -0
- package/dist/metrics/model-usage-tracker.d.ts +21 -0
- package/dist/metrics/model-usage-tracker.d.ts.map +1 -0
- package/dist/metrics/model-usage-tracker.js +53 -0
- package/dist/metrics/model-usage-tracker.js.map +1 -0
- package/dist/metrics/percentile.d.ts +5 -0
- package/dist/metrics/percentile.d.ts.map +1 -0
- package/dist/metrics/percentile.js +10 -0
- package/dist/metrics/percentile.js.map +1 -0
- package/dist/metrics/personal-coach.d.ts +47 -0
- package/dist/metrics/personal-coach.d.ts.map +1 -0
- package/dist/metrics/personal-coach.js +241 -0
- package/dist/metrics/personal-coach.js.map +1 -0
- package/dist/metrics/prompt-feedback.d.ts +75 -0
- package/dist/metrics/prompt-feedback.d.ts.map +1 -0
- package/dist/metrics/prompt-feedback.js +286 -0
- package/dist/metrics/prompt-feedback.js.map +1 -0
- package/dist/metrics/proxy-metrics.d.ts +54 -0
- package/dist/metrics/proxy-metrics.d.ts.map +1 -0
- package/dist/metrics/proxy-metrics.js +228 -0
- package/dist/metrics/proxy-metrics.js.map +1 -0
- package/dist/metrics/quality-proxy-tracker.d.ts +51 -0
- package/dist/metrics/quality-proxy-tracker.d.ts.map +1 -0
- package/dist/metrics/quality-proxy-tracker.js +162 -0
- package/dist/metrics/quality-proxy-tracker.js.map +1 -0
- package/dist/metrics/recommendation-engine.d.ts +72 -0
- package/dist/metrics/recommendation-engine.d.ts.map +1 -0
- package/dist/metrics/recommendation-engine.js +207 -0
- package/dist/metrics/recommendation-engine.js.map +1 -0
- package/dist/metrics/retry-detector.d.ts +43 -0
- package/dist/metrics/retry-detector.d.ts.map +1 -0
- package/dist/metrics/retry-detector.js +179 -0
- package/dist/metrics/retry-detector.js.map +1 -0
- package/dist/metrics/session-tracker.d.ts +75 -0
- package/dist/metrics/session-tracker.d.ts.map +1 -0
- package/dist/metrics/session-tracker.js +249 -0
- package/dist/metrics/session-tracker.js.map +1 -0
- package/dist/metrics/task-completion-tracker.d.ts +15 -0
- package/dist/metrics/task-completion-tracker.d.ts.map +1 -0
- package/dist/metrics/task-completion-tracker.js +27 -0
- package/dist/metrics/task-completion-tracker.js.map +1 -0
- package/dist/metrics/task-detector.d.ts +84 -0
- package/dist/metrics/task-detector.d.ts.map +1 -0
- package/dist/metrics/task-detector.js +302 -0
- package/dist/metrics/task-detector.js.map +1 -0
- package/dist/metrics/tool-selection-scorer.d.ts +39 -0
- package/dist/metrics/tool-selection-scorer.d.ts.map +1 -0
- package/dist/metrics/tool-selection-scorer.js +193 -0
- package/dist/metrics/tool-selection-scorer.js.map +1 -0
- package/dist/metrics/trend-analyzer.d.ts +92 -0
- package/dist/metrics/trend-analyzer.d.ts.map +1 -0
- package/dist/metrics/trend-analyzer.js +293 -0
- package/dist/metrics/trend-analyzer.js.map +1 -0
- package/dist/metrics/turn-cost-attributor.d.ts +41 -0
- package/dist/metrics/turn-cost-attributor.d.ts.map +1 -0
- package/dist/metrics/turn-cost-attributor.js +118 -0
- package/dist/metrics/turn-cost-attributor.js.map +1 -0
- package/dist/metrics/turn-tracker.d.ts +49 -0
- package/dist/metrics/turn-tracker.d.ts.map +1 -0
- package/dist/metrics/turn-tracker.js +192 -0
- package/dist/metrics/turn-tracker.js.map +1 -0
- package/dist/platforms/amazon-q-adapter.d.ts +10 -0
- package/dist/platforms/amazon-q-adapter.d.ts.map +1 -0
- package/dist/platforms/amazon-q-adapter.js +75 -0
- package/dist/platforms/amazon-q-adapter.js.map +1 -0
- package/dist/platforms/claude-code-adapter.d.ts +10 -0
- package/dist/platforms/claude-code-adapter.d.ts.map +1 -0
- package/dist/platforms/claude-code-adapter.js +48 -0
- package/dist/platforms/claude-code-adapter.js.map +1 -0
- package/dist/platforms/continue-adapter.d.ts +10 -0
- package/dist/platforms/continue-adapter.d.ts.map +1 -0
- package/dist/platforms/continue-adapter.js +73 -0
- package/dist/platforms/continue-adapter.js.map +1 -0
- package/dist/platforms/copilot-adapter.d.ts +37 -0
- package/dist/platforms/copilot-adapter.d.ts.map +1 -0
- package/dist/platforms/copilot-adapter.js +66 -0
- package/dist/platforms/copilot-adapter.js.map +1 -0
- package/dist/platforms/cursor-adapter.d.ts +10 -0
- package/dist/platforms/cursor-adapter.d.ts.map +1 -0
- package/dist/platforms/cursor-adapter.js +60 -0
- package/dist/platforms/cursor-adapter.js.map +1 -0
- package/dist/platforms/generic-mcp-adapter.d.ts +113 -0
- package/dist/platforms/generic-mcp-adapter.d.ts.map +1 -0
- package/dist/platforms/generic-mcp-adapter.js +139 -0
- package/dist/platforms/generic-mcp-adapter.js.map +1 -0
- package/dist/platforms/index.d.ts +15 -0
- package/dist/platforms/index.d.ts.map +1 -0
- package/dist/platforms/index.js +12 -0
- package/dist/platforms/index.js.map +1 -0
- package/dist/platforms/platform-registry.d.ts +11 -0
- package/dist/platforms/platform-registry.d.ts.map +1 -0
- package/dist/platforms/platform-registry.js +54 -0
- package/dist/platforms/platform-registry.js.map +1 -0
- package/dist/platforms/types.d.ts +36 -0
- package/dist/platforms/types.d.ts.map +1 -0
- package/dist/platforms/types.js +2 -0
- package/dist/platforms/types.js.map +1 -0
- package/dist/platforms/windsurf-adapter.d.ts +10 -0
- package/dist/platforms/windsurf-adapter.d.ts.map +1 -0
- package/dist/platforms/windsurf-adapter.js +63 -0
- package/dist/platforms/windsurf-adapter.js.map +1 -0
- package/dist/platforms/zed-adapter.d.ts +10 -0
- package/dist/platforms/zed-adapter.d.ts.map +1 -0
- package/dist/platforms/zed-adapter.js +72 -0
- package/dist/platforms/zed-adapter.js.map +1 -0
- package/dist/proxy/index.d.ts +7 -0
- package/dist/proxy/index.d.ts.map +1 -0
- package/dist/proxy/index.js +5 -0
- package/dist/proxy/index.js.map +1 -0
- package/dist/proxy/otlp-receiver.d.ts +28 -0
- package/dist/proxy/otlp-receiver.d.ts.map +1 -0
- package/dist/proxy/otlp-receiver.js +319 -0
- package/dist/proxy/otlp-receiver.js.map +1 -0
- package/dist/proxy/proxy-manager.d.ts +47 -0
- package/dist/proxy/proxy-manager.d.ts.map +1 -0
- package/dist/proxy/proxy-manager.js +338 -0
- package/dist/proxy/proxy-manager.js.map +1 -0
- package/dist/proxy/types.d.ts +72 -0
- package/dist/proxy/types.d.ts.map +1 -0
- package/dist/proxy/types.js +33 -0
- package/dist/proxy/types.js.map +1 -0
- package/dist/proxy/upstream-http.d.ts +26 -0
- package/dist/proxy/upstream-http.d.ts.map +1 -0
- package/dist/proxy/upstream-http.js +209 -0
- package/dist/proxy/upstream-http.js.map +1 -0
- package/dist/proxy/upstream-stdio.d.ts +25 -0
- package/dist/proxy/upstream-stdio.d.ts.map +1 -0
- package/dist/proxy/upstream-stdio.js +256 -0
- package/dist/proxy/upstream-stdio.js.map +1 -0
- package/dist/security/audit-trail.d.ts +74 -0
- package/dist/security/audit-trail.d.ts.map +1 -0
- package/dist/security/audit-trail.js +338 -0
- package/dist/security/audit-trail.js.map +1 -0
- package/dist/security/index.d.ts +5 -0
- package/dist/security/index.d.ts.map +1 -0
- package/dist/security/index.js +4 -0
- package/dist/security/index.js.map +1 -0
- package/dist/security/ssrf.d.ts +2 -0
- package/dist/security/ssrf.d.ts.map +1 -0
- package/dist/security/ssrf.js +126 -0
- package/dist/security/ssrf.js.map +1 -0
- package/dist/server.d.ts +14 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +117 -0
- package/dist/server.js.map +1 -0
- package/dist/shared/__test-utils__/log-output.d.ts +49 -0
- package/dist/shared/__test-utils__/log-output.d.ts.map +1 -0
- package/dist/shared/__test-utils__/log-output.js +38 -0
- package/dist/shared/__test-utils__/log-output.js.map +1 -0
- package/dist/shared/config.d.ts +56 -0
- package/dist/shared/config.d.ts.map +1 -0
- package/dist/shared/config.js +290 -0
- package/dist/shared/config.js.map +1 -0
- package/dist/shared/errors.d.ts +139 -0
- package/dist/shared/errors.d.ts.map +1 -0
- package/dist/shared/errors.js +406 -0
- package/dist/shared/errors.js.map +1 -0
- package/dist/shared/events/factory.d.ts +143 -0
- package/dist/shared/events/factory.d.ts.map +1 -0
- package/dist/shared/events/factory.js +351 -0
- package/dist/shared/events/factory.js.map +1 -0
- package/dist/shared/events/index.d.ts +6 -0
- package/dist/shared/events/index.d.ts.map +1 -0
- package/dist/shared/events/index.js +3 -0
- package/dist/shared/events/index.js.map +1 -0
- package/dist/shared/events/serialize.d.ts +87 -0
- package/dist/shared/events/serialize.d.ts.map +1 -0
- package/dist/shared/events/serialize.js +510 -0
- package/dist/shared/events/serialize.js.map +1 -0
- package/dist/shared/events/types.d.ts +139 -0
- package/dist/shared/events/types.d.ts.map +1 -0
- package/dist/shared/events/types.js +2 -0
- package/dist/shared/events/types.js.map +1 -0
- package/dist/shared/harvest/event-buffer.d.ts +59 -0
- package/dist/shared/harvest/event-buffer.d.ts.map +1 -0
- package/dist/shared/harvest/event-buffer.js +100 -0
- package/dist/shared/harvest/event-buffer.js.map +1 -0
- package/dist/shared/harvest/harvest-scheduler.d.ts +200 -0
- package/dist/shared/harvest/harvest-scheduler.d.ts.map +1 -0
- package/dist/shared/harvest/harvest-scheduler.js +647 -0
- package/dist/shared/harvest/harvest-scheduler.js.map +1 -0
- package/dist/shared/harvest/index.d.ts +7 -0
- package/dist/shared/harvest/index.d.ts.map +1 -0
- package/dist/shared/harvest/index.js +4 -0
- package/dist/shared/harvest/index.js.map +1 -0
- package/dist/shared/harvest/metric-aggregator.d.ts +115 -0
- package/dist/shared/harvest/metric-aggregator.d.ts.map +1 -0
- package/dist/shared/harvest/metric-aggregator.js +247 -0
- package/dist/shared/harvest/metric-aggregator.js.map +1 -0
- package/dist/shared/index.d.ts +22 -0
- package/dist/shared/index.d.ts.map +1 -0
- package/dist/shared/index.js +13 -0
- package/dist/shared/index.js.map +1 -0
- package/dist/shared/logger.d.ts +57 -0
- package/dist/shared/logger.d.ts.map +1 -0
- package/dist/shared/logger.js +166 -0
- package/dist/shared/logger.js.map +1 -0
- package/dist/shared/pricing-data.d.ts +4 -0
- package/dist/shared/pricing-data.d.ts.map +1 -0
- package/dist/shared/pricing-data.js +473 -0
- package/dist/shared/pricing-data.js.map +1 -0
- package/dist/shared/pricing.d.ts +148 -0
- package/dist/shared/pricing.d.ts.map +1 -0
- package/dist/shared/pricing.js +528 -0
- package/dist/shared/pricing.js.map +1 -0
- package/dist/shared/redact.d.ts +33 -0
- package/dist/shared/redact.d.ts.map +1 -0
- package/dist/shared/redact.js +110 -0
- package/dist/shared/redact.js.map +1 -0
- package/dist/shared/timing.d.ts +96 -0
- package/dist/shared/timing.d.ts.map +1 -0
- package/dist/shared/timing.js +173 -0
- package/dist/shared/timing.js.map +1 -0
- package/dist/shared/tokens.d.ts +145 -0
- package/dist/shared/tokens.d.ts.map +1 -0
- package/dist/shared/tokens.js +492 -0
- package/dist/shared/tokens.js.map +1 -0
- package/dist/shared/transport/events-api.d.ts +14 -0
- package/dist/shared/transport/events-api.d.ts.map +1 -0
- package/dist/shared/transport/events-api.js +29 -0
- package/dist/shared/transport/events-api.js.map +1 -0
- package/dist/shared/transport/http-client.d.ts +49 -0
- package/dist/shared/transport/http-client.d.ts.map +1 -0
- package/dist/shared/transport/http-client.js +381 -0
- package/dist/shared/transport/http-client.js.map +1 -0
- package/dist/shared/transport/index.d.ts +10 -0
- package/dist/shared/transport/index.d.ts.map +1 -0
- package/dist/shared/transport/index.js +6 -0
- package/dist/shared/transport/index.js.map +1 -0
- package/dist/shared/transport/logs-api.d.ts +29 -0
- package/dist/shared/transport/logs-api.d.ts.map +1 -0
- package/dist/shared/transport/logs-api.js +40 -0
- package/dist/shared/transport/logs-api.js.map +1 -0
- package/dist/shared/transport/metric-api.d.ts +9 -0
- package/dist/shared/transport/metric-api.d.ts.map +1 -0
- package/dist/shared/transport/metric-api.js +39 -0
- package/dist/shared/transport/metric-api.js.map +1 -0
- package/dist/shared/transport/otlp-event-bridge.d.ts +22 -0
- package/dist/shared/transport/otlp-event-bridge.d.ts.map +1 -0
- package/dist/shared/transport/otlp-event-bridge.js +50 -0
- package/dist/shared/transport/otlp-event-bridge.js.map +1 -0
- package/dist/shared/transport/otlp-shared.d.ts +14 -0
- package/dist/shared/transport/otlp-shared.d.ts.map +1 -0
- package/dist/shared/transport/otlp-shared.js +49 -0
- package/dist/shared/transport/otlp-shared.js.map +1 -0
- package/dist/shared/transport/otlp-transport.d.ts +58 -0
- package/dist/shared/transport/otlp-transport.d.ts.map +1 -0
- package/dist/shared/transport/otlp-transport.js +236 -0
- package/dist/shared/transport/otlp-transport.js.map +1 -0
- package/dist/shared/transport/types.d.ts +129 -0
- package/dist/shared/transport/types.d.ts.map +1 -0
- package/dist/shared/transport/types.js +2 -0
- package/dist/shared/transport/types.js.map +1 -0
- package/dist/shared/version.d.ts +2 -0
- package/dist/shared/version.d.ts.map +1 -0
- package/dist/shared/version.js +2 -0
- package/dist/shared/version.js.map +1 -0
- package/dist/storage/index.d.ts +7 -0
- package/dist/storage/index.d.ts.map +1 -0
- package/dist/storage/index.js +4 -0
- package/dist/storage/index.js.map +1 -0
- package/dist/storage/local-store.d.ts +153 -0
- package/dist/storage/local-store.d.ts.map +1 -0
- package/dist/storage/local-store.js +719 -0
- package/dist/storage/local-store.js.map +1 -0
- package/dist/storage/retention.d.ts +2 -0
- package/dist/storage/retention.d.ts.map +1 -0
- package/dist/storage/retention.js +53 -0
- package/dist/storage/retention.js.map +1 -0
- package/dist/storage/session-store.d.ts +97 -0
- package/dist/storage/session-store.d.ts.map +1 -0
- package/dist/storage/session-store.js +391 -0
- package/dist/storage/session-store.js.map +1 -0
- package/dist/storage/types.d.ts +64 -0
- package/dist/storage/types.d.ts.map +1 -0
- package/dist/storage/types.js +2 -0
- package/dist/storage/types.js.map +1 -0
- package/dist/storage/weekly-summary.d.ts +61 -0
- package/dist/storage/weekly-summary.d.ts.map +1 -0
- package/dist/storage/weekly-summary.js +243 -0
- package/dist/storage/weekly-summary.js.map +1 -0
- package/dist/tools/analytics-tools.d.ts +101 -0
- package/dist/tools/analytics-tools.d.ts.map +1 -0
- package/dist/tools/analytics-tools.js +71 -0
- package/dist/tools/analytics-tools.js.map +1 -0
- package/dist/tools/cost-tools.d.ts +121 -0
- package/dist/tools/cost-tools.d.ts.map +1 -0
- package/dist/tools/cost-tools.js +174 -0
- package/dist/tools/cost-tools.js.map +1 -0
- package/dist/tools/cross-session-tools.d.ts +376 -0
- package/dist/tools/cross-session-tools.d.ts.map +1 -0
- package/dist/tools/cross-session-tools.js +820 -0
- package/dist/tools/cross-session-tools.js.map +1 -0
- package/dist/tools/extended-analytics-tools.d.ts +164 -0
- package/dist/tools/extended-analytics-tools.d.ts.map +1 -0
- package/dist/tools/extended-analytics-tools.js +121 -0
- package/dist/tools/extended-analytics-tools.js.map +1 -0
- package/dist/tools/index.d.ts +7 -0
- package/dist/tools/index.d.ts.map +1 -0
- package/dist/tools/index.js +4 -0
- package/dist/tools/index.js.map +1 -0
- package/dist/tools/session-stats.d.ts +162 -0
- package/dist/tools/session-stats.d.ts.map +1 -0
- package/dist/tools/session-stats.js +1054 -0
- package/dist/tools/session-stats.js.map +1 -0
- package/dist/tools/workflow-tools.d.ts +126 -0
- package/dist/tools/workflow-tools.d.ts.map +1 -0
- package/dist/tools/workflow-tools.js +274 -0
- package/dist/tools/workflow-tools.js.map +1 -0
- package/dist/tracing/mcp-tracer.d.ts +4 -0
- package/dist/tracing/mcp-tracer.d.ts.map +1 -0
- package/dist/tracing/mcp-tracer.js +14 -0
- package/dist/tracing/mcp-tracer.js.map +1 -0
- package/dist/tracing/session-span.d.ts +14 -0
- package/dist/tracing/session-span.d.ts.map +1 -0
- package/dist/tracing/session-span.js +53 -0
- package/dist/tracing/session-span.js.map +1 -0
- package/dist/tracing/task-span-tracker.d.ts +11 -0
- package/dist/tracing/task-span-tracker.d.ts.map +1 -0
- package/dist/tracing/task-span-tracker.js +59 -0
- package/dist/tracing/task-span-tracker.js.map +1 -0
- package/dist/tracing/tool-call-span.d.ts +4 -0
- package/dist/tracing/tool-call-span.d.ts.map +1 -0
- package/dist/tracing/tool-call-span.js +60 -0
- package/dist/tracing/tool-call-span.js.map +1 -0
- package/dist/transport/index.d.ts +3 -0
- package/dist/transport/index.d.ts.map +1 -0
- package/dist/transport/index.js +2 -0
- package/dist/transport/index.js.map +1 -0
- package/dist/transport/log-ingest.d.ts +42 -0
- package/dist/transport/log-ingest.d.ts.map +1 -0
- package/dist/transport/log-ingest.js +151 -0
- package/dist/transport/log-ingest.js.map +1 -0
- package/dist/transport/nr-ingest.d.ts +171 -0
- package/dist/transport/nr-ingest.d.ts.map +1 -0
- package/dist/transport/nr-ingest.js +659 -0
- package/dist/transport/nr-ingest.js.map +1 -0
- package/dist/types.d.ts +45 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/dist/web/assets/index-BrL281N-.css +2 -0
- package/dist/web/assets/index-CcaYZzXm.js +42 -0
- package/dist/web/favicon.svg +15 -0
- package/dist/web/index.html +15 -0
- package/examples/local-alert-rules.json +106 -0
- package/package.json +129 -1
package/dist/index.js
ADDED
|
@@ -0,0 +1,1505 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import 'dotenv/config';
|
|
3
|
+
import { readFileSync, realpathSync } from 'node:fs';
|
|
4
|
+
import { resolve } from 'node:path';
|
|
5
|
+
import { Command } from 'commander';
|
|
6
|
+
import { VERSION, createLogger } from './shared/index.js';
|
|
7
|
+
import { createServer } from './server.js';
|
|
8
|
+
import { loadMcpConfig, DEFAULT_STORAGE_PATH } from './config.js';
|
|
9
|
+
import { ProxyManager } from './proxy/index.js';
|
|
10
|
+
import { LocalStore } from './storage/index.js';
|
|
11
|
+
import { SessionStore, buildSessionSummary } from './storage/session-store.js';
|
|
12
|
+
import { WeeklySummaryGenerator } from './storage/weekly-summary.js';
|
|
13
|
+
import { HookEventProcessor } from './hooks/index.js';
|
|
14
|
+
import { SessionTracker } from './metrics/session-tracker.js';
|
|
15
|
+
import { CostTracker } from './metrics/cost-tracker.js';
|
|
16
|
+
import { buildCostForecastFromInputs } from './metrics/cost-forecast.js';
|
|
17
|
+
import { BudgetTracker } from './metrics/budget-tracker.js';
|
|
18
|
+
import { TaskDetector } from './metrics/task-detector.js';
|
|
19
|
+
import { AntiPatternDetector } from './metrics/anti-patterns.js';
|
|
20
|
+
import { EfficiencyScorer } from './metrics/efficiency-score.js';
|
|
21
|
+
import { TrendAnalyzer } from './metrics/trend-analyzer.js';
|
|
22
|
+
import { CollaborationProfiler } from './metrics/collaboration-profile.js';
|
|
23
|
+
import { ClaudeMdTracker } from './metrics/claudemd-tracker.js';
|
|
24
|
+
import { CostPerOutcomeAnalyzer } from './metrics/cost-per-outcome.js';
|
|
25
|
+
import { PersonalCoach } from './metrics/personal-coach.js';
|
|
26
|
+
import { PromptFeedbackEngine } from './metrics/prompt-feedback.js';
|
|
27
|
+
import { RecommendationEngine } from './metrics/recommendation-engine.js';
|
|
28
|
+
import { ContextWindowTracker } from './metrics/context-window-tracker.js';
|
|
29
|
+
import { LatencyTracker } from './metrics/latency-tracker.js';
|
|
30
|
+
import { TaskCompletionTracker } from './metrics/task-completion-tracker.js';
|
|
31
|
+
import { ModelUsageTracker } from './metrics/model-usage-tracker.js';
|
|
32
|
+
import { RetryDetector } from './metrics/retry-detector.js';
|
|
33
|
+
import { ContextCompositionTracker } from './metrics/context-composition-tracker.js';
|
|
34
|
+
import { ContextTrackerRegistry } from './metrics/context-tracker.js';
|
|
35
|
+
import { DecisionTracker } from './metrics/decision-tracker.js';
|
|
36
|
+
import { InstructionDriftTracker } from './metrics/instruction-drift-tracker.js';
|
|
37
|
+
import { ToolSelectionScorer } from './metrics/tool-selection-scorer.js';
|
|
38
|
+
import { QualityProxyTracker } from './metrics/quality-proxy-tracker.js';
|
|
39
|
+
import { ApiFailureTracker } from './metrics/api-failure-tracker.js';
|
|
40
|
+
import { LiveSessionRegistry } from './metrics/live-session-registry.js';
|
|
41
|
+
import { TurnCostAttributor } from './metrics/turn-cost-attributor.js';
|
|
42
|
+
import { TurnTracker } from './metrics/turn-tracker.js';
|
|
43
|
+
import { GitEfficiencyTracker } from './metrics/git-efficiency-tracker.js';
|
|
44
|
+
import { NrIngestManager } from './transport/nr-ingest.js';
|
|
45
|
+
import { AuditTrailManager } from './security/audit-trail.js';
|
|
46
|
+
import { LiveEventBus } from './dashboard/index.js';
|
|
47
|
+
import { DashboardServer } from './dashboard/dashboard-server.js';
|
|
48
|
+
import { LocalAlertEngine } from './alerts/local-alert-engine.js';
|
|
49
|
+
import { AlertSnapshotCollector } from './alerts/alert-snapshot-collector.js';
|
|
50
|
+
import { AlertLog } from './alerts/alert-log.js';
|
|
51
|
+
import { OsNotifier } from './alerts/os-notifier.js';
|
|
52
|
+
import { parseLocalAlertRules } from './alerts/local-alert-rule.js';
|
|
53
|
+
import { localDateKey, todayPortionOfSessionCost } from './lib/date.js';
|
|
54
|
+
import { FeedbackCollector } from './tools/workflow-tools.js';
|
|
55
|
+
import { registerTools, registerPendingTools } from './tools/session-stats.js';
|
|
56
|
+
import { resolveSessionId, resolveFromJobDir, resolveFromBreadcrumb, } from './hooks/session-resolver.js';
|
|
57
|
+
import { initMcpTracer } from './tracing/mcp-tracer.js';
|
|
58
|
+
import { SessionSpan } from './tracing/session-span.js';
|
|
59
|
+
import { TaskSpanTracker } from './tracing/task-span-tracker.js';
|
|
60
|
+
import { emitToolCallSpan } from './tracing/tool-call-span.js';
|
|
61
|
+
import { migrateStoragePath } from './install/migrate.js';
|
|
62
|
+
export { VERSION };
|
|
63
|
+
export { NrMcpServer, createServer } from './server.js';
|
|
64
|
+
export { loadMcpConfig, redactSensitive } from './config.js';
|
|
65
|
+
export { LocalStore } from './storage/index.js';
|
|
66
|
+
export { ProxyManager } from './proxy/index.js';
|
|
67
|
+
export { ClaudeCodeAdapter, CursorAdapter, WindsurfAdapter, CopilotAdapter, ZedAdapter, ContinueAdapter, AmazonQAdapter, parseCopilotUsageResponse, GenericMcpAdapter, validateReportToolCallInput, REPORT_TOOL_CALL_TOOL, REPORT_SESSION_START_TOOL, REPORT_SESSION_END_TOOL, PlatformRegistry, createDefaultRegistry, } from './platforms/index.js';
|
|
68
|
+
const logger = createLogger('mcp-cli');
|
|
69
|
+
// Show first-4 and last-4 chars of a credential. Guards against short values
|
|
70
|
+
// (e.g. test stubs) that would otherwise expose the full secret.
|
|
71
|
+
export function maskCredential(key) {
|
|
72
|
+
if (key.length <= 8)
|
|
73
|
+
return '***';
|
|
74
|
+
return key.slice(0, 4) + '...' + key.slice(-4);
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Decide how to handle a failure returned from `DashboardServer.start()`.
|
|
78
|
+
*
|
|
79
|
+
* When N concurrent `preflight --stdio` instances launch (one per
|
|
80
|
+
* Claude Code session) only one can bind the dashboard port; the rest receive
|
|
81
|
+
* EADDRINUSE. Rather than fataling the whole MCP server (which would render
|
|
82
|
+
* the session's tools unusable in Claude Code's UI), we log an INFO line and
|
|
83
|
+
* continue without the dashboard. Other errors still propagate.
|
|
84
|
+
*/
|
|
85
|
+
export function classifyDashboardStartError(err, host, port) {
|
|
86
|
+
if (err &&
|
|
87
|
+
typeof err === 'object' &&
|
|
88
|
+
'code' in err &&
|
|
89
|
+
err.code === 'EADDRINUSE') {
|
|
90
|
+
return {
|
|
91
|
+
kind: 'skip',
|
|
92
|
+
message: `Dashboard already owned by another preflight instance at ` +
|
|
93
|
+
`http://${host}:${port}; continuing without dashboard.`,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
return { kind: 'rethrow', error: err };
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Default interval (ms) between dashboard re-bind attempts when this MCP
|
|
100
|
+
* started in headless mode (Fix 1 EADDRINUSE skip path). Overridable via
|
|
101
|
+
* NR_AI_DASHBOARD_REPOLL_MS — kept simple to avoid threading a new config
|
|
102
|
+
* field through the loader for what is essentially a knob for tests.
|
|
103
|
+
*/
|
|
104
|
+
export const DEFAULT_DASHBOARD_REPOLL_MS = 30_000;
|
|
105
|
+
export function getDashboardRepollIntervalMs() {
|
|
106
|
+
const raw = process.env.NR_AI_DASHBOARD_REPOLL_MS;
|
|
107
|
+
if (raw === undefined || raw === '')
|
|
108
|
+
return DEFAULT_DASHBOARD_REPOLL_MS;
|
|
109
|
+
const parsed = Number.parseInt(raw, 10);
|
|
110
|
+
if (!Number.isFinite(parsed) || parsed <= 0)
|
|
111
|
+
return DEFAULT_DASHBOARD_REPOLL_MS;
|
|
112
|
+
return parsed;
|
|
113
|
+
}
|
|
114
|
+
export function setupDashboardPostBind(addr, deps) {
|
|
115
|
+
const log = createLogger('mcp-cli');
|
|
116
|
+
log.info(`Dashboard ready at http://${addr.address}:${addr.port}`);
|
|
117
|
+
// Task #18: only the dashboard owner runs orphan-buffer/breadcrumb GC —
|
|
118
|
+
// running it from every MCP would race with itself and re-archive files
|
|
119
|
+
// repeatedly. Run once at startup, then every 5 minutes. The interval is
|
|
120
|
+
// unref'd so it doesn't keep the event loop alive.
|
|
121
|
+
const { localStore, liveSessionRegistry } = deps;
|
|
122
|
+
const runGc = () => {
|
|
123
|
+
try {
|
|
124
|
+
localStore.gcStaleBreadcrumbs();
|
|
125
|
+
const live = localStore.getActiveSessionIdsFromHeartbeats();
|
|
126
|
+
if (liveSessionRegistry) {
|
|
127
|
+
for (const id of liveSessionRegistry.getLiveSessions())
|
|
128
|
+
live.add(id);
|
|
129
|
+
}
|
|
130
|
+
localStore.gcOrphanBuffers(live);
|
|
131
|
+
}
|
|
132
|
+
catch (err) {
|
|
133
|
+
log.warn('GC pass failed', { error: String(err) });
|
|
134
|
+
}
|
|
135
|
+
};
|
|
136
|
+
runGc();
|
|
137
|
+
const interval = setInterval(runGc, 5 * 60 * 1000);
|
|
138
|
+
interval.unref?.();
|
|
139
|
+
// openOnStart is declared in config but auto-open isn't implemented
|
|
140
|
+
// in v1 — log a warning so a user who set it doesn't assume the feature
|
|
141
|
+
// works silently.
|
|
142
|
+
if (deps.openOnStart) {
|
|
143
|
+
log.warn('dashboard.openOnStart is not implemented in v1; the dashboard URL is logged above. ' +
|
|
144
|
+
'Open it manually in your browser.');
|
|
145
|
+
}
|
|
146
|
+
return interval;
|
|
147
|
+
}
|
|
148
|
+
export function startDashboardRepoll(opts) {
|
|
149
|
+
const ms = opts.intervalMs ?? getDashboardRepollIntervalMs();
|
|
150
|
+
const log = opts.logger ?? createLogger('mcp-cli');
|
|
151
|
+
let inFlight = false;
|
|
152
|
+
const interval = setInterval(() => {
|
|
153
|
+
if (inFlight)
|
|
154
|
+
return;
|
|
155
|
+
inFlight = true;
|
|
156
|
+
void (async () => {
|
|
157
|
+
try {
|
|
158
|
+
const addr = await opts.dashboardServer.start();
|
|
159
|
+
clearInterval(interval);
|
|
160
|
+
log.info(`Dashboard ownership taken over at http://${addr.address}:${addr.port}; previous owner exited.`);
|
|
161
|
+
const gcInterval = opts.postBind({ address: addr.address, port: addr.port });
|
|
162
|
+
opts.onTakeover?.(gcInterval);
|
|
163
|
+
}
|
|
164
|
+
catch (err) {
|
|
165
|
+
const decision = classifyDashboardStartError(err, opts.host, opts.port);
|
|
166
|
+
if (decision.kind === 'rethrow') {
|
|
167
|
+
// Non-EADDRINUSE failure (e.g. permissions) — stop polling. We
|
|
168
|
+
// can't recover by retrying and we don't want to spam the log.
|
|
169
|
+
clearInterval(interval);
|
|
170
|
+
log.warn('Dashboard re-poll stopped after unexpected error', {
|
|
171
|
+
error: String(decision.error),
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
// EADDRINUSE: port still owned — keep polling silently.
|
|
175
|
+
}
|
|
176
|
+
finally {
|
|
177
|
+
inFlight = false;
|
|
178
|
+
}
|
|
179
|
+
})();
|
|
180
|
+
}, ms);
|
|
181
|
+
interval.unref?.();
|
|
182
|
+
return interval;
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* Subcommand names handled by `dispatchSubcommand` below. When `argv[2]` is one
|
|
186
|
+
* of these, we route to a dedicated handler and bypass the flag-driven main()
|
|
187
|
+
* path entirely. This lets users who installed via `npm install -g` invoke
|
|
188
|
+
* `preflight deploy-dashboards [...]` and similar without cloning the
|
|
189
|
+
* repo to run a `scripts/*.ts` file.
|
|
190
|
+
*/
|
|
191
|
+
const SUBCOMMAND_NAMES = [
|
|
192
|
+
'deploy-dashboards',
|
|
193
|
+
'deploy-alerts',
|
|
194
|
+
'install',
|
|
195
|
+
'uninstall',
|
|
196
|
+
'setup',
|
|
197
|
+
'validate',
|
|
198
|
+
'update',
|
|
199
|
+
'schedule',
|
|
200
|
+
];
|
|
201
|
+
function isSubcommand(value) {
|
|
202
|
+
return typeof value === 'string' && SUBCOMMAND_NAMES.includes(value);
|
|
203
|
+
}
|
|
204
|
+
/**
|
|
205
|
+
* If argv[2] is a known subcommand, run it and return its exit code.
|
|
206
|
+
* Otherwise return null so main() can continue with its flag-based dispatch.
|
|
207
|
+
*/
|
|
208
|
+
export async function dispatchSubcommand(argv) {
|
|
209
|
+
const sub = argv[2];
|
|
210
|
+
if (!isSubcommand(sub))
|
|
211
|
+
return null;
|
|
212
|
+
// CLI subcommands (install/setup/etc.) delegate entirely to the install CLI.
|
|
213
|
+
if (['install', 'uninstall', 'setup', 'validate', 'update', 'schedule'].includes(sub)) {
|
|
214
|
+
const { runInstallCli } = await import('./install/cli.js');
|
|
215
|
+
await runInstallCli(argv.slice(2));
|
|
216
|
+
return typeof process.exitCode === 'number' ? process.exitCode : 0;
|
|
217
|
+
}
|
|
218
|
+
const program = new Command();
|
|
219
|
+
program.name('preflight').version(VERSION);
|
|
220
|
+
const subargs = ['node', 'preflight', ...argv.slice(2)];
|
|
221
|
+
if (sub === 'deploy-dashboards') {
|
|
222
|
+
program
|
|
223
|
+
.command('deploy-dashboards')
|
|
224
|
+
.description('Deploy AI Coding Assistant dashboards to a New Relic account')
|
|
225
|
+
.option('--all', 'deploy all dashboard JSON files')
|
|
226
|
+
.option('--update', 'update existing dashboards in-place (matched by name)')
|
|
227
|
+
.option('--teardown', 'delete deployed dashboards (matched by name)')
|
|
228
|
+
.option('--print', 'print dashboard JSON with accountIds filled in (no API key required)')
|
|
229
|
+
.option('--eu', 'target the New Relic EU API')
|
|
230
|
+
.option('--developer <name>', 'inject developer name into the dashboard "developer" variable default')
|
|
231
|
+
.argument('[file]', 'specific dashboard JSON file (defaults to ai-coding-assistant-overview.json)')
|
|
232
|
+
.action(async (file, opts) => {
|
|
233
|
+
const { runDeployDashboards } = await import('./deploy/deploy-dashboards.js');
|
|
234
|
+
const code = await runDeployDashboards({
|
|
235
|
+
all: opts.all === true,
|
|
236
|
+
update: opts.update === true,
|
|
237
|
+
teardown: opts.teardown === true,
|
|
238
|
+
print: opts.print === true,
|
|
239
|
+
eu: opts.eu === true,
|
|
240
|
+
developer: typeof opts.developer === 'string' ? opts.developer : null,
|
|
241
|
+
file: file ?? null,
|
|
242
|
+
});
|
|
243
|
+
process.exitCode = code;
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
else {
|
|
247
|
+
program
|
|
248
|
+
.command('deploy-alerts')
|
|
249
|
+
.description('Deploy AI Coding Assistant alert conditions to a New Relic account')
|
|
250
|
+
.option('--dry-run', 'print the policy + conditions that would be created and exit')
|
|
251
|
+
.option('--teardown', 'delete the alert policy and all its conditions')
|
|
252
|
+
.option('--update', 'sync conditions on an existing policy in place (matched by name)')
|
|
253
|
+
.option('--eu', 'target the New Relic EU API')
|
|
254
|
+
.option('--developer <name>', 'deploy a personal alert policy scoped to <name>')
|
|
255
|
+
.action(async (opts) => {
|
|
256
|
+
const { runDeployAlerts } = await import('./deploy/deploy-alerts.js');
|
|
257
|
+
const code = await runDeployAlerts({
|
|
258
|
+
dryRun: opts.dryRun === true,
|
|
259
|
+
teardown: opts.teardown === true,
|
|
260
|
+
update: opts.update === true,
|
|
261
|
+
eu: opts.eu === true,
|
|
262
|
+
developer: typeof opts.developer === 'string' ? opts.developer : null,
|
|
263
|
+
});
|
|
264
|
+
process.exitCode = code;
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
await program.parseAsync(subargs);
|
|
268
|
+
const code = process.exitCode;
|
|
269
|
+
return typeof code === 'number' ? code : 0;
|
|
270
|
+
}
|
|
271
|
+
export function parseArgs(argv) {
|
|
272
|
+
const program = new Command();
|
|
273
|
+
program
|
|
274
|
+
.name('preflight')
|
|
275
|
+
.description('New Relic MCP server for observing AI coding assistants')
|
|
276
|
+
.version(VERSION)
|
|
277
|
+
.option('-p, --port <number>', 'HTTP port for proxy mode', '9847')
|
|
278
|
+
.option('-c, --config <path>', 'path to config file')
|
|
279
|
+
.option('-l, --log-level <level>', 'log level (debug|info|warn|error)', 'info')
|
|
280
|
+
.option('--stdio', 'use stdio transport (for Claude Code MCP connection)')
|
|
281
|
+
.option('--local', 'start dashboard and event processor without MCP stdio transport')
|
|
282
|
+
.option('--validate', 'validate config file and exit (combine with --config to check a specific file)');
|
|
283
|
+
program.parse(argv);
|
|
284
|
+
const opts = program.opts();
|
|
285
|
+
const parsed = parseInt(opts.port, 10);
|
|
286
|
+
if (!Number.isFinite(parsed) || parsed <= 0 || parsed > 65535) {
|
|
287
|
+
throw new Error(`Invalid port "${opts.port}": must be an integer between 1 and 65535`);
|
|
288
|
+
}
|
|
289
|
+
const stdio = opts.stdio ?? false;
|
|
290
|
+
const local = opts.local ?? false;
|
|
291
|
+
const validate = opts.validate ?? false;
|
|
292
|
+
if (stdio && local) {
|
|
293
|
+
throw new Error('--stdio and --local are mutually exclusive. Use one or the other.');
|
|
294
|
+
}
|
|
295
|
+
if (validate && (stdio || local)) {
|
|
296
|
+
throw new Error('--validate is mutually exclusive with --stdio and --local.');
|
|
297
|
+
}
|
|
298
|
+
return {
|
|
299
|
+
port: parsed,
|
|
300
|
+
config: opts.config ?? null,
|
|
301
|
+
logLevel: opts.logLevel,
|
|
302
|
+
stdio,
|
|
303
|
+
local,
|
|
304
|
+
validate,
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
async function main() {
|
|
308
|
+
// Subcommand dispatch (e.g. `preflight deploy-dashboards --all`)
|
|
309
|
+
// happens before flag parsing — they don't share the option schema with the
|
|
310
|
+
// server modes (--stdio / --local / --validate / proxy), and they exit
|
|
311
|
+
// independently rather than booting the full pipeline.
|
|
312
|
+
const subcommandExit = await dispatchSubcommand(process.argv);
|
|
313
|
+
if (subcommandExit !== null) {
|
|
314
|
+
process.exit(subcommandExit);
|
|
315
|
+
}
|
|
316
|
+
migrateStoragePath();
|
|
317
|
+
const options = parseArgs(process.argv);
|
|
318
|
+
// Propagate --log-level into the env var that createLogger() reads.
|
|
319
|
+
// Must be set before any subsystem loggers are constructed.
|
|
320
|
+
process.env.NEW_RELIC_AI_LOG_LEVEL = options.logLevel;
|
|
321
|
+
logger.info('Starting preflight', {
|
|
322
|
+
version: VERSION,
|
|
323
|
+
stdio: options.stdio,
|
|
324
|
+
port: options.port,
|
|
325
|
+
logLevel: options.logLevel,
|
|
326
|
+
});
|
|
327
|
+
if (options.validate) {
|
|
328
|
+
const configPath = options.config ?? resolve(DEFAULT_STORAGE_PATH, 'config.json');
|
|
329
|
+
process.stdout.write(`Validating config: ${configPath}\n\n`);
|
|
330
|
+
try {
|
|
331
|
+
const cfg = loadMcpConfig(options);
|
|
332
|
+
process.stdout.write(` mode: ${cfg.mode}\n`);
|
|
333
|
+
process.stdout.write(` developer: ${cfg.developer}\n`);
|
|
334
|
+
if (cfg.accountId)
|
|
335
|
+
process.stdout.write(` accountId: ${cfg.accountId}\n`);
|
|
336
|
+
if (cfg.licenseKey)
|
|
337
|
+
process.stdout.write(` licenseKey: ${maskCredential(cfg.licenseKey)}\n`);
|
|
338
|
+
if (cfg.nrApiKey)
|
|
339
|
+
process.stdout.write(` nrApiKey: ${maskCredential(cfg.nrApiKey)}\n`);
|
|
340
|
+
process.stdout.write(` region: ${cfg.collectorHost ?? 'us'}\n`);
|
|
341
|
+
process.stdout.write(` storage: ${cfg.storagePath}\n`);
|
|
342
|
+
process.stdout.write(` dashboard: http://${cfg.dashboard.host}:${cfg.dashboard.port}\n`);
|
|
343
|
+
process.stdout.write(`\nConfig is valid.\n`);
|
|
344
|
+
process.exit(0);
|
|
345
|
+
}
|
|
346
|
+
catch (err) {
|
|
347
|
+
process.stdout.write(` error: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
348
|
+
process.stdout.write(`\nConfig validation failed.\n`);
|
|
349
|
+
process.exit(1);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
// Declare resource holders before any async work so the shutdown handler
|
|
353
|
+
// safely cleans up whatever was initialized before a signal arrives.
|
|
354
|
+
let mcpServer;
|
|
355
|
+
let eventProcessor;
|
|
356
|
+
let nrIngest;
|
|
357
|
+
let proxyManager;
|
|
358
|
+
let sessionStore;
|
|
359
|
+
let weeklySummaryGenerator;
|
|
360
|
+
let persistSession;
|
|
361
|
+
let config;
|
|
362
|
+
let sessionTracker;
|
|
363
|
+
let taskDetector;
|
|
364
|
+
let sessionSpan;
|
|
365
|
+
let taskSpanTracker;
|
|
366
|
+
let dashboardServer;
|
|
367
|
+
let liveSessionRegistry;
|
|
368
|
+
let alertEvaluationInterval;
|
|
369
|
+
let alertRulesWatcher;
|
|
370
|
+
let alertRulesWatchTimer;
|
|
371
|
+
let localStoreForShutdown;
|
|
372
|
+
let gcInterval;
|
|
373
|
+
// Task #13: when this MCP starts headless (Fix 1 EADDRINUSE skip), this
|
|
374
|
+
// interval retries dashboardServer.start() periodically so we can take
|
|
375
|
+
// over if the current owner exits. Cleared in the shutdown handler.
|
|
376
|
+
let dashboardRepollInterval;
|
|
377
|
+
let shuttingDown = false;
|
|
378
|
+
const shutdown = async () => {
|
|
379
|
+
if (shuttingDown)
|
|
380
|
+
return;
|
|
381
|
+
shuttingDown = true;
|
|
382
|
+
logger.info('Shutting down...');
|
|
383
|
+
try {
|
|
384
|
+
persistSession?.();
|
|
385
|
+
if (config?.transport !== 'nr-events-api' && sessionTracker && taskDetector && sessionSpan) {
|
|
386
|
+
taskSpanTracker?.closeAll();
|
|
387
|
+
const stats = sessionTracker.getMetrics();
|
|
388
|
+
const taskMetrics = taskDetector.getMetrics();
|
|
389
|
+
sessionSpan.end(stats.toolCallCount, taskMetrics.totalTasksCompleted);
|
|
390
|
+
}
|
|
391
|
+
if (alertEvaluationInterval)
|
|
392
|
+
clearInterval(alertEvaluationInterval);
|
|
393
|
+
if (gcInterval)
|
|
394
|
+
clearInterval(gcInterval);
|
|
395
|
+
if (dashboardRepollInterval)
|
|
396
|
+
clearInterval(dashboardRepollInterval);
|
|
397
|
+
// Task #18: remove this MCP's heartbeat so the next dashboard-owner GC
|
|
398
|
+
// pass doesn't have to mtime-archive our buffer file.
|
|
399
|
+
localStoreForShutdown?.removeHeartbeat();
|
|
400
|
+
if (alertRulesWatchTimer)
|
|
401
|
+
clearTimeout(alertRulesWatchTimer);
|
|
402
|
+
if (alertRulesWatcher) {
|
|
403
|
+
try {
|
|
404
|
+
alertRulesWatcher.close();
|
|
405
|
+
}
|
|
406
|
+
catch {
|
|
407
|
+
// ignore close errors during shutdown
|
|
408
|
+
}
|
|
409
|
+
alertRulesWatcher = undefined;
|
|
410
|
+
}
|
|
411
|
+
eventProcessor?.stop();
|
|
412
|
+
liveSessionRegistry?.stopSampling();
|
|
413
|
+
// Use allSettled so a failure in one stop() doesn't prevent the others.
|
|
414
|
+
const stopResults = await Promise.allSettled([
|
|
415
|
+
dashboardServer ? dashboardServer.stop() : Promise.resolve(),
|
|
416
|
+
nrIngest ? nrIngest.stop() : Promise.resolve(),
|
|
417
|
+
mcpServer ? mcpServer.close() : Promise.resolve(),
|
|
418
|
+
proxyManager ? proxyManager.stop() : Promise.resolve(),
|
|
419
|
+
]);
|
|
420
|
+
for (const r of stopResults) {
|
|
421
|
+
if (r.status === 'rejected') {
|
|
422
|
+
logger.warn('Error stopping service during shutdown', { error: String(r.reason) });
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
catch (err) {
|
|
427
|
+
logger.error('Error during shutdown cleanup', { error: String(err) });
|
|
428
|
+
}
|
|
429
|
+
finally {
|
|
430
|
+
process.exit(0);
|
|
431
|
+
}
|
|
432
|
+
};
|
|
433
|
+
const handleSignal = () => {
|
|
434
|
+
shutdown().catch((err) => {
|
|
435
|
+
process.stderr.write(`Shutdown error: ${String(err)}\n`);
|
|
436
|
+
process.exit(1);
|
|
437
|
+
});
|
|
438
|
+
};
|
|
439
|
+
process.on('SIGINT', handleSignal);
|
|
440
|
+
process.on('SIGTERM', handleSignal);
|
|
441
|
+
if (options.stdio || options.local) {
|
|
442
|
+
let sessionTraceId;
|
|
443
|
+
if (options.stdio) {
|
|
444
|
+
// Connect stdio FIRST so the MCP handshake can complete immediately.
|
|
445
|
+
// Tools are registered after initialization; tool calls before that
|
|
446
|
+
// will return MethodNotFound (which the SDK handles gracefully).
|
|
447
|
+
mcpServer = createServer();
|
|
448
|
+
await mcpServer.connectStdio();
|
|
449
|
+
// Register stdin shutdown handlers immediately after connecting so that
|
|
450
|
+
// shutdown() is called even if stdin closes during the session-ID
|
|
451
|
+
// resolution window (before the handlers were previously registered).
|
|
452
|
+
process.stdin.once('end', () => {
|
|
453
|
+
logger.info('stdin closed, shutting down');
|
|
454
|
+
void shutdown();
|
|
455
|
+
});
|
|
456
|
+
process.stdin.on('error', (err) => {
|
|
457
|
+
logger.warn('stdin error, shutting down', { error: String(err) });
|
|
458
|
+
void shutdown();
|
|
459
|
+
});
|
|
460
|
+
config = loadMcpConfig(options);
|
|
461
|
+
if (!config.enabled) {
|
|
462
|
+
logger.info('Server disabled via config — exiting');
|
|
463
|
+
await mcpServer.close();
|
|
464
|
+
process.exit(0);
|
|
465
|
+
}
|
|
466
|
+
// Fix 3 / D2: resolve the Claude Code session_id BEFORE constructing
|
|
467
|
+
// anything that takes sessionTraceId as input. We try the cheap
|
|
468
|
+
// synchronous paths first (CLAUDE_JOB_DIR, then a one-shot breadcrumb
|
|
469
|
+
// probe). If both miss, register a "pending" tool handler so the MCP
|
|
470
|
+
// can answer health/config requests while we poll for the breadcrumb,
|
|
471
|
+
// then await full resolution.
|
|
472
|
+
const configFilePathEarly = options.config ?? resolve(DEFAULT_STORAGE_PATH, 'config.json');
|
|
473
|
+
const configSummaryEarly = {
|
|
474
|
+
mode: config.mode,
|
|
475
|
+
developer: config.developer,
|
|
476
|
+
accountId: config.accountId ?? null,
|
|
477
|
+
licenseKeyMasked: config.licenseKey ? maskCredential(config.licenseKey) : null,
|
|
478
|
+
nrApiKeyMasked: config.nrApiKey ? maskCredential(config.nrApiKey) : null,
|
|
479
|
+
region: config.collectorHost ?? 'us',
|
|
480
|
+
storagePath: config.storagePath,
|
|
481
|
+
dashboardUrl: `http://${config.dashboard.host}:${config.dashboard.port}`,
|
|
482
|
+
configFilePath: configFilePathEarly,
|
|
483
|
+
};
|
|
484
|
+
const synchronouslyResolved = resolveFromJobDir(process.env.CLAUDE_JOB_DIR ?? null) ??
|
|
485
|
+
resolveFromBreadcrumb(config.storagePath, process.ppid);
|
|
486
|
+
if (synchronouslyResolved) {
|
|
487
|
+
sessionTraceId = synchronouslyResolved;
|
|
488
|
+
logger.info('Session ID resolved synchronously', { sessionTraceId });
|
|
489
|
+
}
|
|
490
|
+
else {
|
|
491
|
+
// Tools must respond with a structured error during the resolution
|
|
492
|
+
// window — registerPendingTools wires that up. Once resolved we
|
|
493
|
+
// overwrite the handlers via registerTools().
|
|
494
|
+
registerPendingTools(mcpServer.server, {
|
|
495
|
+
sessionStartMs: Date.now(),
|
|
496
|
+
developer: config.developer,
|
|
497
|
+
configSummary: configSummaryEarly,
|
|
498
|
+
});
|
|
499
|
+
logger.info('Awaiting session_id resolution (breadcrumb poll)');
|
|
500
|
+
try {
|
|
501
|
+
sessionTraceId = await resolveSessionId({ storagePath: config.storagePath });
|
|
502
|
+
}
|
|
503
|
+
catch (err) {
|
|
504
|
+
logger.error('Session ID resolution failed; shutting down', { error: String(err) });
|
|
505
|
+
await shutdown();
|
|
506
|
+
return;
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
if (config.transport !== 'nr-events-api') {
|
|
510
|
+
initMcpTracer();
|
|
511
|
+
}
|
|
512
|
+
sessionSpan = new SessionSpan(sessionTraceId, config.developer);
|
|
513
|
+
taskSpanTracker = new TaskSpanTracker();
|
|
514
|
+
if (config.transport !== 'nr-events-api') {
|
|
515
|
+
sessionSpan.start();
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
else {
|
|
519
|
+
// --local: force local mode so config validation skips cloud credentials.
|
|
520
|
+
process.env.NR_AI_MODE = 'local';
|
|
521
|
+
config = loadMcpConfig(options);
|
|
522
|
+
if (!config.enabled) {
|
|
523
|
+
logger.info('Server disabled via config — exiting');
|
|
524
|
+
process.exit(0);
|
|
525
|
+
}
|
|
526
|
+
// --local has no owning Claude Code session — derive a deterministic
|
|
527
|
+
// identifier so the rest of the codebase can rely on a non-empty
|
|
528
|
+
// sessionTraceId without fabricating a UUID.
|
|
529
|
+
sessionTraceId = `local-${Date.now()}`;
|
|
530
|
+
}
|
|
531
|
+
// Per-session buffer scoping: in --stdio mode the LocalStore is bound to
|
|
532
|
+
// this MCP's resolved session_id so drainBuffer() only sees this session's
|
|
533
|
+
// events. In --local mode no single session owns the buffer; we drain all
|
|
534
|
+
// buffer-*.jsonl files via drainAllBuffers() instead.
|
|
535
|
+
const localStore = options.stdio
|
|
536
|
+
? new LocalStore(config.storagePath, sessionTraceId)
|
|
537
|
+
: new LocalStore(config.storagePath);
|
|
538
|
+
localStore.initialize();
|
|
539
|
+
// Task #18: every MCP writes its heartbeat once it has bound a session_id
|
|
540
|
+
// so the dashboard owner's GC pass can tell which buffer files still have
|
|
541
|
+
// a live owner. Removed in the shutdown handler below. No-op in --local
|
|
542
|
+
// mode (no sessionId).
|
|
543
|
+
if (options.stdio)
|
|
544
|
+
localStore.writeHeartbeat();
|
|
545
|
+
localStoreForShutdown = localStore;
|
|
546
|
+
// Migrate any pre-Fix-3 events from the legacy shared `buffer.jsonl` into
|
|
547
|
+
// per-session files. Idempotent and a no-op on fresh installs.
|
|
548
|
+
try {
|
|
549
|
+
localStore.migrateLegacyBuffer();
|
|
550
|
+
}
|
|
551
|
+
catch (err) {
|
|
552
|
+
logger.warn('Legacy buffer migration failed (continuing)', { error: String(err) });
|
|
553
|
+
}
|
|
554
|
+
if (config.retainSessionsDays !== null && config.retainSessionsDays > 0) {
|
|
555
|
+
const { purgeOldSessions } = await import('./storage/retention.js');
|
|
556
|
+
const purged = purgeOldSessions(config.storagePath, config.retainSessionsDays);
|
|
557
|
+
if (purged > 0) {
|
|
558
|
+
logger.info('Retention purge complete', { deletedSessionFiles: purged });
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
sessionTracker = new SessionTracker(sessionTraceId);
|
|
562
|
+
const costTracker = new CostTracker(sessionTracker);
|
|
563
|
+
taskDetector = new TaskDetector({ costTracker });
|
|
564
|
+
const antiPatternDetector = new AntiPatternDetector();
|
|
565
|
+
const efficiencyScorer = new EfficiencyScorer();
|
|
566
|
+
const feedbackCollector = new FeedbackCollector();
|
|
567
|
+
const contextWindowTracker = new ContextWindowTracker();
|
|
568
|
+
const latencyTracker = new LatencyTracker();
|
|
569
|
+
const taskCompletionTracker = new TaskCompletionTracker();
|
|
570
|
+
const modelUsageTracker = new ModelUsageTracker();
|
|
571
|
+
const retryDetector = new RetryDetector();
|
|
572
|
+
const contextCompositionTracker = new ContextCompositionTracker();
|
|
573
|
+
const contextTracker = new ContextTrackerRegistry();
|
|
574
|
+
// LatencyDecompositionTracker requires turn-level LLM vs tool timing that is
|
|
575
|
+
// only available in proxy mode (where we see upstream response latency). In
|
|
576
|
+
// stdio mode the data cannot be auto-populated so we skip instantiation.
|
|
577
|
+
const latencyDecompositionTracker = undefined;
|
|
578
|
+
const decisionTracker = new DecisionTracker();
|
|
579
|
+
const instructionDriftTracker = new InstructionDriftTracker();
|
|
580
|
+
const toolSelectionScorer = new ToolSelectionScorer();
|
|
581
|
+
const qualityProxyTracker = new QualityProxyTracker();
|
|
582
|
+
const apiFailureTracker = new ApiFailureTracker();
|
|
583
|
+
liveSessionRegistry = new LiveSessionRegistry();
|
|
584
|
+
liveSessionRegistry.startSampling();
|
|
585
|
+
const turnCostAttributor = new TurnCostAttributor();
|
|
586
|
+
const turnTracker = new TurnTracker();
|
|
587
|
+
const gitEfficiencyTracker = new GitEfficiencyTracker();
|
|
588
|
+
const toolCallBuffer = [];
|
|
589
|
+
const toolCallBufferAccessor = {
|
|
590
|
+
getRecords: () => toolCallBuffer,
|
|
591
|
+
};
|
|
592
|
+
sessionStore = new SessionStore({ storagePath: config.storagePath });
|
|
593
|
+
const currentSessionId = sessionTracker.getMetrics().sessionId;
|
|
594
|
+
let currentRepoName = null;
|
|
595
|
+
// Hydrate git efficiency tracker with today's prior sessions so the
|
|
596
|
+
// dashboard shows all-day git activity, not just the current session.
|
|
597
|
+
const todaySessions = sessionStore.loadTodaySessions();
|
|
598
|
+
for (const session of todaySessions) {
|
|
599
|
+
if (session.sessionId === currentSessionId)
|
|
600
|
+
continue;
|
|
601
|
+
if (session.timeline && session.timeline.length > 0) {
|
|
602
|
+
gitEfficiencyTracker.replayTimeline(session.timeline);
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
// Also hydrate from git log — commit commands often aren't captured by
|
|
606
|
+
// tool hooks (Claude Code commits internally), so we read the actual
|
|
607
|
+
// repo history to get an accurate commit count for today.
|
|
608
|
+
// Each command is isolated so a slow/missing git or remote doesn't block
|
|
609
|
+
// the others. Uses spawnSync (no shell) to avoid injection; stderr is
|
|
610
|
+
// suppressed via stdio rather than shell redirection. Timeout 2s per call.
|
|
611
|
+
const { spawnSync } = await import('node:child_process');
|
|
612
|
+
const GIT_OPTS = {
|
|
613
|
+
encoding: 'utf-8',
|
|
614
|
+
timeout: 2000,
|
|
615
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
616
|
+
};
|
|
617
|
+
// spawnSync with ENOENT doesn't throw — it returns { status: null, error: Error }.
|
|
618
|
+
// The status === 0 guard handles unavailable-git without a try/catch.
|
|
619
|
+
const todayStr = new Date().toISOString().slice(0, 10);
|
|
620
|
+
const logResult = spawnSync('git', ['log', `--since=${todayStr}T00:00:00Z`, '--format=%H %ct'], GIT_OPTS);
|
|
621
|
+
if (logResult.status === 0 && logResult.stdout !== null) {
|
|
622
|
+
const commits = logResult.stdout
|
|
623
|
+
.trim()
|
|
624
|
+
.split('\n')
|
|
625
|
+
.filter(Boolean)
|
|
626
|
+
.map((line) => {
|
|
627
|
+
const [hash, epochStr] = line.split(' ');
|
|
628
|
+
return { hash: hash ?? '', timestamp: parseInt(epochStr ?? '0', 10) * 1000 };
|
|
629
|
+
});
|
|
630
|
+
gitEfficiencyTracker.hydrateGitLog(commits);
|
|
631
|
+
}
|
|
632
|
+
// Repo context for the dashboard header
|
|
633
|
+
const remoteResult = spawnSync('git', ['remote', 'get-url', 'origin'], GIT_OPTS);
|
|
634
|
+
const branchResult = spawnSync('git', ['branch', '--show-current'], GIT_OPTS);
|
|
635
|
+
if (remoteResult.status === 0 && branchResult.status === 0) {
|
|
636
|
+
const remoteUrl = remoteResult.stdout.trim();
|
|
637
|
+
const branch = branchResult.stdout.trim();
|
|
638
|
+
// Extract repo name from remote URL (handles both HTTPS and SSH)
|
|
639
|
+
const repoMatch = remoteUrl.match(/[/:]([^/]+\/[^/]+?)(?:\.git)?$/);
|
|
640
|
+
const repoName = repoMatch ? repoMatch[1] : null;
|
|
641
|
+
currentRepoName = repoName;
|
|
642
|
+
gitEfficiencyTracker.hydrateRepoContext({
|
|
643
|
+
repoName,
|
|
644
|
+
branch: branch || null,
|
|
645
|
+
remoteName: 'origin',
|
|
646
|
+
defaultBranch: 'main',
|
|
647
|
+
});
|
|
648
|
+
}
|
|
649
|
+
// Branch divergence from main — how far ahead/behind are we?
|
|
650
|
+
const aheadResult = spawnSync('git', ['rev-list', '--count', 'origin/main..HEAD'], GIT_OPTS);
|
|
651
|
+
const behindResult = spawnSync('git', ['rev-list', '--count', 'HEAD..origin/main'], GIT_OPTS);
|
|
652
|
+
if (aheadResult.status === 0 && behindResult.status === 0) {
|
|
653
|
+
const ahead = parseInt(aheadResult.stdout.trim(), 10);
|
|
654
|
+
const behind = parseInt(behindResult.stdout.trim(), 10);
|
|
655
|
+
if (!Number.isNaN(ahead) && !Number.isNaN(behind)) {
|
|
656
|
+
gitEfficiencyTracker.hydrateBranchDivergence(ahead, behind);
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
// Cached prior-cost baseline. Refreshed lazily so:
|
|
660
|
+
// - sessions persisted by other MCPs during this session land in totals
|
|
661
|
+
// - day rollover invalidates immediately (a long-running session past
|
|
662
|
+
// midnight previously kept yesterday-as-today bookkeeping forever
|
|
663
|
+
// because the baseline was computed once at startup)
|
|
664
|
+
// - cross-midnight prior sessions contribute only their today-portion
|
|
665
|
+
// (todayPortionOfSessionCost pro-rates by timeline overlap)
|
|
666
|
+
//
|
|
667
|
+
// Cache TTL is 30 s so the disk scan over ~/.newrelic-preflight/sessions/ runs
|
|
668
|
+
// at most twice a minute even when cost-updates fire on every token event.
|
|
669
|
+
const PRIOR_COST_CACHE_TTL_MS = 30_000;
|
|
670
|
+
// Capture a non-null reference so the refresh closures don't have to
|
|
671
|
+
// re-narrow `sessionStore: SessionStore | undefined` on every call.
|
|
672
|
+
const sessionStoreForCostBaseline = sessionStore;
|
|
673
|
+
const priorCostCache = {
|
|
674
|
+
priorDailyCostUsd: 0,
|
|
675
|
+
priorWeeklyCostUsd: 0,
|
|
676
|
+
// Date key used to invalidate on day rollover even mid-TTL.
|
|
677
|
+
lastDayKey: localDateKey(),
|
|
678
|
+
lastRefreshMs: 0,
|
|
679
|
+
};
|
|
680
|
+
const refreshPriorCostBaseline = () => {
|
|
681
|
+
const now = Date.now();
|
|
682
|
+
const baseline = computeHistoricalCosts(sessionStoreForCostBaseline, currentSessionId, now);
|
|
683
|
+
priorCostCache.priorDailyCostUsd = baseline.priorDailyCostUsd;
|
|
684
|
+
priorCostCache.priorWeeklyCostUsd = baseline.priorWeeklyCostUsd;
|
|
685
|
+
priorCostCache.lastDayKey = localDateKey(now);
|
|
686
|
+
priorCostCache.lastRefreshMs = now;
|
|
687
|
+
};
|
|
688
|
+
const refreshPriorCostBaselineIfStale = () => {
|
|
689
|
+
const now = Date.now();
|
|
690
|
+
const dayChanged = priorCostCache.lastDayKey !== localDateKey(now);
|
|
691
|
+
const expired = now - priorCostCache.lastRefreshMs > PRIOR_COST_CACHE_TTL_MS;
|
|
692
|
+
if (dayChanged || expired)
|
|
693
|
+
refreshPriorCostBaseline();
|
|
694
|
+
};
|
|
695
|
+
refreshPriorCostBaseline();
|
|
696
|
+
weeklySummaryGenerator = new WeeklySummaryGenerator({
|
|
697
|
+
storagePath: config.storagePath,
|
|
698
|
+
sessionStore,
|
|
699
|
+
});
|
|
700
|
+
const trendAnalyzer = new TrendAnalyzer({ sessionStore });
|
|
701
|
+
const collaborationProfiler = new CollaborationProfiler({ sessionStore });
|
|
702
|
+
const claudeMdTracker = new ClaudeMdTracker({ sessionStore });
|
|
703
|
+
const costPerOutcomeAnalyzer = new CostPerOutcomeAnalyzer();
|
|
704
|
+
const personalCoach = new PersonalCoach(weeklySummaryGenerator, config.developer);
|
|
705
|
+
const promptFeedbackEngine = new PromptFeedbackEngine({
|
|
706
|
+
sessionStore,
|
|
707
|
+
collaborationProfiler,
|
|
708
|
+
claudeMdTracker,
|
|
709
|
+
});
|
|
710
|
+
const recommendationEngine = new RecommendationEngine({
|
|
711
|
+
sessionStore,
|
|
712
|
+
trendAnalyzer,
|
|
713
|
+
collaborationProfiler,
|
|
714
|
+
claudeMdTracker,
|
|
715
|
+
promptFeedbackEngine,
|
|
716
|
+
costPerOutcomeAnalyzer,
|
|
717
|
+
taskDetector,
|
|
718
|
+
});
|
|
719
|
+
const sessionStartMs = Date.now();
|
|
720
|
+
const liveBus = new LiveEventBus();
|
|
721
|
+
const budgetTracker = new BudgetTracker({
|
|
722
|
+
sessionBudgetUsd: config.sessionBudgetUsd,
|
|
723
|
+
dailyBudgetUsd: config.dailyBudgetUsd,
|
|
724
|
+
weeklyBudgetUsd: config.weeklyBudgetUsd,
|
|
725
|
+
});
|
|
726
|
+
// Construct AuditTrailManager once and share it across NrIngestManager and the
|
|
727
|
+
// DashboardServer. In local mode there is no NrIngestManager, but the dashboard
|
|
728
|
+
// and McpServer still need an audit log.
|
|
729
|
+
const auditTrail = new AuditTrailManager({
|
|
730
|
+
developer: config.developer,
|
|
731
|
+
sessionId: sessionTraceId,
|
|
732
|
+
localStore,
|
|
733
|
+
});
|
|
734
|
+
const dashboardEnabled = config.mode === 'local' || config.mode === 'both';
|
|
735
|
+
let alertEngine;
|
|
736
|
+
let alertSnapshotCollector;
|
|
737
|
+
let alertLog;
|
|
738
|
+
if (dashboardEnabled) {
|
|
739
|
+
const { dirname, resolve: resolvePath, join: joinPath } = await import('node:path');
|
|
740
|
+
// Resolve symlinks (e.g. npm link) before dirname so staticDir points
|
|
741
|
+
// to the actual dist/ directory, not the symlink's parent.
|
|
742
|
+
const entryScript = realpathSync(process.argv[1] ?? process.cwd());
|
|
743
|
+
const here = dirname(entryScript);
|
|
744
|
+
const staticDir = resolvePath(here, 'web');
|
|
745
|
+
// Local alerts: construct engine + log + snapshot collector only when
|
|
746
|
+
// alerts are enabled (default true outside cloud-only mode). Rules are
|
|
747
|
+
// loaded from disk (config.alerts.rulesPath); fs.watch reloads them
|
|
748
|
+
// when the file changes.
|
|
749
|
+
if (config.alerts.enabled) {
|
|
750
|
+
const osNotifier = new OsNotifier();
|
|
751
|
+
alertEngine = new LocalAlertEngine({
|
|
752
|
+
osNotifier,
|
|
753
|
+
osNotificationsEnabled: config.alerts.osNotifications,
|
|
754
|
+
});
|
|
755
|
+
alertLog = new AlertLog({
|
|
756
|
+
path: joinPath(config.storagePath, 'alerts', 'log.jsonl'),
|
|
757
|
+
});
|
|
758
|
+
// Adapter for EfficiencyScorer: collector wants a numeric score or
|
|
759
|
+
// null. Internally use getSessionAverage() rather than adding a new
|
|
760
|
+
// public method on the scorer.
|
|
761
|
+
const efficiencyAdapter = {
|
|
762
|
+
getCurrentScore: () => efficiencyScorer.getSessionAverage()?.score ?? null,
|
|
763
|
+
};
|
|
764
|
+
alertSnapshotCollector = new AlertSnapshotCollector({
|
|
765
|
+
costTracker,
|
|
766
|
+
// BudgetTracker carries the cumulative daily/weekly totals that
|
|
767
|
+
// feed cost.window alert rules with `today`/`week` periods. Without
|
|
768
|
+
// this dep those rules silently match against 0 forever.
|
|
769
|
+
budgetTracker,
|
|
770
|
+
efficiencyScorer: efficiencyAdapter,
|
|
771
|
+
antiPatternDetector,
|
|
772
|
+
latencyTracker,
|
|
773
|
+
});
|
|
774
|
+
const capturedAlertLog = alertLog;
|
|
775
|
+
alertEngine.setOnAlert((event) => {
|
|
776
|
+
liveBus.emit('alert', event);
|
|
777
|
+
void capturedAlertLog.append(event);
|
|
778
|
+
});
|
|
779
|
+
// Initial rule load and fs.watch wiring. rulesPath is always a
|
|
780
|
+
// resolved string after config load (validateRulesPath falls back
|
|
781
|
+
// to the default when user input is invalid), so no null guard
|
|
782
|
+
// is needed here.
|
|
783
|
+
const rulesPath = config.alerts.rulesPath;
|
|
784
|
+
loadAlertRulesFromDisk(alertEngine, rulesPath);
|
|
785
|
+
try {
|
|
786
|
+
const fs = await import('node:fs');
|
|
787
|
+
// fs.watch on macOS fires twice (write + rename) for many editors;
|
|
788
|
+
// debounce via a 200 ms timer. The watch handle is closed during
|
|
789
|
+
// shutdown.
|
|
790
|
+
alertRulesWatcher = fs.watch(rulesPath, { persistent: false }, () => {
|
|
791
|
+
try {
|
|
792
|
+
if (alertRulesWatchTimer)
|
|
793
|
+
clearTimeout(alertRulesWatchTimer);
|
|
794
|
+
alertRulesWatchTimer = setTimeout(() => {
|
|
795
|
+
if (alertEngine) {
|
|
796
|
+
loadAlertRulesFromDisk(alertEngine, rulesPath);
|
|
797
|
+
}
|
|
798
|
+
}, 200);
|
|
799
|
+
alertRulesWatchTimer.unref?.();
|
|
800
|
+
}
|
|
801
|
+
catch (err) {
|
|
802
|
+
logger.warn('Alert rules watch handler errored', { error: String(err) });
|
|
803
|
+
}
|
|
804
|
+
});
|
|
805
|
+
alertRulesWatcher.on('error', (err) => {
|
|
806
|
+
logger.warn('Alert rules watcher errored', { error: String(err) });
|
|
807
|
+
});
|
|
808
|
+
}
|
|
809
|
+
catch (err) {
|
|
810
|
+
logger.warn('Could not start fs.watch on alert rules file', {
|
|
811
|
+
rulesPath,
|
|
812
|
+
error: String(err),
|
|
813
|
+
});
|
|
814
|
+
}
|
|
815
|
+
// Periodic evaluation. The interval is unref'd so the Node event
|
|
816
|
+
// loop can exit cleanly during shutdown / when stdin closes.
|
|
817
|
+
const evaluationIntervalMs = config.alerts.evaluationIntervalSeconds * 1000;
|
|
818
|
+
const capturedEngine = alertEngine;
|
|
819
|
+
const capturedCollector = alertSnapshotCollector;
|
|
820
|
+
alertEvaluationInterval = setInterval(() => {
|
|
821
|
+
try {
|
|
822
|
+
const nowTs = Date.now();
|
|
823
|
+
const windows = capturedEngine.getRequiredWindows();
|
|
824
|
+
const snapshot = capturedCollector.snapshot(nowTs, windows);
|
|
825
|
+
capturedEngine.evaluate(snapshot, nowTs);
|
|
826
|
+
}
|
|
827
|
+
catch (err) {
|
|
828
|
+
logger.warn('Alert evaluation tick failed', { error: String(err) });
|
|
829
|
+
}
|
|
830
|
+
}, evaluationIntervalMs);
|
|
831
|
+
// Don't keep the process alive solely on this interval.
|
|
832
|
+
alertEvaluationInterval.unref?.();
|
|
833
|
+
}
|
|
834
|
+
dashboardServer = new DashboardServer({
|
|
835
|
+
port: config.dashboard.port,
|
|
836
|
+
host: config.dashboard.host,
|
|
837
|
+
bus: liveBus,
|
|
838
|
+
staticDir,
|
|
839
|
+
api: {
|
|
840
|
+
sessionTracker,
|
|
841
|
+
auditTrailManager: auditTrail,
|
|
842
|
+
sessionStore,
|
|
843
|
+
costTracker,
|
|
844
|
+
costForecast: () => {
|
|
845
|
+
const todayKey = localDateKey();
|
|
846
|
+
return buildCostForecastFromInputs({
|
|
847
|
+
sessionSpentUsd: costTracker.getMetrics().sessionTotalCostUsd ?? 0,
|
|
848
|
+
sessionStartMs,
|
|
849
|
+
dailySpentUsd: costTracker.getCostForDay(todayKey),
|
|
850
|
+
dailyFirstActivityMs: costTracker.getFirstActivityMsForDay(todayKey),
|
|
851
|
+
});
|
|
852
|
+
},
|
|
853
|
+
antiPatternDetector,
|
|
854
|
+
weeklySummaryGenerator,
|
|
855
|
+
budgetTracker,
|
|
856
|
+
latencyTracker,
|
|
857
|
+
personalCoach,
|
|
858
|
+
alertLog,
|
|
859
|
+
taskDetector,
|
|
860
|
+
efficiencyScorer,
|
|
861
|
+
qualityProxyTracker,
|
|
862
|
+
toolSelectionScorer,
|
|
863
|
+
modelUsageTracker,
|
|
864
|
+
toolCallBuffer: toolCallBufferAccessor,
|
|
865
|
+
liveSessionRegistry,
|
|
866
|
+
gitEfficiencyTracker,
|
|
867
|
+
concurrencyTracker: liveSessionRegistry,
|
|
868
|
+
contextTracker,
|
|
869
|
+
config,
|
|
870
|
+
configFilePath: options.config ?? resolve(DEFAULT_STORAGE_PATH, 'config.json'),
|
|
871
|
+
// Task #17 (D3): the dashboard owner reads every per-session
|
|
872
|
+
// buffer file in read-only mode for the Today aggregate endpoint.
|
|
873
|
+
// peekAllBuffers() returns HookEvent[] — widen at the boundary
|
|
874
|
+
// so the dashboard tree stays decoupled from storage internals.
|
|
875
|
+
localStore: {
|
|
876
|
+
peekAllBuffers: () => localStore.peekAllBuffers(),
|
|
877
|
+
},
|
|
878
|
+
},
|
|
879
|
+
alertEngine,
|
|
880
|
+
alertLog,
|
|
881
|
+
});
|
|
882
|
+
let addr;
|
|
883
|
+
try {
|
|
884
|
+
addr = await dashboardServer.start();
|
|
885
|
+
}
|
|
886
|
+
catch (err) {
|
|
887
|
+
// Multi-instance launch: when several `preflight --stdio`
|
|
888
|
+
// processes start at once (e.g. one per Claude Code session) only
|
|
889
|
+
// the first can bind the dashboard port; the rest receive
|
|
890
|
+
// EADDRINUSE. Treat that case as a graceful no-op so the MCP
|
|
891
|
+
// session still serves stdio + tool handlers; other errors
|
|
892
|
+
// propagate untouched.
|
|
893
|
+
const decision = classifyDashboardStartError(err, config.dashboard.host, config.dashboard.port);
|
|
894
|
+
if (decision.kind === 'rethrow') {
|
|
895
|
+
throw decision.error;
|
|
896
|
+
}
|
|
897
|
+
// In --local mode the HTTP server IS the process — without it there is
|
|
898
|
+
// nothing to keep the event loop alive. Treat EADDRINUSE as fatal so
|
|
899
|
+
// the user gets an actionable error instead of a silent exit.
|
|
900
|
+
if (options.local) {
|
|
901
|
+
logger.error(`Dashboard port ${config.dashboard.port} is already in use. ` +
|
|
902
|
+
`Stop the existing --local instance before starting another.`);
|
|
903
|
+
process.exit(1);
|
|
904
|
+
}
|
|
905
|
+
logger.info(decision.message);
|
|
906
|
+
addr = undefined;
|
|
907
|
+
}
|
|
908
|
+
// Capture deps for the post-bind helper. Both the initial-bind path
|
|
909
|
+
// and the re-poll takeover path call this; keeping the closure small
|
|
910
|
+
// ensures the two paths produce identical side effects (GC interval,
|
|
911
|
+
// openOnStart warning, etc. — Task #18 + #13).
|
|
912
|
+
const postBindDeps = {
|
|
913
|
+
localStore,
|
|
914
|
+
liveSessionRegistry,
|
|
915
|
+
openOnStart: config.dashboard.openOnStart,
|
|
916
|
+
};
|
|
917
|
+
const runPostBind = (boundAddr) => setupDashboardPostBind(boundAddr, postBindDeps);
|
|
918
|
+
if (addr) {
|
|
919
|
+
gcInterval = runPostBind(addr);
|
|
920
|
+
}
|
|
921
|
+
else {
|
|
922
|
+
// Task #13: this MCP is headless. Schedule periodic re-bind attempts
|
|
923
|
+
// so it can take over if the current dashboard owner exits. The
|
|
924
|
+
// interval is unref'd and cleared by the shutdown handler.
|
|
925
|
+
dashboardRepollInterval = startDashboardRepoll({
|
|
926
|
+
dashboardServer,
|
|
927
|
+
host: config.dashboard.host,
|
|
928
|
+
port: config.dashboard.port,
|
|
929
|
+
postBind: runPostBind,
|
|
930
|
+
onTakeover: (handle) => {
|
|
931
|
+
gcInterval = handle;
|
|
932
|
+
},
|
|
933
|
+
logger,
|
|
934
|
+
});
|
|
935
|
+
}
|
|
936
|
+
}
|
|
937
|
+
let capturedNrIngest;
|
|
938
|
+
if (config.mode !== 'local') {
|
|
939
|
+
if (!config.licenseKey || !config.accountId) {
|
|
940
|
+
throw new Error('licenseKey and accountId must be defined. ' +
|
|
941
|
+
'This should have been caught by config validation. ' +
|
|
942
|
+
'Check that mode is not "local" or that cloud credentials are configured.');
|
|
943
|
+
}
|
|
944
|
+
nrIngest = new NrIngestManager({
|
|
945
|
+
licenseKey: config.licenseKey,
|
|
946
|
+
transportOptions: {
|
|
947
|
+
accountId: config.accountId,
|
|
948
|
+
collectorHost: config.collectorHost,
|
|
949
|
+
},
|
|
950
|
+
developer: config.developer,
|
|
951
|
+
appName: config.appName,
|
|
952
|
+
teamId: config.teamId,
|
|
953
|
+
projectId: config.projectId,
|
|
954
|
+
orgId: config.orgId,
|
|
955
|
+
sessionTracker,
|
|
956
|
+
localStore,
|
|
957
|
+
auditTrail,
|
|
958
|
+
eventHarvestIntervalMs: config.harvestIntervalMs.events,
|
|
959
|
+
metricHarvestIntervalMs: config.harvestIntervalMs.metrics,
|
|
960
|
+
costTracker,
|
|
961
|
+
efficiencyScorer,
|
|
962
|
+
turnCostAttributor,
|
|
963
|
+
sessionTraceId,
|
|
964
|
+
});
|
|
965
|
+
capturedNrIngest = nrIngest;
|
|
966
|
+
}
|
|
967
|
+
const capturedAlertEngine = alertEngine;
|
|
968
|
+
const capturedAlertSnapshotCollector = alertSnapshotCollector;
|
|
969
|
+
budgetTracker.setOnThreshold((event) => {
|
|
970
|
+
capturedNrIngest?.ingestBudgetWarning(event);
|
|
971
|
+
logger.warn('Budget threshold reached', {
|
|
972
|
+
period: event.period,
|
|
973
|
+
pct: event.thresholdPct,
|
|
974
|
+
spentUsd: event.spentUsd.toFixed(4),
|
|
975
|
+
budgetUsd: event.budgetUsd.toFixed(2),
|
|
976
|
+
});
|
|
977
|
+
// Route into the local alert engine so configured rules can fire.
|
|
978
|
+
if (capturedAlertEngine) {
|
|
979
|
+
capturedAlertEngine.evaluate({
|
|
980
|
+
timestamp: event.timestamp,
|
|
981
|
+
cost: { sessionUsd: 0, todayUsd: 0, weekUsd: 0 },
|
|
982
|
+
efficiency: { score: null },
|
|
983
|
+
antiPatterns: [],
|
|
984
|
+
latency: [],
|
|
985
|
+
toolFailures: [],
|
|
986
|
+
budgetThresholds: [
|
|
987
|
+
{
|
|
988
|
+
period: event.period,
|
|
989
|
+
thresholdPct: event.thresholdPct,
|
|
990
|
+
spentUsd: event.spentUsd,
|
|
991
|
+
budgetUsd: event.budgetUsd,
|
|
992
|
+
},
|
|
993
|
+
],
|
|
994
|
+
}, Date.now());
|
|
995
|
+
}
|
|
996
|
+
});
|
|
997
|
+
eventProcessor = new HookEventProcessor({
|
|
998
|
+
store: localStore,
|
|
999
|
+
// --local mode owns no specific Claude Code session, so drain every
|
|
1000
|
+
// per-session buffer so the dashboard sees all live sessions' events.
|
|
1001
|
+
drainAllSessions: !options.stdio,
|
|
1002
|
+
onRecord: (record) => {
|
|
1003
|
+
if (!config || !sessionTracker || !taskDetector) {
|
|
1004
|
+
logger.warn('onRecord called before full initialization; skipping');
|
|
1005
|
+
return;
|
|
1006
|
+
}
|
|
1007
|
+
// Capture active task ID before recordToolCall may close the current task
|
|
1008
|
+
const taskIdBeforeRecord = config.transport !== 'nr-events-api' ? taskDetector.getActiveTaskId() : null;
|
|
1009
|
+
sessionTracker.recordToolCall(record);
|
|
1010
|
+
taskDetector.recordToolCall(record);
|
|
1011
|
+
if (record.sessionId) {
|
|
1012
|
+
liveSessionRegistry.touch(record.sessionId, record.cwd);
|
|
1013
|
+
}
|
|
1014
|
+
if (config.transport !== 'nr-events-api' && taskSpanTracker && sessionSpan) {
|
|
1015
|
+
// Emit tool call span — parent is the active task span (or session span if no task)
|
|
1016
|
+
const activeTaskId = taskDetector.getActiveTaskId();
|
|
1017
|
+
const parentCtx = taskIdBeforeRecord
|
|
1018
|
+
? taskSpanTracker.getContext(taskIdBeforeRecord, sessionSpan.getContext())
|
|
1019
|
+
: sessionSpan.getContext();
|
|
1020
|
+
emitToolCallSpan(record, parentCtx, activeTaskId ?? undefined);
|
|
1021
|
+
// Open a task span if a new task was started by this record
|
|
1022
|
+
if (activeTaskId !== null && activeTaskId !== taskIdBeforeRecord) {
|
|
1023
|
+
taskSpanTracker.openTask(activeTaskId, record.toolName, sessionSpan.getContext());
|
|
1024
|
+
}
|
|
1025
|
+
}
|
|
1026
|
+
contextWindowTracker.recordToolCall(record);
|
|
1027
|
+
contextTracker.recordToolCall(record);
|
|
1028
|
+
latencyTracker.recordToolCall(record);
|
|
1029
|
+
retryDetector.recordToolCall(record);
|
|
1030
|
+
qualityProxyTracker.recordToolCall(record);
|
|
1031
|
+
const turnId = turnTracker.recordToolCall(record);
|
|
1032
|
+
const turnNumber = turnTracker.getCurrentTurnNumber();
|
|
1033
|
+
turnCostAttributor.recordToolCall(record, turnId);
|
|
1034
|
+
decisionTracker.recordToolCall(record);
|
|
1035
|
+
instructionDriftTracker.recordToolCall(record);
|
|
1036
|
+
gitEfficiencyTracker.recordToolCall(record);
|
|
1037
|
+
record.turn_id = turnId;
|
|
1038
|
+
record.turn_number = turnNumber;
|
|
1039
|
+
toolCallBuffer.push(record);
|
|
1040
|
+
// Record audit trail unconditionally so the local dashboard's Audit view
|
|
1041
|
+
// populates regardless of mode. NrIngestManager (when present) reuses the
|
|
1042
|
+
// returned AuditRecord rather than recording a second time.
|
|
1043
|
+
const auditRecord = auditTrail.recordToolCall(record);
|
|
1044
|
+
capturedNrIngest?.ingestToolCall(record, auditRecord);
|
|
1045
|
+
// Task #17 (D3): SSE consumers filter by sessionId for the per-
|
|
1046
|
+
// session live tail. Records without a sessionId are pre-Fix-3
|
|
1047
|
+
// legacy buffer leaks during the migrateLegacyBuffer() window on
|
|
1048
|
+
// first boot — skip the live emit rather than fabricate a session
|
|
1049
|
+
// by falling back to the MCP's resolved sessionTraceId, which would
|
|
1050
|
+
// re-introduce the fictional-session-ID bug Fix 3 removed.
|
|
1051
|
+
if (record.sessionId) {
|
|
1052
|
+
liveBus.emit('tool-call', {
|
|
1053
|
+
id: record.id,
|
|
1054
|
+
sessionId: record.sessionId,
|
|
1055
|
+
tool: record.toolName,
|
|
1056
|
+
durationMs: record.durationMs ?? 0,
|
|
1057
|
+
costUsd: 0,
|
|
1058
|
+
ts: record.timestamp,
|
|
1059
|
+
});
|
|
1060
|
+
}
|
|
1061
|
+
// Push into the alert collector's rolling tool-call buffer so
|
|
1062
|
+
// tool.failure rules have data to evaluate against.
|
|
1063
|
+
capturedAlertSnapshotCollector?.recordToolCall({
|
|
1064
|
+
toolName: record.toolName,
|
|
1065
|
+
success: record.success,
|
|
1066
|
+
ts: record.timestamp,
|
|
1067
|
+
});
|
|
1068
|
+
// Fallback cost estimation from tool payload byte sizes.
|
|
1069
|
+
// Only fires when no exact token report has been received yet for this session,
|
|
1070
|
+
// to avoid double-counting with explicit nr_observe_report_tokens calls.
|
|
1071
|
+
const estimateBytes = (record.inputSizeBytes ?? 0) + (record.outputSizeBytes ?? 0);
|
|
1072
|
+
if (estimateBytes > 0 && costTracker.getMetrics().reportCount === 0) {
|
|
1073
|
+
// Prefer a model already learned from real token events over the config
|
|
1074
|
+
// default (which is just a guess). Falls back to config.model on cold start.
|
|
1075
|
+
const estimateModel = costTracker.getMetrics().model ?? config.model;
|
|
1076
|
+
costTracker.recordEstimatedTokens(record.inputSizeBytes ?? 0, record.outputSizeBytes ?? 0, estimateModel);
|
|
1077
|
+
}
|
|
1078
|
+
const costMetrics = costTracker.getMetrics();
|
|
1079
|
+
if (costMetrics.sessionTotalCostUsd !== null) {
|
|
1080
|
+
refreshPriorCostBaselineIfStale();
|
|
1081
|
+
const todayKey = localDateKey();
|
|
1082
|
+
const sessionTodayUsd = costTracker.getCostForDay(todayKey);
|
|
1083
|
+
const dailyFirstActivityMs = costTracker.getFirstActivityMsForDay(todayKey);
|
|
1084
|
+
const todayTotalUsd = priorCostCache.priorDailyCostUsd + sessionTodayUsd;
|
|
1085
|
+
// Weekly total still uses session-total because the whole session
|
|
1086
|
+
// falls within the rolling 7-day window for the prior baseline.
|
|
1087
|
+
const weeklyTotalUsd = priorCostCache.priorWeeklyCostUsd + costMetrics.sessionTotalCostUsd;
|
|
1088
|
+
budgetTracker.updateCost(costMetrics.sessionTotalCostUsd, todayTotalUsd, weeklyTotalUsd);
|
|
1089
|
+
const sessionForecast = buildCostForecastFromInputs({
|
|
1090
|
+
sessionSpentUsd: costMetrics.sessionTotalCostUsd,
|
|
1091
|
+
sessionStartMs,
|
|
1092
|
+
dailySpentUsd: sessionTodayUsd,
|
|
1093
|
+
dailyFirstActivityMs,
|
|
1094
|
+
});
|
|
1095
|
+
liveBus.emit('cost-update', {
|
|
1096
|
+
// Task #17 (D3): MCP-owned cost totals — sessionId is always the
|
|
1097
|
+
// resolved Claude Code session_id for this MCP instance.
|
|
1098
|
+
sessionId: sessionTraceId,
|
|
1099
|
+
sessionTotalUsd: costMetrics.sessionTotalCostUsd,
|
|
1100
|
+
todayTotalUsd,
|
|
1101
|
+
forecastEodUsd: sessionForecast.forecastEndOfDayUsd !== null
|
|
1102
|
+
? priorCostCache.priorDailyCostUsd + sessionForecast.forecastEndOfDayUsd
|
|
1103
|
+
: null,
|
|
1104
|
+
});
|
|
1105
|
+
}
|
|
1106
|
+
// Emit any tasks that completed as a result of this record,
|
|
1107
|
+
// and detect anti-patterns across each completed task's tool calls
|
|
1108
|
+
for (const task of taskDetector.drainNewlyCompletedTasks()) {
|
|
1109
|
+
capturedNrIngest?.ingestCodingTask(task);
|
|
1110
|
+
taskCompletionTracker.recordTask(task);
|
|
1111
|
+
// Close the task span — this handles both signal-driven and idle-timer-driven closures
|
|
1112
|
+
if (config.transport !== 'nr-events-api' && taskSpanTracker) {
|
|
1113
|
+
taskSpanTracker.closeTask(task.taskId, task.toolCallCount);
|
|
1114
|
+
}
|
|
1115
|
+
const firstRecord = task.toolCalls[0];
|
|
1116
|
+
// Fix 3: sessionTraceId is the resolved Claude Code session_id and is
|
|
1117
|
+
// shared across the whole MCP, so we use it directly rather than
|
|
1118
|
+
// peeking at the first record's sessionId (which may now be null).
|
|
1119
|
+
const context = {
|
|
1120
|
+
sessionId: sessionTraceId,
|
|
1121
|
+
platform: typeof firstRecord?.platform === 'string' ? firstRecord.platform : undefined,
|
|
1122
|
+
taskId: task.taskId,
|
|
1123
|
+
};
|
|
1124
|
+
const { patterns } = antiPatternDetector.analyze(task.toolCalls);
|
|
1125
|
+
efficiencyScorer.computeScore(task, patterns);
|
|
1126
|
+
for (const pattern of patterns) {
|
|
1127
|
+
capturedNrIngest?.ingestAntiPattern(pattern, context);
|
|
1128
|
+
liveBus.emit('anti-pattern', {
|
|
1129
|
+
// Task #17 (D3): tag with the originating session so the Today
|
|
1130
|
+
// view can render a "Session: <name>" pill on each alert row.
|
|
1131
|
+
sessionId: sessionTraceId,
|
|
1132
|
+
type: pattern.type,
|
|
1133
|
+
target: pattern.file ?? pattern.command ?? 'unknown',
|
|
1134
|
+
count: pattern.iterations ??
|
|
1135
|
+
pattern.readCount ??
|
|
1136
|
+
pattern.repeatCount ??
|
|
1137
|
+
pattern.editCount ??
|
|
1138
|
+
pattern.agentCount ??
|
|
1139
|
+
1,
|
|
1140
|
+
});
|
|
1141
|
+
// Mirror each detected pattern into the alert collector's
|
|
1142
|
+
// rolling buffer so antipattern.count rules have data.
|
|
1143
|
+
capturedAlertSnapshotCollector?.recordAntiPattern({
|
|
1144
|
+
type: pattern.type,
|
|
1145
|
+
ts: Date.now(),
|
|
1146
|
+
});
|
|
1147
|
+
}
|
|
1148
|
+
}
|
|
1149
|
+
},
|
|
1150
|
+
onTokenEvent: (tokenEvent) => {
|
|
1151
|
+
if (!costTracker || !config)
|
|
1152
|
+
return;
|
|
1153
|
+
turnCostAttributor.recordTokenEvent(tokenEvent);
|
|
1154
|
+
const usage = {
|
|
1155
|
+
inputTokens: tokenEvent.inputTokens,
|
|
1156
|
+
outputTokens: tokenEvent.outputTokens,
|
|
1157
|
+
thinkingTokens: 0,
|
|
1158
|
+
cacheReadTokens: tokenEvent.cacheReadTokens,
|
|
1159
|
+
cacheCreationTokens: tokenEvent.cacheCreationTokens,
|
|
1160
|
+
totalTokens: tokenEvent.inputTokens + tokenEvent.outputTokens,
|
|
1161
|
+
};
|
|
1162
|
+
const breakdown = costTracker.recordTokenUsage(usage, tokenEvent.model);
|
|
1163
|
+
modelUsageTracker.recordUsage(tokenEvent.model, tokenEvent.inputTokens, tokenEvent.outputTokens, breakdown.totalUsd);
|
|
1164
|
+
contextCompositionTracker.recordTokenEvent(tokenEvent);
|
|
1165
|
+
const ctxSnapshot = contextTracker.recordTurn(tokenEvent);
|
|
1166
|
+
if (ctxSnapshot && tokenEvent.sessionId) {
|
|
1167
|
+
const sid = tokenEvent.sessionId;
|
|
1168
|
+
const ctxMetrics = contextTracker.getMetrics(sid);
|
|
1169
|
+
const ctxTopTools = ctxMetrics.toolContributions.slice(0, 5);
|
|
1170
|
+
liveBus.emit('context-update', {
|
|
1171
|
+
sessionId: sid,
|
|
1172
|
+
turnNumber: ctxSnapshot.turnNumber,
|
|
1173
|
+
totalTokens: ctxSnapshot.inputTokens,
|
|
1174
|
+
fillPercent: ctxSnapshot.fillPercent,
|
|
1175
|
+
// Carry the model-aware cap so the client renders "X / Y"
|
|
1176
|
+
// from a single source of truth — see ContextUpdateEvent
|
|
1177
|
+
// doc-comment for the rationale.
|
|
1178
|
+
contextWindow: ctxMetrics.contextWindow,
|
|
1179
|
+
breakdown: ctxSnapshot.breakdown,
|
|
1180
|
+
growth: {
|
|
1181
|
+
startTokens: ctxMetrics.growth.startTokens,
|
|
1182
|
+
currentTokens: ctxMetrics.growth.currentTokens,
|
|
1183
|
+
delta: ctxMetrics.growth.deltaTokens,
|
|
1184
|
+
},
|
|
1185
|
+
topTools: ctxTopTools.map((t) => ({
|
|
1186
|
+
tool: t.tool,
|
|
1187
|
+
estimatedTokens: t.estimatedTokens,
|
|
1188
|
+
})),
|
|
1189
|
+
});
|
|
1190
|
+
capturedNrIngest?.ingestContextSnapshot(ctxSnapshot, ctxTopTools);
|
|
1191
|
+
}
|
|
1192
|
+
const costMetrics = costTracker.getMetrics();
|
|
1193
|
+
if (costMetrics.sessionTotalCostUsd !== null) {
|
|
1194
|
+
refreshPriorCostBaselineIfStale();
|
|
1195
|
+
const todayKey = localDateKey();
|
|
1196
|
+
const sessionTodayUsd = costTracker.getCostForDay(todayKey);
|
|
1197
|
+
const dailyFirstActivityMs = costTracker.getFirstActivityMsForDay(todayKey);
|
|
1198
|
+
const todayTotalUsd = priorCostCache.priorDailyCostUsd + sessionTodayUsd;
|
|
1199
|
+
const weeklyTotalUsd = priorCostCache.priorWeeklyCostUsd + costMetrics.sessionTotalCostUsd;
|
|
1200
|
+
budgetTracker.updateCost(costMetrics.sessionTotalCostUsd, todayTotalUsd, weeklyTotalUsd);
|
|
1201
|
+
const sessionForecast = buildCostForecastFromInputs({
|
|
1202
|
+
sessionSpentUsd: costMetrics.sessionTotalCostUsd,
|
|
1203
|
+
sessionStartMs,
|
|
1204
|
+
dailySpentUsd: sessionTodayUsd,
|
|
1205
|
+
dailyFirstActivityMs,
|
|
1206
|
+
});
|
|
1207
|
+
liveBus.emit('cost-update', {
|
|
1208
|
+
// Task #17 (D3): same as the per-tool-call cost-update emission —
|
|
1209
|
+
// tag with the MCP's owning session_id.
|
|
1210
|
+
sessionId: sessionTraceId,
|
|
1211
|
+
sessionTotalUsd: costMetrics.sessionTotalCostUsd,
|
|
1212
|
+
todayTotalUsd,
|
|
1213
|
+
forecastEodUsd: sessionForecast.forecastEndOfDayUsd !== null
|
|
1214
|
+
? priorCostCache.priorDailyCostUsd + sessionForecast.forecastEndOfDayUsd
|
|
1215
|
+
: null,
|
|
1216
|
+
});
|
|
1217
|
+
}
|
|
1218
|
+
},
|
|
1219
|
+
});
|
|
1220
|
+
persistSession = () => {
|
|
1221
|
+
if (!sessionStore || !sessionTracker || !taskDetector || !config)
|
|
1222
|
+
return;
|
|
1223
|
+
try {
|
|
1224
|
+
const summary = buildSessionSummary({
|
|
1225
|
+
sessionTracker,
|
|
1226
|
+
costTracker,
|
|
1227
|
+
taskDetector,
|
|
1228
|
+
antiPatternDetector,
|
|
1229
|
+
efficiencyScorer,
|
|
1230
|
+
developer: config.developer ?? 'unknown',
|
|
1231
|
+
repoName: currentRepoName,
|
|
1232
|
+
});
|
|
1233
|
+
// Skip persisting the synthetic session JSON written by --local /
|
|
1234
|
+
// proxy modes. These IDs (local-<ts>, proxy-<ts>) are MCP-internal
|
|
1235
|
+
// bookkeeping; they don't correspond to a real Claude Code session
|
|
1236
|
+
// and produce confusing `local-...` rows in the dashboard's history
|
|
1237
|
+
// view that have no useful content to show.
|
|
1238
|
+
const isSyntheticId = summary.sessionId.startsWith('local-') || summary.sessionId.startsWith('proxy-');
|
|
1239
|
+
if (isSyntheticId) {
|
|
1240
|
+
logger.info('Skipping synthetic session JSON persistence', {
|
|
1241
|
+
sessionId: summary.sessionId,
|
|
1242
|
+
});
|
|
1243
|
+
}
|
|
1244
|
+
else {
|
|
1245
|
+
sessionStore.saveSession(summary);
|
|
1246
|
+
weeklySummaryGenerator?.checkAndGenerateLastWeek();
|
|
1247
|
+
logger.info('Session saved', { sessionId: summary.sessionId });
|
|
1248
|
+
}
|
|
1249
|
+
}
|
|
1250
|
+
catch (err) {
|
|
1251
|
+
logger.warn('Failed to save session on shutdown', { error: String(err) });
|
|
1252
|
+
}
|
|
1253
|
+
};
|
|
1254
|
+
eventProcessor.start();
|
|
1255
|
+
if (options.stdio) {
|
|
1256
|
+
// Wire audit trail into resource handlers (was undefined at createServer() time).
|
|
1257
|
+
// Same instance is shared with the DashboardServer and NrIngestManager so all
|
|
1258
|
+
// three see the same audit log.
|
|
1259
|
+
mcpServer.auditTrailManager = auditTrail;
|
|
1260
|
+
// Re-register tools with full dependencies (replaces empty handlers)
|
|
1261
|
+
const configFilePath = options.config ?? resolve(DEFAULT_STORAGE_PATH, 'config.json');
|
|
1262
|
+
const configSummary = {
|
|
1263
|
+
mode: config.mode,
|
|
1264
|
+
developer: config.developer,
|
|
1265
|
+
accountId: config.accountId ?? null,
|
|
1266
|
+
licenseKeyMasked: config.licenseKey ? maskCredential(config.licenseKey) : null,
|
|
1267
|
+
nrApiKeyMasked: config.nrApiKey ? maskCredential(config.nrApiKey) : null,
|
|
1268
|
+
region: config.collectorHost ?? 'us',
|
|
1269
|
+
storagePath: config.storagePath,
|
|
1270
|
+
dashboardUrl: `http://${config.dashboard.host}:${config.dashboard.port}`,
|
|
1271
|
+
configFilePath,
|
|
1272
|
+
};
|
|
1273
|
+
registerTools(mcpServer.server, {
|
|
1274
|
+
sessionTracker,
|
|
1275
|
+
costTracker,
|
|
1276
|
+
budgetTracker,
|
|
1277
|
+
taskDetector,
|
|
1278
|
+
antiPatternDetector,
|
|
1279
|
+
efficiencyScorer,
|
|
1280
|
+
feedbackCollector,
|
|
1281
|
+
sessionStore,
|
|
1282
|
+
weeklySummaryGenerator,
|
|
1283
|
+
trendAnalyzer,
|
|
1284
|
+
collaborationProfiler,
|
|
1285
|
+
claudeMdTracker,
|
|
1286
|
+
costPerOutcomeAnalyzer,
|
|
1287
|
+
recommendationEngine,
|
|
1288
|
+
contextWindowTracker,
|
|
1289
|
+
contextTracker,
|
|
1290
|
+
latencyTracker,
|
|
1291
|
+
taskCompletionTracker,
|
|
1292
|
+
modelUsageTracker,
|
|
1293
|
+
retryDetector,
|
|
1294
|
+
contextCompositionTracker,
|
|
1295
|
+
latencyDecompositionTracker,
|
|
1296
|
+
decisionTracker,
|
|
1297
|
+
instructionDriftTracker,
|
|
1298
|
+
toolSelectionScorer,
|
|
1299
|
+
toolCallBuffer: toolCallBufferAccessor,
|
|
1300
|
+
qualityProxyTracker,
|
|
1301
|
+
apiFailureTracker,
|
|
1302
|
+
turnCostAttributor,
|
|
1303
|
+
turnTracker,
|
|
1304
|
+
gitEfficiencyTracker,
|
|
1305
|
+
sessionTraceId,
|
|
1306
|
+
sessionStartMs,
|
|
1307
|
+
accountId: config.accountId,
|
|
1308
|
+
teamId: config.teamId,
|
|
1309
|
+
projectId: config.projectId,
|
|
1310
|
+
developer: config.developer,
|
|
1311
|
+
nrApiKey: config.nrApiKey,
|
|
1312
|
+
collectorHost: config.collectorHost,
|
|
1313
|
+
configFilePath,
|
|
1314
|
+
configSummary,
|
|
1315
|
+
});
|
|
1316
|
+
nrIngest?.start();
|
|
1317
|
+
logger.info('Server running on stdio transport');
|
|
1318
|
+
// stdin 'end' and 'error' handlers are registered immediately after
|
|
1319
|
+
// connectStdio() above so shutdown fires even during session-ID resolution.
|
|
1320
|
+
}
|
|
1321
|
+
else {
|
|
1322
|
+
logger.info('Server running in local dashboard mode (Ctrl+C to stop)');
|
|
1323
|
+
// DashboardServer HTTP listener keeps the process alive.
|
|
1324
|
+
// SIGINT/SIGTERM are handled by the global shutdown handler registered above.
|
|
1325
|
+
}
|
|
1326
|
+
}
|
|
1327
|
+
else {
|
|
1328
|
+
// Proxy mode: start HTTP proxy server that forwards to upstream MCP servers
|
|
1329
|
+
const config = loadMcpConfig(options);
|
|
1330
|
+
if (!config.enabled) {
|
|
1331
|
+
logger.info('Server disabled via config — exiting');
|
|
1332
|
+
process.exit(0);
|
|
1333
|
+
}
|
|
1334
|
+
if (config.proxyUpstreams.length === 0) {
|
|
1335
|
+
logger.error('No proxy upstreams configured. Either use --stdio for direct MCP mode ' +
|
|
1336
|
+
'or configure proxyUpstreams in the config file.');
|
|
1337
|
+
process.exit(1);
|
|
1338
|
+
}
|
|
1339
|
+
// Proxy mode has no Claude Code session to resolve; use a deterministic
|
|
1340
|
+
// identifier instead of randomUUID so we don't fabricate something that
|
|
1341
|
+
// looks like a real session id (Fix 3).
|
|
1342
|
+
const sessionTraceId = `proxy-${Date.now()}`;
|
|
1343
|
+
proxyManager = new ProxyManager({
|
|
1344
|
+
port: config.port,
|
|
1345
|
+
onToolCall: (record) => {
|
|
1346
|
+
logger.debug('Proxy tool call', {
|
|
1347
|
+
server: record.serverName,
|
|
1348
|
+
tool: record.toolName,
|
|
1349
|
+
durationMs: record.durationMs,
|
|
1350
|
+
});
|
|
1351
|
+
},
|
|
1352
|
+
onRequest: (record) => {
|
|
1353
|
+
logger.debug('Proxy request', {
|
|
1354
|
+
server: record.serverName,
|
|
1355
|
+
method: record.method,
|
|
1356
|
+
durationMs: record.durationMs,
|
|
1357
|
+
});
|
|
1358
|
+
},
|
|
1359
|
+
otlpReceiverEnabled: config.otlpReceiverEnabled,
|
|
1360
|
+
otlpReceiverPort: config.otlpReceiverPort,
|
|
1361
|
+
otlpReceiverBindAddress: config.otlpReceiverBindAddress,
|
|
1362
|
+
otlpForwardEndpoint: config.otlpForwardEndpoint,
|
|
1363
|
+
otlpForwardHeaders: config.otlpForwardHeaders,
|
|
1364
|
+
otlpEnrichmentAttributes: {
|
|
1365
|
+
'ai.session.id': sessionTraceId,
|
|
1366
|
+
'ai.developer': config.developer,
|
|
1367
|
+
...(config.projectId && { 'ai.project_id': config.projectId }),
|
|
1368
|
+
...(config.teamId && { 'ai.team_id': config.teamId }),
|
|
1369
|
+
},
|
|
1370
|
+
});
|
|
1371
|
+
for (const upstream of config.proxyUpstreams) {
|
|
1372
|
+
proxyManager.registerUpstream(upstream);
|
|
1373
|
+
}
|
|
1374
|
+
try {
|
|
1375
|
+
await proxyManager.start();
|
|
1376
|
+
}
|
|
1377
|
+
catch (err) {
|
|
1378
|
+
logger.error('Failed to start proxy server', { error: String(err) });
|
|
1379
|
+
await proxyManager.stop().catch(() => { });
|
|
1380
|
+
throw err;
|
|
1381
|
+
}
|
|
1382
|
+
logger.info('Proxy server running', {
|
|
1383
|
+
port: config.port,
|
|
1384
|
+
upstreams: proxyManager.getUpstreamNames(),
|
|
1385
|
+
});
|
|
1386
|
+
}
|
|
1387
|
+
}
|
|
1388
|
+
/**
|
|
1389
|
+
* Read the rules file from disk, validate it via `parseLocalAlertRules`,
|
|
1390
|
+
* and call `engine.loadRules()` with the valid subset. Invalid entries are
|
|
1391
|
+
* logged and skipped — one bad rule does not disable the engine. Failures
|
|
1392
|
+
* to read or parse the file (e.g. it doesn't exist on first boot, or is
|
|
1393
|
+
* mid-write during a watch reload) are non-fatal: the engine simply keeps
|
|
1394
|
+
* its previous rule set in that case.
|
|
1395
|
+
*/
|
|
1396
|
+
function loadAlertRulesFromDisk(engine, rulesPath) {
|
|
1397
|
+
try {
|
|
1398
|
+
let raw;
|
|
1399
|
+
try {
|
|
1400
|
+
raw = readFileSync(rulesPath, 'utf-8');
|
|
1401
|
+
}
|
|
1402
|
+
catch (err) {
|
|
1403
|
+
if (err.code === 'ENOENT') {
|
|
1404
|
+
logger.info('Alert rules file not found; engine running with no rules', { rulesPath });
|
|
1405
|
+
engine.loadRules([]);
|
|
1406
|
+
return;
|
|
1407
|
+
}
|
|
1408
|
+
throw err;
|
|
1409
|
+
}
|
|
1410
|
+
let json;
|
|
1411
|
+
try {
|
|
1412
|
+
json = JSON.parse(raw);
|
|
1413
|
+
}
|
|
1414
|
+
catch (err) {
|
|
1415
|
+
logger.warn('Alert rules file has invalid JSON; keeping previous rules', {
|
|
1416
|
+
rulesPath,
|
|
1417
|
+
error: String(err),
|
|
1418
|
+
});
|
|
1419
|
+
return;
|
|
1420
|
+
}
|
|
1421
|
+
const { valid, invalid } = parseLocalAlertRules(json);
|
|
1422
|
+
if (invalid.length > 0) {
|
|
1423
|
+
logger.warn('Some alert rules failed validation', {
|
|
1424
|
+
invalidCount: invalid.length,
|
|
1425
|
+
validCount: valid.length,
|
|
1426
|
+
});
|
|
1427
|
+
}
|
|
1428
|
+
// Warn about cost.window rules with today/week period — the snapshot
|
|
1429
|
+
// collector only populates sessionUsd, so today/week rules always read 0
|
|
1430
|
+
// and never fire. Fires for both explicitly-configured AND defaulted
|
|
1431
|
+
// values (default is 'session' but if a rules.json sets
|
|
1432
|
+
// 'today' or 'week' explicitly, we still want the user to know it
|
|
1433
|
+
// silently no-ops).
|
|
1434
|
+
for (const rule of valid) {
|
|
1435
|
+
if (rule.type === 'cost.window' && rule.costPeriod !== 'session') {
|
|
1436
|
+
logger.warn(`Rule '${rule.id}' uses costPeriod='${rule.costPeriod}', which is not yet implemented. ` +
|
|
1437
|
+
`The rule will read 0 every cycle and never fire. ` +
|
|
1438
|
+
`Use costPeriod='session' until daily/weekly cost aggregation is supported.`);
|
|
1439
|
+
}
|
|
1440
|
+
}
|
|
1441
|
+
engine.loadRules(valid);
|
|
1442
|
+
logger.info('Alert rules loaded', { rulesPath, count: valid.length });
|
|
1443
|
+
}
|
|
1444
|
+
catch (err) {
|
|
1445
|
+
logger.warn('Failed to load alert rules from disk', {
|
|
1446
|
+
rulesPath,
|
|
1447
|
+
error: String(err),
|
|
1448
|
+
});
|
|
1449
|
+
}
|
|
1450
|
+
}
|
|
1451
|
+
// Compute cost baselines from prior sessions for daily/weekly budget tracking.
|
|
1452
|
+
//
|
|
1453
|
+
// Called on every cost-update emission, not just at session start. Three reasons:
|
|
1454
|
+
// 1) Sessions persisted by other MCP instances during this session need to
|
|
1455
|
+
// land in the daily/weekly totals.
|
|
1456
|
+
// 2) Day rollover — a session running past midnight needs a refreshed
|
|
1457
|
+
// "today" baseline. Snapshotting at startup left long-running sessions
|
|
1458
|
+
// with stale yesterday-as-today bookkeeping forever.
|
|
1459
|
+
// 3) Cross-midnight prior sessions need today-portion attribution, not
|
|
1460
|
+
// whole-session attribution by startTime. We use timeline-based
|
|
1461
|
+
// pro-rating via todayPortionOfSessionCost() so a session that ran
|
|
1462
|
+
// 11pm→2am only contributes its 2-hour today slice to the daily total.
|
|
1463
|
+
//
|
|
1464
|
+
// The current in-flight session is excluded from the prior totals so we don't
|
|
1465
|
+
// double-count with costTracker.getCostForDay(today) on the caller side.
|
|
1466
|
+
function computeHistoricalCosts(sessionStore, currentSessionId, refTs = Date.now()) {
|
|
1467
|
+
const weekAgo = new Date(refTs - 7 * 24 * 60 * 60 * 1000);
|
|
1468
|
+
let priorDailyCostUsd = 0;
|
|
1469
|
+
let priorWeeklyCostUsd = 0;
|
|
1470
|
+
try {
|
|
1471
|
+
const sessions = sessionStore.loadAllSessions({ since: weekAgo });
|
|
1472
|
+
for (const session of sessions) {
|
|
1473
|
+
if (session.sessionId === currentSessionId)
|
|
1474
|
+
continue;
|
|
1475
|
+
if (session.estimatedCostUsd === null)
|
|
1476
|
+
continue;
|
|
1477
|
+
priorDailyCostUsd += todayPortionOfSessionCost(session, refTs);
|
|
1478
|
+
priorWeeklyCostUsd += session.estimatedCostUsd;
|
|
1479
|
+
}
|
|
1480
|
+
}
|
|
1481
|
+
catch (err) {
|
|
1482
|
+
// Non-fatal: fall back to session-only costs if history is unreadable
|
|
1483
|
+
logger.warn('Failed to load historical costs — budget thresholds may be inaccurate', {
|
|
1484
|
+
error: String(err),
|
|
1485
|
+
});
|
|
1486
|
+
}
|
|
1487
|
+
return { priorDailyCostUsd, priorWeeklyCostUsd };
|
|
1488
|
+
}
|
|
1489
|
+
// Only run main() when executed directly (not when imported for testing).
|
|
1490
|
+
// Resolve symlinks so this also matches when invoked via the `preflight` bin link.
|
|
1491
|
+
const resolvedArgv1 = (() => {
|
|
1492
|
+
try {
|
|
1493
|
+
return realpathSync(process.argv[1]);
|
|
1494
|
+
}
|
|
1495
|
+
catch {
|
|
1496
|
+
return process.argv[1];
|
|
1497
|
+
}
|
|
1498
|
+
})();
|
|
1499
|
+
if (resolvedArgv1 && /index\.[jt]s$/.test(resolvedArgv1)) {
|
|
1500
|
+
main().catch((err) => {
|
|
1501
|
+
logger.error('Fatal error', { error: String(err) });
|
|
1502
|
+
process.exit(1);
|
|
1503
|
+
});
|
|
1504
|
+
}
|
|
1505
|
+
//# sourceMappingURL=index.js.map
|