@agentdeck/bridge 0.2.2 → 0.2.3

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 (383) hide show
  1. package/assets/fonts/IBMPlexSans-Bold.ttf +0 -0
  2. package/assets/fonts/IBMPlexSans-Regular.ttf +0 -0
  3. package/assets/fonts/JetBrainsMono-Bold.ttf +0 -0
  4. package/assets/fonts/JetBrainsMono-Regular.ttf +0 -0
  5. package/assets/fonts/LICENSES.md +22 -0
  6. package/dist/adapters/claude-code.d.ts +6 -0
  7. package/dist/adapters/claude-code.d.ts.map +1 -1
  8. package/dist/adapters/claude-code.js +8 -0
  9. package/dist/adapters/claude-code.js.map +1 -1
  10. package/dist/adapters/openclaw.d.ts +24 -0
  11. package/dist/adapters/openclaw.d.ts.map +1 -1
  12. package/dist/adapters/openclaw.js +291 -35
  13. package/dist/adapters/openclaw.js.map +1 -1
  14. package/dist/adapters/opencode-adapter.d.ts +59 -0
  15. package/dist/adapters/opencode-adapter.d.ts.map +1 -1
  16. package/dist/adapters/opencode-adapter.js +297 -35
  17. package/dist/adapters/opencode-adapter.js.map +1 -1
  18. package/dist/adb-reverse.d.ts +1 -1
  19. package/dist/adb-reverse.js +1 -1
  20. package/dist/antigravity-local.d.ts +1 -0
  21. package/dist/antigravity-local.d.ts.map +1 -1
  22. package/dist/antigravity-local.js +33 -0
  23. package/dist/antigravity-local.js.map +1 -1
  24. package/dist/apme/adapters/claude-hook.d.ts.map +1 -1
  25. package/dist/apme/adapters/claude-hook.js +5 -0
  26. package/dist/apme/adapters/claude-hook.js.map +1 -1
  27. package/dist/apme/adapters/codex-hook.d.ts +17 -0
  28. package/dist/apme/adapters/codex-hook.d.ts.map +1 -0
  29. package/dist/apme/adapters/codex-hook.js +95 -0
  30. package/dist/apme/adapters/codex-hook.js.map +1 -0
  31. package/dist/apme/adapters/codex-turn-manager.d.ts +86 -0
  32. package/dist/apme/adapters/codex-turn-manager.d.ts.map +1 -0
  33. package/dist/apme/adapters/codex-turn-manager.js +496 -0
  34. package/dist/apme/adapters/codex-turn-manager.js.map +1 -0
  35. package/dist/apme/adapters/openclaw-hook.d.ts +99 -0
  36. package/dist/apme/adapters/openclaw-hook.d.ts.map +1 -0
  37. package/dist/apme/adapters/openclaw-hook.js +234 -0
  38. package/dist/apme/adapters/openclaw-hook.js.map +1 -0
  39. package/dist/apme/adapters/opencode-hook.d.ts +56 -0
  40. package/dist/apme/adapters/opencode-hook.d.ts.map +1 -0
  41. package/dist/apme/adapters/opencode-hook.js +182 -0
  42. package/dist/apme/adapters/opencode-hook.js.map +1 -0
  43. package/dist/apme/adapters/timeline.d.ts.map +1 -1
  44. package/dist/apme/adapters/timeline.js +6 -1
  45. package/dist/apme/adapters/timeline.js.map +1 -1
  46. package/dist/apme/antigravity-transcript.d.ts +18 -0
  47. package/dist/apme/antigravity-transcript.d.ts.map +1 -0
  48. package/dist/apme/antigravity-transcript.js +94 -0
  49. package/dist/apme/antigravity-transcript.js.map +1 -0
  50. package/dist/apme/classify-turn.d.ts +13 -0
  51. package/dist/apme/classify-turn.d.ts.map +1 -0
  52. package/dist/apme/classify-turn.js +49 -0
  53. package/dist/apme/classify-turn.js.map +1 -0
  54. package/dist/apme/claude-transcript-reader.d.ts +10 -0
  55. package/dist/apme/claude-transcript-reader.d.ts.map +1 -1
  56. package/dist/apme/claude-transcript-reader.js +35 -0
  57. package/dist/apme/claude-transcript-reader.js.map +1 -1
  58. package/dist/apme/collector.d.ts +129 -6
  59. package/dist/apme/collector.d.ts.map +1 -1
  60. package/dist/apme/collector.js +470 -23
  61. package/dist/apme/collector.js.map +1 -1
  62. package/dist/apme/dashboard-html.d.ts.map +1 -1
  63. package/dist/apme/dashboard-html.js +87 -11
  64. package/dist/apme/dashboard-html.js.map +1 -1
  65. package/dist/apme/http.d.ts.map +1 -1
  66. package/dist/apme/http.js +43 -6
  67. package/dist/apme/http.js.map +1 -1
  68. package/dist/apme/index.d.ts +26 -5
  69. package/dist/apme/index.d.ts.map +1 -1
  70. package/dist/apme/index.js +135 -9
  71. package/dist/apme/index.js.map +1 -1
  72. package/dist/apme/pareto.d.ts +39 -0
  73. package/dist/apme/pareto.d.ts.map +1 -0
  74. package/dist/apme/pareto.js +59 -0
  75. package/dist/apme/pareto.js.map +1 -0
  76. package/dist/apme/recommend.d.ts +3 -0
  77. package/dist/apme/recommend.d.ts.map +1 -1
  78. package/dist/apme/recommend.js +43 -1
  79. package/dist/apme/recommend.js.map +1 -1
  80. package/dist/apme/runner.d.ts +92 -4
  81. package/dist/apme/runner.d.ts.map +1 -1
  82. package/dist/apme/runner.js +586 -70
  83. package/dist/apme/runner.js.map +1 -1
  84. package/dist/apme/sample-to-timeline.d.ts +26 -0
  85. package/dist/apme/sample-to-timeline.d.ts.map +1 -0
  86. package/dist/apme/sample-to-timeline.js +95 -0
  87. package/dist/apme/sample-to-timeline.js.map +1 -0
  88. package/dist/apme/scorers/index.d.ts +47 -0
  89. package/dist/apme/scorers/index.d.ts.map +1 -0
  90. package/dist/apme/scorers/index.js +96 -0
  91. package/dist/apme/scorers/index.js.map +1 -0
  92. package/dist/apme/settings.d.ts +14 -10
  93. package/dist/apme/settings.d.ts.map +1 -1
  94. package/dist/apme/settings.js +34 -8
  95. package/dist/apme/settings.js.map +1 -1
  96. package/dist/apme/store.d.ts +33 -1
  97. package/dist/apme/store.d.ts.map +1 -1
  98. package/dist/apme/store.js +334 -25
  99. package/dist/apme/store.js.map +1 -1
  100. package/dist/awaiting-overlay.d.ts +99 -0
  101. package/dist/awaiting-overlay.d.ts.map +1 -0
  102. package/dist/awaiting-overlay.js +148 -0
  103. package/dist/awaiting-overlay.js.map +1 -0
  104. package/dist/ble-sync-spawn.d.ts +63 -0
  105. package/dist/ble-sync-spawn.d.ts.map +1 -0
  106. package/dist/ble-sync-spawn.js +196 -0
  107. package/dist/ble-sync-spawn.js.map +1 -0
  108. package/dist/bridge-core.d.ts +24 -1
  109. package/dist/bridge-core.d.ts.map +1 -1
  110. package/dist/bridge-core.js +157 -24
  111. package/dist/bridge-core.js.map +1 -1
  112. package/dist/broadcast.d.ts +3 -0
  113. package/dist/broadcast.d.ts.map +1 -0
  114. package/dist/broadcast.js +122 -0
  115. package/dist/broadcast.js.map +1 -0
  116. package/dist/check-deps.d.ts.map +1 -1
  117. package/dist/check-deps.js +25 -9
  118. package/dist/check-deps.js.map +1 -1
  119. package/dist/cli.d.ts +15 -1
  120. package/dist/cli.d.ts.map +1 -1
  121. package/dist/cli.js +1021 -93
  122. package/dist/cli.js.map +1 -1
  123. package/dist/codex-auth.d.ts +21 -0
  124. package/dist/codex-auth.d.ts.map +1 -1
  125. package/dist/codex-auth.js +52 -1
  126. package/dist/codex-auth.js.map +1 -1
  127. package/dist/codex-output-parser.d.ts +8 -0
  128. package/dist/codex-output-parser.d.ts.map +1 -1
  129. package/dist/codex-output-parser.js +110 -25
  130. package/dist/codex-output-parser.js.map +1 -1
  131. package/dist/codex-rate-limits.d.ts +16 -0
  132. package/dist/codex-rate-limits.d.ts.map +1 -0
  133. package/dist/codex-rate-limits.js +197 -0
  134. package/dist/codex-rate-limits.js.map +1 -0
  135. package/dist/codex-rollout-response.d.ts +31 -0
  136. package/dist/codex-rollout-response.d.ts.map +1 -0
  137. package/dist/codex-rollout-response.js +135 -0
  138. package/dist/codex-rollout-response.js.map +1 -0
  139. package/dist/daemon-server.d.ts +85 -0
  140. package/dist/daemon-server.d.ts.map +1 -1
  141. package/dist/daemon-server.js +1627 -159
  142. package/dist/daemon-server.js.map +1 -1
  143. package/dist/daemon.js +15 -6
  144. package/dist/daemon.js.map +1 -1
  145. package/dist/display-dim.d.ts +9 -0
  146. package/dist/display-dim.d.ts.map +1 -0
  147. package/dist/display-dim.js +53 -0
  148. package/dist/display-dim.js.map +1 -0
  149. package/dist/display-monitor.d.ts +15 -2
  150. package/dist/display-monitor.d.ts.map +1 -1
  151. package/dist/display-monitor.js +104 -26
  152. package/dist/display-monitor.js.map +1 -1
  153. package/dist/esp32-serial.d.ts +160 -1
  154. package/dist/esp32-serial.d.ts.map +1 -1
  155. package/dist/esp32-serial.js +950 -153
  156. package/dist/esp32-serial.js.map +1 -1
  157. package/dist/fallback-task-timeline.d.ts +42 -0
  158. package/dist/fallback-task-timeline.d.ts.map +1 -0
  159. package/dist/fallback-task-timeline.js +112 -0
  160. package/dist/fallback-task-timeline.js.map +1 -0
  161. package/dist/foundation-models-helper.d.ts +11 -0
  162. package/dist/foundation-models-helper.d.ts.map +1 -0
  163. package/dist/foundation-models-helper.js +241 -0
  164. package/dist/foundation-models-helper.js.map +1 -0
  165. package/dist/hook-migration.js +1 -1
  166. package/dist/hook-migration.js.map +1 -1
  167. package/dist/hook-server.d.ts +2 -3
  168. package/dist/hook-server.d.ts.map +1 -1
  169. package/dist/hook-server.js +22 -10
  170. package/dist/hook-server.js.map +1 -1
  171. package/dist/idotmatrix/idotmatrix-daemon-sync.d.ts +33 -0
  172. package/dist/idotmatrix/idotmatrix-daemon-sync.d.ts.map +1 -0
  173. package/dist/idotmatrix/idotmatrix-daemon-sync.js +164 -0
  174. package/dist/idotmatrix/idotmatrix-daemon-sync.js.map +1 -0
  175. package/dist/idotmatrix/idotmatrix-discover.d.ts +18 -0
  176. package/dist/idotmatrix/idotmatrix-discover.d.ts.map +1 -0
  177. package/dist/idotmatrix/idotmatrix-discover.js +85 -0
  178. package/dist/idotmatrix/idotmatrix-discover.js.map +1 -0
  179. package/dist/idotmatrix/idotmatrix-settings.d.ts +20 -0
  180. package/dist/idotmatrix/idotmatrix-settings.d.ts.map +1 -0
  181. package/dist/idotmatrix/idotmatrix-settings.js +57 -0
  182. package/dist/idotmatrix/idotmatrix-settings.js.map +1 -0
  183. package/dist/index.d.ts.map +1 -1
  184. package/dist/index.js +215 -179
  185. package/dist/index.js.map +1 -1
  186. package/dist/log-stream.d.ts +15 -16
  187. package/dist/log-stream.d.ts.map +1 -1
  188. package/dist/log-stream.js +18 -96
  189. package/dist/log-stream.js.map +1 -1
  190. package/dist/logger.d.ts.map +1 -1
  191. package/dist/logger.js +12 -3
  192. package/dist/logger.js.map +1 -1
  193. package/dist/mdns.d.ts +17 -0
  194. package/dist/mdns.d.ts.map +1 -1
  195. package/dist/mdns.js +52 -18
  196. package/dist/mdns.js.map +1 -1
  197. package/dist/modules/adb-module.d.ts +1 -1
  198. package/dist/modules/adb-module.d.ts.map +1 -1
  199. package/dist/modules/adb-module.js +4 -3
  200. package/dist/modules/adb-module.js.map +1 -1
  201. package/dist/modules/broadcast-module.d.ts +17 -0
  202. package/dist/modules/broadcast-module.d.ts.map +1 -0
  203. package/dist/modules/broadcast-module.js +26 -0
  204. package/dist/modules/broadcast-module.js.map +1 -0
  205. package/dist/modules/idotmatrix-module.d.ts +9 -0
  206. package/dist/modules/idotmatrix-module.d.ts.map +1 -0
  207. package/dist/modules/idotmatrix-module.js +46 -0
  208. package/dist/modules/idotmatrix-module.js.map +1 -0
  209. package/dist/modules/index.d.ts +3 -1
  210. package/dist/modules/index.d.ts.map +1 -1
  211. package/dist/modules/index.js +9 -3
  212. package/dist/modules/index.js.map +1 -1
  213. package/dist/modules/pixoo-module.d.ts.map +1 -1
  214. package/dist/modules/pixoo-module.js +14 -4
  215. package/dist/modules/pixoo-module.js.map +1 -1
  216. package/dist/modules/serial-module.d.ts +4 -0
  217. package/dist/modules/serial-module.d.ts.map +1 -1
  218. package/dist/modules/serial-module.js +14 -12
  219. package/dist/modules/serial-module.js.map +1 -1
  220. package/dist/modules/timebox-module.d.ts +9 -0
  221. package/dist/modules/timebox-module.d.ts.map +1 -0
  222. package/dist/modules/timebox-module.js +46 -0
  223. package/dist/modules/timebox-module.js.map +1 -0
  224. package/dist/modules/types.d.ts +4 -2
  225. package/dist/modules/types.d.ts.map +1 -1
  226. package/dist/openclaw-session.d.ts +18 -0
  227. package/dist/openclaw-session.d.ts.map +1 -0
  228. package/dist/openclaw-session.js +34 -0
  229. package/dist/openclaw-session.js.map +1 -0
  230. package/dist/output-parser.d.ts +30 -0
  231. package/dist/output-parser.d.ts.map +1 -1
  232. package/dist/output-parser.js +125 -9
  233. package/dist/output-parser.js.map +1 -1
  234. package/dist/passive-observer.d.ts +26 -0
  235. package/dist/passive-observer.d.ts.map +1 -1
  236. package/dist/passive-observer.js +258 -49
  237. package/dist/passive-observer.js.map +1 -1
  238. package/dist/permission-resolver.d.ts +47 -0
  239. package/dist/permission-resolver.d.ts.map +1 -0
  240. package/dist/permission-resolver.js +103 -0
  241. package/dist/permission-resolver.js.map +1 -0
  242. package/dist/pixoo/micro-glyphs.d.ts +49 -0
  243. package/dist/pixoo/micro-glyphs.d.ts.map +1 -0
  244. package/dist/pixoo/micro-glyphs.js +276 -0
  245. package/dist/pixoo/micro-glyphs.js.map +1 -0
  246. package/dist/pixoo/pixoo-bridge.d.ts +2 -2
  247. package/dist/pixoo/pixoo-bridge.d.ts.map +1 -1
  248. package/dist/pixoo/pixoo-bridge.js +93 -25
  249. package/dist/pixoo/pixoo-bridge.js.map +1 -1
  250. package/dist/pixoo/pixoo-camera.d.ts +19 -3
  251. package/dist/pixoo/pixoo-camera.d.ts.map +1 -1
  252. package/dist/pixoo/pixoo-camera.js +62 -27
  253. package/dist/pixoo/pixoo-camera.js.map +1 -1
  254. package/dist/pixoo/pixoo-client.d.ts +13 -3
  255. package/dist/pixoo/pixoo-client.d.ts.map +1 -1
  256. package/dist/pixoo/pixoo-client.js +51 -22
  257. package/dist/pixoo/pixoo-client.js.map +1 -1
  258. package/dist/pixoo/pixoo-discover.d.ts +37 -0
  259. package/dist/pixoo/pixoo-discover.d.ts.map +1 -0
  260. package/dist/pixoo/pixoo-discover.js +105 -0
  261. package/dist/pixoo/pixoo-discover.js.map +1 -0
  262. package/dist/pixoo/pixoo-renderer.d.ts +8 -3
  263. package/dist/pixoo/pixoo-renderer.d.ts.map +1 -1
  264. package/dist/pixoo/pixoo-renderer.js +189 -60
  265. package/dist/pixoo/pixoo-renderer.js.map +1 -1
  266. package/dist/pixoo/pixoo-settings.d.ts +6 -0
  267. package/dist/pixoo/pixoo-settings.d.ts.map +1 -1
  268. package/dist/pixoo/pixoo-settings.js +9 -0
  269. package/dist/pixoo/pixoo-settings.js.map +1 -1
  270. package/dist/pixoo/pixoo-sprites.d.ts +60 -11
  271. package/dist/pixoo/pixoo-sprites.d.ts.map +1 -1
  272. package/dist/pixoo/pixoo-sprites.js +512 -209
  273. package/dist/pixoo/pixoo-sprites.js.map +1 -1
  274. package/dist/pty-manager.d.ts.map +1 -1
  275. package/dist/pty-manager.js +8 -2
  276. package/dist/pty-manager.js.map +1 -1
  277. package/dist/session-activity.d.ts +14 -0
  278. package/dist/session-activity.d.ts.map +1 -0
  279. package/dist/session-activity.js +160 -0
  280. package/dist/session-activity.js.map +1 -0
  281. package/dist/session-aggregator.d.ts +17 -0
  282. package/dist/session-aggregator.d.ts.map +1 -1
  283. package/dist/session-aggregator.js +48 -9
  284. package/dist/session-aggregator.js.map +1 -1
  285. package/dist/session-registry.d.ts +71 -3
  286. package/dist/session-registry.d.ts.map +1 -1
  287. package/dist/session-registry.js +137 -8
  288. package/dist/session-registry.js.map +1 -1
  289. package/dist/session-timeline-relay.d.ts.map +1 -1
  290. package/dist/session-timeline-relay.js +13 -4
  291. package/dist/session-timeline-relay.js.map +1 -1
  292. package/dist/session-transcript-timeline.d.ts +49 -0
  293. package/dist/session-transcript-timeline.d.ts.map +1 -0
  294. package/dist/session-transcript-timeline.js +276 -0
  295. package/dist/session-transcript-timeline.js.map +1 -0
  296. package/dist/state-machine.d.ts +6 -1
  297. package/dist/state-machine.d.ts.map +1 -1
  298. package/dist/state-machine.js +86 -28
  299. package/dist/state-machine.js.map +1 -1
  300. package/dist/timebox/timebox-daemon-sync.d.ts +19 -0
  301. package/dist/timebox/timebox-daemon-sync.d.ts.map +1 -0
  302. package/dist/timebox/timebox-daemon-sync.js +135 -0
  303. package/dist/timebox/timebox-daemon-sync.js.map +1 -0
  304. package/dist/timebox/timebox-discover.d.ts +19 -0
  305. package/dist/timebox/timebox-discover.d.ts.map +1 -0
  306. package/dist/timebox/timebox-discover.js +87 -0
  307. package/dist/timebox/timebox-discover.js.map +1 -0
  308. package/dist/timebox/timebox-settings.d.ts +32 -0
  309. package/dist/timebox/timebox-settings.d.ts.map +1 -0
  310. package/dist/timebox/timebox-settings.js +80 -0
  311. package/dist/timebox/timebox-settings.js.map +1 -0
  312. package/dist/timeline-store.d.ts +63 -3
  313. package/dist/timeline-store.d.ts.map +1 -1
  314. package/dist/timeline-store.js +292 -14
  315. package/dist/timeline-store.js.map +1 -1
  316. package/dist/timeline-summarizer.d.ts.map +1 -1
  317. package/dist/timeline-summarizer.js +29 -5
  318. package/dist/timeline-summarizer.js.map +1 -1
  319. package/dist/tui/ansi.d.ts +0 -4
  320. package/dist/tui/ansi.d.ts.map +1 -1
  321. package/dist/tui/ansi.js +14 -7
  322. package/dist/tui/ansi.js.map +1 -1
  323. package/dist/tui/dashboard.d.ts +1 -0
  324. package/dist/tui/dashboard.d.ts.map +1 -1
  325. package/dist/tui/dashboard.js +24 -7
  326. package/dist/tui/dashboard.js.map +1 -1
  327. package/dist/tui/renderer.d.ts +36 -0
  328. package/dist/tui/renderer.d.ts.map +1 -1
  329. package/dist/tui/renderer.js +303 -119
  330. package/dist/tui/renderer.js.map +1 -1
  331. package/dist/tui/terrarium.d.ts.map +1 -1
  332. package/dist/tui/terrarium.js +37 -49
  333. package/dist/tui/terrarium.js.map +1 -1
  334. package/dist/types.d.ts +2 -2
  335. package/dist/types.d.ts.map +1 -1
  336. package/dist/types.js +2 -2
  337. package/dist/types.js.map +1 -1
  338. package/dist/usage-api.d.ts.map +1 -1
  339. package/dist/usage-api.js +7 -1
  340. package/dist/usage-api.js.map +1 -1
  341. package/dist/usage-event.d.ts +10 -3
  342. package/dist/usage-event.d.ts.map +1 -1
  343. package/dist/usage-event.js +74 -12
  344. package/dist/usage-event.js.map +1 -1
  345. package/dist/utility-proxy.d.ts.map +1 -1
  346. package/dist/utility-proxy.js +8 -0
  347. package/dist/utility-proxy.js.map +1 -1
  348. package/dist/utils/project-name.d.ts +3 -0
  349. package/dist/utils/project-name.d.ts.map +1 -1
  350. package/dist/utils/project-name.js +44 -2
  351. package/dist/utils/project-name.js.map +1 -1
  352. package/dist/windows-service.d.ts +26 -0
  353. package/dist/windows-service.d.ts.map +1 -0
  354. package/dist/windows-service.js +145 -0
  355. package/dist/windows-service.js.map +1 -0
  356. package/dist/ws-server.d.ts +22 -1
  357. package/dist/ws-server.d.ts.map +1 -1
  358. package/dist/ws-server.js +89 -4
  359. package/dist/ws-server.js.map +1 -1
  360. package/fm-helper/AgentDeckFMHelper.swift +112 -0
  361. package/package.json +9 -3
  362. package/scripts/build-fm-helper.mjs +26 -0
  363. package/LICENSE +0 -21
  364. package/dist/apme/tuner.d.ts +0 -64
  365. package/dist/apme/tuner.d.ts.map +0 -1
  366. package/dist/apme/tuner.js +0 -297
  367. package/dist/apme/tuner.js.map +0 -1
  368. package/dist/d200h/hid-protocol.d.ts +0 -62
  369. package/dist/d200h/hid-protocol.d.ts.map +0 -1
  370. package/dist/d200h/hid-protocol.js +0 -124
  371. package/dist/d200h/hid-protocol.js.map +0 -1
  372. package/dist/d200h/image-renderer.d.ts +0 -36
  373. package/dist/d200h/image-renderer.d.ts.map +0 -1
  374. package/dist/d200h/image-renderer.js +0 -487
  375. package/dist/d200h/image-renderer.js.map +0 -1
  376. package/dist/modules/d200h-module.d.ts +0 -43
  377. package/dist/modules/d200h-module.d.ts.map +0 -1
  378. package/dist/modules/d200h-module.js +0 -360
  379. package/dist/modules/d200h-module.js.map +0 -1
  380. package/dist/modules/d200h-renderer.d.ts +0 -7
  381. package/dist/modules/d200h-renderer.d.ts.map +0 -1
  382. package/dist/modules/d200h-renderer.js +0 -321
  383. package/dist/modules/d200h-renderer.js.map +0 -1
package/dist/cli.js CHANGED
@@ -1,17 +1,63 @@
1
1
  #!/usr/bin/env node
2
2
  import { Command } from 'commander';
3
- import { writeFileSync, unlinkSync, existsSync } from 'fs';
3
+ import { writeFileSync, unlinkSync, existsSync, realpathSync } from 'fs';
4
4
  import { homedir } from 'os';
5
- import { join } from 'path';
6
- import { execSync, spawn } from 'child_process';
5
+ import { dirname, join } from 'path';
6
+ import { execFileSync, execSync, spawn } from 'child_process';
7
7
  import { fileURLToPath } from 'url';
8
8
  import { createRequire } from 'module';
9
+ import { request } from 'http';
9
10
  import { BRIDGE_WS_PORT } from './types.js';
11
+ import { TASK_NAME, installWindowsTask, taskExists, runWindowsTask, endWindowsTask, deleteWindowsTask, } from './windows-service.js';
10
12
  const require = createRequire(import.meta.url);
11
13
  const packageJson = require('../package.json');
12
14
  function log(msg) {
13
15
  process.stderr.write(msg + '\n');
14
16
  }
17
+ function formatBytes(value) {
18
+ const bytes = typeof value === 'number' && Number.isFinite(value) ? value : 0;
19
+ if (bytes >= 1024 * 1024)
20
+ return `${(bytes / (1024 * 1024)).toFixed(1)}MB`;
21
+ if (bytes >= 1024)
22
+ return `${Math.round(bytes / 1024)}KB`;
23
+ return `${bytes}B`;
24
+ }
25
+ function postJsonWithTimeout(urlString, body, timeoutMs) {
26
+ return new Promise((resolve, reject) => {
27
+ const payload = JSON.stringify(body);
28
+ const url = new URL(urlString);
29
+ const req = request({
30
+ hostname: url.hostname,
31
+ port: url.port,
32
+ path: `${url.pathname}${url.search}`,
33
+ method: 'POST',
34
+ headers: {
35
+ 'Content-Type': 'application/json',
36
+ 'Content-Length': Buffer.byteLength(payload),
37
+ },
38
+ }, (res) => {
39
+ const chunks = [];
40
+ res.on('data', (chunk) => {
41
+ chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
42
+ });
43
+ res.on('end', () => {
44
+ const text = Buffer.concat(chunks).toString('utf8');
45
+ try {
46
+ resolve({ statusCode: res.statusCode ?? 0, body: JSON.parse(text) });
47
+ }
48
+ catch (err) {
49
+ reject(new Error(`Invalid JSON response: ${err instanceof Error ? err.message : String(err)}`));
50
+ }
51
+ });
52
+ });
53
+ req.setTimeout(timeoutMs, () => {
54
+ req.destroy(new Error(`Request timed out after ${Math.round(timeoutMs / 1000)}s`));
55
+ });
56
+ req.on('error', reject);
57
+ req.write(payload);
58
+ req.end();
59
+ });
60
+ }
15
61
  // ===== LaunchAgent plist =====
16
62
  const PLIST_LABEL = 'dev.agentdeck.daemon';
17
63
  const PLIST_PATH = join(homedir(), 'Library', 'LaunchAgents', `${PLIST_LABEL}.plist`);
@@ -65,13 +111,85 @@ async function stopDaemon(port) {
65
111
  const info = readDaemonInfo();
66
112
  const targetPort = info?.httpPort ?? info?.port ?? findDaemonPort() ?? port;
67
113
  try {
68
- await fetch(`http://127.0.0.1:${targetPort}/shutdown`, { method: 'POST' });
114
+ await fetch(`http://127.0.0.1:${targetPort}/shutdown`, {
115
+ method: 'POST',
116
+ signal: AbortSignal.timeout(2000),
117
+ });
69
118
  log('Shutdown signal sent');
70
119
  }
71
120
  catch {
72
121
  log('Daemon is not running');
73
122
  }
74
123
  }
124
+ async function isDaemonPort(port) {
125
+ try {
126
+ const res = await fetch(`http://127.0.0.1:${port}/health`, {
127
+ signal: AbortSignal.timeout(1000),
128
+ });
129
+ if (!res.ok)
130
+ return false;
131
+ const data = await res.json();
132
+ return data.mode === 'daemon' || data.agentType === 'daemon';
133
+ }
134
+ catch {
135
+ return false;
136
+ }
137
+ }
138
+ function resolveTimeboxSyncPaths() {
139
+ const distPath = dirname(fileURLToPath(import.meta.url));
140
+ const projectRoot = join(distPath, '..', '..');
141
+ const timeboxDir = join(projectRoot, 'bridge', 'src', 'timebox');
142
+ return {
143
+ python: join(projectRoot, '.venv', 'bin', 'python'),
144
+ bleScript: join(timeboxDir, 'sync_ble.py'),
145
+ scanScript: join(timeboxDir, 'scan_ble.py'),
146
+ };
147
+ }
148
+ function projectRootPath() {
149
+ return join(dirname(fileURLToPath(import.meta.url)), '..', '..');
150
+ }
151
+ const ESP32_OTA_BOARDS = [
152
+ { board: 'inkdeck', env: 'inkdeck', aliases: [] },
153
+ { board: 'ulanzi_tc001', env: 'led8x32', aliases: ['led8x32'] },
154
+ { board: 'ttgo_t_display', env: 'ttgo', aliases: ['ttgo'] },
155
+ { board: 'round_amoled', env: 'amoled', aliases: ['amoled'] },
156
+ { board: 'ips_35', env: 'ips35', aliases: ['ips35'] },
157
+ { board: '86box', env: 'box_86', aliases: ['box_86', 'box_40'] },
158
+ { board: 'ips_10', env: 'ips10', aliases: ['ips10', 'ips_101'] },
159
+ ];
160
+ // alias/board/env-key → { env, board }. Built from the SSOT above.
161
+ export const ESP32_OTA_BY_TARGET = {};
162
+ for (const b of ESP32_OTA_BOARDS) {
163
+ for (const key of [b.board, ...b.aliases]) {
164
+ ESP32_OTA_BY_TARGET[key] = { env: b.env, board: b.board };
165
+ }
166
+ }
167
+ /**
168
+ * Resolve an esp32-ota `target` (alias, canonical board string, or an IP) to the
169
+ * canonical `device_info.board` string the daemon matches against. Unknown
170
+ * targets (e.g. a raw IP or one-off board) pass through unchanged so IP-based
171
+ * and future targeting still works. Exported for testing.
172
+ */
173
+ export function resolveEsp32OtaDaemonTarget(target) {
174
+ return ESP32_OTA_BY_TARGET[target]?.board ?? target;
175
+ }
176
+ function resolveEsp32FirmwarePath(target, envOpt, firmwareOpt) {
177
+ if (firmwareOpt)
178
+ return realpathSync(firmwareOpt);
179
+ const env = envOpt || ESP32_OTA_BY_TARGET[target]?.env || target;
180
+ const path = join(projectRootPath(), 'esp32', '.pio', 'build', env, 'firmware.bin');
181
+ if (!existsSync(path)) {
182
+ throw new Error(`Firmware not found: ${path}. Build it first with: /opt/homebrew/bin/pio run -e ${env}`);
183
+ }
184
+ return path;
185
+ }
186
+ function platformioBin() {
187
+ if (process.env.PLATFORMIO)
188
+ return process.env.PLATFORMIO;
189
+ if (existsSync('/opt/homebrew/bin/pio'))
190
+ return '/opt/homebrew/bin/pio';
191
+ return 'pio';
192
+ }
75
193
  // ===== Program =====
76
194
  const program = new Command();
77
195
  program
@@ -101,12 +219,12 @@ program
101
219
  noUpdateCheck: opts.updateCheck === false,
102
220
  postit: opts.postit !== false,
103
221
  wakeWord: !!opts.wakeWord,
104
- modules: opts.local ? { mdns: false, adb: false, serial: false, pixoo: false, d200h: false } : {
222
+ modules: opts.local ? { mdns: false, adb: false, serial: false, pixoo: false, timebox: false } : {
105
223
  mdns: false, // daemon-only — session bridges never advertise mDNS
106
224
  adb: opts.adb !== false ? 'auto' : false,
107
225
  serial: false, // daemon-only — session bridges never talk to ESP32
108
226
  pixoo: false, // daemon-only — session bridges never talk to Pixoo
109
- d200h: false, // daemon-only — session bridges never talk to D200H
227
+ timebox: false, // daemon-only — session bridges never talk to Timebox
110
228
  },
111
229
  });
112
230
  });
@@ -119,7 +237,32 @@ program
119
237
  .option('--local', 'Disable all device modules (WS only)')
120
238
  .option('--no-adb', 'Disable ADB reverse setup')
121
239
  .option('--no-postit', 'Disable terminal tab title updates')
240
+ .option('--no-codex-hooks', 'Skip ~/.codex/config.toml hook install')
122
241
  .action(async (opts) => {
242
+ // Install Codex lifecycle hooks before starting the session so the
243
+ // first prompt's UserPromptSubmit / Stop events reach the daemon.
244
+ // Idempotent: re-running with the same daemon port is a no-op.
245
+ if (opts.codexHooks !== false) {
246
+ try {
247
+ const { installCodexHooksIfNeeded } = await import('@agentdeck/hooks');
248
+ const result = installCodexHooksIfNeeded();
249
+ if (result.installed) {
250
+ log('Codex lifecycle hooks ready in ~/.codex/config.toml');
251
+ if (result.warning)
252
+ log(`Codex hooks degraded: ${result.warning}`);
253
+ }
254
+ else if (result.reason) {
255
+ log(`Codex hooks skipped: ${result.reason}`);
256
+ }
257
+ }
258
+ catch (err) {
259
+ log(`Codex hooks unavailable: ${String(err)}`);
260
+ // PTY parser fallback still works without lifecycle hooks.
261
+ }
262
+ }
263
+ else {
264
+ log('Codex hooks skipped: --no-codex-hooks');
265
+ }
123
266
  const { startSession } = await import('./index.js');
124
267
  await startSession({
125
268
  agentType: 'codex-cli',
@@ -127,12 +270,12 @@ program
127
270
  command: opts.command,
128
271
  debug: opts.debug,
129
272
  postit: opts.postit !== false,
130
- modules: opts.local ? { mdns: false, adb: false, serial: false, pixoo: false, d200h: false } : {
273
+ modules: opts.local ? { mdns: false, adb: false, serial: false, pixoo: false, timebox: false } : {
131
274
  mdns: false, // daemon-only
132
275
  adb: opts.adb !== false ? 'auto' : false,
133
276
  serial: false, // daemon-only
134
277
  pixoo: false, // daemon-only
135
- d200h: false, // daemon-only
278
+ timebox: false, // daemon-only
136
279
  },
137
280
  });
138
281
  });
@@ -145,7 +288,31 @@ program
145
288
  .option('--local', 'Disable all device modules (WS only)')
146
289
  .option('--no-adb', 'Disable ADB reverse setup')
147
290
  .option('--no-postit', 'Disable terminal tab title updates')
291
+ .option('--no-opencode-hooks', 'Skip OpenCode observer plugin install')
148
292
  .action(async (opts) => {
293
+ // Install the OpenCode observer plugin so standalone `opencode` runs
294
+ // (outside this managed session) also reach the daemon timeline. The
295
+ // plugin self-disables inside managed PTYs via AGENTDECK_PORT, so this
296
+ // session is unaffected. Idempotent content-compare write.
297
+ if (opts.opencodeHooks !== false) {
298
+ try {
299
+ const { installOpenCodeHooksIfNeeded, opencodePluginPath } = await import('@agentdeck/hooks');
300
+ const result = installOpenCodeHooksIfNeeded();
301
+ if (result.installed) {
302
+ log(`OpenCode observer plugin ready at ${opencodePluginPath()}`);
303
+ }
304
+ else if (result.reason) {
305
+ log(`OpenCode observer plugin skipped: ${result.reason}`);
306
+ }
307
+ }
308
+ catch (err) {
309
+ log(`OpenCode observer plugin unavailable: ${String(err)}`);
310
+ // Managed SSE bridge works without the plugin.
311
+ }
312
+ }
313
+ else {
314
+ log('OpenCode observer plugin skipped: --no-opencode-hooks');
315
+ }
149
316
  const { startSession } = await import('./index.js');
150
317
  await startSession({
151
318
  agentType: 'opencode',
@@ -153,12 +320,12 @@ program
153
320
  command: opts.command,
154
321
  debug: opts.debug,
155
322
  postit: opts.postit !== false,
156
- modules: opts.local ? { mdns: false, adb: false, serial: false, pixoo: false, d200h: false } : {
323
+ modules: opts.local ? { mdns: false, adb: false, serial: false, pixoo: false, timebox: false } : {
157
324
  mdns: false,
158
325
  adb: opts.adb !== false ? 'auto' : false,
159
326
  serial: false,
160
327
  pixoo: false,
161
- d200h: false,
328
+ timebox: false,
162
329
  },
163
330
  });
164
331
  });
@@ -177,9 +344,15 @@ program
177
344
  agentType: 'monitor',
178
345
  port,
179
346
  debug: opts.debug,
180
- modules: opts.local ? { mdns: false, adb: false, serial: false, pixoo: false, d200h: false } : undefined,
347
+ modules: opts.local ? { mdns: false, adb: false, serial: false, pixoo: false, timebox: false } : undefined,
181
348
  });
182
349
  });
350
+ // The worktree-compare feature ("cockpit") was extracted into its own
351
+ // standalone tool, Worktree Cockpit (`wtcp`):
352
+ // https://github.com/puritysb/worktree-cockpit . It no longer ships with
353
+ // AgentDeck. AgentDeck still exposes
354
+ // the on-device Apple Intelligence helper via the daemon `/generate` endpoint,
355
+ // which wtcp can optionally use for branch naming.
183
356
  // ===== Daemon commands =====
184
357
  const daemon = program.command('daemon').description('Manage monitoring daemon');
185
358
  daemon
@@ -190,20 +363,65 @@ daemon
190
363
  .option('-f, --foreground', 'Run in foreground (default: background fork)')
191
364
  .option('--wake-word', 'Enable wake word voice assistant ("오픈클로")')
192
365
  .action(async (opts) => {
193
- const { findExistingDaemon, readDaemonInfo } = await import('./session-registry.js');
366
+ const { findExistingDaemon, probeDaemonHealth, readDaemonInfo, removeDaemonInfo, removeDaemonSession, requestDaemonStandDown, requestDaemonShutdown, waitForDaemonExit } = await import('./session-registry.js');
367
+ // Reverse two-tier upgrade path: the macOS app may already own the canonical
368
+ // port with its in-process Swift daemon (Tier 1 — limited: no ADB devices,
369
+ // no subscription usage, …). A plain "already running → exit" would strand
370
+ // the user on Tier 1. Instead, if the incumbent identifies as a Swift
371
+ // daemon, ask it to STAND DOWN (demote to client, app keeps running) so this
372
+ // CLI daemon can take over with the full feature set, then wait for the port
373
+ // to clear before binding. A real Node daemon on the port still means
374
+ // "already running".
375
+ const targetPort = opts.port ? parseInt(String(opts.port), 10) : BRIDGE_WS_PORT;
376
+ const incumbent = await probeDaemonHealth(targetPort);
377
+ if (incumbent?.mode === 'daemon') {
378
+ if (incumbent.isSwift) {
379
+ log(`AgentDeck app's in-process daemon holds port ${targetPort} — requesting stand-down to take over with the full CLI feature set…`);
380
+ // Prefer /stand-down (clean demote: the app stays running as a client).
381
+ // Fall back to /shutdown for older app builds that predate the endpoint.
382
+ let acked = await requestDaemonStandDown(targetPort);
383
+ if (!acked) {
384
+ await requestDaemonShutdown(targetPort);
385
+ acked = true; // shutdown is best-effort (no ack body); rely on the exit wait
386
+ }
387
+ if (acked && await waitForDaemonExit(targetPort, 12000)) {
388
+ log(`App daemon yielded port ${targetPort}. Starting CLI daemon…`);
389
+ // fall through — port is clear, proceed to bind below
390
+ }
391
+ else {
392
+ log(`The AgentDeck app did not yield port ${targetPort} in time. Quit the app and retry 'agentdeck daemon start', or start on a different port with -p.`);
393
+ process.exit(1);
394
+ }
395
+ }
396
+ else {
397
+ log(`Daemon already running on port ${targetPort}. Use 'agentdeck daemon stop' first.`);
398
+ process.exit(0);
399
+ }
400
+ }
194
401
  const daemonInfo = readDaemonInfo();
195
402
  if (daemonInfo) {
196
- log(`Daemon already running on port ${daemonInfo.port} (PID ${daemonInfo.pid}). Use 'agentdeck daemon stop' first.`);
197
- process.exit(0);
403
+ const probePort = daemonInfo.httpPort ?? daemonInfo.port;
404
+ const health = await probeDaemonHealth(probePort);
405
+ if (health?.mode === 'daemon') {
406
+ log(`Daemon already running on port ${daemonInfo.port} (PID ${daemonInfo.pid}). Use 'agentdeck daemon stop' first.`);
407
+ process.exit(0);
408
+ }
409
+ log(`Ignoring stale daemon entry on port ${daemonInfo.port} (PID ${daemonInfo.pid}; /health did not respond).`);
410
+ removeDaemonInfo();
198
411
  }
199
412
  const existing = findExistingDaemon();
200
413
  if (existing) {
201
- log(`Daemon already running on port ${existing.port} (PID ${existing.pid}). Use 'agentdeck daemon stop' first.`);
202
- process.exit(0);
414
+ const health = await probeDaemonHealth(existing.port);
415
+ if (health?.mode === 'daemon') {
416
+ log(`Daemon already running on port ${existing.port} (PID ${existing.pid}). Use 'agentdeck daemon stop' first.`);
417
+ process.exit(0);
418
+ }
419
+ log(`Ignoring stale daemon session on port ${existing.port} (PID ${existing.pid}; /health did not respond).`);
420
+ removeDaemonSession(existing);
203
421
  }
204
422
  // Background fork unless --foreground
205
423
  if (!opts.foreground) {
206
- const { openSync } = await import('fs');
424
+ const { openSync, statSync, renameSync } = await import('fs');
207
425
  const logDir = join(homedir(), '.agentdeck');
208
426
  const scriptPath = fileURLToPath(import.meta.url);
209
427
  const args = [scriptPath, 'daemon', 'start', '--foreground'];
@@ -214,8 +432,22 @@ daemon
214
432
  if (opts.wakeWord)
215
433
  args.push('--wake-word');
216
434
  // Use log files instead of 'ignore' — preserves device access (mic, etc.)
217
- const out = openSync(join(logDir, 'daemon-stdout.log'), 'w');
218
- const err = openSync(join(logDir, 'daemon-stderr.log'), 'w');
435
+ // Append (never truncate) so multi-day history survives restarts —
436
+ // overnight device incidents can only be correlated against logs that
437
+ // are still there in the morning. Rotate once past 5MB instead.
438
+ const openDaemonLog = (name) => {
439
+ const path = join(logDir, name);
440
+ try {
441
+ if (statSync(path).size > 5 * 1024 * 1024)
442
+ renameSync(path, `${path}.1`);
443
+ }
444
+ catch {
445
+ /* first run — no log yet */
446
+ }
447
+ return openSync(path, 'a');
448
+ };
449
+ const out = openDaemonLog('daemon-stdout.log');
450
+ const err = openDaemonLog('daemon-stderr.log');
219
451
  const child = spawn(process.execPath, args, {
220
452
  detached: true,
221
453
  stdio: ['ignore', out, err],
@@ -267,7 +499,7 @@ daemon
267
499
  .option('-p, --port <port>', 'Server port', String(BRIDGE_WS_PORT))
268
500
  .action(async (opts) => {
269
501
  const port = parseInt(opts.port, 10);
270
- const { listActive, readDaemonInfo } = await import('./session-registry.js');
502
+ const { listActive, readDaemonInfo, removeDaemonInfo, removeDaemonSession } = await import('./session-registry.js');
271
503
  const info = readDaemonInfo();
272
504
  const sessions = listActive();
273
505
  const d = sessions.find(s => s.agentType === 'daemon');
@@ -278,14 +510,70 @@ daemon
278
510
  log(`Daemon status (port ${targetPort}): ${JSON.stringify(data, null, 2)}`);
279
511
  }
280
512
  catch {
513
+ if (info)
514
+ removeDaemonInfo();
515
+ if (d)
516
+ removeDaemonSession(d);
281
517
  log('Daemon is not running');
282
518
  process.exit(1);
283
519
  }
284
520
  });
285
521
  daemon
286
522
  .command('install')
287
- .description('Install macOS LaunchAgent for auto-start')
288
- .action(() => {
523
+ .description('Install daemon auto-start (LaunchAgent on macOS, Scheduled Task on Windows)')
524
+ .action(async () => {
525
+ if (process.platform === 'win32') {
526
+ try {
527
+ installWindowsTask();
528
+ log(`Scheduled task '${TASK_NAME}' registered. Daemon will auto-start on logon.`);
529
+ }
530
+ catch (e) {
531
+ const detail = e.stderr?.toString().trim() || e.message;
532
+ log('Failed to register the AgentDeck scheduled task.');
533
+ if (detail)
534
+ log(detail);
535
+ log('You can still run the daemon manually with: agentdeck daemon start');
536
+ log('(or add a shortcut to shell:startup to autostart it yourself).');
537
+ process.exit(1);
538
+ }
539
+ // Start it now so the user does not have to log out/in (the singleton
540
+ // guard makes a double-start a safe no-op).
541
+ try {
542
+ runWindowsTask();
543
+ log('Daemon started.');
544
+ }
545
+ catch {
546
+ log('Task registered; immediate start failed — it will start on next logon.');
547
+ }
548
+ // Install Codex lifecycle hooks for parity with the macOS install path.
549
+ try {
550
+ const { installCodexHooksIfNeeded } = await import('@agentdeck/hooks');
551
+ const result = installCodexHooksIfNeeded();
552
+ if (result.installed) {
553
+ log('Codex lifecycle hooks installed in ~/.codex/config.toml');
554
+ if (result.warning)
555
+ log(`Codex hooks degraded: ${result.warning}`);
556
+ }
557
+ else if (result.reason) {
558
+ log(`Codex hooks skipped: ${result.reason}`);
559
+ }
560
+ }
561
+ catch { /* hooks package not built yet — task install still succeeds */ }
562
+ // OpenCode observer plugin — standalone `opencode` runs report to the
563
+ // daemon timeline the same way direct `codex` runs do.
564
+ try {
565
+ const { installOpenCodeHooksIfNeeded } = await import('@agentdeck/hooks');
566
+ const result = installOpenCodeHooksIfNeeded();
567
+ if (result.installed) {
568
+ log('OpenCode observer plugin installed.');
569
+ }
570
+ else if (result.reason) {
571
+ log(`OpenCode observer plugin skipped: ${result.reason}`);
572
+ }
573
+ }
574
+ catch { /* hooks package not built yet — task install still succeeds */ }
575
+ process.exit(0);
576
+ }
289
577
  if (process.platform !== 'darwin') {
290
578
  log('LaunchAgent is macOS-only');
291
579
  process.exit(1);
@@ -299,11 +587,61 @@ daemon
299
587
  catch { }
300
588
  execSync(`launchctl load "${PLIST_PATH}"`);
301
589
  log('LaunchAgent loaded. Daemon will auto-start on login.');
590
+ // Install Codex lifecycle hooks parallel to the LaunchAgent install
591
+ // so the daemon hub gets codex_* events as soon as Codex CLI runs.
592
+ try {
593
+ const { installCodexHooksIfNeeded } = await import('@agentdeck/hooks');
594
+ const result = installCodexHooksIfNeeded();
595
+ if (result.installed) {
596
+ log('Codex lifecycle hooks installed in ~/.codex/config.toml');
597
+ }
598
+ else if (result.reason) {
599
+ log(`Codex hooks skipped: ${result.reason}`);
600
+ }
601
+ }
602
+ catch { /* hooks package not built yet — daemon install still succeeds */ }
603
+ // OpenCode observer plugin — standalone `opencode` runs report to the
604
+ // daemon hub as opencode_* events, same pipeline as codex_*.
605
+ try {
606
+ const { installOpenCodeHooksIfNeeded, opencodePluginPath } = await import('@agentdeck/hooks');
607
+ const result = installOpenCodeHooksIfNeeded();
608
+ if (result.installed) {
609
+ log(`OpenCode observer plugin installed at ${opencodePluginPath()}`);
610
+ }
611
+ else if (result.reason) {
612
+ log(`OpenCode observer plugin skipped: ${result.reason}`);
613
+ }
614
+ }
615
+ catch { /* hooks package not built yet — daemon install still succeeds */ }
302
616
  });
303
617
  daemon
304
618
  .command('uninstall')
305
- .description('Remove macOS LaunchAgent')
306
- .action(() => {
619
+ .description('Remove daemon auto-start (LaunchAgent on macOS, Scheduled Task on Windows)')
620
+ .action(async () => {
621
+ if (process.platform === 'win32') {
622
+ if (!taskExists()) {
623
+ log(`Scheduled task '${TASK_NAME}' is not installed.`);
624
+ process.exit(0);
625
+ }
626
+ // Graceful daemon shutdown first, then stop + delete the task.
627
+ await stopDaemon(BRIDGE_WS_PORT);
628
+ try {
629
+ endWindowsTask();
630
+ }
631
+ catch { /* not running */ }
632
+ try {
633
+ deleteWindowsTask();
634
+ log(`Scheduled task '${TASK_NAME}' removed.`);
635
+ }
636
+ catch (e) {
637
+ const detail = e.stderr?.toString().trim() || e.message;
638
+ log('Failed to remove the scheduled task.');
639
+ if (detail)
640
+ log(detail);
641
+ process.exit(1);
642
+ }
643
+ process.exit(0);
644
+ }
307
645
  if (!existsSync(PLIST_PATH)) {
308
646
  log('LaunchAgent not installed');
309
647
  return;
@@ -358,8 +696,15 @@ program
358
696
  }
359
697
  else {
360
698
  try {
361
- await fetch(`http://127.0.0.1:${port}/hooks/shutdown`, { method: 'POST' });
362
- log('Shutdown signal sent');
699
+ const daemonPort = await isDaemonPort(port);
700
+ const url = daemonPort
701
+ ? `http://127.0.0.1:${port}/shutdown`
702
+ : `http://127.0.0.1:${port}/hooks/shutdown`;
703
+ await fetch(url, {
704
+ method: 'POST',
705
+ signal: AbortSignal.timeout(2000),
706
+ });
707
+ log(daemonPort ? 'Daemon shutdown signal sent' : 'Shutdown signal sent');
363
708
  }
364
709
  catch {
365
710
  log('Session is not running');
@@ -369,10 +714,14 @@ program
369
714
  // ===== Device commands =====
370
715
  program
371
716
  .command('devices')
372
- .description('Show connected devices (WebSocket, ESP32, Pixoo, ADB)')
373
- .option('-p, --port <port>', 'Bridge server port', String(BRIDGE_WS_PORT))
717
+ .description('Show connected devices (WebSocket, ESP32, Pixoo, Timebox, ADB)')
718
+ .option('-p, --port <port>', 'Bridge server port')
374
719
  .action(async (opts) => {
375
- const port = parseInt(opts.port, 10);
720
+ const { readDaemonInfo, findDaemonPort } = await import('./session-registry.js');
721
+ const info = readDaemonInfo();
722
+ const port = opts.port != null
723
+ ? parseInt(opts.port, 10)
724
+ : (info?.httpPort ?? info?.port ?? findDaemonPort() ?? BRIDGE_WS_PORT);
376
725
  try {
377
726
  const res = await fetch(`http://127.0.0.1:${port}/devices`, {
378
727
  signal: AbortSignal.timeout(2000),
@@ -386,15 +735,51 @@ program
386
735
  total += d.count;
387
736
  }
388
737
  else if ((d.type === 'esp32' || d.type === 'esp32_serial') && (d.count || d.connections)) {
389
- // Node daemon: { count, ports }, Swift daemon: { connections: [{ port, connected, deviceInfo }] }
738
+ // Node daemon: { count, ports, devices }, Swift daemon: { connections: [{ port, connected, deviceInfo }] }
390
739
  const count = d.count ?? d.connections?.filter((c) => c.connected).length ?? 0;
391
740
  const ports = d.ports ?? d.connections?.map((c) => c.port) ?? [];
741
+ // Per-device build identity, so a stale flash is distinguishable from the latest.
742
+ const devices = d.devices ?? d.connections?.filter((c) => c.connected).map((c) => ({ port: c.port, ...c.deviceInfo })) ?? [];
392
743
  if (count > 0) {
393
- const portInfo = ports.length ? ` (${ports.join(', ')})` : '';
394
- lines.push(` ESP32 ${count} serial${portInfo}`);
744
+ if (devices.length) {
745
+ lines.push(` ESP32 ${count} serial`);
746
+ for (const dev of devices) {
747
+ const ver = dev.version ? ` v${dev.version}` : '';
748
+ const hash = dev.buildHash ? ` (${dev.buildHash})` : '';
749
+ const board = dev.board ?? 'esp32';
750
+ const ota = dev.otaSupported === true
751
+ ? ` OTA ${formatBytes(dev.otaSlotSize)}`
752
+ : dev.otaReason ? ` OTA no:${dev.otaReason}` : '';
753
+ lines.push(` ${board}${ver}${hash}${ota} @ ${dev.port}`);
754
+ }
755
+ }
756
+ else {
757
+ const portInfo = ports.length ? ` (${ports.join(', ')})` : '';
758
+ lines.push(` ESP32 ${count} serial${portInfo}`);
759
+ }
395
760
  total += count;
396
761
  }
397
762
  }
763
+ else if (d.type === 'esp32-wifi') {
764
+ const devices = (d.devices ?? []);
765
+ if (devices.length) {
766
+ lines.push(` ESP32 WiFi ${devices.length} client${devices.length !== 1 ? 's' : ''}`);
767
+ for (const dev of devices) {
768
+ const ver = dev.version ? ` v${dev.version}` : '';
769
+ const hash = dev.buildHash ? ` (${dev.buildHash})` : '';
770
+ const board = dev.board ?? 'esp32';
771
+ const ota = dev.otaSupported === true
772
+ ? ` OTA ${formatBytes(dev.otaSlotSize)}`
773
+ : dev.otaReason ? ` OTA no:${dev.otaReason}` : '';
774
+ const stale = dev.stale ? ' stale' : '';
775
+ // Single-path: a board also live on USB serial is driven over
776
+ // serial; its WiFi link is a hot standby (no duplicate traffic).
777
+ const transport = dev.serialActive ? ' [serial-active · wifi standby]' : '';
778
+ lines.push(` ${board}${ver}${hash}${ota}${stale}${transport} @ ${dev.ip ?? 'wifi'}`);
779
+ }
780
+ total += devices.length;
781
+ }
782
+ }
398
783
  else if (d.type === 'pixoo') {
399
784
  // Node daemon: { details: [...] }, Swift daemon: { deviceIps: [...] }
400
785
  if (d.details) {
@@ -418,6 +803,43 @@ program
418
803
  }
419
804
  }
420
805
  }
806
+ else if (d.type === 'timebox') {
807
+ // Node daemon: { devices: [{address, name, brightness}] }, Swift
808
+ // daemon: { configuredDeviceCount, connected, deviceName, statusReason }.
809
+ if (Array.isArray(d.devices)) {
810
+ for (const tb of d.devices) {
811
+ const brightnessInfo = tb.brightness !== undefined ? ` brightness=${tb.brightness}%` : '';
812
+ lines.push(` Timebox ${tb.address} (${tb.name || 'Timebox Mini Light'})${brightnessInfo}`);
813
+ total++;
814
+ }
815
+ }
816
+ else if ((d.configuredDeviceCount ?? 0) > 0) {
817
+ const status = d.connected ? '✓' : `⚠ ${d.statusReason ?? 'not connected'}`;
818
+ lines.push(` Timebox ${d.deviceName ?? 'Timebox Mini'} ${status}`);
819
+ total++;
820
+ }
821
+ }
822
+ else if (d.type === 'idotmatrix') {
823
+ // Node daemon: { devices: [{address, name}] }, Swift daemon:
824
+ // { configuredDeviceCount, connected, deviceName, statusReason }.
825
+ if (Array.isArray(d.devices)) {
826
+ for (const dm of d.devices) {
827
+ lines.push(` iDotMatrix ${dm.address} (${dm.name || 'iDotMatrix'})`);
828
+ total++;
829
+ }
830
+ }
831
+ else if ((d.configuredDeviceCount ?? 0) > 0) {
832
+ const status = d.connected ? '✓' : `⚠ ${d.statusReason ?? 'not connected'}`;
833
+ lines.push(` iDotMatrix ${d.deviceName ?? 'iDotMatrix'} ${status}`);
834
+ total++;
835
+ }
836
+ }
837
+ else if (d.type === 'tui') {
838
+ for (const t of (d.devices ?? [])) {
839
+ lines.push(` TUI ${t.name ?? 'terminal'} (agentdeck dashboard)`);
840
+ total++;
841
+ }
842
+ }
421
843
  else if (d.type === 'adb') {
422
844
  const count = d.count ?? d.devices?.length ?? 0;
423
845
  if (count > 0) {
@@ -426,8 +848,7 @@ program
426
848
  }
427
849
  }
428
850
  else if (d.type === 'd200h' && d.connected) {
429
- const writeInfo = d.writeOK != null ? ` (writes: ${d.writeOK} ok, ${d.writeFail} fail)` : '';
430
- lines.push(` D200H HID connected${writeInfo}`);
851
+ lines.push(` D200H Ulanzi Studio connected (via WebSocket)`);
431
852
  total++;
432
853
  }
433
854
  }
@@ -442,11 +863,17 @@ program
442
863
  }
443
864
  catch {
444
865
  const { loadPixooDevices } = await import('./pixoo/pixoo-settings.js');
866
+ const { loadTimeboxDevices } = await import('./timebox/timebox-settings.js');
445
867
  const pixoo = loadPixooDevices();
446
- if (pixoo.length > 0) {
447
- log('Bridge is not running.\nConfigured Pixoo devices:');
868
+ const timebox = loadTimeboxDevices();
869
+ if (pixoo.length > 0 || timebox.length > 0) {
870
+ log('Bridge is not running.\nConfigured devices:');
448
871
  for (const d of pixoo) {
449
- log(` ${d.ip} (${d.name || 'Pixoo64'})`);
872
+ log(` Pixoo64 ${d.ip} (${d.name || 'Pixoo64'})`);
873
+ }
874
+ for (const d of timebox) {
875
+ const brightnessInfo = d.brightness !== undefined ? ` brightness=${d.brightness}%` : '';
876
+ log(` Timebox ${d.address} (${d.name || 'Timebox Mini Light'})${brightnessInfo}`);
450
877
  }
451
878
  }
452
879
  else {
@@ -454,6 +881,39 @@ program
454
881
  }
455
882
  }
456
883
  });
884
+ program
885
+ .command('esp32-ota <target>')
886
+ .description('Upload built ESP32 firmware to a WiFi-connected AgentDeck ESP32')
887
+ .option('-p, --port <port>', 'Daemon port')
888
+ .option('-e, --env <env>', 'PlatformIO environment for default firmware path')
889
+ .option('-f, --firmware <path>', 'Firmware .bin path')
890
+ .option('--build', 'Build the PlatformIO environment before upload')
891
+ .action(async (target, opts) => {
892
+ const { readDaemonInfo, findDaemonPort } = await import('./session-registry.js');
893
+ const info = readDaemonInfo();
894
+ const port = opts.port != null
895
+ ? parseInt(opts.port, 10)
896
+ : (info?.httpPort ?? info?.port ?? findDaemonPort() ?? BRIDGE_WS_PORT);
897
+ const env = opts.env || ESP32_OTA_BY_TARGET[target]?.env || target;
898
+ if (opts.build) {
899
+ const pio = platformioBin();
900
+ log(`Building ${env} with ${pio}...`);
901
+ execFileSync(pio, ['run', '-e', env], {
902
+ cwd: join(projectRootPath(), 'esp32'),
903
+ stdio: 'inherit',
904
+ });
905
+ }
906
+ // Send the canonical board string, not the raw alias — the daemon matches
907
+ // `target` against device_info.board, so `ttgo` must become `ttgo_t_display`.
908
+ const daemonTarget = resolveEsp32OtaDaemonTarget(target);
909
+ const firmwarePath = resolveEsp32FirmwarePath(target, opts.env, opts.firmware);
910
+ log(`Uploading ${firmwarePath} to ${daemonTarget} via daemon :${port}...`);
911
+ const { statusCode, body } = await postJsonWithTimeout(`http://127.0.0.1:${port}/esp32/ota`, { target: daemonTarget, firmwarePath }, 15 * 60_000);
912
+ if (statusCode < 200 || statusCode >= 300 || body.ok === false) {
913
+ throw new Error(String(body.error ?? `HTTP ${statusCode}`));
914
+ }
915
+ log(`OTA complete: ${body.board ?? target} ${formatBytes(body.bytes)} in ${body.chunks} chunks`);
916
+ });
457
917
  program
458
918
  .command('qr')
459
919
  .description('Show pairing URL and QR code')
@@ -496,7 +956,9 @@ program
496
956
  .option('-a, --analyze', 'Run AI analysis on the dump')
497
957
  .option('-t, --tail <lines>', 'Number of journal entries', '200')
498
958
  .action(async (opts) => {
499
- const port = parseInt(opts.port, 10);
959
+ const { readDaemonInfo, findDaemonPort } = await import('./session-registry.js');
960
+ const info = readDaemonInfo();
961
+ const port = info?.httpPort ?? info?.port ?? findDaemonPort() ?? parseInt(opts.port, 10);
500
962
  const tail = parseInt(opts.tail, 10);
501
963
  try {
502
964
  const res = await fetch(`http://127.0.0.1:${port}/diag?tail=${tail}`);
@@ -525,6 +987,390 @@ program
525
987
  process.exit(1);
526
988
  }
527
989
  });
990
+ // ===== Task control =====
991
+ //
992
+ // User-driven APME task boundary signal. Closes the currently-active task on
993
+ // the daemon's APME collector via POST /task/close — same path the macOS
994
+ // detail-pane "Mark task complete" button uses. Without this command, the
995
+ // only ways to declare task completion are TodoWrite all-completed (Claude /
996
+ // Codex / OpenCode) or /clear (also splits the run); OpenClaw users had no
997
+ // manual gesture at all before idle_gap landed.
998
+ const task = program.command('task').description('Manage APME task boundaries');
999
+ async function postTaskClose(opts) {
1000
+ const { readDaemonInfo, findDaemonPort } = await import('./session-registry.js');
1001
+ const info = readDaemonInfo();
1002
+ const port = info?.httpPort ?? info?.port ?? findDaemonPort();
1003
+ if (!port) {
1004
+ log('Daemon not running. Start it with `agentdeck daemon start`.');
1005
+ process.exit(1);
1006
+ }
1007
+ const body = JSON.stringify({
1008
+ signal: opts.signal,
1009
+ ...(opts.outcome ? { outcome: opts.outcome } : {}),
1010
+ ...(opts.sessionId ? { sessionId: opts.sessionId } : {}),
1011
+ });
1012
+ try {
1013
+ const res = await fetch(`http://127.0.0.1:${port}/task/close`, {
1014
+ method: 'POST',
1015
+ headers: { 'Content-Type': 'application/json' },
1016
+ body,
1017
+ });
1018
+ const json = await res.json().catch(() => ({}));
1019
+ if (!res.ok) {
1020
+ log(`Failed (${res.status}): ${JSON.stringify(json)}`);
1021
+ process.exit(1);
1022
+ }
1023
+ if (json.closed) {
1024
+ log(`Task closed (signal=${opts.signal}${opts.outcome ? `, outcome=${opts.outcome}` : ''})`);
1025
+ }
1026
+ else {
1027
+ log('No active task to close.');
1028
+ }
1029
+ }
1030
+ catch (err) {
1031
+ log(`Request failed: ${String(err)}`);
1032
+ process.exit(1);
1033
+ }
1034
+ }
1035
+ task
1036
+ .command('done')
1037
+ .description('Mark the active task as complete (manual boundary signal)')
1038
+ .option('-o, --outcome <v>', 'Outcome class: success | fail | partial | abandoned', 'success')
1039
+ .option('-s, --session <id>', 'Target session id (defaults to active OpenClaw session)')
1040
+ .action(async (opts) => {
1041
+ await postTaskClose({ signal: 'manual', outcome: opts.outcome, sessionId: opts.session });
1042
+ });
1043
+ task
1044
+ .command('cancel')
1045
+ .description('Mark the active task as abandoned (shorthand for `task done --outcome abandoned`)')
1046
+ .option('-s, --session <id>', 'Target session id (defaults to active OpenClaw session)')
1047
+ .action(async (opts) => {
1048
+ await postTaskClose({ signal: 'manual', outcome: 'abandoned', sessionId: opts.session });
1049
+ });
1050
+ // ===== iDotMatrix commands =====
1051
+ const idotmatrix = program.command('idotmatrix').description('Manage iDotMatrix BLE pixel display devices');
1052
+ idotmatrix
1053
+ .command('scan')
1054
+ .description('Discover iDotMatrix devices via BLE')
1055
+ .action(async () => {
1056
+ const { dirname } = await import('path');
1057
+ const { fileURLToPath } = await import('url');
1058
+ const { spawn } = await import('child_process');
1059
+ const distPath = dirname(fileURLToPath(import.meta.url));
1060
+ const projectRoot = join(distPath, '..', '..');
1061
+ const venvPython = join(projectRoot, '.venv', 'bin', 'python');
1062
+ const scanScript = join(projectRoot, 'bridge', 'src', 'idotmatrix', 'scan.py');
1063
+ log('Scanning for BLE devices (5 seconds)...');
1064
+ const py = spawn(venvPython, [scanScript]);
1065
+ let stdoutData = '';
1066
+ py.stdout.on('data', (data) => {
1067
+ stdoutData += data.toString();
1068
+ });
1069
+ py.on('close', (code) => {
1070
+ if (code !== 0) {
1071
+ log('Failed to run scan script.');
1072
+ process.exit(1);
1073
+ }
1074
+ try {
1075
+ const devices = JSON.parse(stdoutData.trim());
1076
+ if (devices.error) {
1077
+ log(`Scan error: ${devices.error}`);
1078
+ process.exit(1);
1079
+ }
1080
+ if (!Array.isArray(devices) || devices.length === 0) {
1081
+ log('No BLE devices found.');
1082
+ return;
1083
+ }
1084
+ log('\nFound devices:');
1085
+ for (const d of devices) {
1086
+ const prefix = d.is_idotmatrix ? '★ [iDotMatrix] ' : ' ';
1087
+ log(`${prefix}${d.name} (${d.address})`);
1088
+ }
1089
+ }
1090
+ catch (e) {
1091
+ log(`Failed to parse scan output: ${stdoutData}`);
1092
+ }
1093
+ });
1094
+ });
1095
+ idotmatrix
1096
+ .command('add <address>')
1097
+ .description('Add an iDotMatrix device address')
1098
+ .option('-n, --name <name>', 'Device name', 'iDotMatrix')
1099
+ .option('-b, --brightness <value>', 'Brightness 5-100', '100')
1100
+ .action(async (address, opts) => {
1101
+ const { addIDotMatrixDevice } = await import('./idotmatrix/idotmatrix-settings.js');
1102
+ const brightness = parseInt(opts.brightness, 10);
1103
+ if (addIDotMatrixDevice({ address, name: opts.name, brightness: isNaN(brightness) ? 100 : brightness })) {
1104
+ log(`Added ${opts.name} (${address}) with brightness ${brightness}% to settings.`);
1105
+ }
1106
+ else {
1107
+ log(`Device ${address} already exists.`);
1108
+ }
1109
+ });
1110
+ idotmatrix
1111
+ .command('list')
1112
+ .description('List configured iDotMatrix devices')
1113
+ .action(async () => {
1114
+ const { loadIDotMatrixDevices } = await import('./idotmatrix/idotmatrix-settings.js');
1115
+ const devices = loadIDotMatrixDevices();
1116
+ if (devices.length === 0) {
1117
+ log('No iDotMatrix devices configured. Run `agentdeck idotmatrix scan` or `add`.');
1118
+ return;
1119
+ }
1120
+ log(`${devices.length} device(s) configured:`);
1121
+ for (const d of devices) {
1122
+ const brightnessInfo = d.brightness !== undefined ? ` (brightness=${d.brightness}%)` : '';
1123
+ log(` ${d.name || 'iDotMatrix'} (${d.address})${brightnessInfo}`);
1124
+ }
1125
+ });
1126
+ idotmatrix
1127
+ .command('remove <address>')
1128
+ .description('Remove an iDotMatrix device')
1129
+ .action(async (address) => {
1130
+ const { removeIDotMatrixDevice } = await import('./idotmatrix/idotmatrix-settings.js');
1131
+ if (removeIDotMatrixDevice(address)) {
1132
+ log(`Removed device ${address}.`);
1133
+ }
1134
+ else {
1135
+ log(`Device ${address} not found.`);
1136
+ }
1137
+ });
1138
+ idotmatrix
1139
+ .command('brightness <value>')
1140
+ .description('Set brightness of the iDotMatrix device (5-100)')
1141
+ .option('-a, --address <address>', 'BLE Address (defaults to first configured device)')
1142
+ .action(async (valueStr, opts) => {
1143
+ const { loadIDotMatrixDevices } = await import('./idotmatrix/idotmatrix-settings.js');
1144
+ const { dirname } = await import('path');
1145
+ const { fileURLToPath } = await import('url');
1146
+ const { spawn } = await import('child_process');
1147
+ const value = parseInt(valueStr, 10);
1148
+ if (isNaN(value) || value < 5 || value > 100) {
1149
+ log('Brightness value must be between 5 and 100.');
1150
+ process.exit(1);
1151
+ }
1152
+ let targetAddress = opts.address;
1153
+ if (!targetAddress) {
1154
+ const devices = loadIDotMatrixDevices();
1155
+ if (devices.length === 0) {
1156
+ log('No device specified and none configured. Use `agentdeck idotmatrix add <address>`.');
1157
+ process.exit(1);
1158
+ }
1159
+ targetAddress = devices[0].address;
1160
+ }
1161
+ const distPath = dirname(fileURLToPath(import.meta.url));
1162
+ const projectRoot = join(distPath, '..', '..');
1163
+ const venvPython = join(projectRoot, '.venv', 'bin', 'python');
1164
+ const brightnessScript = join(projectRoot, 'bridge', 'src', 'idotmatrix', 'brightness.py');
1165
+ log(`Setting brightness of ${targetAddress} to ${value}%...`);
1166
+ const py = spawn(venvPython, [brightnessScript, '-a', targetAddress, '-b', String(value)], {
1167
+ stdio: 'inherit'
1168
+ });
1169
+ py.on('close', (code) => {
1170
+ if (code === 0) {
1171
+ log('Brightness updated successfully.');
1172
+ }
1173
+ else {
1174
+ log(`Failed to update brightness, process exited with code ${code}`);
1175
+ }
1176
+ });
1177
+ });
1178
+ idotmatrix
1179
+ .command('sync [address]')
1180
+ .description('Sync AgentDeck dashboard frames to iDotMatrix BLE device')
1181
+ .option('-p, --port <port>', 'Bridge server port')
1182
+ .option('-b, --brightness <value>', 'Override brightness (5-100)')
1183
+ .option('--boost <value>', 'Software brightness boost factor (default: 1.6)')
1184
+ .action(async (addressOpt, opts) => {
1185
+ const { loadIDotMatrixDevices } = await import('./idotmatrix/idotmatrix-settings.js');
1186
+ const { readDaemonInfo, findDaemonPort } = await import('./session-registry.js');
1187
+ const { dirname } = await import('path');
1188
+ const { fileURLToPath } = await import('url');
1189
+ const { spawn } = await import('child_process');
1190
+ let targetAddress = addressOpt;
1191
+ let defaultBrightness = 100;
1192
+ const devices = loadIDotMatrixDevices();
1193
+ if (!targetAddress) {
1194
+ if (devices.length === 0) {
1195
+ log('No device specified and none configured. Use `agentdeck idotmatrix add <address>`.');
1196
+ process.exit(1);
1197
+ }
1198
+ targetAddress = devices[0].address;
1199
+ defaultBrightness = devices[0].brightness ?? 100;
1200
+ log(`Using first configured device: ${targetAddress}`);
1201
+ }
1202
+ else {
1203
+ const matched = devices.find(d => d.address.toLowerCase() === targetAddress.toLowerCase());
1204
+ if (matched) {
1205
+ defaultBrightness = matched.brightness ?? 100;
1206
+ }
1207
+ }
1208
+ const brightness = opts.brightness ? parseInt(opts.brightness, 10) : defaultBrightness;
1209
+ const boost = opts.boost || '1.6';
1210
+ const info = readDaemonInfo();
1211
+ const port = opts.port != null
1212
+ ? parseInt(opts.port, 10)
1213
+ : (info?.httpPort ?? info?.port ?? findDaemonPort() ?? BRIDGE_WS_PORT);
1214
+ const url = `http://127.0.0.1:${port}`;
1215
+ const distPath = dirname(fileURLToPath(import.meta.url));
1216
+ const projectRoot = join(distPath, '..', '..');
1217
+ const venvPython = join(projectRoot, '.venv', 'bin', 'python');
1218
+ const syncScript = join(projectRoot, 'bridge', 'src', 'idotmatrix', 'sync.py');
1219
+ log(`Starting BLE sync client linking to bridge at ${url} (brightness ${brightness}%, boost ${boost}x)...`);
1220
+ const py = spawn(venvPython, [syncScript, '-a', targetAddress, '-u', url, '-b', String(brightness), '--boost', String(boost)], {
1221
+ stdio: 'inherit'
1222
+ });
1223
+ py.on('close', (code) => {
1224
+ log(`Sync process exited with code ${code}`);
1225
+ });
1226
+ });
1227
+ // ===== Divoom Timebox Mini Light commands =====
1228
+ const timebox = program.command('timebox').description('Manage Divoom Timebox Mini devices (Bluetooth LE)');
1229
+ timebox
1230
+ .command('scan')
1231
+ .description('Discover BLE TimeBox-mini-light peripherals')
1232
+ .action(async () => {
1233
+ const paths = resolveTimeboxSyncPaths();
1234
+ log('Scanning for BLE devices (5 seconds)...');
1235
+ const py = spawn(paths.python, [paths.scanScript]);
1236
+ let stdoutData = '';
1237
+ py.stdout.on('data', (data) => {
1238
+ stdoutData += data.toString();
1239
+ });
1240
+ py.on('close', (code) => {
1241
+ if (code !== 0) {
1242
+ log('BLE scan failed (is bleak installed in .venv?).');
1243
+ return;
1244
+ }
1245
+ try {
1246
+ const devices = JSON.parse(stdoutData.trim());
1247
+ if (devices.error) {
1248
+ log(`BLE scan error: ${devices.error}`);
1249
+ return;
1250
+ }
1251
+ if (!Array.isArray(devices) || devices.length === 0) {
1252
+ log('No BLE devices found.');
1253
+ return;
1254
+ }
1255
+ log('\nFound BLE devices:');
1256
+ for (const d of devices) {
1257
+ const prefix = d.is_timebox ? '★ [TimeBox-mini-light] ' : ' ';
1258
+ const rssi = typeof d.rssi === 'number' ? ` ${d.rssi}dBm` : '';
1259
+ log(`${prefix}${d.name} (${d.address})${rssi}`);
1260
+ }
1261
+ log('\nAdd the starred BLE device with: agentdeck timebox add <address>');
1262
+ }
1263
+ catch {
1264
+ log(`Failed to parse BLE scan output: ${stdoutData}`);
1265
+ }
1266
+ });
1267
+ });
1268
+ timebox
1269
+ .command('add <address>')
1270
+ .description('Add a Timebox device by its BLE address')
1271
+ .option('-n, --name <name>', 'Device name', 'Timebox Mini')
1272
+ .option('-b, --brightness <value>', 'Brightness 0-100', '80')
1273
+ .action(async (address, opts) => {
1274
+ const { addTimeboxDevice } = await import('./timebox/timebox-settings.js');
1275
+ const parsed = parseInt(opts.brightness, 10);
1276
+ const brightness = Number.isFinite(parsed) ? Math.max(0, Math.min(100, parsed)) : 80;
1277
+ if (addTimeboxDevice({ address, name: opts.name, brightness })) {
1278
+ log(`Added ${opts.name} (BLE ${address}) with brightness ${brightness}% to settings.`);
1279
+ log('Restart the daemon to start automatic Timebox sync.');
1280
+ }
1281
+ else {
1282
+ log(`Device ${address} already exists (or has no valid address).`);
1283
+ }
1284
+ });
1285
+ timebox
1286
+ .command('list')
1287
+ .description('List configured Timebox devices')
1288
+ .action(async () => {
1289
+ const { loadTimeboxDevices, deviceId } = await import('./timebox/timebox-settings.js');
1290
+ const devices = loadTimeboxDevices();
1291
+ if (devices.length === 0) {
1292
+ log('No Timebox devices configured. Run `agentdeck timebox scan`, then `agentdeck timebox add <address>`.');
1293
+ return;
1294
+ }
1295
+ log(`${devices.length} device(s):`);
1296
+ for (const d of devices) {
1297
+ const brightnessInfo = d.brightness !== undefined ? ` brightness=${d.brightness}%` : '';
1298
+ log(` [BLE] ${d.name || 'Timebox Mini'} (${deviceId(d)})${brightnessInfo}`);
1299
+ }
1300
+ });
1301
+ timebox
1302
+ .command('remove <address>')
1303
+ .description('Remove a Timebox device by BLE address')
1304
+ .action(async (target) => {
1305
+ const { removeTimeboxDevice } = await import('./timebox/timebox-settings.js');
1306
+ if (removeTimeboxDevice(target)) {
1307
+ log(`Removed ${target}.`);
1308
+ }
1309
+ else {
1310
+ log(`Device ${target} not found.`);
1311
+ }
1312
+ });
1313
+ /**
1314
+ * Spawn the BLE sync writer for a one-shot test or a continuous sync,
1315
+ * resolving the target device from CLI args / configured devices.
1316
+ */
1317
+ async function runTimeboxSync(targetOpt, opts, once) {
1318
+ const { loadTimeboxDevices, deviceId } = await import('./timebox/timebox-settings.js');
1319
+ const { readDaemonInfo, findDaemonPort } = await import('./session-registry.js');
1320
+ const devices = loadTimeboxDevices();
1321
+ let device = targetOpt
1322
+ ? devices.find((d) => deviceId(d).toLowerCase() === targetOpt.toLowerCase())
1323
+ : devices[0];
1324
+ // Allow an ad-hoc BLE address that isn't in settings.
1325
+ if (!device && targetOpt) {
1326
+ device = { address: targetOpt };
1327
+ }
1328
+ if (!device) {
1329
+ log('No device specified and none configured. Use `agentdeck timebox scan` and `agentdeck timebox add <address>`.');
1330
+ process.exit(1);
1331
+ }
1332
+ const id = deviceId(device);
1333
+ if (!targetOpt)
1334
+ log(`Using first configured device: ${id} (BLE)`);
1335
+ const defaultBrightness = device.brightness ?? 80;
1336
+ const parsedBrightness = opts.brightness ? parseInt(opts.brightness, 10) : defaultBrightness;
1337
+ const brightness = Number.isFinite(parsedBrightness) ? Math.max(0, Math.min(100, parsedBrightness)) : defaultBrightness;
1338
+ const info = readDaemonInfo();
1339
+ const bridgePort = opts.bridgePort != null
1340
+ ? parseInt(opts.bridgePort, 10)
1341
+ : (info?.httpPort ?? info?.port ?? findDaemonPort() ?? BRIDGE_WS_PORT);
1342
+ const url = `http://127.0.0.1:${bridgePort}`;
1343
+ const paths = resolveTimeboxSyncPaths();
1344
+ const args = [paths.bleScript, '--address', id, '--url', url, '--brightness', String(brightness), ...(once ? ['--once'] : [])];
1345
+ log(`${once ? 'Sending one Timebox frame' : 'Starting Timebox sync'} via BLE ${id} from ${url} (brightness ${brightness}%)...`);
1346
+ const py = spawn(paths.python, args, { stdio: 'inherit' });
1347
+ py.on('close', (code) => {
1348
+ if (once) {
1349
+ if (code === 0) {
1350
+ log('Timebox test frame sent.');
1351
+ }
1352
+ else {
1353
+ log(`Timebox test exited with code ${code}`);
1354
+ process.exit(code ?? 1);
1355
+ }
1356
+ }
1357
+ else {
1358
+ log(`Timebox sync process exited with code ${code}`);
1359
+ }
1360
+ });
1361
+ }
1362
+ timebox
1363
+ .command('test [target]')
1364
+ .description('Send one AgentDeck frame to a Timebox Mini (BLE)')
1365
+ .option('-p, --bridge-port <port>', 'Bridge server port')
1366
+ .option('-b, --brightness <value>', 'Override brightness 0-100')
1367
+ .action((targetOpt, opts) => runTimeboxSync(targetOpt, opts, true));
1368
+ timebox
1369
+ .command('sync [target]')
1370
+ .description('Sync AgentDeck dashboard frames to a Timebox Mini (BLE)')
1371
+ .option('-p, --bridge-port <port>', 'Bridge server port')
1372
+ .option('-b, --brightness <value>', 'Override brightness 0-100')
1373
+ .action((targetOpt, opts) => runTimeboxSync(targetOpt, opts, false));
528
1374
  // ===== Pixoo commands =====
529
1375
  const pixoo = program.command('pixoo').description('Manage Pixoo64 LED matrix devices');
530
1376
  pixoo
@@ -735,29 +1581,11 @@ program
735
1581
  const { startDashboard } = await import('./tui/dashboard.js');
736
1582
  await startDashboard(opts);
737
1583
  });
738
- // ===== Backward compat: `agentdeck start` `agentdeck daemon start` =====
739
- program
740
- .command('start', { hidden: true })
741
- .description('(legacy) Start daemon in foreground — use `agentdeck daemon start`')
742
- .option('-p, --port <port>', 'Server port', String(BRIDGE_WS_PORT))
743
- .option('-d, --debug', 'Enable debug logging')
744
- .action(async (opts) => {
745
- // Legacy: runs foreground (LaunchAgent plist compatibility)
746
- const { findExistingDaemon } = await import('./session-registry.js');
747
- const existing = findExistingDaemon();
748
- if (existing) {
749
- log(`Daemon already running on port ${existing.port} (PID ${existing.pid}).`);
750
- process.exit(0);
751
- }
752
- const { startDaemon } = await import('./daemon-server.js');
753
- await startDaemon({
754
- port: parseInt(opts.port, 10),
755
- debug: opts.debug,
756
- });
757
- });
1584
+ // Note: there is no top-level `agentdeck start`. The daemon is started via
1585
+ // `agentdeck daemon start` (what the LaunchAgent/install paths already use —
1586
+ // see cli.ts ~368/413).
758
1587
  // ===== Default command (no args) =====
759
- // If no command specified, show status or help
760
- program.action(async () => {
1588
+ async function showDefaultStatusOrHelp() {
761
1589
  const { listActive } = await import('./session-registry.js');
762
1590
  const sessions = listActive();
763
1591
  if (sessions.length === 0) {
@@ -771,7 +1599,7 @@ program.action(async () => {
771
1599
  log(` [${type}] :${s.port} — ${s.projectName} (PID ${s.pid}, ${age}s)`);
772
1600
  }
773
1601
  log('\nRun `agentdeck --help` for all commands.');
774
- });
1602
+ }
775
1603
  // ===== APME commands =====
776
1604
  const apme = program.command('apme').description('Agent Performance Monitoring & Evaluation');
777
1605
  apme
@@ -1042,25 +1870,6 @@ apme
1042
1870
  await apme.runner.drain();
1043
1871
  log(`Done. ${completed} run(s) evaluated.`);
1044
1872
  });
1045
- apme
1046
- .command('tune')
1047
- .description('Manually trigger rubric auto-tuning')
1048
- .action(async () => {
1049
- const { initApme } = await import('./apme/index.js');
1050
- const apme = await initApme();
1051
- if (!apme) {
1052
- log('APME not available');
1053
- process.exit(1);
1054
- }
1055
- log('Running rubric tuner...');
1056
- const outcome = await apme.tuner.tune();
1057
- if (outcome.accepted) {
1058
- log(`Accepted rubric v${outcome.newVersion} (correlation ${outcome.baselineCorrelation?.toFixed(2) ?? '?'} → ${outcome.proposedCorrelation?.toFixed(2) ?? '?'})`);
1059
- }
1060
- else {
1061
- log(`Rejected: ${outcome.reason}`);
1062
- }
1063
- });
1064
1873
  apme
1065
1874
  .command('vibe <runId> <verdict>')
1066
1875
  .description('Submit vibe feedback (approve|reject|neutral)')
@@ -1098,9 +1907,12 @@ apme
1098
1907
  });
1099
1908
  apme
1100
1909
  .command('export')
1101
- .description('Export runs + evals as JSONL')
1102
- .option('-n, --limit <n>', 'Number of runs', '100')
1910
+ .description('Export runs (default) or tasks as JSONL/CSV — dataset for offline analysis or meta-eval')
1911
+ .option('-n, --limit <n>', 'Maximum rows', '100')
1103
1912
  .option('-o, --output <file>', 'Output file (default: stdout)')
1913
+ .option('-b, --by <unit>', 'Row unit: run | task', 'run')
1914
+ .option('-f, --format <fmt>', 'Format: jsonl | csv', 'jsonl')
1915
+ .option('--closed-only', 'Tasks only: skip in-progress tasks (no ended_at)', false)
1104
1916
  .action(async (opts) => {
1105
1917
  const { initApme } = await import('./apme/index.js');
1106
1918
  const { writeFileSync: wfs } = await import('fs');
@@ -1109,22 +1921,117 @@ apme
1109
1921
  log('APME not available');
1110
1922
  process.exit(1);
1111
1923
  }
1112
- const runs = apme.store.listRuns({ limit: parseInt(opts.limit, 10) || 100 });
1113
- const lines = [];
1114
- for (const r of runs) {
1115
- const evals = apme.store.listEvalsForRun(r.id);
1116
- const vibe = apme.store.latestVibeForRun(r.id);
1117
- lines.push(JSON.stringify({ ...r, evals, vibe }));
1924
+ const limit = parseInt(opts.limit, 10) || 100;
1925
+ const by = opts.by === 'task' ? 'task' : 'run';
1926
+ const format = opts.format === 'csv' ? 'csv' : 'jsonl';
1927
+ let rows;
1928
+ if (by === 'task') {
1929
+ // Task-unit dataset: one row per task (group of turns between
1930
+ // boundary signals). This is the dataset format that supports
1931
+ // meaningful agent evaluation per the user requirement —
1932
+ // "의미 있는 단위의 세션을 데이터셋으로 저장".
1933
+ const tasks = apme.store.listAllTasks({ limit, closedOnly: !!opts.closedOnly });
1934
+ rows = tasks.map((t) => {
1935
+ const turns = apme.store.listTurnsForTask(t.id);
1936
+ const evals = apme.store.listEvalsForTask(t.id);
1937
+ const run = apme.store.getRun(t.runId);
1938
+ const vibe = apme.store.latestVibeForRun(t.runId);
1939
+ const axes = {};
1940
+ for (const e of evals) {
1941
+ if (e.layer === 'task_judge')
1942
+ axes[e.metric] = e.score;
1943
+ }
1944
+ const overallEval = evals.find((e) => e.layer === 'task_judge' && e.metric === 'overall');
1945
+ let reasoning = null;
1946
+ let done = null;
1947
+ let missed = null;
1948
+ if (overallEval?.raw) {
1949
+ try {
1950
+ const parsed = JSON.parse(overallEval.raw);
1951
+ reasoning = parsed.reasoning ?? null;
1952
+ done = parsed.done ?? null;
1953
+ missed = parsed.missed ?? null;
1954
+ }
1955
+ catch { /* ignore parse failure */ }
1956
+ }
1957
+ const toolCalls = turns.reduce((n, tu) => n + (tu.tool_calls ?? 0), 0);
1958
+ const filesModified = turns.reduce((n, tu) => n + (tu.files_modified ?? 0), 0);
1959
+ const filesCreated = turns.reduce((n, tu) => n + (tu.files_created ?? 0), 0);
1960
+ const firstPrompt = turns[0]?.prompt ?? null;
1961
+ const lastResponse = turns[turns.length - 1]?.response ?? null;
1962
+ return {
1963
+ taskId: t.id,
1964
+ runId: t.runId,
1965
+ taskIndex: t.taskIndex,
1966
+ agentType: run?.agentType ?? null,
1967
+ modelId: run?.modelId ?? null,
1968
+ projectName: run?.projectName ?? null,
1969
+ taskCategory: t.taskCategory ?? run?.taskCategory ?? null,
1970
+ boundarySignal: t.boundarySignal,
1971
+ startedAt: t.startedAt,
1972
+ endedAt: t.endedAt,
1973
+ durationSec: t.endedAt ? Math.round((t.endedAt - t.startedAt) / 1000) : null,
1974
+ turnCount: turns.length,
1975
+ firstTurnIndex: t.firstTurnIndex,
1976
+ lastTurnIndex: t.lastTurnIndex,
1977
+ toolCalls,
1978
+ filesModified,
1979
+ filesCreated,
1980
+ firstPrompt: firstPrompt ? firstPrompt.slice(0, 500) : null,
1981
+ lastResponse: lastResponse ? lastResponse.slice(0, 500) : null,
1982
+ summary: t.summary ?? null,
1983
+ composite: t.compositeScore ?? null,
1984
+ outcome: t.outcome ?? null,
1985
+ axes,
1986
+ overall: axes.overall ?? null,
1987
+ reasoning,
1988
+ done,
1989
+ missed,
1990
+ vibeVerdict: vibe?.verdict ?? null,
1991
+ vibeNote: vibe?.note ?? null,
1992
+ };
1993
+ });
1118
1994
  }
1995
+ else {
1996
+ const runRows = apme.store.listRuns({ limit });
1997
+ rows = runRows.map((r) => {
1998
+ const evals = apme.store.listEvalsForRun(r.id);
1999
+ const vibe = apme.store.latestVibeForRun(r.id);
2000
+ return { ...r, evals, vibe };
2001
+ });
2002
+ }
2003
+ const output = format === 'csv' ? rowsToCsv(rows) : rowsToJsonl(rows);
1119
2004
  if (opts.output) {
1120
- wfs(opts.output, lines.join('\n') + '\n', 'utf-8');
1121
- log(`Exported ${runs.length} run(s) to ${opts.output}`);
2005
+ wfs(opts.output, output, 'utf-8');
2006
+ log(`Exported ${rows.length} ${by}(s) to ${opts.output} (${format})`);
1122
2007
  }
1123
2008
  else {
1124
- for (const line of lines)
1125
- process.stdout.write(line + '\n');
2009
+ process.stdout.write(output);
1126
2010
  }
1127
2011
  });
2012
+ function rowsToJsonl(rows) {
2013
+ return rows.map((r) => JSON.stringify(r)).join('\n') + (rows.length ? '\n' : '');
2014
+ }
2015
+ function rowsToCsv(rows) {
2016
+ if (rows.length === 0)
2017
+ return '';
2018
+ const headerSet = new Set();
2019
+ for (const r of rows)
2020
+ for (const k of Object.keys(r))
2021
+ headerSet.add(k);
2022
+ const headers = Array.from(headerSet);
2023
+ const escape = (v) => {
2024
+ if (v === null || v === undefined)
2025
+ return '';
2026
+ const s = typeof v === 'object' ? JSON.stringify(v) : String(v);
2027
+ return /[,"\n]/.test(s) ? '"' + s.replace(/"/g, '""') + '"' : s;
2028
+ };
2029
+ const lines = [
2030
+ headers.join(','),
2031
+ ...rows.map((r) => headers.map((h) => escape(r[h])).join(',')),
2032
+ ];
2033
+ return lines.join('\n') + '\n';
2034
+ }
1128
2035
  apme
1129
2036
  .command('rubric')
1130
2037
  .description('Show current judge rubric')
@@ -1234,5 +2141,26 @@ apme
1234
2141
  log(` ${c.taskCategory.padEnd(16)} ${c.modelId.slice(0, 22).padEnd(22)} ${String(c.runs).padEnd(6)} ${score.padEnd(8)} ${tests.padEnd(8)} ${cost}`);
1235
2142
  }
1236
2143
  });
1237
- program.parse();
2144
+ function isMainModule() {
2145
+ if (!process.argv[1])
2146
+ return false;
2147
+ const thisFile = fileURLToPath(import.meta.url);
2148
+ try {
2149
+ return realpathSync(thisFile) === realpathSync(process.argv[1]);
2150
+ }
2151
+ catch {
2152
+ return thisFile === process.argv[1];
2153
+ }
2154
+ }
2155
+ export async function runCli(argv = process.argv) {
2156
+ if (argv.slice(2).length === 0) {
2157
+ await showDefaultStatusOrHelp();
2158
+ return;
2159
+ }
2160
+ program.parse(argv);
2161
+ }
2162
+ export { program };
2163
+ if (isMainModule()) {
2164
+ void runCli();
2165
+ }
1238
2166
  //# sourceMappingURL=cli.js.map