@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
|
@@ -0,0 +1,719 @@
|
|
|
1
|
+
import { appendFileSync, existsSync, mkdirSync, readFileSync, renameSync, unlinkSync, writeFileSync, readdirSync, statSync, } from 'node:fs';
|
|
2
|
+
import { resolve, join, sep } from 'node:path';
|
|
3
|
+
import { createLogger } from '../shared/index.js';
|
|
4
|
+
const logger = createLogger('local-store');
|
|
5
|
+
const SESSION_ID_RE = /^[a-zA-Z0-9_-]{1,128}$/;
|
|
6
|
+
/**
|
|
7
|
+
* A buffer file is treated as orphan when its mtime is older than this AND
|
|
8
|
+
* no live heartbeat is present. Five minutes is well past any normal harvest
|
|
9
|
+
* tick (poll interval is sub-second) so a healthy MCP that's just sitting
|
|
10
|
+
* idle without tool calls won't trip the threshold.
|
|
11
|
+
*/
|
|
12
|
+
const ORPHAN_BUFFER_MTIME_MS = 5 * 60 * 1000;
|
|
13
|
+
/**
|
|
14
|
+
* Liveness probe via signal 0 — POSIX sends no signal but performs the
|
|
15
|
+
* permission/existence checks. Returns true iff the PID names a live process
|
|
16
|
+
* we have rights to signal. ESRCH = dead, EPERM = alive (different uid),
|
|
17
|
+
* anything else (e.g. EINVAL on a malformed PID) = treat as dead.
|
|
18
|
+
*/
|
|
19
|
+
function isPidAlive(pid) {
|
|
20
|
+
if (!Number.isFinite(pid) || pid <= 0)
|
|
21
|
+
return false;
|
|
22
|
+
try {
|
|
23
|
+
process.kill(pid, 0);
|
|
24
|
+
return true;
|
|
25
|
+
}
|
|
26
|
+
catch (err) {
|
|
27
|
+
const code = err.code;
|
|
28
|
+
if (code === 'EPERM')
|
|
29
|
+
return true; // exists, owned by another uid
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
export class LocalStore {
|
|
34
|
+
storagePath;
|
|
35
|
+
bufferPath;
|
|
36
|
+
sessionId;
|
|
37
|
+
/**
|
|
38
|
+
* @param storagePath The base storage directory (e.g. ~/.newrelic-preflight).
|
|
39
|
+
* @param bufferPathOrSessionId Either an explicit absolute buffer path
|
|
40
|
+
* (legacy / test override; takes precedence over sessionId) or a sessionId
|
|
41
|
+
* that scopes the per-session buffer file (`buffer-<sessionId>.jsonl`).
|
|
42
|
+
* Pass `undefined` for fan-out drain mode (`--local`) where there is no
|
|
43
|
+
* single owning session — `drainBuffer()` will then return [] and callers
|
|
44
|
+
* should use `drainAllBuffers()` instead.
|
|
45
|
+
* @param sessionId Optional sessionId for scoping per-session buffers.
|
|
46
|
+
* Used when an explicit bufferPath is also provided (rare — the explicit
|
|
47
|
+
* path still wins for the buffer location, sessionId is then ignored).
|
|
48
|
+
*/
|
|
49
|
+
constructor(storagePath, bufferPathOrSessionId, sessionId) {
|
|
50
|
+
this.storagePath = storagePath;
|
|
51
|
+
let resolvedBufferPath;
|
|
52
|
+
let resolvedSessionId = null;
|
|
53
|
+
if (bufferPathOrSessionId !== undefined && bufferPathOrSessionId.includes(sep)) {
|
|
54
|
+
// Treat anything containing a path separator as an explicit path
|
|
55
|
+
// override. Tests and `NEW_RELIC_AI_MCP_BUFFER_PATH` use this form.
|
|
56
|
+
resolvedBufferPath = bufferPathOrSessionId;
|
|
57
|
+
resolvedSessionId =
|
|
58
|
+
typeof sessionId === 'string' && SESSION_ID_RE.test(sessionId) ? sessionId : null;
|
|
59
|
+
}
|
|
60
|
+
else if (bufferPathOrSessionId !== undefined && SESSION_ID_RE.test(bufferPathOrSessionId)) {
|
|
61
|
+
// Per-session buffer scoping (Fix 3 happy path).
|
|
62
|
+
resolvedBufferPath = resolve(storagePath, `buffer-${bufferPathOrSessionId}.jsonl`);
|
|
63
|
+
resolvedSessionId = bufferPathOrSessionId;
|
|
64
|
+
}
|
|
65
|
+
else if (bufferPathOrSessionId === undefined) {
|
|
66
|
+
// No-session mode: --local drain-all, or pre-resolution startup. Leave
|
|
67
|
+
// bufferPath at the legacy default so single-call drainBuffer() doesn't
|
|
68
|
+
// crash on undefined; drainAllBuffers() is the right entry point here.
|
|
69
|
+
resolvedBufferPath = resolve(storagePath, 'buffer.jsonl');
|
|
70
|
+
}
|
|
71
|
+
else {
|
|
72
|
+
// Bare value that's neither a path nor a valid sessionId — refuse to
|
|
73
|
+
// construct rather than silently routing live data to
|
|
74
|
+
// buffer-unknown.jsonl, which the MCP's session-scoped drainBuffer()
|
|
75
|
+
// would never read and which gcOrphanBuffers() would archive after 5
|
|
76
|
+
// minutes of mtime staleness. The caller (src/index.ts) is responsible
|
|
77
|
+
// for validating sessionTraceId against SESSION_ID_RE before passing it
|
|
78
|
+
// here. session-resolver already enforces this; if this throws it's a
|
|
79
|
+
// real bug worth surfacing.
|
|
80
|
+
throw new Error(`LocalStore: invalid sessionId (must match SESSION_ID_RE /^[a-zA-Z0-9_-]{1,128}$/): ${JSON.stringify(bufferPathOrSessionId)}`);
|
|
81
|
+
}
|
|
82
|
+
this.bufferPath = resolvedBufferPath;
|
|
83
|
+
this.sessionId = resolvedSessionId;
|
|
84
|
+
}
|
|
85
|
+
initialize() {
|
|
86
|
+
const dirs = [
|
|
87
|
+
this.storagePath,
|
|
88
|
+
resolve(this.storagePath, 'sessions'),
|
|
89
|
+
resolve(this.storagePath, 'weekly_summaries'),
|
|
90
|
+
resolve(this.storagePath, 'audit'),
|
|
91
|
+
];
|
|
92
|
+
for (const dir of dirs) {
|
|
93
|
+
if (!existsSync(dir)) {
|
|
94
|
+
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
logger.debug('Storage initialized', { path: this.storagePath });
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Append a hook event as a JSON line to the buffer file.
|
|
101
|
+
* Uses appendFileSync for minimal latency (<5ms budget).
|
|
102
|
+
*/
|
|
103
|
+
appendToBuffer(event) {
|
|
104
|
+
try {
|
|
105
|
+
appendFileSync(this.bufferPath, JSON.stringify(event) + '\n', { mode: 0o600 });
|
|
106
|
+
}
|
|
107
|
+
catch (err) {
|
|
108
|
+
// Never block the caller — log and move on
|
|
109
|
+
logger.warn('Failed to append to buffer', { error: String(err) });
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Atomically drain all events from the buffer file.
|
|
114
|
+
* Renames the file to a temp path (atomic on POSIX), reads it, then deletes.
|
|
115
|
+
* This avoids data loss from concurrent hook writes during drain.
|
|
116
|
+
*/
|
|
117
|
+
drainBuffer() {
|
|
118
|
+
return this.drainPath(this.bufferPath);
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Drain every per-session buffer file under the storage path. Used by
|
|
122
|
+
* `--local` standalone mode where the dashboard owner sees all live
|
|
123
|
+
* sessions' events. Each file is drained atomically (rename-then-read) so a
|
|
124
|
+
* concurrent writer for that session can't lose events; orphan files (whose
|
|
125
|
+
* MCP isn't running) are picked up too.
|
|
126
|
+
*/
|
|
127
|
+
drainAllBuffers() {
|
|
128
|
+
const all = [];
|
|
129
|
+
let entries;
|
|
130
|
+
try {
|
|
131
|
+
if (!existsSync(this.storagePath))
|
|
132
|
+
return [];
|
|
133
|
+
entries = readdirSync(this.storagePath);
|
|
134
|
+
}
|
|
135
|
+
catch (err) {
|
|
136
|
+
logger.warn('Failed to enumerate storage path for drainAllBuffers', { error: String(err) });
|
|
137
|
+
return [];
|
|
138
|
+
}
|
|
139
|
+
for (const name of entries) {
|
|
140
|
+
// Per-session: buffer-<id>.jsonl. Also pick up the legacy shared
|
|
141
|
+
// buffer.jsonl so a freshly-upgraded user's pre-Fix-3 events still flow.
|
|
142
|
+
if (!name.endsWith('.jsonl'))
|
|
143
|
+
continue;
|
|
144
|
+
if (name !== 'buffer.jsonl' && !name.startsWith('buffer-'))
|
|
145
|
+
continue;
|
|
146
|
+
const drained = this.drainPath(resolve(this.storagePath, name));
|
|
147
|
+
if (drained.length > 0) {
|
|
148
|
+
for (const event of drained)
|
|
149
|
+
all.push(event);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
return all;
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Read every per-session buffer file under the storage path WITHOUT draining
|
|
156
|
+
* (no rename, no unlink). Used by the dashboard owner's
|
|
157
|
+
* `/api/sessions/today/aggregate` endpoint, which surfaces a global view
|
|
158
|
+
* across every live session — its own session_id-scoped drainBuffer() only
|
|
159
|
+
* covers its own events, so we need a read-only fan-out for cross-session
|
|
160
|
+
* KPIs.
|
|
161
|
+
*
|
|
162
|
+
* Torn-line handling: the LAST non-empty line is the only line that can
|
|
163
|
+
* legitimately be torn (a concurrent appender's incomplete write). If it
|
|
164
|
+
* fails to parse we drop it silently. Any earlier line that fails to parse
|
|
165
|
+
* is real corruption — events >PIPE_BUF (typically 4 KiB) interleaved by
|
|
166
|
+
* concurrent appenders, or an on-disk write that flushed partially. We log
|
|
167
|
+
* a WARN so the loss is visible rather than silently dropped.
|
|
168
|
+
*
|
|
169
|
+
* The legacy shared `buffer.jsonl` is included so an upgrading user with
|
|
170
|
+
* unmigrated events still appears in aggregate views before the next
|
|
171
|
+
* `migrateLegacyBuffer()` runs.
|
|
172
|
+
*/
|
|
173
|
+
peekAllBuffers() {
|
|
174
|
+
const all = [];
|
|
175
|
+
let entries;
|
|
176
|
+
try {
|
|
177
|
+
if (!existsSync(this.storagePath))
|
|
178
|
+
return [];
|
|
179
|
+
entries = readdirSync(this.storagePath);
|
|
180
|
+
}
|
|
181
|
+
catch (err) {
|
|
182
|
+
logger.warn('Failed to enumerate storage path for peekAllBuffers', { error: String(err) });
|
|
183
|
+
return [];
|
|
184
|
+
}
|
|
185
|
+
for (const name of entries) {
|
|
186
|
+
if (!name.endsWith('.jsonl'))
|
|
187
|
+
continue;
|
|
188
|
+
if (name !== 'buffer.jsonl' && !name.startsWith('buffer-'))
|
|
189
|
+
continue;
|
|
190
|
+
const path = resolve(this.storagePath, name);
|
|
191
|
+
let raw;
|
|
192
|
+
try {
|
|
193
|
+
raw = readFileSync(path, 'utf-8');
|
|
194
|
+
}
|
|
195
|
+
catch {
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
198
|
+
if (!raw.trim())
|
|
199
|
+
continue;
|
|
200
|
+
// Find the index of the last non-empty line so we can distinguish a
|
|
201
|
+
// legitimately-torn tail from real mid-file corruption.
|
|
202
|
+
const lines = raw.split('\n');
|
|
203
|
+
let lastNonEmptyIdx = -1;
|
|
204
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
205
|
+
if (lines[i].trim()) {
|
|
206
|
+
lastNonEmptyIdx = i;
|
|
207
|
+
break;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
for (let i = 0; i < lines.length; i++) {
|
|
211
|
+
const line = lines[i];
|
|
212
|
+
if (!line.trim())
|
|
213
|
+
continue;
|
|
214
|
+
try {
|
|
215
|
+
all.push(JSON.parse(line));
|
|
216
|
+
}
|
|
217
|
+
catch {
|
|
218
|
+
if (i === lastNonEmptyIdx) {
|
|
219
|
+
// Torn-tail race against a concurrent appender — expected.
|
|
220
|
+
continue;
|
|
221
|
+
}
|
|
222
|
+
// Mid-file parse failure means real data corruption — surface it.
|
|
223
|
+
logger.warn('peekAllBuffers: dropping malformed mid-file line', {
|
|
224
|
+
file: name,
|
|
225
|
+
lineIndex: i,
|
|
226
|
+
preview: line.slice(0, 100),
|
|
227
|
+
eventsSoFar: all.length,
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
return all;
|
|
233
|
+
}
|
|
234
|
+
/**
|
|
235
|
+
* Write a heartbeat file `active-<sessionId>.pid` containing this process's
|
|
236
|
+
* PID. Used by the dashboard owner's GC pass to determine which buffer
|
|
237
|
+
* files still have a live owner. The MCP should call this once after
|
|
238
|
+
* resolving its session_id, and remove it on graceful shutdown via
|
|
239
|
+
* `removeHeartbeat()`.
|
|
240
|
+
*
|
|
241
|
+
* No-op if the LocalStore is not bound to a sessionId (e.g. --local mode).
|
|
242
|
+
*/
|
|
243
|
+
writeHeartbeat(pid = process.pid) {
|
|
244
|
+
if (!this.sessionId)
|
|
245
|
+
return;
|
|
246
|
+
if (!Number.isFinite(pid) || pid <= 0)
|
|
247
|
+
return;
|
|
248
|
+
const heartbeatPath = resolve(this.storagePath, `active-${this.sessionId}.pid`);
|
|
249
|
+
try {
|
|
250
|
+
if (!existsSync(this.storagePath)) {
|
|
251
|
+
mkdirSync(this.storagePath, { recursive: true, mode: 0o700 });
|
|
252
|
+
}
|
|
253
|
+
writeFileSync(heartbeatPath, String(pid), { mode: 0o600 });
|
|
254
|
+
}
|
|
255
|
+
catch (err) {
|
|
256
|
+
logger.warn('Failed to write heartbeat file', { error: String(err) });
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
/**
|
|
260
|
+
* Remove this MCP's heartbeat file. Called from the shutdown handler so the
|
|
261
|
+
* dashboard owner's next GC pass knows the buffer is up for adoption.
|
|
262
|
+
*
|
|
263
|
+
* No-op if no heartbeat was ever written or if the file is already gone.
|
|
264
|
+
*/
|
|
265
|
+
removeHeartbeat() {
|
|
266
|
+
if (!this.sessionId)
|
|
267
|
+
return;
|
|
268
|
+
const heartbeatPath = resolve(this.storagePath, `active-${this.sessionId}.pid`);
|
|
269
|
+
try {
|
|
270
|
+
if (existsSync(heartbeatPath))
|
|
271
|
+
unlinkSync(heartbeatPath);
|
|
272
|
+
}
|
|
273
|
+
catch (err) {
|
|
274
|
+
logger.debug('Failed to remove heartbeat file', { error: String(err) });
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
/**
|
|
278
|
+
* Garbage-collect orphan per-session buffer files. A buffer is orphan when
|
|
279
|
+
* no live process is currently draining it; we determine that with two
|
|
280
|
+
* signals:
|
|
281
|
+
*
|
|
282
|
+
* 1. Heartbeat (authoritative): if `active-<sessionId>.pid` exists and
|
|
283
|
+
* its PID is alive, the buffer has an owner — leave it alone. If the
|
|
284
|
+
* heartbeat exists but the PID is dead (process crashed before
|
|
285
|
+
* cleanup), archive the buffer and delete the stale heartbeat.
|
|
286
|
+
* 2. Mtime fallback: if no heartbeat exists AND the buffer hasn't been
|
|
287
|
+
* written to in `ORPHAN_BUFFER_MTIME_MS`, treat as orphan. This catches
|
|
288
|
+
* crashes that happened before any heartbeat was written.
|
|
289
|
+
*
|
|
290
|
+
* Recently-modified buffers without a heartbeat are left alone — the MCP
|
|
291
|
+
* for that session may be in the breadcrumb-resolution window and not yet
|
|
292
|
+
* have written its heartbeat.
|
|
293
|
+
*
|
|
294
|
+
* The `activeSessionIds` set is also consulted: any session id present
|
|
295
|
+
* there is considered live regardless of heartbeat/mtime, which protects
|
|
296
|
+
* against heartbeat write delays on slow disks.
|
|
297
|
+
*
|
|
298
|
+
* Orphan buffers are renamed to `buffer-<sessionId>.jsonl.archived-<ts>`
|
|
299
|
+
* (preserved on disk for forensics, no longer scanned by the aggregator).
|
|
300
|
+
*
|
|
301
|
+
* @returns counts of files archived and stale heartbeats removed
|
|
302
|
+
*/
|
|
303
|
+
gcOrphanBuffers(activeSessionIds) {
|
|
304
|
+
if (!existsSync(this.storagePath)) {
|
|
305
|
+
return { archived: 0, staleHeartbeats: 0 };
|
|
306
|
+
}
|
|
307
|
+
let entries;
|
|
308
|
+
try {
|
|
309
|
+
entries = readdirSync(this.storagePath);
|
|
310
|
+
}
|
|
311
|
+
catch (err) {
|
|
312
|
+
logger.warn('Failed to enumerate storage path for gcOrphanBuffers', { error: String(err) });
|
|
313
|
+
return { archived: 0, staleHeartbeats: 0 };
|
|
314
|
+
}
|
|
315
|
+
let archived = 0;
|
|
316
|
+
let staleHeartbeats = 0;
|
|
317
|
+
const now = Date.now();
|
|
318
|
+
for (const name of entries) {
|
|
319
|
+
// Match `buffer-<sessionId>.jsonl` exactly. Skip `.drain` companions and
|
|
320
|
+
// already-archived files so a follow-up pass doesn't re-process them.
|
|
321
|
+
if (!name.startsWith('buffer-') || !name.endsWith('.jsonl'))
|
|
322
|
+
continue;
|
|
323
|
+
if (name === 'buffer.jsonl')
|
|
324
|
+
continue;
|
|
325
|
+
const sessionId = name.slice('buffer-'.length, -'.jsonl'.length);
|
|
326
|
+
// Defense in depth — a malformed sessionId would mean a malformed
|
|
327
|
+
// heartbeat path; skip silently rather than archive.
|
|
328
|
+
if (!SESSION_ID_RE.test(sessionId))
|
|
329
|
+
continue;
|
|
330
|
+
const bufferPath = resolve(this.storagePath, name);
|
|
331
|
+
const heartbeatPath = resolve(this.storagePath, `active-${sessionId}.pid`);
|
|
332
|
+
const heartbeatExists = existsSync(heartbeatPath);
|
|
333
|
+
let ownerAlive = false;
|
|
334
|
+
let heartbeatStale = false;
|
|
335
|
+
if (heartbeatExists) {
|
|
336
|
+
try {
|
|
337
|
+
const raw = readFileSync(heartbeatPath, 'utf-8').trim();
|
|
338
|
+
const pid = Number.parseInt(raw, 10);
|
|
339
|
+
if (isPidAlive(pid)) {
|
|
340
|
+
ownerAlive = true;
|
|
341
|
+
}
|
|
342
|
+
else {
|
|
343
|
+
heartbeatStale = true;
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
catch {
|
|
347
|
+
// Unreadable heartbeat — treat as stale so we don't leak the file.
|
|
348
|
+
heartbeatStale = true;
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
// Cross-check against the caller-supplied set of session ids that are
|
|
352
|
+
// known to be live (via tool-call activity / LiveSessionRegistry). If
|
|
353
|
+
// present here, the buffer is owned even if no heartbeat is on disk yet.
|
|
354
|
+
if (activeSessionIds.has(sessionId))
|
|
355
|
+
ownerAlive = true;
|
|
356
|
+
if (ownerAlive)
|
|
357
|
+
continue;
|
|
358
|
+
// Mtime fallback only applies when no heartbeat is on disk: a stale
|
|
359
|
+
// heartbeat is enough on its own.
|
|
360
|
+
let archive = heartbeatStale;
|
|
361
|
+
if (!heartbeatExists) {
|
|
362
|
+
try {
|
|
363
|
+
const stat = statSync(bufferPath);
|
|
364
|
+
if (now - stat.mtimeMs > ORPHAN_BUFFER_MTIME_MS) {
|
|
365
|
+
archive = true;
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
catch {
|
|
369
|
+
// Buffer file vanished mid-scan — nothing to do.
|
|
370
|
+
continue;
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
if (!archive)
|
|
374
|
+
continue;
|
|
375
|
+
const archivedPath = `${bufferPath}.archived-${now}`;
|
|
376
|
+
try {
|
|
377
|
+
renameSync(bufferPath, archivedPath);
|
|
378
|
+
archived++;
|
|
379
|
+
logger.info('Archived orphan buffer', { sessionId, archivedPath });
|
|
380
|
+
// Only remove the heartbeat once the archive rename has succeeded.
|
|
381
|
+
// If the rename fails, leave the heartbeat so the next GC pass can
|
|
382
|
+
// retry rather than treating the buffer as heartbeat-free.
|
|
383
|
+
if (heartbeatExists) {
|
|
384
|
+
try {
|
|
385
|
+
unlinkSync(heartbeatPath);
|
|
386
|
+
if (heartbeatStale)
|
|
387
|
+
staleHeartbeats++;
|
|
388
|
+
}
|
|
389
|
+
catch (err) {
|
|
390
|
+
logger.debug('Failed to remove heartbeat after archive', {
|
|
391
|
+
sessionId,
|
|
392
|
+
error: String(err),
|
|
393
|
+
});
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
catch (err) {
|
|
398
|
+
logger.warn('Failed to archive orphan buffer', { sessionId, error: String(err) });
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
return { archived, staleHeartbeats };
|
|
402
|
+
}
|
|
403
|
+
/**
|
|
404
|
+
* Scan `<storage>/active-*.pid` heartbeat files and return the set of
|
|
405
|
+
* session ids whose owner PID is currently alive. Used by the dashboard
|
|
406
|
+
* owner to construct the `activeSessionIds` argument to
|
|
407
|
+
* `gcOrphanBuffers()`.
|
|
408
|
+
*/
|
|
409
|
+
getActiveSessionIdsFromHeartbeats() {
|
|
410
|
+
const active = new Set();
|
|
411
|
+
if (!existsSync(this.storagePath))
|
|
412
|
+
return active;
|
|
413
|
+
let entries;
|
|
414
|
+
try {
|
|
415
|
+
entries = readdirSync(this.storagePath);
|
|
416
|
+
}
|
|
417
|
+
catch {
|
|
418
|
+
return active;
|
|
419
|
+
}
|
|
420
|
+
for (const name of entries) {
|
|
421
|
+
if (!name.startsWith('active-') || !name.endsWith('.pid'))
|
|
422
|
+
continue;
|
|
423
|
+
const sessionId = name.slice('active-'.length, -'.pid'.length);
|
|
424
|
+
if (!SESSION_ID_RE.test(sessionId))
|
|
425
|
+
continue;
|
|
426
|
+
try {
|
|
427
|
+
const raw = readFileSync(resolve(this.storagePath, name), 'utf-8').trim();
|
|
428
|
+
const pid = Number.parseInt(raw, 10);
|
|
429
|
+
if (isPidAlive(pid))
|
|
430
|
+
active.add(sessionId);
|
|
431
|
+
}
|
|
432
|
+
catch {
|
|
433
|
+
// Unreadable heartbeat — leave the session out of the live set so a
|
|
434
|
+
// mtime-based gc can still archive it eventually.
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
return active;
|
|
438
|
+
}
|
|
439
|
+
/**
|
|
440
|
+
* Garbage-collect stale per-PPID breadcrumb files at
|
|
441
|
+
* `<storage>/session-by-ppid/<pid>.txt`. A breadcrumb is stale when its
|
|
442
|
+
* PID names a process that no longer exists (Claude Code session ended).
|
|
443
|
+
*
|
|
444
|
+
* @returns the number of breadcrumb files deleted
|
|
445
|
+
*/
|
|
446
|
+
gcStaleBreadcrumbs() {
|
|
447
|
+
const breadcrumbDir = resolve(this.storagePath, 'session-by-ppid');
|
|
448
|
+
if (!existsSync(breadcrumbDir))
|
|
449
|
+
return 0;
|
|
450
|
+
let entries;
|
|
451
|
+
try {
|
|
452
|
+
entries = readdirSync(breadcrumbDir);
|
|
453
|
+
}
|
|
454
|
+
catch (err) {
|
|
455
|
+
logger.warn('Failed to enumerate breadcrumb dir for gcStaleBreadcrumbs', {
|
|
456
|
+
error: String(err),
|
|
457
|
+
});
|
|
458
|
+
return 0;
|
|
459
|
+
}
|
|
460
|
+
let deleted = 0;
|
|
461
|
+
for (const name of entries) {
|
|
462
|
+
if (!name.endsWith('.txt'))
|
|
463
|
+
continue;
|
|
464
|
+
const pidStr = name.slice(0, -'.txt'.length);
|
|
465
|
+
const pid = Number.parseInt(pidStr, 10);
|
|
466
|
+
if (!Number.isFinite(pid) || pid <= 0)
|
|
467
|
+
continue;
|
|
468
|
+
if (isPidAlive(pid))
|
|
469
|
+
continue;
|
|
470
|
+
const path = resolve(breadcrumbDir, name);
|
|
471
|
+
try {
|
|
472
|
+
unlinkSync(path);
|
|
473
|
+
deleted++;
|
|
474
|
+
}
|
|
475
|
+
catch (err) {
|
|
476
|
+
logger.debug('Failed to delete stale breadcrumb', { pid, error: String(err) });
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
if (deleted > 0) {
|
|
480
|
+
logger.info('Cleaned up stale breadcrumbs', { deleted });
|
|
481
|
+
}
|
|
482
|
+
return deleted;
|
|
483
|
+
}
|
|
484
|
+
/**
|
|
485
|
+
* Migrate any pre-Fix-3 events left in the legacy shared `buffer.jsonl` into
|
|
486
|
+
* per-session `buffer-<sessionId>.jsonl` files, then delete the legacy file.
|
|
487
|
+
* Idempotent: returns 0 when the legacy file is absent or already empty.
|
|
488
|
+
*
|
|
489
|
+
* Concurrent-MCP-safe: the very first step is to atomically rename
|
|
490
|
+
* `buffer.jsonl` to `buffer.jsonl.migrating-<pid>`. The rename is the
|
|
491
|
+
* mutual-exclusion primitive — only one MCP wins; the loser sees ENOENT
|
|
492
|
+
* (the file is no longer at the legacy path) and skips migration.
|
|
493
|
+
*
|
|
494
|
+
* Failure recovery: if any partition append fails partway through, the
|
|
495
|
+
* `.migrating-<pid>` file is intentionally left in place. On the next boot
|
|
496
|
+
* the legacy file is gone, so we no-op; the partitioned files written so
|
|
497
|
+
* far hold the events that did make it across. The straggler events stuck
|
|
498
|
+
* in `.migrating-<pid>` are recovered manually (forensics) or dropped — we
|
|
499
|
+
* accept this trade because re-migrating from `.migrating-<pid>` would
|
|
500
|
+
* re-append already-migrated partitions and produce duplicate events,
|
|
501
|
+
* which is worse than a once-only loss bounded by the size of the legacy
|
|
502
|
+
* file at upgrade time.
|
|
503
|
+
*
|
|
504
|
+
* Returns the number of events migrated.
|
|
505
|
+
*/
|
|
506
|
+
migrateLegacyBuffer() {
|
|
507
|
+
const legacyPath = resolve(this.storagePath, 'buffer.jsonl');
|
|
508
|
+
if (!existsSync(legacyPath))
|
|
509
|
+
return 0;
|
|
510
|
+
// Step 1: atomic rename for mutual exclusion against concurrent MCPs.
|
|
511
|
+
const migratingPath = resolve(this.storagePath, `buffer.jsonl.migrating-${process.pid}`);
|
|
512
|
+
// Guard against a prior crashed migration for this PID. On POSIX, rename(2)
|
|
513
|
+
// atomically replaces the destination (never EEXIST), so without this check
|
|
514
|
+
// a recycled PID would silently overwrite the stranded .migrating file and
|
|
515
|
+
// lose its events.
|
|
516
|
+
if (existsSync(migratingPath)) {
|
|
517
|
+
logger.info('Legacy buffer migration: prior crashed migration file found; skipping', {
|
|
518
|
+
migratingPath,
|
|
519
|
+
});
|
|
520
|
+
return 0;
|
|
521
|
+
}
|
|
522
|
+
try {
|
|
523
|
+
renameSync(legacyPath, migratingPath);
|
|
524
|
+
}
|
|
525
|
+
catch (err) {
|
|
526
|
+
const code = err.code;
|
|
527
|
+
if (code === 'ENOENT') {
|
|
528
|
+
// Another instance won the race.
|
|
529
|
+
logger.info('Legacy buffer migration: another instance is migrating; skipping');
|
|
530
|
+
return 0;
|
|
531
|
+
}
|
|
532
|
+
logger.warn('Failed to rename legacy buffer for migration', { error: String(err) });
|
|
533
|
+
return 0;
|
|
534
|
+
}
|
|
535
|
+
let raw;
|
|
536
|
+
try {
|
|
537
|
+
raw = readFileSync(migratingPath, 'utf-8');
|
|
538
|
+
}
|
|
539
|
+
catch (err) {
|
|
540
|
+
logger.warn('Failed to read legacy buffer.jsonl during migration', { error: String(err) });
|
|
541
|
+
// Leave the .migrating file in place for forensic recovery; we already
|
|
542
|
+
// own it via the rename so no concurrent process will retry.
|
|
543
|
+
return 0;
|
|
544
|
+
}
|
|
545
|
+
if (!raw.trim()) {
|
|
546
|
+
try {
|
|
547
|
+
unlinkSync(migratingPath);
|
|
548
|
+
}
|
|
549
|
+
catch {
|
|
550
|
+
// ignore
|
|
551
|
+
}
|
|
552
|
+
return 0;
|
|
553
|
+
}
|
|
554
|
+
const partitioned = new Map();
|
|
555
|
+
let count = 0;
|
|
556
|
+
for (const line of raw.split('\n')) {
|
|
557
|
+
if (!line.trim())
|
|
558
|
+
continue;
|
|
559
|
+
let parsed;
|
|
560
|
+
try {
|
|
561
|
+
parsed = JSON.parse(line);
|
|
562
|
+
}
|
|
563
|
+
catch {
|
|
564
|
+
continue;
|
|
565
|
+
}
|
|
566
|
+
if (parsed === null || typeof parsed !== 'object')
|
|
567
|
+
continue;
|
|
568
|
+
const sid = parsed.sessionId;
|
|
569
|
+
const key = typeof sid === 'string' && SESSION_ID_RE.test(sid) ? sid : 'unknown';
|
|
570
|
+
const bucket = partitioned.get(key) ?? [];
|
|
571
|
+
bucket.push(line);
|
|
572
|
+
partitioned.set(key, bucket);
|
|
573
|
+
count++;
|
|
574
|
+
}
|
|
575
|
+
for (const [sid, lines] of partitioned) {
|
|
576
|
+
const target = resolve(this.storagePath, `buffer-${sid}.jsonl`);
|
|
577
|
+
try {
|
|
578
|
+
appendFileSync(target, lines.join('\n') + '\n', { mode: 0o600 });
|
|
579
|
+
}
|
|
580
|
+
catch (err) {
|
|
581
|
+
// Mid-partition failure: the .migrating file stays on disk so the
|
|
582
|
+
// operator can recover forensically. We don't retry on next boot to
|
|
583
|
+
// avoid duplicate events in already-migrated partitions.
|
|
584
|
+
logger.warn('Failed to migrate legacy buffer entries; leaving .migrating file for recovery', { sessionId: sid, migratingPath, error: String(err) });
|
|
585
|
+
return count;
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
try {
|
|
589
|
+
unlinkSync(migratingPath);
|
|
590
|
+
}
|
|
591
|
+
catch (err) {
|
|
592
|
+
logger.warn('Migrated legacy buffer but failed to remove .migrating file', {
|
|
593
|
+
migratingPath,
|
|
594
|
+
error: String(err),
|
|
595
|
+
});
|
|
596
|
+
}
|
|
597
|
+
logger.info('Legacy buffer.jsonl migrated to per-session files', {
|
|
598
|
+
events: count,
|
|
599
|
+
sessions: partitioned.size,
|
|
600
|
+
});
|
|
601
|
+
return count;
|
|
602
|
+
}
|
|
603
|
+
drainPath(bufferPath) {
|
|
604
|
+
const tmpPath = bufferPath + '.drain';
|
|
605
|
+
// Recover from a previous failed drain — the .drain file has events that
|
|
606
|
+
// were never processed.
|
|
607
|
+
if (existsSync(tmpPath)) {
|
|
608
|
+
try {
|
|
609
|
+
if (existsSync(bufferPath)) {
|
|
610
|
+
const drainData = readFileSync(tmpPath, 'utf-8');
|
|
611
|
+
const bufferData = readFileSync(bufferPath, 'utf-8');
|
|
612
|
+
writeFileSync(bufferPath, drainData + (drainData.endsWith('\n') ? '' : '\n') + bufferData, { mode: 0o600 });
|
|
613
|
+
unlinkSync(tmpPath);
|
|
614
|
+
}
|
|
615
|
+
else {
|
|
616
|
+
renameSync(tmpPath, bufferPath);
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
catch {
|
|
620
|
+
logger.warn('Failed to recover .drain file — will retry next poll');
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
if (!existsSync(bufferPath)) {
|
|
624
|
+
return [];
|
|
625
|
+
}
|
|
626
|
+
try {
|
|
627
|
+
renameSync(bufferPath, tmpPath);
|
|
628
|
+
}
|
|
629
|
+
catch {
|
|
630
|
+
return [];
|
|
631
|
+
}
|
|
632
|
+
try {
|
|
633
|
+
const raw = readFileSync(tmpPath, 'utf-8');
|
|
634
|
+
unlinkSync(tmpPath);
|
|
635
|
+
if (!raw.trim()) {
|
|
636
|
+
return [];
|
|
637
|
+
}
|
|
638
|
+
const events = [];
|
|
639
|
+
for (const line of raw.split('\n')) {
|
|
640
|
+
if (!line.trim())
|
|
641
|
+
continue;
|
|
642
|
+
try {
|
|
643
|
+
events.push(JSON.parse(line));
|
|
644
|
+
}
|
|
645
|
+
catch {
|
|
646
|
+
logger.warn('Skipping malformed buffer line', { line: line.slice(0, 100) });
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
return events;
|
|
650
|
+
}
|
|
651
|
+
catch (err) {
|
|
652
|
+
logger.warn('Failed to drain buffer — will retry next poll', { error: String(err) });
|
|
653
|
+
return [];
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
/** The buffer path this store is currently scoped to (test helper). */
|
|
657
|
+
getBufferPath() {
|
|
658
|
+
return this.bufferPath;
|
|
659
|
+
}
|
|
660
|
+
/** The sessionId this store was scoped to, if any (test helper). */
|
|
661
|
+
getSessionId() {
|
|
662
|
+
return this.sessionId;
|
|
663
|
+
}
|
|
664
|
+
saveSession(session) {
|
|
665
|
+
if (!SESSION_ID_RE.test(session.sessionId)) {
|
|
666
|
+
logger.warn('Rejecting invalid sessionId for file path', { sessionId: session.sessionId });
|
|
667
|
+
return;
|
|
668
|
+
}
|
|
669
|
+
const sessionsDir = resolve(this.storagePath, 'sessions');
|
|
670
|
+
const filepath = resolve(sessionsDir, `${session.sessionId}.json`);
|
|
671
|
+
if (!filepath.startsWith(sessionsDir + sep)) {
|
|
672
|
+
throw new Error(`Session path escaped storage directory: ${filepath}`);
|
|
673
|
+
}
|
|
674
|
+
writeFileSync(filepath, JSON.stringify(session, null, 2) + '\n', { mode: 0o600 });
|
|
675
|
+
logger.debug('Session saved', { sessionId: session.sessionId });
|
|
676
|
+
}
|
|
677
|
+
loadRecentSessions(days) {
|
|
678
|
+
const sessionsDir = resolve(this.storagePath, 'sessions');
|
|
679
|
+
if (!existsSync(sessionsDir)) {
|
|
680
|
+
return [];
|
|
681
|
+
}
|
|
682
|
+
const cutoff = Date.now() - days * 24 * 60 * 60 * 1000;
|
|
683
|
+
const sessions = [];
|
|
684
|
+
for (const file of readdirSync(sessionsDir)) {
|
|
685
|
+
if (!file.endsWith('.json'))
|
|
686
|
+
continue;
|
|
687
|
+
const filepath = join(sessionsDir, file);
|
|
688
|
+
try {
|
|
689
|
+
const stat = statSync(filepath);
|
|
690
|
+
if (stat.mtimeMs < cutoff)
|
|
691
|
+
continue;
|
|
692
|
+
const raw = readFileSync(filepath, 'utf-8');
|
|
693
|
+
const parsed = JSON.parse(raw);
|
|
694
|
+
// Guard against corrupted files (null, numbers, arrays) that would crash
|
|
695
|
+
// the downstream sort on .startTime.
|
|
696
|
+
if (typeof parsed === 'object' && parsed !== null && !Array.isArray(parsed)) {
|
|
697
|
+
sessions.push(parsed);
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
catch {
|
|
701
|
+
logger.warn('Skipping unreadable session file', { file });
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
return sessions.sort((a, b) => a.startTime - b.startTime);
|
|
705
|
+
}
|
|
706
|
+
appendAuditLog(entry) {
|
|
707
|
+
const date = new Date(entry.timestamp);
|
|
708
|
+
const dateStr = date.toISOString().slice(0, 10); // YYYY-MM-DD
|
|
709
|
+
const filename = `${dateStr}.jsonl`;
|
|
710
|
+
const filepath = resolve(this.storagePath, 'audit', filename);
|
|
711
|
+
try {
|
|
712
|
+
appendFileSync(filepath, JSON.stringify(entry) + '\n', { mode: 0o600 });
|
|
713
|
+
}
|
|
714
|
+
catch (err) {
|
|
715
|
+
logger.warn('Failed to append audit log', { error: String(err) });
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
//# sourceMappingURL=local-store.js.map
|