@agentdeck/bridge 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (380) hide show
  1. package/LICENSE +21 -0
  2. package/dist/adapters/claude-code.d.ts +11 -27
  3. package/dist/adapters/claude-code.d.ts.map +1 -1
  4. package/dist/adapters/claude-code.js +29 -123
  5. package/dist/adapters/claude-code.js.map +1 -1
  6. package/dist/adapters/codex-cli.d.ts +21 -0
  7. package/dist/adapters/codex-cli.d.ts.map +1 -0
  8. package/dist/adapters/codex-cli.js +53 -0
  9. package/dist/adapters/codex-cli.js.map +1 -0
  10. package/dist/adapters/index.d.ts +4 -0
  11. package/dist/adapters/index.d.ts.map +1 -1
  12. package/dist/adapters/index.js +13 -0
  13. package/dist/adapters/index.js.map +1 -1
  14. package/dist/adapters/monitor.d.ts +34 -0
  15. package/dist/adapters/monitor.d.ts.map +1 -0
  16. package/dist/adapters/monitor.js +86 -0
  17. package/dist/adapters/monitor.js.map +1 -0
  18. package/dist/adapters/openclaw.d.ts +36 -2
  19. package/dist/adapters/openclaw.d.ts.map +1 -1
  20. package/dist/adapters/openclaw.js +329 -59
  21. package/dist/adapters/openclaw.js.map +1 -1
  22. package/dist/adapters/opencode-adapter.d.ts +48 -0
  23. package/dist/adapters/opencode-adapter.d.ts.map +1 -0
  24. package/dist/adapters/opencode-adapter.js +345 -0
  25. package/dist/adapters/opencode-adapter.js.map +1 -0
  26. package/dist/adapters/pty-adapter.d.ts +56 -0
  27. package/dist/adapters/pty-adapter.d.ts.map +1 -0
  28. package/dist/adapters/pty-adapter.js +143 -0
  29. package/dist/adapters/pty-adapter.js.map +1 -0
  30. package/dist/adb-reverse.d.ts +25 -0
  31. package/dist/adb-reverse.d.ts.map +1 -0
  32. package/dist/adb-reverse.js +129 -0
  33. package/dist/adb-reverse.js.map +1 -0
  34. package/dist/antigravity-local.d.ts +7 -0
  35. package/dist/antigravity-local.d.ts.map +1 -0
  36. package/dist/antigravity-local.js +153 -0
  37. package/dist/antigravity-local.js.map +1 -0
  38. package/dist/apme/adapters/claude-hook.d.ts +15 -0
  39. package/dist/apme/adapters/claude-hook.d.ts.map +1 -0
  40. package/dist/apme/adapters/claude-hook.js +85 -0
  41. package/dist/apme/adapters/claude-hook.js.map +1 -0
  42. package/dist/apme/adapters/claude-pty.d.ts +28 -0
  43. package/dist/apme/adapters/claude-pty.d.ts.map +1 -0
  44. package/dist/apme/adapters/claude-pty.js +88 -0
  45. package/dist/apme/adapters/claude-pty.js.map +1 -0
  46. package/dist/apme/adapters/timeline.d.ts +16 -0
  47. package/dist/apme/adapters/timeline.d.ts.map +1 -0
  48. package/dist/apme/adapters/timeline.js +66 -0
  49. package/dist/apme/adapters/timeline.js.map +1 -0
  50. package/dist/apme/classifier.d.ts +53 -0
  51. package/dist/apme/classifier.d.ts.map +1 -0
  52. package/dist/apme/classifier.js +266 -0
  53. package/dist/apme/classifier.js.map +1 -0
  54. package/dist/apme/claude-transcript-reader.d.ts +38 -0
  55. package/dist/apme/claude-transcript-reader.d.ts.map +1 -0
  56. package/dist/apme/claude-transcript-reader.js +127 -0
  57. package/dist/apme/claude-transcript-reader.js.map +1 -0
  58. package/dist/apme/collector.d.ts +108 -0
  59. package/dist/apme/collector.d.ts.map +1 -0
  60. package/dist/apme/collector.js +659 -0
  61. package/dist/apme/collector.js.map +1 -0
  62. package/dist/apme/dashboard-html.d.ts +9 -0
  63. package/dist/apme/dashboard-html.d.ts.map +1 -0
  64. package/dist/apme/dashboard-html.js +416 -0
  65. package/dist/apme/dashboard-html.js.map +1 -0
  66. package/dist/apme/http.d.ts +19 -0
  67. package/dist/apme/http.d.ts.map +1 -0
  68. package/dist/apme/http.js +210 -0
  69. package/dist/apme/http.js.map +1 -0
  70. package/dist/apme/hw-sampler.d.ts +30 -0
  71. package/dist/apme/hw-sampler.d.ts.map +1 -0
  72. package/dist/apme/hw-sampler.js +106 -0
  73. package/dist/apme/hw-sampler.js.map +1 -0
  74. package/dist/apme/index.d.ts +43 -0
  75. package/dist/apme/index.d.ts.map +1 -0
  76. package/dist/apme/index.js +75 -0
  77. package/dist/apme/index.js.map +1 -0
  78. package/dist/apme/outcome.d.ts +48 -0
  79. package/dist/apme/outcome.d.ts.map +1 -0
  80. package/dist/apme/outcome.js +232 -0
  81. package/dist/apme/outcome.js.map +1 -0
  82. package/dist/apme/recommend.d.ts +32 -0
  83. package/dist/apme/recommend.d.ts.map +1 -0
  84. package/dist/apme/recommend.js +41 -0
  85. package/dist/apme/recommend.js.map +1 -0
  86. package/dist/apme/runner.d.ts +94 -0
  87. package/dist/apme/runner.d.ts.map +1 -0
  88. package/dist/apme/runner.js +812 -0
  89. package/dist/apme/runner.js.map +1 -0
  90. package/dist/apme/settings.d.ts +65 -0
  91. package/dist/apme/settings.d.ts.map +1 -0
  92. package/dist/apme/settings.js +97 -0
  93. package/dist/apme/settings.js.map +1 -0
  94. package/dist/apme/store.d.ts +99 -0
  95. package/dist/apme/store.d.ts.map +1 -0
  96. package/dist/apme/store.js +911 -0
  97. package/dist/apme/store.js.map +1 -0
  98. package/dist/apme/tuner.d.ts +64 -0
  99. package/dist/apme/tuner.d.ts.map +1 -0
  100. package/dist/apme/tuner.js +297 -0
  101. package/dist/apme/tuner.js.map +1 -0
  102. package/dist/apme/types.d.ts +11 -0
  103. package/dist/apme/types.d.ts.map +1 -0
  104. package/dist/apme/types.js +10 -0
  105. package/dist/apme/types.js.map +1 -0
  106. package/dist/auth.d.ts +1 -1
  107. package/dist/auth.d.ts.map +1 -1
  108. package/dist/auth.js +15 -5
  109. package/dist/auth.js.map +1 -1
  110. package/dist/bridge-core.d.ts +162 -0
  111. package/dist/bridge-core.d.ts.map +1 -0
  112. package/dist/bridge-core.js +627 -0
  113. package/dist/bridge-core.js.map +1 -0
  114. package/dist/check-deps.d.ts +3 -1
  115. package/dist/check-deps.d.ts.map +1 -1
  116. package/dist/check-deps.js +34 -13
  117. package/dist/check-deps.js.map +1 -1
  118. package/dist/cli.d.ts +3 -0
  119. package/dist/cli.d.ts.map +1 -0
  120. package/dist/cli.js +1235 -0
  121. package/dist/cli.js.map +1 -0
  122. package/dist/codex-auth.d.ts +10 -0
  123. package/dist/codex-auth.d.ts.map +1 -0
  124. package/dist/codex-auth.js +63 -0
  125. package/dist/codex-auth.js.map +1 -0
  126. package/dist/codex-output-parser.d.ts +21 -0
  127. package/dist/codex-output-parser.d.ts.map +1 -0
  128. package/dist/codex-output-parser.js +253 -0
  129. package/dist/codex-output-parser.js.map +1 -0
  130. package/dist/d200h/hid-protocol.d.ts +62 -0
  131. package/dist/d200h/hid-protocol.d.ts.map +1 -0
  132. package/dist/d200h/hid-protocol.js +124 -0
  133. package/dist/d200h/hid-protocol.js.map +1 -0
  134. package/dist/d200h/image-renderer.d.ts +36 -0
  135. package/dist/d200h/image-renderer.d.ts.map +1 -0
  136. package/dist/d200h/image-renderer.js +487 -0
  137. package/dist/d200h/image-renderer.js.map +1 -0
  138. package/dist/daemon-server.d.ts +19 -0
  139. package/dist/daemon-server.d.ts.map +1 -0
  140. package/dist/daemon-server.js +1168 -0
  141. package/dist/daemon-server.js.map +1 -0
  142. package/dist/daemon-ws-client.d.ts +82 -0
  143. package/dist/daemon-ws-client.d.ts.map +1 -0
  144. package/dist/daemon-ws-client.js +179 -0
  145. package/dist/daemon-ws-client.js.map +1 -0
  146. package/dist/daemon.d.ts +10 -0
  147. package/dist/daemon.d.ts.map +1 -0
  148. package/dist/daemon.js +56 -0
  149. package/dist/daemon.js.map +1 -0
  150. package/dist/display-monitor.d.ts +25 -0
  151. package/dist/display-monitor.d.ts.map +1 -0
  152. package/dist/display-monitor.js +143 -0
  153. package/dist/display-monitor.js.map +1 -0
  154. package/dist/esp32-serial.d.ts +112 -0
  155. package/dist/esp32-serial.d.ts.map +1 -0
  156. package/dist/esp32-serial.js +502 -0
  157. package/dist/esp32-serial.js.map +1 -0
  158. package/dist/gateway-probe.d.ts +12 -0
  159. package/dist/gateway-probe.d.ts.map +1 -0
  160. package/dist/gateway-probe.js +39 -0
  161. package/dist/gateway-probe.js.map +1 -0
  162. package/dist/hook-server.d.ts +29 -0
  163. package/dist/hook-server.d.ts.map +1 -1
  164. package/dist/hook-server.js +366 -8
  165. package/dist/hook-server.js.map +1 -1
  166. package/dist/index.d.ts +20 -2
  167. package/dist/index.d.ts.map +1 -1
  168. package/dist/index.js +1305 -562
  169. package/dist/index.js.map +1 -1
  170. package/dist/log-stream.d.ts +26 -0
  171. package/dist/log-stream.d.ts.map +1 -0
  172. package/dist/log-stream.js +111 -0
  173. package/dist/log-stream.js.map +1 -0
  174. package/dist/logger.d.ts +9 -2
  175. package/dist/logger.d.ts.map +1 -1
  176. package/dist/logger.js +36 -5
  177. package/dist/logger.js.map +1 -1
  178. package/dist/mdns.d.ts +15 -1
  179. package/dist/mdns.d.ts.map +1 -1
  180. package/dist/mdns.js +143 -28
  181. package/dist/mdns.js.map +1 -1
  182. package/dist/mlx-probe.d.ts +14 -0
  183. package/dist/mlx-probe.d.ts.map +1 -0
  184. package/dist/mlx-probe.js +44 -0
  185. package/dist/mlx-probe.js.map +1 -0
  186. package/dist/model-catalog.d.ts +5 -3
  187. package/dist/model-catalog.d.ts.map +1 -1
  188. package/dist/model-catalog.js +45 -29
  189. package/dist/model-catalog.js.map +1 -1
  190. package/dist/modules/adb-module.d.ts +14 -0
  191. package/dist/modules/adb-module.d.ts.map +1 -0
  192. package/dist/modules/adb-module.js +36 -0
  193. package/dist/modules/adb-module.js.map +1 -0
  194. package/dist/modules/d200h-module.d.ts +43 -0
  195. package/dist/modules/d200h-module.d.ts.map +1 -0
  196. package/dist/modules/d200h-module.js +360 -0
  197. package/dist/modules/d200h-module.js.map +1 -0
  198. package/dist/modules/d200h-renderer.d.ts +7 -0
  199. package/dist/modules/d200h-renderer.d.ts.map +1 -0
  200. package/dist/modules/d200h-renderer.js +321 -0
  201. package/dist/modules/d200h-renderer.js.map +1 -0
  202. package/dist/modules/index.d.ts +23 -0
  203. package/dist/modules/index.d.ts.map +1 -0
  204. package/dist/modules/index.js +65 -0
  205. package/dist/modules/index.js.map +1 -0
  206. package/dist/modules/mdns-module.d.ts +12 -0
  207. package/dist/modules/mdns-module.d.ts.map +1 -0
  208. package/dist/modules/mdns-module.js +21 -0
  209. package/dist/modules/mdns-module.js.map +1 -0
  210. package/dist/modules/pixoo-module.d.ts +8 -0
  211. package/dist/modules/pixoo-module.d.ts.map +1 -0
  212. package/dist/modules/pixoo-module.js +24 -0
  213. package/dist/modules/pixoo-module.js.map +1 -0
  214. package/dist/modules/serial-module.d.ts +18 -0
  215. package/dist/modules/serial-module.d.ts.map +1 -0
  216. package/dist/modules/serial-module.js +47 -0
  217. package/dist/modules/serial-module.js.map +1 -0
  218. package/dist/modules/types.d.ts +39 -0
  219. package/dist/modules/types.d.ts.map +1 -0
  220. package/dist/modules/types.js +2 -0
  221. package/dist/modules/types.js.map +1 -0
  222. package/dist/ollama-probe.d.ts +13 -0
  223. package/dist/ollama-probe.d.ts.map +1 -0
  224. package/dist/ollama-probe.js +46 -0
  225. package/dist/ollama-probe.js.map +1 -0
  226. package/dist/opencode-client.d.ts +126 -0
  227. package/dist/opencode-client.d.ts.map +1 -0
  228. package/dist/opencode-client.js +181 -0
  229. package/dist/opencode-client.js.map +1 -0
  230. package/dist/output-parser.d.ts +3 -0
  231. package/dist/output-parser.d.ts.map +1 -1
  232. package/dist/output-parser.js +29 -3
  233. package/dist/output-parser.js.map +1 -1
  234. package/dist/passive-observer.d.ts +38 -0
  235. package/dist/passive-observer.d.ts.map +1 -0
  236. package/dist/passive-observer.js +632 -0
  237. package/dist/passive-observer.js.map +1 -0
  238. package/dist/pixoo/pixoo-bridge.d.ts +50 -0
  239. package/dist/pixoo/pixoo-bridge.d.ts.map +1 -0
  240. package/dist/pixoo/pixoo-bridge.js +280 -0
  241. package/dist/pixoo/pixoo-bridge.js.map +1 -0
  242. package/dist/pixoo/pixoo-camera.d.ts +80 -0
  243. package/dist/pixoo/pixoo-camera.d.ts.map +1 -0
  244. package/dist/pixoo/pixoo-camera.js +271 -0
  245. package/dist/pixoo/pixoo-camera.js.map +1 -0
  246. package/dist/pixoo/pixoo-client.d.ts +67 -0
  247. package/dist/pixoo/pixoo-client.d.ts.map +1 -0
  248. package/dist/pixoo/pixoo-client.js +335 -0
  249. package/dist/pixoo/pixoo-client.js.map +1 -0
  250. package/dist/pixoo/pixoo-font.d.ts +24 -0
  251. package/dist/pixoo/pixoo-font.d.ts.map +1 -0
  252. package/dist/pixoo/pixoo-font.js +121 -0
  253. package/dist/pixoo/pixoo-font.js.map +1 -0
  254. package/dist/pixoo/pixoo-renderer.d.ts +32 -0
  255. package/dist/pixoo/pixoo-renderer.d.ts.map +1 -0
  256. package/dist/pixoo/pixoo-renderer.js +696 -0
  257. package/dist/pixoo/pixoo-renderer.js.map +1 -0
  258. package/dist/pixoo/pixoo-settings.d.ts +10 -0
  259. package/dist/pixoo/pixoo-settings.d.ts.map +1 -0
  260. package/dist/pixoo/pixoo-settings.js +47 -0
  261. package/dist/pixoo/pixoo-settings.js.map +1 -0
  262. package/dist/pixoo/pixoo-sprites.d.ts +187 -0
  263. package/dist/pixoo/pixoo-sprites.d.ts.map +1 -0
  264. package/dist/pixoo/pixoo-sprites.js +926 -0
  265. package/dist/pixoo/pixoo-sprites.js.map +1 -0
  266. package/dist/pty-manager.d.ts +1 -1
  267. package/dist/pty-manager.d.ts.map +1 -1
  268. package/dist/pty-manager.js +36 -15
  269. package/dist/pty-manager.js.map +1 -1
  270. package/dist/session-aggregator.d.ts +41 -0
  271. package/dist/session-aggregator.d.ts.map +1 -0
  272. package/dist/session-aggregator.js +76 -0
  273. package/dist/session-aggregator.js.map +1 -0
  274. package/dist/session-focus-relay.d.ts +35 -0
  275. package/dist/session-focus-relay.d.ts.map +1 -0
  276. package/dist/session-focus-relay.js +127 -0
  277. package/dist/session-focus-relay.js.map +1 -0
  278. package/dist/session-registry.d.ts +88 -1
  279. package/dist/session-registry.d.ts.map +1 -1
  280. package/dist/session-registry.js +222 -15
  281. package/dist/session-registry.js.map +1 -1
  282. package/dist/session-timeline-relay.d.ts +27 -0
  283. package/dist/session-timeline-relay.d.ts.map +1 -0
  284. package/dist/session-timeline-relay.js +124 -0
  285. package/dist/session-timeline-relay.js.map +1 -0
  286. package/dist/state-machine.d.ts +1 -0
  287. package/dist/state-machine.d.ts.map +1 -1
  288. package/dist/state-machine.js +14 -0
  289. package/dist/state-machine.js.map +1 -1
  290. package/dist/terminal-status.d.ts +28 -0
  291. package/dist/terminal-status.d.ts.map +1 -0
  292. package/dist/terminal-status.js +152 -0
  293. package/dist/terminal-status.js.map +1 -0
  294. package/dist/timeline-store.d.ts +22 -0
  295. package/dist/timeline-store.d.ts.map +1 -0
  296. package/dist/timeline-store.js +79 -0
  297. package/dist/timeline-store.js.map +1 -0
  298. package/dist/timeline-summarizer.d.ts +14 -0
  299. package/dist/timeline-summarizer.d.ts.map +1 -0
  300. package/dist/timeline-summarizer.js +154 -0
  301. package/dist/timeline-summarizer.js.map +1 -0
  302. package/dist/tts.d.ts +25 -0
  303. package/dist/tts.d.ts.map +1 -0
  304. package/dist/tts.js +99 -0
  305. package/dist/tts.js.map +1 -0
  306. package/dist/tui/ansi.d.ts +92 -0
  307. package/dist/tui/ansi.d.ts.map +1 -0
  308. package/dist/tui/ansi.js +181 -0
  309. package/dist/tui/ansi.js.map +1 -0
  310. package/dist/tui/dashboard.d.ts +35 -0
  311. package/dist/tui/dashboard.d.ts.map +1 -0
  312. package/dist/tui/dashboard.js +436 -0
  313. package/dist/tui/dashboard.js.map +1 -0
  314. package/dist/tui/gauge.d.ts +15 -0
  315. package/dist/tui/gauge.d.ts.map +1 -0
  316. package/dist/tui/gauge.js +82 -0
  317. package/dist/tui/gauge.js.map +1 -0
  318. package/dist/tui/renderer.d.ts +15 -0
  319. package/dist/tui/renderer.d.ts.map +1 -0
  320. package/dist/tui/renderer.js +758 -0
  321. package/dist/tui/renderer.js.map +1 -0
  322. package/dist/tui/screen.d.ts +27 -0
  323. package/dist/tui/screen.d.ts.map +1 -0
  324. package/dist/tui/screen.js +109 -0
  325. package/dist/tui/screen.js.map +1 -0
  326. package/dist/tui/terrarium.d.ts +96 -0
  327. package/dist/tui/terrarium.d.ts.map +1 -0
  328. package/dist/tui/terrarium.js +912 -0
  329. package/dist/tui/terrarium.js.map +1 -0
  330. package/dist/types.d.ts +4 -2
  331. package/dist/types.d.ts.map +1 -1
  332. package/dist/types.js +3 -2
  333. package/dist/types.js.map +1 -1
  334. package/dist/usage-api.d.ts +9 -2
  335. package/dist/usage-api.d.ts.map +1 -1
  336. package/dist/usage-api.js +224 -42
  337. package/dist/usage-api.js.map +1 -1
  338. package/dist/usage-event.d.ts +12 -0
  339. package/dist/usage-event.d.ts.map +1 -0
  340. package/dist/usage-event.js +78 -0
  341. package/dist/usage-event.js.map +1 -0
  342. package/dist/usage-tracker.d.ts +4 -0
  343. package/dist/usage-tracker.d.ts.map +1 -1
  344. package/dist/usage-tracker.js +17 -0
  345. package/dist/usage-tracker.js.map +1 -1
  346. package/dist/utility-proxy.d.ts +30 -0
  347. package/dist/utility-proxy.d.ts.map +1 -0
  348. package/dist/utility-proxy.js +217 -0
  349. package/dist/utility-proxy.js.map +1 -0
  350. package/dist/utils/project-name.d.ts +22 -0
  351. package/dist/utils/project-name.d.ts.map +1 -0
  352. package/dist/utils/project-name.js +72 -0
  353. package/dist/utils/project-name.js.map +1 -0
  354. package/dist/version-check.d.ts +26 -0
  355. package/dist/version-check.d.ts.map +1 -0
  356. package/dist/version-check.js +225 -0
  357. package/dist/version-check.js.map +1 -0
  358. package/dist/voice-assistant.d.ts +63 -0
  359. package/dist/voice-assistant.d.ts.map +1 -0
  360. package/dist/voice-assistant.js +406 -0
  361. package/dist/voice-assistant.js.map +1 -0
  362. package/dist/voice.d.ts +2 -0
  363. package/dist/voice.d.ts.map +1 -1
  364. package/dist/voice.js +26 -1
  365. package/dist/voice.js.map +1 -1
  366. package/dist/wake-word.d.ts +35 -0
  367. package/dist/wake-word.d.ts.map +1 -0
  368. package/dist/wake-word.js +225 -0
  369. package/dist/wake-word.js.map +1 -0
  370. package/dist/whisper-server-manager.d.ts +1 -1
  371. package/dist/whisper-server-manager.js +1 -1
  372. package/dist/wifi-config.d.ts +24 -0
  373. package/dist/wifi-config.d.ts.map +1 -0
  374. package/dist/wifi-config.js +84 -0
  375. package/dist/wifi-config.js.map +1 -0
  376. package/dist/ws-server.d.ts +12 -0
  377. package/dist/ws-server.d.ts.map +1 -1
  378. package/dist/ws-server.js +81 -3
  379. package/dist/ws-server.js.map +1 -1
  380. package/package.json +22 -13
@@ -0,0 +1,659 @@
1
+ /**
2
+ * APME Collector — the ingestion boundary.
3
+ *
4
+ * Responsibilities:
5
+ * - openRun(session): create a `runs` row when a bridge session starts
6
+ * - ingestHook(evt): write a `steps` row for every hook POST
7
+ * - updateUsage(snapshot): keep token/cost columns in sync
8
+ * - closeRun(session, exitCode): finalize the row, capture git SHA, enqueue eval
9
+ *
10
+ * The collector is lazy — it's created once at daemon/bridge startup and gated on
11
+ * `store.enabled`. All methods are no-ops if the store failed to initialize
12
+ * (e.g. better-sqlite3 missing), so the rest of the bridge never needs to care.
13
+ */
14
+ import { randomUUID, createHash } from 'crypto';
15
+ import { execSync } from 'child_process';
16
+ import { join } from 'path';
17
+ import { mkdirSync, writeFileSync } from 'fs';
18
+ import { homedir } from 'os';
19
+ import { debug } from '../logger.js';
20
+ import { resolveProjectName } from '../utils/project-name.js';
21
+ import { classifyRunSmart } from './classifier.js';
22
+ export class ApmeCollector {
23
+ store;
24
+ hwSampler;
25
+ sessionToRun = new Map(); // sessionId → runId
26
+ sessionToTurn = new Map(); // sessionId → current turn
27
+ sessionToLastTurnId = new Map(); // survives closeTurn()
28
+ sessionToTask = new Map(); // sessionId → current task
29
+ runTaskCount = new Map(); // runId → next task_index
30
+ /** Optional listener fired after `closeTask` persists the row. The runner
31
+ * wires this to enqueue a task-level judge call. */
32
+ onTaskClosed = null;
33
+ constructor(store, hwSampler) {
34
+ this.store = store;
35
+ this.hwSampler = hwSampler;
36
+ }
37
+ /** Start a new run and return its id. Safe to call if store disabled (returns ''). */
38
+ openRun(input) {
39
+ if (!this.store.enabled)
40
+ return '';
41
+ const runId = randomUUID();
42
+ const gitBefore = readGitHead(input.projectPath);
43
+ const row = {
44
+ id: runId,
45
+ sessionId: input.sessionId,
46
+ agentType: input.agentType,
47
+ modelId: input.modelId ?? null,
48
+ projectName: input.projectName ?? (input.projectPath ? resolveProjectName({ cwd: input.projectPath }) : null),
49
+ projectPath: input.projectPath ?? null,
50
+ taskPrompt: input.taskPrompt ?? null,
51
+ startedAt: Date.now(),
52
+ gitBefore,
53
+ };
54
+ try {
55
+ this.store.insertRun(row);
56
+ this.sessionToRun.set(input.sessionId, runId);
57
+ debug('APME', `openRun ${runId} session=${input.sessionId} agent=${input.agentType} model=${input.modelId ?? '-'}`);
58
+ }
59
+ catch (err) {
60
+ debug('APME', `openRun failed: ${String(err)}`);
61
+ }
62
+ return runId;
63
+ }
64
+ /** Record a hook event as a step + manage turn lifecycle. */
65
+ ingestHook(sessionId, event, data) {
66
+ if (!this.store.enabled)
67
+ return;
68
+ const runId = this.sessionToRun.get(sessionId);
69
+ if (!runId)
70
+ return;
71
+ const toolName = typeof data.tool_name === 'string' ? data.tool_name : null;
72
+ // ── Turn management ──
73
+ if (event === 'UserPromptSubmit') {
74
+ // Claude Code sends { message: { content: "..." } }, legacy sends { prompt: "..." }
75
+ const rawPrompt = typeof data.prompt === 'string' ? data.prompt
76
+ : (typeof data.message === 'object'
77
+ ? data.message?.content
78
+ : undefined);
79
+ const prompt = typeof rawPrompt === 'string' ? rawPrompt.slice(0, 8_000) : null;
80
+ // Close previous turn if open — read index BEFORE delete
81
+ const prevIndex = this.sessionToTurn.get(sessionId)?.index ?? -1;
82
+ this.closeTurn(sessionId);
83
+ // Open new turn
84
+ const turnIndex = prevIndex + 1;
85
+ const run = this.store.getRun(runId);
86
+ const projectPath = run?.projectPath ?? undefined;
87
+ const turnId = randomUUID();
88
+ const turn = {
89
+ id: turnId, runId, index: turnIndex,
90
+ startedAt: Date.now(), toolCalls: 0,
91
+ filesModified: 0, filesCreated: 0,
92
+ gitBefore: readGitHead(projectPath),
93
+ };
94
+ this.sessionToTurn.set(sessionId, turn);
95
+ // Ensure an active task exists so this turn can attach to it. Tasks group
96
+ // consecutive turns between boundary signals (TodoWrite all-completed,
97
+ // /clear, session_end). First turn in a run opens task 0.
98
+ const task = this.openTaskIfNone(sessionId, runId);
99
+ if (task) {
100
+ if (task.firstTurnIndex === null)
101
+ task.firstTurnIndex = turnIndex;
102
+ task.lastTurnIndex = turnIndex;
103
+ }
104
+ try {
105
+ this.store.insertTurn({
106
+ id: turnId, runId, taskId: task?.id ?? null, turnIndex,
107
+ prompt: prompt ?? undefined,
108
+ startedAt: turn.startedAt, gitBefore: turn.gitBefore ?? undefined,
109
+ });
110
+ }
111
+ catch (err) {
112
+ debug('APME', `insertTurn failed: ${String(err)}`);
113
+ }
114
+ // Also set run's task_prompt from first prompt
115
+ try {
116
+ if (run && !run.taskPrompt && prompt) {
117
+ this.store.updateRun(runId, { taskPrompt: prompt });
118
+ }
119
+ }
120
+ catch { /* ignore */ }
121
+ }
122
+ // Track tool calls on the active turn
123
+ const activeTurn = this.sessionToTurn.get(sessionId);
124
+ if (activeTurn && (event === 'PreToolUse' || event === 'tool_start')) {
125
+ activeTurn.toolCalls++;
126
+ if (toolName === 'Edit')
127
+ activeTurn.filesModified++;
128
+ if (toolName === 'Write')
129
+ activeTurn.filesCreated++;
130
+ }
131
+ // ── Task boundary: TodoWrite all-completed ──
132
+ // Claude Code's TodoWrite PostToolUse payload contains tool_input.todos.
133
+ // When every todo status is "completed", treat it as the agent declaring
134
+ // the task finished. Close the current task; the next UserPromptSubmit
135
+ // opens a fresh one.
136
+ if (event === 'PostToolUse' && toolName === 'TodoWrite') {
137
+ const todos = extractTodos(data);
138
+ if (todos && todos.length > 0 && todos.every((t) => t.status === 'completed')) {
139
+ this.closeTask(sessionId, 'todo_complete');
140
+ }
141
+ }
142
+ // Record step
143
+ try {
144
+ this.store.insertStep({
145
+ runId, ts: Date.now(), kind: event,
146
+ toolName, payload: safeStringify(data),
147
+ });
148
+ }
149
+ catch (err) {
150
+ debug('APME', `ingestHook failed: ${String(err)}`);
151
+ }
152
+ }
153
+ /** Close the current turn for a session (called on new prompt or session end). */
154
+ closeTurn(sessionId) {
155
+ const turn = this.sessionToTurn.get(sessionId);
156
+ if (!turn)
157
+ return;
158
+ this.sessionToLastTurnId.set(sessionId, turn.id);
159
+ this.sessionToTurn.delete(sessionId);
160
+ const run = this.store.getRun(turn.runId);
161
+ const projectPath = run?.projectPath ?? undefined;
162
+ const gitAfter = readGitHead(projectPath);
163
+ try {
164
+ this.store.updateTurn(turn.id, {
165
+ endedAt: Date.now(),
166
+ toolCalls: turn.toolCalls,
167
+ filesModified: turn.filesModified,
168
+ filesCreated: turn.filesCreated,
169
+ gitAfter,
170
+ });
171
+ debug('APME', `closeTurn ${turn.id.slice(0, 8)} index=${turn.index} tools=${turn.toolCalls}`);
172
+ }
173
+ catch (err) {
174
+ debug('APME', `closeTurn failed: ${String(err)}`);
175
+ }
176
+ }
177
+ /** Get the current active turn ID for a session (if any). */
178
+ getActiveTurnId(sessionId) {
179
+ return this.sessionToTurn.get(sessionId)?.id ?? null;
180
+ }
181
+ /** Get the current active task ID for a session (if any). Exposed for tests. */
182
+ getActiveTaskId(sessionId) {
183
+ return this.sessionToTask.get(sessionId)?.id ?? null;
184
+ }
185
+ /** Open a new task if none is active for this session. Returns the active
186
+ * task (new or existing), or null if no run is open. Idempotent: repeat
187
+ * calls while a task is already active are no-ops. */
188
+ openTaskIfNone(sessionId, runId) {
189
+ const existing = this.sessionToTask.get(sessionId);
190
+ if (existing)
191
+ return existing;
192
+ const nextIndex = this.runTaskCount.get(runId) ?? 0;
193
+ this.runTaskCount.set(runId, nextIndex + 1);
194
+ const task = {
195
+ id: randomUUID(),
196
+ runId,
197
+ index: nextIndex,
198
+ startedAt: Date.now(),
199
+ firstTurnIndex: null,
200
+ lastTurnIndex: null,
201
+ };
202
+ this.sessionToTask.set(sessionId, task);
203
+ try {
204
+ this.store.insertTask({
205
+ id: task.id,
206
+ runId,
207
+ taskIndex: task.index,
208
+ boundarySignal: 'open',
209
+ startedAt: task.startedAt,
210
+ });
211
+ }
212
+ catch (err) {
213
+ debug('APME', `insertTask failed: ${String(err)}`);
214
+ }
215
+ return task;
216
+ }
217
+ /** Close the current task for a session, persisting boundary metadata.
218
+ * No-op if no task is active. Fires `onTaskClosed` so the runner can
219
+ * enqueue a task-level judge call. Tasks that never saw a turn
220
+ * (firstTurnIndex === null) are deleted rather than left as noise. */
221
+ closeTask(sessionId, boundarySignal) {
222
+ const task = this.sessionToTask.get(sessionId);
223
+ if (!task)
224
+ return;
225
+ this.sessionToTask.delete(sessionId);
226
+ // Empty task: no turns ever attached. Drop the row so the dashboard
227
+ // doesn't show phantom entries from back-to-back boundary signals.
228
+ if (task.firstTurnIndex === null) {
229
+ try {
230
+ // Direct delete — no DAO for it; tasks FK is ON DELETE CASCADE.
231
+ // We reach through the store via a raw statement to avoid adding
232
+ // a dedicated method for this edge case.
233
+ this.store.db
234
+ ?.prepare('DELETE FROM tasks WHERE id = ?').run(task.id);
235
+ }
236
+ catch { /* ignore */ }
237
+ debug('APME', `closeTask ${task.id.slice(0, 8)} — empty, dropped`);
238
+ return;
239
+ }
240
+ // Derive task category from the run (best-effort). Turn-level categories
241
+ // may diverge, but for now tasks inherit from the run.
242
+ const run = this.store.getRun(task.runId);
243
+ const taskCategory = run?.taskCategory ?? null;
244
+ try {
245
+ this.store.updateTask(task.id, {
246
+ endedAt: Date.now(),
247
+ lastTurnIndex: task.lastTurnIndex ?? task.firstTurnIndex,
248
+ boundarySignal,
249
+ taskCategory,
250
+ });
251
+ }
252
+ catch (err) {
253
+ debug('APME', `updateTask failed: ${String(err)}`);
254
+ }
255
+ debug('APME', `closeTask ${task.id.slice(0, 8)} signal=${boundarySignal} turns=${task.firstTurnIndex}..${task.lastTurnIndex}`);
256
+ // Notify listeners (runner enqueueTask is wired via apme/index.ts).
257
+ if (this.onTaskClosed) {
258
+ try {
259
+ this.onTaskClosed({
260
+ taskId: task.id,
261
+ runId: task.runId,
262
+ boundarySignal,
263
+ taskCategory,
264
+ });
265
+ }
266
+ catch (err) {
267
+ debug('APME', `onTaskClosed listener threw: ${String(err)}`);
268
+ }
269
+ }
270
+ }
271
+ /** Get the current run ID for a session (if any). */
272
+ getRunId(sessionId) {
273
+ return this.sessionToRun.get(sessionId) ?? null;
274
+ }
275
+ /** Store Claude's response text on the current turn.
276
+ * Falls back to the last closed turn if closeTurn() already ran (race with session exit).
277
+ * Tags turns.efficiency_json.response_kind so the runner can skip tool-only / empty
278
+ * turns — judging silence produces noise scores. */
279
+ setTurnResponse(sessionId, response) {
280
+ if (!this.store.enabled)
281
+ return;
282
+ const turn = this.sessionToTurn.get(sessionId);
283
+ const turnId = turn?.id ?? this.sessionToLastTurnId.get(sessionId);
284
+ debug('APME', `setTurnResponse session=${sessionId.slice(0, 8)} turnId=${turnId?.slice(0, 8) ?? 'null'} respLen=${response.length}`);
285
+ if (!turnId)
286
+ return;
287
+ const trimmedLen = response.trim().length;
288
+ // Active-turn toolCalls is authoritative; after closeTurn() the counter is
289
+ // already flushed to the DB row, so we fetch from there instead.
290
+ const toolCalls = turn?.toolCalls
291
+ ?? (this.store.getTurn(turnId)?.tool_calls ?? 0);
292
+ const kind = trimmedLen >= 1
293
+ ? 'text'
294
+ : (toolCalls > 0 ? 'tool_only' : 'empty');
295
+ const efficiencyJson = mergeEfficiencyJson(this.store.getTurn(turnId), { response_kind: kind });
296
+ try {
297
+ this.store.updateTurn(turnId, {
298
+ response: response.slice(0, 10_000),
299
+ efficiencyJson,
300
+ });
301
+ }
302
+ catch (err) {
303
+ debug('APME', `setTurnResponse failed: ${String(err)}`);
304
+ }
305
+ }
306
+ /** Apply response to the last closed turn if it has no response yet.
307
+ * Used as fallback when Stop hook doesn't fire (PTY output capture). */
308
+ setLastClosedTurnResponse(sessionId, response) {
309
+ if (!this.store.enabled)
310
+ return;
311
+ const turnId = this.sessionToLastTurnId.get(sessionId);
312
+ if (!turnId)
313
+ return;
314
+ const existing = this.store.getTurn(turnId);
315
+ if (existing?.response)
316
+ return;
317
+ const trimmedLen = response.trim().length;
318
+ const toolCalls = existing?.tool_calls ?? 0;
319
+ const kind = trimmedLen >= 1
320
+ ? 'text'
321
+ : (toolCalls > 0 ? 'tool_only' : 'empty');
322
+ const efficiencyJson = mergeEfficiencyJson(existing, { response_kind: kind });
323
+ try {
324
+ this.store.updateTurn(turnId, {
325
+ response: response.slice(0, 10_000),
326
+ efficiencyJson,
327
+ });
328
+ }
329
+ catch { /* ignore */ }
330
+ }
331
+ /** Single-entrypoint ingestion using the shared TelemetrySpan envelope.
332
+ *
333
+ * Adapters in `bridge/src/apme/adapters/*` translate per-source events
334
+ * (Claude hooks / PTY parser / OpenClaw timeline / Codex) into spans;
335
+ * this method dispatches each span to the appropriate legacy collector
336
+ * method so existing race-handling and step-row insertion logic stays
337
+ * intact. New ingestion paths should prefer this entrypoint over
338
+ * `ingestHook` / `setTurnResponse` directly. */
339
+ ingestSpan(sessionId, span) {
340
+ if (!this.store.enabled)
341
+ return;
342
+ const a = span.attributes;
343
+ switch (span.kind) {
344
+ case 'turn_start': {
345
+ const prompt = a['agentdeck.prompt_text'] ?? '';
346
+ // Reuse the canonical UserPromptSubmit path so step row, prev-turn close,
347
+ // task auto-open, and run.task_prompt seeding all behave identically.
348
+ this.ingestHook(sessionId, 'UserPromptSubmit', { message: { content: prompt } });
349
+ return;
350
+ }
351
+ case 'turn_response': {
352
+ const text = a['agentdeck.response_text'] ?? '';
353
+ const fallback = a['agentdeck.fallback_to_last_closed'] === true;
354
+ if (fallback)
355
+ this.setLastClosedTurnResponse(sessionId, text);
356
+ else
357
+ this.setTurnResponse(sessionId, text);
358
+ return;
359
+ }
360
+ case 'turn_end':
361
+ // No-op: turns auto-close on the next `turn_start` or session end.
362
+ return;
363
+ case 'tool_call': {
364
+ const toolName = (a['gen_ai.tool.name'] ?? a['agentdeck.tool_name']);
365
+ const raw = a['agentdeck.raw_payload'] ?? {};
366
+ this.ingestHook(sessionId, 'PreToolUse', { tool_name: toolName, ...raw });
367
+ return;
368
+ }
369
+ case 'tool_result': {
370
+ const toolName = (a['gen_ai.tool.name'] ?? a['agentdeck.tool_name']);
371
+ const raw = a['agentdeck.raw_payload'] ?? {};
372
+ // PostToolUse + TodoWrite all-completed → existing ingestHook path
373
+ // detects todo_complete boundary automatically, so adapters don't
374
+ // need to emit a separate task_boundary span for that case.
375
+ this.ingestHook(sessionId, 'PostToolUse', { tool_name: toolName, ...raw });
376
+ return;
377
+ }
378
+ case 'task_boundary': {
379
+ const signal = a['agentdeck.boundary_signal'];
380
+ if (signal === 'clear') {
381
+ this.splitRun(sessionId, a['agentdeck.cwd']);
382
+ return;
383
+ }
384
+ // Other boundary signals (todo_complete, session_end) are handled
385
+ // automatically: tool_result detects todo_complete, closeRun closes
386
+ // the current task with session_end. Adapters that emit those
387
+ // spans explicitly are no-ops here by design — preserving the
388
+ // single-source-of-truth for those transitions.
389
+ return;
390
+ }
391
+ case 'session_meta': {
392
+ const model = a['gen_ai.request.model'];
393
+ if (model)
394
+ this.updateModel(sessionId, model);
395
+ const inputTokens = a['agentdeck.usage.input_tokens'];
396
+ const outputTokens = a['agentdeck.usage.output_tokens'];
397
+ const costUsd = a['agentdeck.usage.cost_usd'];
398
+ if (inputTokens !== undefined || outputTokens !== undefined || costUsd !== undefined) {
399
+ // Synthesize a UsageSnapshot-compatible shape. Missing fields stay null.
400
+ this.updateUsage(sessionId, {
401
+ inputTokens: inputTokens ?? 0,
402
+ outputTokens: outputTokens ?? 0,
403
+ estimatedCostUsd: costUsd ?? null,
404
+ });
405
+ }
406
+ return;
407
+ }
408
+ case 'raw_step': {
409
+ const event = a['agentdeck.raw_event'] ?? 'raw';
410
+ const payload = a['agentdeck.raw_payload'] ?? {};
411
+ this.ingestHook(sessionId, event, payload);
412
+ return;
413
+ }
414
+ }
415
+ }
416
+ /** Ingest a generic timeline-style event (non-hook). */
417
+ ingestStep(sessionId, kind, payload, toolName) {
418
+ if (!this.store.enabled)
419
+ return;
420
+ const runId = this.sessionToRun.get(sessionId);
421
+ if (!runId)
422
+ return;
423
+ try {
424
+ this.store.insertStep({
425
+ runId, ts: Date.now(), kind,
426
+ toolName: toolName ?? null,
427
+ payload: safeStringify(payload),
428
+ });
429
+ }
430
+ catch { /* ignore */ }
431
+ }
432
+ /** Update token / cost columns from the bridge's UsageTracker snapshot. */
433
+ updateUsage(sessionId, snapshot) {
434
+ if (!this.store.enabled)
435
+ return;
436
+ const runId = this.sessionToRun.get(sessionId);
437
+ if (!runId)
438
+ return;
439
+ try {
440
+ this.store.updateRun(runId, {
441
+ inputTokens: snapshot.inputTokens,
442
+ outputTokens: snapshot.outputTokens,
443
+ costUsd: snapshot.estimatedCostUsd ?? snapshot.costSpent ?? null,
444
+ });
445
+ }
446
+ catch { /* ignore */ }
447
+ }
448
+ /** Split the current run — closes the active run and opens a fresh one.
449
+ * Triggered on `/clear` or other context-reset events so each logical
450
+ * conversation gets its own evaluation unit. */
451
+ splitRun(sessionId, projectPath) {
452
+ if (!this.store.enabled)
453
+ return null;
454
+ const runId = this.sessionToRun.get(sessionId);
455
+ if (!runId)
456
+ return null;
457
+ const run = this.store.getRun(runId);
458
+ if (!run)
459
+ return null;
460
+ // The active task (if any) belongs to the run being closed; mark it as
461
+ // boundary=clear before closeRun tears everything down.
462
+ this.closeTask(sessionId, 'clear');
463
+ // Close current run (no exitCode — session is still alive)
464
+ this.closeRun(sessionId, undefined, projectPath);
465
+ // Open a new run with the same session parameters
466
+ return this.openRun({
467
+ sessionId,
468
+ agentType: run.agentType,
469
+ modelId: run.modelId ?? undefined,
470
+ projectName: run.projectName ?? undefined,
471
+ projectPath: run.projectPath ?? undefined,
472
+ });
473
+ }
474
+ /** Update model id when the bridge resolves which model is in use. */
475
+ updateModel(sessionId, modelId) {
476
+ if (!this.store.enabled || !modelId)
477
+ return;
478
+ const runId = this.sessionToRun.get(sessionId);
479
+ if (!runId)
480
+ return;
481
+ try {
482
+ this.store.updateRun(runId, { modelId });
483
+ }
484
+ catch { /* ignore */ }
485
+ }
486
+ /** Finalize a run. Returns the runId so callers can enqueue evaluation.
487
+ * Empty runs (no prompts, no steps, no turns) are deleted — they're just
488
+ * connection noise and clutter the dashboard. */
489
+ closeRun(sessionId, exitCode, projectPath) {
490
+ if (!this.store.enabled)
491
+ return null;
492
+ const runId = this.sessionToRun.get(sessionId);
493
+ if (!runId)
494
+ return null;
495
+ // Close the last open turn + task before finalizing the run.
496
+ this.closeTurn(sessionId);
497
+ // splitRun already called closeTask('clear') before us, so this is usually
498
+ // a no-op in the split path. Direct closeRun (session exit) still needs it.
499
+ this.closeTask(sessionId, 'session_end');
500
+ this.sessionToRun.delete(sessionId);
501
+ this.sessionToLastTurnId.delete(sessionId);
502
+ this.runTaskCount.delete(runId);
503
+ // Mark empty runs so the dashboard can filter them out.
504
+ // Don't delete — FK constraints and concurrent access make deletion risky.
505
+ const run = this.store.getRun(runId);
506
+ const steps = this.store.listSteps(runId);
507
+ const meaningfulSteps = steps.filter(s => s.kind !== 'SessionEnd' && s.kind !== 'session_end' && s.kind !== 'session_start' && s.kind !== 'SessionStart');
508
+ const isEmpty = !run?.taskPrompt && meaningfulSteps.length === 0;
509
+ const gitAfter = readGitHead(projectPath);
510
+ try {
511
+ this.store.updateRun(runId, {
512
+ endedAt: Date.now(),
513
+ exitCode: exitCode ?? null,
514
+ gitAfter,
515
+ // Tag empty runs so dashboard can filter them
516
+ ...(isEmpty ? { taskCategory: '_empty' } : {}),
517
+ });
518
+ if (isEmpty) {
519
+ debug('APME', `closeRun ${runId} — empty (no prompt, no steps)`);
520
+ return runId;
521
+ }
522
+ debug('APME', `closeRun ${runId} exit=${exitCode ?? '-'} gitAfter=${gitAfter ?? '-'}`);
523
+ }
524
+ catch (err) {
525
+ debug('APME', `closeRun failed: ${String(err)}`);
526
+ }
527
+ // Capture hardware profile asynchronously — don't block shutdown.
528
+ if (this.hwSampler) {
529
+ this.hwSampler.snapshot().then((snap) => {
530
+ try {
531
+ this.store.updateRun(runId, { hwProfile: JSON.stringify(snap) });
532
+ }
533
+ catch { /* ignore */ }
534
+ }).catch(() => { });
535
+ }
536
+ // Classify the run — rule-based first, LLM fallback if unknown.
537
+ // Fire-and-forget since classifyRunSmart is async (LLM call).
538
+ void classifyRunSmart(this.store, runId).then(({ signals, category, source }) => {
539
+ this.store.updateRun(runId, {
540
+ taskSignals: JSON.stringify(signals),
541
+ taskCategory: category,
542
+ taskCategorySource: source,
543
+ });
544
+ debug('APME', `classified ${runId} as ${category} (${source})`);
545
+ }).catch((err) => {
546
+ debug('APME', `classify failed: ${String(err)}`);
547
+ });
548
+ // Save git diff as artifact (best-effort, capped at 1MB).
549
+ this.saveDiffArtifact(runId, projectPath);
550
+ return runId;
551
+ }
552
+ /** Save the git diff produced by this run as an artifact file. */
553
+ saveDiffArtifact(runId, projectPath) {
554
+ if (!projectPath)
555
+ return;
556
+ try {
557
+ const run = this.store.getRun(runId);
558
+ if (!run)
559
+ return;
560
+ const args = run.gitBefore && run.gitAfter && run.gitBefore !== run.gitAfter
561
+ ? `diff ${run.gitBefore}..${run.gitAfter}`
562
+ : 'diff HEAD';
563
+ const diff = execSync(`git ${args}`, {
564
+ cwd: projectPath, encoding: 'utf-8', timeout: 5000,
565
+ maxBuffer: 1024 * 1024, stdio: ['ignore', 'pipe', 'ignore'],
566
+ });
567
+ if (!diff || diff.length < 10)
568
+ return;
569
+ const hash = createHash('sha256').update(diff).digest('hex').slice(0, 16);
570
+ const artifactDir = getArtifactDir(runId);
571
+ mkdirSync(artifactDir, { recursive: true });
572
+ const filePath = join(artifactDir, `${hash}.diff`);
573
+ writeFileSync(filePath, diff, 'utf-8');
574
+ this.store.insertArtifact({
575
+ runId, kind: 'diff', path: filePath,
576
+ sha256: createHash('sha256').update(diff).digest('hex'),
577
+ bytes: Buffer.byteLength(diff, 'utf-8'),
578
+ });
579
+ }
580
+ catch { /* ignore — artifact storage is best-effort */ }
581
+ }
582
+ /** Translate a `SessionEntry` + optional extras into an OpenRunInput. */
583
+ static fromSessionEntry(entry, extras = {}) {
584
+ return {
585
+ sessionId: entry.id,
586
+ agentType: (entry.agentType ?? 'claude-code'),
587
+ modelId: extras.modelId,
588
+ projectName: entry.projectName,
589
+ projectPath: extras.projectPath,
590
+ };
591
+ }
592
+ }
593
+ /** Extract the todos array from a TodoWrite PostToolUse payload. Returns null
594
+ * if the shape doesn't match (payload malformed, older CC versions). Accepts
595
+ * `tool_input.todos` (hook standard) and `todos` (legacy flat shape). */
596
+ function extractTodos(data) {
597
+ const fromToolInput = data.tool_input?.todos;
598
+ const fromFlat = data.todos;
599
+ const raw = fromToolInput ?? fromFlat;
600
+ if (!Array.isArray(raw))
601
+ return null;
602
+ const items = [];
603
+ for (const entry of raw) {
604
+ if (!entry || typeof entry !== 'object')
605
+ continue;
606
+ const e = entry;
607
+ const status = typeof e.status === 'string' ? e.status : '';
608
+ if (!status)
609
+ continue;
610
+ items.push({
611
+ status,
612
+ content: typeof e.content === 'string' ? e.content : undefined,
613
+ activeForm: typeof e.activeForm === 'string' ? e.activeForm : undefined,
614
+ });
615
+ }
616
+ return items;
617
+ }
618
+ function readGitHead(cwd) {
619
+ if (!cwd)
620
+ return null;
621
+ try {
622
+ return execSync('git rev-parse HEAD', {
623
+ cwd, encoding: 'utf-8', stdio: ['ignore', 'pipe', 'ignore'], timeout: 2000,
624
+ }).trim() || null;
625
+ }
626
+ catch {
627
+ return null;
628
+ }
629
+ }
630
+ function getArtifactDir(runId) {
631
+ const dataDir = process.env.AGENTDECK_DATA_DIR || join(homedir(), '.agentdeck');
632
+ return join(dataDir, 'apme', 'artifacts', runId);
633
+ }
634
+ function safeStringify(v) {
635
+ try {
636
+ return JSON.stringify(v);
637
+ }
638
+ catch {
639
+ return '"<unserializable>"';
640
+ }
641
+ }
642
+ /** Merge `patch` into an existing turns.efficiency_json string without losing
643
+ * sibling keys. Returns a JSON string suitable for the column. Unparseable
644
+ * existing values are replaced outright. */
645
+ function mergeEfficiencyJson(turn, patch) {
646
+ let base = {};
647
+ const raw = turn?.efficiency_json;
648
+ if (typeof raw === 'string' && raw.length > 0) {
649
+ try {
650
+ const parsed = JSON.parse(raw);
651
+ if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
652
+ base = parsed;
653
+ }
654
+ }
655
+ catch { /* replace */ }
656
+ }
657
+ return JSON.stringify({ ...base, ...patch });
658
+ }
659
+ //# sourceMappingURL=collector.js.map