@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.
Files changed (578) hide show
  1. package/LICENSE +183 -0
  2. package/README.md +498 -0
  3. package/dist/alerts/alert-log.d.ts +24 -0
  4. package/dist/alerts/alert-log.d.ts.map +1 -0
  5. package/dist/alerts/alert-log.js +159 -0
  6. package/dist/alerts/alert-log.js.map +1 -0
  7. package/dist/alerts/alert-snapshot-collector.d.ts +168 -0
  8. package/dist/alerts/alert-snapshot-collector.d.ts.map +1 -0
  9. package/dist/alerts/alert-snapshot-collector.js +243 -0
  10. package/dist/alerts/alert-snapshot-collector.js.map +1 -0
  11. package/dist/alerts/local-alert-engine.d.ts +86 -0
  12. package/dist/alerts/local-alert-engine.d.ts.map +1 -0
  13. package/dist/alerts/local-alert-engine.js +466 -0
  14. package/dist/alerts/local-alert-engine.js.map +1 -0
  15. package/dist/alerts/local-alert-rule.d.ts +439 -0
  16. package/dist/alerts/local-alert-rule.d.ts.map +1 -0
  17. package/dist/alerts/local-alert-rule.js +139 -0
  18. package/dist/alerts/local-alert-rule.js.map +1 -0
  19. package/dist/alerts/os-notifier.d.ts +39 -0
  20. package/dist/alerts/os-notifier.d.ts.map +1 -0
  21. package/dist/alerts/os-notifier.js +170 -0
  22. package/dist/alerts/os-notifier.js.map +1 -0
  23. package/dist/alerts/types.d.ts +35 -0
  24. package/dist/alerts/types.d.ts.map +1 -0
  25. package/dist/alerts/types.js +8 -0
  26. package/dist/alerts/types.js.map +1 -0
  27. package/dist/config.d.ts +169 -0
  28. package/dist/config.d.ts.map +1 -0
  29. package/dist/config.js +868 -0
  30. package/dist/config.js.map +1 -0
  31. package/dist/dashboard/dashboard-server.d.ts +38 -0
  32. package/dist/dashboard/dashboard-server.d.ts.map +1 -0
  33. package/dist/dashboard/dashboard-server.js +207 -0
  34. package/dist/dashboard/dashboard-server.js.map +1 -0
  35. package/dist/dashboard/index.d.ts +3 -0
  36. package/dist/dashboard/index.d.ts.map +1 -0
  37. package/dist/dashboard/index.js +2 -0
  38. package/dist/dashboard/index.js.map +1 -0
  39. package/dist/dashboard/live-event-bus.d.ts +99 -0
  40. package/dist/dashboard/live-event-bus.d.ts.map +1 -0
  41. package/dist/dashboard/live-event-bus.js +56 -0
  42. package/dist/dashboard/live-event-bus.js.map +1 -0
  43. package/dist/dashboard/routes/api-handler.d.ts +122 -0
  44. package/dist/dashboard/routes/api-handler.d.ts.map +1 -0
  45. package/dist/dashboard/routes/api-handler.js +1414 -0
  46. package/dist/dashboard/routes/api-handler.js.map +1 -0
  47. package/dist/dashboard/routes/replay-analyzer.d.ts +15 -0
  48. package/dist/dashboard/routes/replay-analyzer.d.ts.map +1 -0
  49. package/dist/dashboard/routes/replay-analyzer.js +227 -0
  50. package/dist/dashboard/routes/replay-analyzer.js.map +1 -0
  51. package/dist/dashboard/routes/sse-handler.d.ts +4 -0
  52. package/dist/dashboard/routes/sse-handler.d.ts.map +1 -0
  53. package/dist/dashboard/routes/sse-handler.js +122 -0
  54. package/dist/dashboard/routes/sse-handler.js.map +1 -0
  55. package/dist/dashboard/routes/static-handler.d.ts +3 -0
  56. package/dist/dashboard/routes/static-handler.d.ts.map +1 -0
  57. package/dist/dashboard/routes/static-handler.js +123 -0
  58. package/dist/dashboard/routes/static-handler.js.map +1 -0
  59. package/dist/data/alerts/conditions/01-daily-cost-spike.json +16 -0
  60. package/dist/data/alerts/conditions/02-low-efficiency-score.json +16 -0
  61. package/dist/data/alerts/conditions/03-stuck-loop-rate.json +16 -0
  62. package/dist/data/alerts/conditions/04-anti-pattern-rate.json +16 -0
  63. package/dist/data/alerts/conditions/05-session-cost-budget.json +16 -0
  64. package/dist/data/alerts/conditions-personal/01-personal-daily-cost.json +16 -0
  65. package/dist/data/alerts/conditions-personal/02-personal-session-cost.json +16 -0
  66. package/dist/data/alerts/conditions-personal/03-personal-low-efficiency.json +16 -0
  67. package/dist/data/alerts/conditions-personal/04-personal-anti-pattern-rate.json +16 -0
  68. package/dist/data/alerts/conditions-personal/05-personal-stuck-loop.json +16 -0
  69. package/dist/data/alerts/policy.json +4 -0
  70. package/dist/data/dashboards/ai-coding-assistant-manager-view.json +103 -0
  71. package/dist/data/dashboards/ai-coding-assistant-overview.json +239 -0
  72. package/dist/data/dashboards/ai-coding-assistant-personal.json +442 -0
  73. package/dist/data/dashboards/ai-coding-assistant-platform-comparison.json +320 -0
  74. package/dist/data/dashboards/ai-coding-assistant-security.json +275 -0
  75. package/dist/data/dashboards/ai-coding-assistant-session-detail.json +296 -0
  76. package/dist/data/dashboards/ai-coding-assistant-team-view.json +345 -0
  77. package/dist/deploy/data-paths.d.ts +22 -0
  78. package/dist/deploy/data-paths.d.ts.map +1 -0
  79. package/dist/deploy/data-paths.js +69 -0
  80. package/dist/deploy/data-paths.js.map +1 -0
  81. package/dist/deploy/deploy-alerts.d.ts +58 -0
  82. package/dist/deploy/deploy-alerts.d.ts.map +1 -0
  83. package/dist/deploy/deploy-alerts.js +371 -0
  84. package/dist/deploy/deploy-alerts.js.map +1 -0
  85. package/dist/deploy/deploy-dashboards.d.ts +92 -0
  86. package/dist/deploy/deploy-dashboards.d.ts.map +1 -0
  87. package/dist/deploy/deploy-dashboards.js +282 -0
  88. package/dist/deploy/deploy-dashboards.js.map +1 -0
  89. package/dist/digest/digest-formatter.d.ts +3 -0
  90. package/dist/digest/digest-formatter.d.ts.map +1 -0
  91. package/dist/digest/digest-formatter.js +37 -0
  92. package/dist/digest/digest-formatter.js.map +1 -0
  93. package/dist/digest/digest-sender.d.ts +2 -0
  94. package/dist/digest/digest-sender.d.ts.map +1 -0
  95. package/dist/digest/digest-sender.js +29 -0
  96. package/dist/digest/digest-sender.js.map +1 -0
  97. package/dist/hooks/bash-classifier.d.ts +26 -0
  98. package/dist/hooks/bash-classifier.d.ts.map +1 -0
  99. package/dist/hooks/bash-classifier.js +409 -0
  100. package/dist/hooks/bash-classifier.js.map +1 -0
  101. package/dist/hooks/collector-script.d.ts +47 -0
  102. package/dist/hooks/collector-script.d.ts.map +1 -0
  103. package/dist/hooks/collector-script.js +662 -0
  104. package/dist/hooks/collector-script.js.map +1 -0
  105. package/dist/hooks/event-processor.d.ts +65 -0
  106. package/dist/hooks/event-processor.d.ts.map +1 -0
  107. package/dist/hooks/event-processor.js +342 -0
  108. package/dist/hooks/event-processor.js.map +1 -0
  109. package/dist/hooks/index.d.ts +7 -0
  110. package/dist/hooks/index.d.ts.map +1 -0
  111. package/dist/hooks/index.js +5 -0
  112. package/dist/hooks/index.js.map +1 -0
  113. package/dist/hooks/session-resolver.d.ts +66 -0
  114. package/dist/hooks/session-resolver.d.ts.map +1 -0
  115. package/dist/hooks/session-resolver.js +196 -0
  116. package/dist/hooks/session-resolver.js.map +1 -0
  117. package/dist/hooks/tool-parsers.d.ts +19 -0
  118. package/dist/hooks/tool-parsers.d.ts.map +1 -0
  119. package/dist/hooks/tool-parsers.js +260 -0
  120. package/dist/hooks/tool-parsers.js.map +1 -0
  121. package/dist/index.d.ts +107 -0
  122. package/dist/index.d.ts.map +1 -0
  123. package/dist/index.js +1505 -0
  124. package/dist/index.js.map +1 -0
  125. package/dist/install/cli.d.ts +11 -0
  126. package/dist/install/cli.d.ts.map +1 -0
  127. package/dist/install/cli.js +365 -0
  128. package/dist/install/cli.js.map +1 -0
  129. package/dist/install/index.d.ts +4 -0
  130. package/dist/install/index.d.ts.map +1 -0
  131. package/dist/install/index.js +3 -0
  132. package/dist/install/index.js.map +1 -0
  133. package/dist/install/install-helper.d.ts +35 -0
  134. package/dist/install/install-helper.d.ts.map +1 -0
  135. package/dist/install/install-helper.js +227 -0
  136. package/dist/install/install-helper.js.map +1 -0
  137. package/dist/install/key-validator.d.ts +19 -0
  138. package/dist/install/key-validator.d.ts.map +1 -0
  139. package/dist/install/key-validator.js +122 -0
  140. package/dist/install/key-validator.js.map +1 -0
  141. package/dist/install/migrate.d.ts +12 -0
  142. package/dist/install/migrate.d.ts.map +1 -0
  143. package/dist/install/migrate.js +115 -0
  144. package/dist/install/migrate.js.map +1 -0
  145. package/dist/install/schedule.d.ts +11 -0
  146. package/dist/install/schedule.d.ts.map +1 -0
  147. package/dist/install/schedule.js +114 -0
  148. package/dist/install/schedule.js.map +1 -0
  149. package/dist/install/setup-wizard.d.ts +40 -0
  150. package/dist/install/setup-wizard.d.ts.map +1 -0
  151. package/dist/install/setup-wizard.js +489 -0
  152. package/dist/install/setup-wizard.js.map +1 -0
  153. package/dist/lib/date.d.ts +54 -0
  154. package/dist/lib/date.d.ts.map +1 -0
  155. package/dist/lib/date.js +85 -0
  156. package/dist/lib/date.js.map +1 -0
  157. package/dist/metrics/anti-patterns.d.ts +62 -0
  158. package/dist/metrics/anti-patterns.d.ts.map +1 -0
  159. package/dist/metrics/anti-patterns.js +301 -0
  160. package/dist/metrics/anti-patterns.js.map +1 -0
  161. package/dist/metrics/api-failure-tracker.d.ts +82 -0
  162. package/dist/metrics/api-failure-tracker.d.ts.map +1 -0
  163. package/dist/metrics/api-failure-tracker.js +202 -0
  164. package/dist/metrics/api-failure-tracker.js.map +1 -0
  165. package/dist/metrics/budget-tracker.d.ts +60 -0
  166. package/dist/metrics/budget-tracker.d.ts.map +1 -0
  167. package/dist/metrics/budget-tracker.js +130 -0
  168. package/dist/metrics/budget-tracker.js.map +1 -0
  169. package/dist/metrics/claudemd-tracker.d.ts +108 -0
  170. package/dist/metrics/claudemd-tracker.d.ts.map +1 -0
  171. package/dist/metrics/claudemd-tracker.js +337 -0
  172. package/dist/metrics/claudemd-tracker.js.map +1 -0
  173. package/dist/metrics/collaboration-profile.d.ts +65 -0
  174. package/dist/metrics/collaboration-profile.d.ts.map +1 -0
  175. package/dist/metrics/collaboration-profile.js +231 -0
  176. package/dist/metrics/collaboration-profile.js.map +1 -0
  177. package/dist/metrics/context-composition-tracker.d.ts +74 -0
  178. package/dist/metrics/context-composition-tracker.d.ts.map +1 -0
  179. package/dist/metrics/context-composition-tracker.js +202 -0
  180. package/dist/metrics/context-composition-tracker.js.map +1 -0
  181. package/dist/metrics/context-tracker.d.ts +78 -0
  182. package/dist/metrics/context-tracker.d.ts.map +1 -0
  183. package/dist/metrics/context-tracker.js +222 -0
  184. package/dist/metrics/context-tracker.js.map +1 -0
  185. package/dist/metrics/context-window-tracker.d.ts +18 -0
  186. package/dist/metrics/context-window-tracker.d.ts.map +1 -0
  187. package/dist/metrics/context-window-tracker.js +35 -0
  188. package/dist/metrics/context-window-tracker.js.map +1 -0
  189. package/dist/metrics/cost-forecast.d.ts +36 -0
  190. package/dist/metrics/cost-forecast.d.ts.map +1 -0
  191. package/dist/metrics/cost-forecast.js +91 -0
  192. package/dist/metrics/cost-forecast.js.map +1 -0
  193. package/dist/metrics/cost-per-outcome.d.ts +102 -0
  194. package/dist/metrics/cost-per-outcome.d.ts.map +1 -0
  195. package/dist/metrics/cost-per-outcome.js +266 -0
  196. package/dist/metrics/cost-per-outcome.js.map +1 -0
  197. package/dist/metrics/cost-tracker.d.ts +78 -0
  198. package/dist/metrics/cost-tracker.d.ts.map +1 -0
  199. package/dist/metrics/cost-tracker.js +169 -0
  200. package/dist/metrics/cost-tracker.js.map +1 -0
  201. package/dist/metrics/decision-tracker.d.ts +49 -0
  202. package/dist/metrics/decision-tracker.d.ts.map +1 -0
  203. package/dist/metrics/decision-tracker.js +161 -0
  204. package/dist/metrics/decision-tracker.js.map +1 -0
  205. package/dist/metrics/efficiency-score.d.ts +80 -0
  206. package/dist/metrics/efficiency-score.d.ts.map +1 -0
  207. package/dist/metrics/efficiency-score.js +219 -0
  208. package/dist/metrics/efficiency-score.js.map +1 -0
  209. package/dist/metrics/git-efficiency-tracker.d.ts +165 -0
  210. package/dist/metrics/git-efficiency-tracker.d.ts.map +1 -0
  211. package/dist/metrics/git-efficiency-tracker.js +1056 -0
  212. package/dist/metrics/git-efficiency-tracker.js.map +1 -0
  213. package/dist/metrics/index.d.ts +26 -0
  214. package/dist/metrics/index.d.ts.map +1 -0
  215. package/dist/metrics/index.js +14 -0
  216. package/dist/metrics/index.js.map +1 -0
  217. package/dist/metrics/instruction-drift-tracker.d.ts +69 -0
  218. package/dist/metrics/instruction-drift-tracker.d.ts.map +1 -0
  219. package/dist/metrics/instruction-drift-tracker.js +213 -0
  220. package/dist/metrics/instruction-drift-tracker.js.map +1 -0
  221. package/dist/metrics/latency-decomposition.d.ts +50 -0
  222. package/dist/metrics/latency-decomposition.d.ts.map +1 -0
  223. package/dist/metrics/latency-decomposition.js +112 -0
  224. package/dist/metrics/latency-decomposition.js.map +1 -0
  225. package/dist/metrics/latency-tracker.d.ts +33 -0
  226. package/dist/metrics/latency-tracker.d.ts.map +1 -0
  227. package/dist/metrics/latency-tracker.js +93 -0
  228. package/dist/metrics/latency-tracker.js.map +1 -0
  229. package/dist/metrics/live-session-registry.d.ts +29 -0
  230. package/dist/metrics/live-session-registry.d.ts.map +1 -0
  231. package/dist/metrics/live-session-registry.js +103 -0
  232. package/dist/metrics/live-session-registry.js.map +1 -0
  233. package/dist/metrics/model-usage-tracker.d.ts +21 -0
  234. package/dist/metrics/model-usage-tracker.d.ts.map +1 -0
  235. package/dist/metrics/model-usage-tracker.js +53 -0
  236. package/dist/metrics/model-usage-tracker.js.map +1 -0
  237. package/dist/metrics/percentile.d.ts +5 -0
  238. package/dist/metrics/percentile.d.ts.map +1 -0
  239. package/dist/metrics/percentile.js +10 -0
  240. package/dist/metrics/percentile.js.map +1 -0
  241. package/dist/metrics/personal-coach.d.ts +47 -0
  242. package/dist/metrics/personal-coach.d.ts.map +1 -0
  243. package/dist/metrics/personal-coach.js +241 -0
  244. package/dist/metrics/personal-coach.js.map +1 -0
  245. package/dist/metrics/prompt-feedback.d.ts +75 -0
  246. package/dist/metrics/prompt-feedback.d.ts.map +1 -0
  247. package/dist/metrics/prompt-feedback.js +286 -0
  248. package/dist/metrics/prompt-feedback.js.map +1 -0
  249. package/dist/metrics/proxy-metrics.d.ts +54 -0
  250. package/dist/metrics/proxy-metrics.d.ts.map +1 -0
  251. package/dist/metrics/proxy-metrics.js +228 -0
  252. package/dist/metrics/proxy-metrics.js.map +1 -0
  253. package/dist/metrics/quality-proxy-tracker.d.ts +51 -0
  254. package/dist/metrics/quality-proxy-tracker.d.ts.map +1 -0
  255. package/dist/metrics/quality-proxy-tracker.js +162 -0
  256. package/dist/metrics/quality-proxy-tracker.js.map +1 -0
  257. package/dist/metrics/recommendation-engine.d.ts +72 -0
  258. package/dist/metrics/recommendation-engine.d.ts.map +1 -0
  259. package/dist/metrics/recommendation-engine.js +207 -0
  260. package/dist/metrics/recommendation-engine.js.map +1 -0
  261. package/dist/metrics/retry-detector.d.ts +43 -0
  262. package/dist/metrics/retry-detector.d.ts.map +1 -0
  263. package/dist/metrics/retry-detector.js +179 -0
  264. package/dist/metrics/retry-detector.js.map +1 -0
  265. package/dist/metrics/session-tracker.d.ts +75 -0
  266. package/dist/metrics/session-tracker.d.ts.map +1 -0
  267. package/dist/metrics/session-tracker.js +249 -0
  268. package/dist/metrics/session-tracker.js.map +1 -0
  269. package/dist/metrics/task-completion-tracker.d.ts +15 -0
  270. package/dist/metrics/task-completion-tracker.d.ts.map +1 -0
  271. package/dist/metrics/task-completion-tracker.js +27 -0
  272. package/dist/metrics/task-completion-tracker.js.map +1 -0
  273. package/dist/metrics/task-detector.d.ts +84 -0
  274. package/dist/metrics/task-detector.d.ts.map +1 -0
  275. package/dist/metrics/task-detector.js +302 -0
  276. package/dist/metrics/task-detector.js.map +1 -0
  277. package/dist/metrics/tool-selection-scorer.d.ts +39 -0
  278. package/dist/metrics/tool-selection-scorer.d.ts.map +1 -0
  279. package/dist/metrics/tool-selection-scorer.js +193 -0
  280. package/dist/metrics/tool-selection-scorer.js.map +1 -0
  281. package/dist/metrics/trend-analyzer.d.ts +92 -0
  282. package/dist/metrics/trend-analyzer.d.ts.map +1 -0
  283. package/dist/metrics/trend-analyzer.js +293 -0
  284. package/dist/metrics/trend-analyzer.js.map +1 -0
  285. package/dist/metrics/turn-cost-attributor.d.ts +41 -0
  286. package/dist/metrics/turn-cost-attributor.d.ts.map +1 -0
  287. package/dist/metrics/turn-cost-attributor.js +118 -0
  288. package/dist/metrics/turn-cost-attributor.js.map +1 -0
  289. package/dist/metrics/turn-tracker.d.ts +49 -0
  290. package/dist/metrics/turn-tracker.d.ts.map +1 -0
  291. package/dist/metrics/turn-tracker.js +192 -0
  292. package/dist/metrics/turn-tracker.js.map +1 -0
  293. package/dist/platforms/amazon-q-adapter.d.ts +10 -0
  294. package/dist/platforms/amazon-q-adapter.d.ts.map +1 -0
  295. package/dist/platforms/amazon-q-adapter.js +75 -0
  296. package/dist/platforms/amazon-q-adapter.js.map +1 -0
  297. package/dist/platforms/claude-code-adapter.d.ts +10 -0
  298. package/dist/platforms/claude-code-adapter.d.ts.map +1 -0
  299. package/dist/platforms/claude-code-adapter.js +48 -0
  300. package/dist/platforms/claude-code-adapter.js.map +1 -0
  301. package/dist/platforms/continue-adapter.d.ts +10 -0
  302. package/dist/platforms/continue-adapter.d.ts.map +1 -0
  303. package/dist/platforms/continue-adapter.js +73 -0
  304. package/dist/platforms/continue-adapter.js.map +1 -0
  305. package/dist/platforms/copilot-adapter.d.ts +37 -0
  306. package/dist/platforms/copilot-adapter.d.ts.map +1 -0
  307. package/dist/platforms/copilot-adapter.js +66 -0
  308. package/dist/platforms/copilot-adapter.js.map +1 -0
  309. package/dist/platforms/cursor-adapter.d.ts +10 -0
  310. package/dist/platforms/cursor-adapter.d.ts.map +1 -0
  311. package/dist/platforms/cursor-adapter.js +60 -0
  312. package/dist/platforms/cursor-adapter.js.map +1 -0
  313. package/dist/platforms/generic-mcp-adapter.d.ts +113 -0
  314. package/dist/platforms/generic-mcp-adapter.d.ts.map +1 -0
  315. package/dist/platforms/generic-mcp-adapter.js +139 -0
  316. package/dist/platforms/generic-mcp-adapter.js.map +1 -0
  317. package/dist/platforms/index.d.ts +15 -0
  318. package/dist/platforms/index.d.ts.map +1 -0
  319. package/dist/platforms/index.js +12 -0
  320. package/dist/platforms/index.js.map +1 -0
  321. package/dist/platforms/platform-registry.d.ts +11 -0
  322. package/dist/platforms/platform-registry.d.ts.map +1 -0
  323. package/dist/platforms/platform-registry.js +54 -0
  324. package/dist/platforms/platform-registry.js.map +1 -0
  325. package/dist/platforms/types.d.ts +36 -0
  326. package/dist/platforms/types.d.ts.map +1 -0
  327. package/dist/platforms/types.js +2 -0
  328. package/dist/platforms/types.js.map +1 -0
  329. package/dist/platforms/windsurf-adapter.d.ts +10 -0
  330. package/dist/platforms/windsurf-adapter.d.ts.map +1 -0
  331. package/dist/platforms/windsurf-adapter.js +63 -0
  332. package/dist/platforms/windsurf-adapter.js.map +1 -0
  333. package/dist/platforms/zed-adapter.d.ts +10 -0
  334. package/dist/platforms/zed-adapter.d.ts.map +1 -0
  335. package/dist/platforms/zed-adapter.js +72 -0
  336. package/dist/platforms/zed-adapter.js.map +1 -0
  337. package/dist/proxy/index.d.ts +7 -0
  338. package/dist/proxy/index.d.ts.map +1 -0
  339. package/dist/proxy/index.js +5 -0
  340. package/dist/proxy/index.js.map +1 -0
  341. package/dist/proxy/otlp-receiver.d.ts +28 -0
  342. package/dist/proxy/otlp-receiver.d.ts.map +1 -0
  343. package/dist/proxy/otlp-receiver.js +319 -0
  344. package/dist/proxy/otlp-receiver.js.map +1 -0
  345. package/dist/proxy/proxy-manager.d.ts +47 -0
  346. package/dist/proxy/proxy-manager.d.ts.map +1 -0
  347. package/dist/proxy/proxy-manager.js +338 -0
  348. package/dist/proxy/proxy-manager.js.map +1 -0
  349. package/dist/proxy/types.d.ts +72 -0
  350. package/dist/proxy/types.d.ts.map +1 -0
  351. package/dist/proxy/types.js +33 -0
  352. package/dist/proxy/types.js.map +1 -0
  353. package/dist/proxy/upstream-http.d.ts +26 -0
  354. package/dist/proxy/upstream-http.d.ts.map +1 -0
  355. package/dist/proxy/upstream-http.js +209 -0
  356. package/dist/proxy/upstream-http.js.map +1 -0
  357. package/dist/proxy/upstream-stdio.d.ts +25 -0
  358. package/dist/proxy/upstream-stdio.d.ts.map +1 -0
  359. package/dist/proxy/upstream-stdio.js +256 -0
  360. package/dist/proxy/upstream-stdio.js.map +1 -0
  361. package/dist/security/audit-trail.d.ts +74 -0
  362. package/dist/security/audit-trail.d.ts.map +1 -0
  363. package/dist/security/audit-trail.js +338 -0
  364. package/dist/security/audit-trail.js.map +1 -0
  365. package/dist/security/index.d.ts +5 -0
  366. package/dist/security/index.d.ts.map +1 -0
  367. package/dist/security/index.js +4 -0
  368. package/dist/security/index.js.map +1 -0
  369. package/dist/security/ssrf.d.ts +2 -0
  370. package/dist/security/ssrf.d.ts.map +1 -0
  371. package/dist/security/ssrf.js +126 -0
  372. package/dist/security/ssrf.js.map +1 -0
  373. package/dist/server.d.ts +14 -0
  374. package/dist/server.d.ts.map +1 -0
  375. package/dist/server.js +117 -0
  376. package/dist/server.js.map +1 -0
  377. package/dist/shared/__test-utils__/log-output.d.ts +49 -0
  378. package/dist/shared/__test-utils__/log-output.d.ts.map +1 -0
  379. package/dist/shared/__test-utils__/log-output.js +38 -0
  380. package/dist/shared/__test-utils__/log-output.js.map +1 -0
  381. package/dist/shared/config.d.ts +56 -0
  382. package/dist/shared/config.d.ts.map +1 -0
  383. package/dist/shared/config.js +290 -0
  384. package/dist/shared/config.js.map +1 -0
  385. package/dist/shared/errors.d.ts +139 -0
  386. package/dist/shared/errors.d.ts.map +1 -0
  387. package/dist/shared/errors.js +406 -0
  388. package/dist/shared/errors.js.map +1 -0
  389. package/dist/shared/events/factory.d.ts +143 -0
  390. package/dist/shared/events/factory.d.ts.map +1 -0
  391. package/dist/shared/events/factory.js +351 -0
  392. package/dist/shared/events/factory.js.map +1 -0
  393. package/dist/shared/events/index.d.ts +6 -0
  394. package/dist/shared/events/index.d.ts.map +1 -0
  395. package/dist/shared/events/index.js +3 -0
  396. package/dist/shared/events/index.js.map +1 -0
  397. package/dist/shared/events/serialize.d.ts +87 -0
  398. package/dist/shared/events/serialize.d.ts.map +1 -0
  399. package/dist/shared/events/serialize.js +510 -0
  400. package/dist/shared/events/serialize.js.map +1 -0
  401. package/dist/shared/events/types.d.ts +139 -0
  402. package/dist/shared/events/types.d.ts.map +1 -0
  403. package/dist/shared/events/types.js +2 -0
  404. package/dist/shared/events/types.js.map +1 -0
  405. package/dist/shared/harvest/event-buffer.d.ts +59 -0
  406. package/dist/shared/harvest/event-buffer.d.ts.map +1 -0
  407. package/dist/shared/harvest/event-buffer.js +100 -0
  408. package/dist/shared/harvest/event-buffer.js.map +1 -0
  409. package/dist/shared/harvest/harvest-scheduler.d.ts +200 -0
  410. package/dist/shared/harvest/harvest-scheduler.d.ts.map +1 -0
  411. package/dist/shared/harvest/harvest-scheduler.js +647 -0
  412. package/dist/shared/harvest/harvest-scheduler.js.map +1 -0
  413. package/dist/shared/harvest/index.d.ts +7 -0
  414. package/dist/shared/harvest/index.d.ts.map +1 -0
  415. package/dist/shared/harvest/index.js +4 -0
  416. package/dist/shared/harvest/index.js.map +1 -0
  417. package/dist/shared/harvest/metric-aggregator.d.ts +115 -0
  418. package/dist/shared/harvest/metric-aggregator.d.ts.map +1 -0
  419. package/dist/shared/harvest/metric-aggregator.js +247 -0
  420. package/dist/shared/harvest/metric-aggregator.js.map +1 -0
  421. package/dist/shared/index.d.ts +22 -0
  422. package/dist/shared/index.d.ts.map +1 -0
  423. package/dist/shared/index.js +13 -0
  424. package/dist/shared/index.js.map +1 -0
  425. package/dist/shared/logger.d.ts +57 -0
  426. package/dist/shared/logger.d.ts.map +1 -0
  427. package/dist/shared/logger.js +166 -0
  428. package/dist/shared/logger.js.map +1 -0
  429. package/dist/shared/pricing-data.d.ts +4 -0
  430. package/dist/shared/pricing-data.d.ts.map +1 -0
  431. package/dist/shared/pricing-data.js +473 -0
  432. package/dist/shared/pricing-data.js.map +1 -0
  433. package/dist/shared/pricing.d.ts +148 -0
  434. package/dist/shared/pricing.d.ts.map +1 -0
  435. package/dist/shared/pricing.js +528 -0
  436. package/dist/shared/pricing.js.map +1 -0
  437. package/dist/shared/redact.d.ts +33 -0
  438. package/dist/shared/redact.d.ts.map +1 -0
  439. package/dist/shared/redact.js +110 -0
  440. package/dist/shared/redact.js.map +1 -0
  441. package/dist/shared/timing.d.ts +96 -0
  442. package/dist/shared/timing.d.ts.map +1 -0
  443. package/dist/shared/timing.js +173 -0
  444. package/dist/shared/timing.js.map +1 -0
  445. package/dist/shared/tokens.d.ts +145 -0
  446. package/dist/shared/tokens.d.ts.map +1 -0
  447. package/dist/shared/tokens.js +492 -0
  448. package/dist/shared/tokens.js.map +1 -0
  449. package/dist/shared/transport/events-api.d.ts +14 -0
  450. package/dist/shared/transport/events-api.d.ts.map +1 -0
  451. package/dist/shared/transport/events-api.js +29 -0
  452. package/dist/shared/transport/events-api.js.map +1 -0
  453. package/dist/shared/transport/http-client.d.ts +49 -0
  454. package/dist/shared/transport/http-client.d.ts.map +1 -0
  455. package/dist/shared/transport/http-client.js +381 -0
  456. package/dist/shared/transport/http-client.js.map +1 -0
  457. package/dist/shared/transport/index.d.ts +10 -0
  458. package/dist/shared/transport/index.d.ts.map +1 -0
  459. package/dist/shared/transport/index.js +6 -0
  460. package/dist/shared/transport/index.js.map +1 -0
  461. package/dist/shared/transport/logs-api.d.ts +29 -0
  462. package/dist/shared/transport/logs-api.d.ts.map +1 -0
  463. package/dist/shared/transport/logs-api.js +40 -0
  464. package/dist/shared/transport/logs-api.js.map +1 -0
  465. package/dist/shared/transport/metric-api.d.ts +9 -0
  466. package/dist/shared/transport/metric-api.d.ts.map +1 -0
  467. package/dist/shared/transport/metric-api.js +39 -0
  468. package/dist/shared/transport/metric-api.js.map +1 -0
  469. package/dist/shared/transport/otlp-event-bridge.d.ts +22 -0
  470. package/dist/shared/transport/otlp-event-bridge.d.ts.map +1 -0
  471. package/dist/shared/transport/otlp-event-bridge.js +50 -0
  472. package/dist/shared/transport/otlp-event-bridge.js.map +1 -0
  473. package/dist/shared/transport/otlp-shared.d.ts +14 -0
  474. package/dist/shared/transport/otlp-shared.d.ts.map +1 -0
  475. package/dist/shared/transport/otlp-shared.js +49 -0
  476. package/dist/shared/transport/otlp-shared.js.map +1 -0
  477. package/dist/shared/transport/otlp-transport.d.ts +58 -0
  478. package/dist/shared/transport/otlp-transport.d.ts.map +1 -0
  479. package/dist/shared/transport/otlp-transport.js +236 -0
  480. package/dist/shared/transport/otlp-transport.js.map +1 -0
  481. package/dist/shared/transport/types.d.ts +129 -0
  482. package/dist/shared/transport/types.d.ts.map +1 -0
  483. package/dist/shared/transport/types.js +2 -0
  484. package/dist/shared/transport/types.js.map +1 -0
  485. package/dist/shared/version.d.ts +2 -0
  486. package/dist/shared/version.d.ts.map +1 -0
  487. package/dist/shared/version.js +2 -0
  488. package/dist/shared/version.js.map +1 -0
  489. package/dist/storage/index.d.ts +7 -0
  490. package/dist/storage/index.d.ts.map +1 -0
  491. package/dist/storage/index.js +4 -0
  492. package/dist/storage/index.js.map +1 -0
  493. package/dist/storage/local-store.d.ts +153 -0
  494. package/dist/storage/local-store.d.ts.map +1 -0
  495. package/dist/storage/local-store.js +719 -0
  496. package/dist/storage/local-store.js.map +1 -0
  497. package/dist/storage/retention.d.ts +2 -0
  498. package/dist/storage/retention.d.ts.map +1 -0
  499. package/dist/storage/retention.js +53 -0
  500. package/dist/storage/retention.js.map +1 -0
  501. package/dist/storage/session-store.d.ts +97 -0
  502. package/dist/storage/session-store.d.ts.map +1 -0
  503. package/dist/storage/session-store.js +391 -0
  504. package/dist/storage/session-store.js.map +1 -0
  505. package/dist/storage/types.d.ts +64 -0
  506. package/dist/storage/types.d.ts.map +1 -0
  507. package/dist/storage/types.js +2 -0
  508. package/dist/storage/types.js.map +1 -0
  509. package/dist/storage/weekly-summary.d.ts +61 -0
  510. package/dist/storage/weekly-summary.d.ts.map +1 -0
  511. package/dist/storage/weekly-summary.js +243 -0
  512. package/dist/storage/weekly-summary.js.map +1 -0
  513. package/dist/tools/analytics-tools.d.ts +101 -0
  514. package/dist/tools/analytics-tools.d.ts.map +1 -0
  515. package/dist/tools/analytics-tools.js +71 -0
  516. package/dist/tools/analytics-tools.js.map +1 -0
  517. package/dist/tools/cost-tools.d.ts +121 -0
  518. package/dist/tools/cost-tools.d.ts.map +1 -0
  519. package/dist/tools/cost-tools.js +174 -0
  520. package/dist/tools/cost-tools.js.map +1 -0
  521. package/dist/tools/cross-session-tools.d.ts +376 -0
  522. package/dist/tools/cross-session-tools.d.ts.map +1 -0
  523. package/dist/tools/cross-session-tools.js +820 -0
  524. package/dist/tools/cross-session-tools.js.map +1 -0
  525. package/dist/tools/extended-analytics-tools.d.ts +164 -0
  526. package/dist/tools/extended-analytics-tools.d.ts.map +1 -0
  527. package/dist/tools/extended-analytics-tools.js +121 -0
  528. package/dist/tools/extended-analytics-tools.js.map +1 -0
  529. package/dist/tools/index.d.ts +7 -0
  530. package/dist/tools/index.d.ts.map +1 -0
  531. package/dist/tools/index.js +4 -0
  532. package/dist/tools/index.js.map +1 -0
  533. package/dist/tools/session-stats.d.ts +162 -0
  534. package/dist/tools/session-stats.d.ts.map +1 -0
  535. package/dist/tools/session-stats.js +1054 -0
  536. package/dist/tools/session-stats.js.map +1 -0
  537. package/dist/tools/workflow-tools.d.ts +126 -0
  538. package/dist/tools/workflow-tools.d.ts.map +1 -0
  539. package/dist/tools/workflow-tools.js +274 -0
  540. package/dist/tools/workflow-tools.js.map +1 -0
  541. package/dist/tracing/mcp-tracer.d.ts +4 -0
  542. package/dist/tracing/mcp-tracer.d.ts.map +1 -0
  543. package/dist/tracing/mcp-tracer.js +14 -0
  544. package/dist/tracing/mcp-tracer.js.map +1 -0
  545. package/dist/tracing/session-span.d.ts +14 -0
  546. package/dist/tracing/session-span.d.ts.map +1 -0
  547. package/dist/tracing/session-span.js +53 -0
  548. package/dist/tracing/session-span.js.map +1 -0
  549. package/dist/tracing/task-span-tracker.d.ts +11 -0
  550. package/dist/tracing/task-span-tracker.d.ts.map +1 -0
  551. package/dist/tracing/task-span-tracker.js +59 -0
  552. package/dist/tracing/task-span-tracker.js.map +1 -0
  553. package/dist/tracing/tool-call-span.d.ts +4 -0
  554. package/dist/tracing/tool-call-span.d.ts.map +1 -0
  555. package/dist/tracing/tool-call-span.js +60 -0
  556. package/dist/tracing/tool-call-span.js.map +1 -0
  557. package/dist/transport/index.d.ts +3 -0
  558. package/dist/transport/index.d.ts.map +1 -0
  559. package/dist/transport/index.js +2 -0
  560. package/dist/transport/index.js.map +1 -0
  561. package/dist/transport/log-ingest.d.ts +42 -0
  562. package/dist/transport/log-ingest.d.ts.map +1 -0
  563. package/dist/transport/log-ingest.js +151 -0
  564. package/dist/transport/log-ingest.js.map +1 -0
  565. package/dist/transport/nr-ingest.d.ts +171 -0
  566. package/dist/transport/nr-ingest.d.ts.map +1 -0
  567. package/dist/transport/nr-ingest.js +659 -0
  568. package/dist/transport/nr-ingest.js.map +1 -0
  569. package/dist/types.d.ts +45 -0
  570. package/dist/types.d.ts.map +1 -0
  571. package/dist/types.js +2 -0
  572. package/dist/types.js.map +1 -0
  573. package/dist/web/assets/index-BrL281N-.css +2 -0
  574. package/dist/web/assets/index-CcaYZzXm.js +42 -0
  575. package/dist/web/favicon.svg +15 -0
  576. package/dist/web/index.html +15 -0
  577. package/examples/local-alert-rules.json +106 -0
  578. 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