@sirenodeck/sirenodeck 0.1.1-alpha.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 (485) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +69 -0
  3. package/bin/_wrapper-shared.js +42 -0
  4. package/bin/dev.js +95 -0
  5. package/bin/sirenodeck.js +42 -0
  6. package/config/default-config.yml +117 -0
  7. package/config/starter-config.yml +54 -0
  8. package/dist/main.mjs +64702 -0
  9. package/package.json +140 -0
  10. package/src/action/executor.ts +156 -0
  11. package/src/action/index.ts +7 -0
  12. package/src/addon/api-types.ts +15 -0
  13. package/src/addon/api.ts +461 -0
  14. package/src/addon/check-runner.ts +84 -0
  15. package/src/addon/index.ts +5 -0
  16. package/src/addon/loader.ts +441 -0
  17. package/src/addon/registry.ts +230 -0
  18. package/src/addon/spec.ts +69 -0
  19. package/src/api/addon.ts +33 -0
  20. package/src/api/asset-url.ts +24 -0
  21. package/src/api/index.ts +50 -0
  22. package/src/api/protocol-internal.ts +656 -0
  23. package/src/api/protocol.ts +2 -0
  24. package/src/api/react/index.ts +11 -0
  25. package/src/api/react/registry.ts +91 -0
  26. package/src/api/react/use-addon-channel.ts +26 -0
  27. package/src/api/react/use-button-state.tsx +23 -0
  28. package/src/api/react/use-deck.ts +24 -0
  29. package/src/assets/logo.png +0 -0
  30. package/src/assets/logo72x72.png +0 -0
  31. package/src/assets/logoFull.png +0 -0
  32. package/src/builtin-addons/brightness/README.md +51 -0
  33. package/src/builtin-addons/brightness/buttons/brightness/backend.ts +13 -0
  34. package/src/builtin-addons/brightness/buttons/brightness/config.ts +10 -0
  35. package/src/builtin-addons/brightness/buttons/brightness/frontend.tsx +50 -0
  36. package/src/builtin-addons/brightness/domain/macos.ts +46 -0
  37. package/src/builtin-addons/brightness/index.d.ts +5 -0
  38. package/src/builtin-addons/brightness/index.ts +20 -0
  39. package/src/builtin-addons/brightness/poller.ts +27 -0
  40. package/src/builtin-addons/brightness/providers/darwin.ts +107 -0
  41. package/src/builtin-addons/brightness/providers/index.ts +81 -0
  42. package/src/builtin-addons/brightness/providers/linux.ts +108 -0
  43. package/src/builtin-addons/brightness/providers/windows.ts +77 -0
  44. package/src/builtin-addons/brightness/sirenodeck.json +6 -0
  45. package/src/builtin-addons/coding-agents/assets/claude-code.svg +1 -0
  46. package/src/builtin-addons/coding-agents/assets/opencode-dark-square.svg +1 -0
  47. package/src/builtin-addons/coding-agents/buttons/agent/backend.ts +82 -0
  48. package/src/builtin-addons/coding-agents/buttons/agent/config.ts +13 -0
  49. package/src/builtin-addons/coding-agents/buttons/agent/frontend.tsx +130 -0
  50. package/src/builtin-addons/coding-agents/buttons/agent/pages.tsx +150 -0
  51. package/src/builtin-addons/coding-agents/buttons/summary/backend.ts +48 -0
  52. package/src/builtin-addons/coding-agents/buttons/summary/config.ts +11 -0
  53. package/src/builtin-addons/coding-agents/buttons/summary/frontend.tsx +254 -0
  54. package/src/builtin-addons/coding-agents/decks/agents.ts +69 -0
  55. package/src/builtin-addons/coding-agents/global/backend.ts +402 -0
  56. package/src/builtin-addons/coding-agents/global-entry.ts +7 -0
  57. package/src/builtin-addons/coding-agents/hook/script.ts +123 -0
  58. package/src/builtin-addons/coding-agents/index.ts +15 -0
  59. package/src/builtin-addons/coding-agents/manifest.ts +115 -0
  60. package/src/builtin-addons/coding-agents/providers/claude-code.ts +470 -0
  61. package/src/builtin-addons/coding-agents/providers/claude-instances.ts +184 -0
  62. package/src/builtin-addons/coding-agents/providers/opencode-instances.ts +108 -0
  63. package/src/builtin-addons/coding-agents/providers/opencode.ts +636 -0
  64. package/src/builtin-addons/coding-agents/providers/registry.ts +120 -0
  65. package/src/builtin-addons/coding-agents/providers/spawn.ts +112 -0
  66. package/src/builtin-addons/coding-agents/shared/claude-hook-events.ts +55 -0
  67. package/src/builtin-addons/coding-agents/shared/claude-status.ts +236 -0
  68. package/src/builtin-addons/coding-agents/shared/live-count.ts +39 -0
  69. package/src/builtin-addons/coding-agents/shared/notifier.ts +58 -0
  70. package/src/builtin-addons/coding-agents/shared/opencode-status.ts +82 -0
  71. package/src/builtin-addons/coding-agents/shared/snapshot.ts +66 -0
  72. package/src/builtin-addons/coding-agents/shared/state.ts +72 -0
  73. package/src/builtin-addons/coding-agents/sirenodeck.json +6 -0
  74. package/src/builtin-addons/coding-agents/types/types.ts +33 -0
  75. package/src/builtin-addons/core/README.md +89 -0
  76. package/src/builtin-addons/core/buttons/action/backend.ts +7 -0
  77. package/src/builtin-addons/core/buttons/action/config.ts +13 -0
  78. package/src/builtin-addons/core/buttons/action/frontend.tsx +16 -0
  79. package/src/builtin-addons/core/buttons/change-deck/backend.ts +18 -0
  80. package/src/builtin-addons/core/buttons/change-deck/config.ts +15 -0
  81. package/src/builtin-addons/core/buttons/change-deck/frontend.tsx +18 -0
  82. package/src/builtin-addons/core/buttons/media-sample/backend.ts +8 -0
  83. package/src/builtin-addons/core/buttons/media-sample/config.ts +7 -0
  84. package/src/builtin-addons/core/buttons/media-sample/frontend.tsx +2 -0
  85. package/src/builtin-addons/core/buttons/page-nav/backend.ts +28 -0
  86. package/src/builtin-addons/core/buttons/page-nav/config.ts +9 -0
  87. package/src/builtin-addons/core/buttons/page-nav/frontend.tsx +68 -0
  88. package/src/builtin-addons/core/buttons/toggle/backend.ts +119 -0
  89. package/src/builtin-addons/core/buttons/toggle/config.ts +57 -0
  90. package/src/builtin-addons/core/buttons/toggle/frontend.tsx +57 -0
  91. package/src/builtin-addons/core/buttons/toggle/global-service.ts +174 -0
  92. package/src/builtin-addons/core/index.ts +120 -0
  93. package/src/builtin-addons/core/sirenodeck.json +6 -0
  94. package/src/builtin-addons/date-time/README.md +73 -0
  95. package/src/builtin-addons/date-time/buttons/analog-clock/backend.ts +10 -0
  96. package/src/builtin-addons/date-time/buttons/analog-clock/config.ts +4 -0
  97. package/src/builtin-addons/date-time/buttons/analog-clock/frontend.tsx +88 -0
  98. package/src/builtin-addons/date-time/buttons/custom/backend.ts +10 -0
  99. package/src/builtin-addons/date-time/buttons/custom/config.ts +9 -0
  100. package/src/builtin-addons/date-time/buttons/custom/frontend.tsx +26 -0
  101. package/src/builtin-addons/date-time/buttons/date/backend.ts +10 -0
  102. package/src/builtin-addons/date-time/buttons/date/config.ts +10 -0
  103. package/src/builtin-addons/date-time/buttons/date/frontend.tsx +42 -0
  104. package/src/builtin-addons/date-time/buttons/time/backend.ts +10 -0
  105. package/src/builtin-addons/date-time/buttons/time/config.ts +9 -0
  106. package/src/builtin-addons/date-time/buttons/time/frontend.tsx +29 -0
  107. package/src/builtin-addons/date-time/index.d.ts +5 -0
  108. package/src/builtin-addons/date-time/index.ts +30 -0
  109. package/src/builtin-addons/date-time/poller.ts +11 -0
  110. package/src/builtin-addons/date-time/shared/format-date.ts +36 -0
  111. package/src/builtin-addons/date-time/shared/format.ts +105 -0
  112. package/src/builtin-addons/date-time/shared/use-now.ts +10 -0
  113. package/src/builtin-addons/date-time/sirenodeck.json +6 -0
  114. package/src/builtin-addons/emoji-selector/README.md +45 -0
  115. package/src/builtin-addons/emoji-selector/assets/launcher.svg +10 -0
  116. package/src/builtin-addons/emoji-selector/assets/smileys.svg +8 -0
  117. package/src/builtin-addons/emoji-selector/buttons/category/backend.ts +14 -0
  118. package/src/builtin-addons/emoji-selector/buttons/category/config.ts +8 -0
  119. package/src/builtin-addons/emoji-selector/buttons/category/frontend.tsx +19 -0
  120. package/src/builtin-addons/emoji-selector/buttons/emoji/backend.ts +7 -0
  121. package/src/builtin-addons/emoji-selector/buttons/emoji/config.ts +9 -0
  122. package/src/builtin-addons/emoji-selector/buttons/emoji/frontend.tsx +11 -0
  123. package/src/builtin-addons/emoji-selector/buttons/launcher/backend.ts +13 -0
  124. package/src/builtin-addons/emoji-selector/buttons/launcher/config.ts +6 -0
  125. package/src/builtin-addons/emoji-selector/buttons/launcher/frontend.tsx +31 -0
  126. package/src/builtin-addons/emoji-selector/data/categories.ts +1084 -0
  127. package/src/builtin-addons/emoji-selector/decks/index.ts +137 -0
  128. package/src/builtin-addons/emoji-selector/index.d.ts +5 -0
  129. package/src/builtin-addons/emoji-selector/index.ts +29 -0
  130. package/src/builtin-addons/emoji-selector/sirenodeck.json +6 -0
  131. package/src/builtin-addons/emoji-selector/support.ts +49 -0
  132. package/src/builtin-addons/index.ts +13 -0
  133. package/src/builtin-addons/internal-settings/README.md +26 -0
  134. package/src/builtin-addons/internal-settings/backend.ts +58 -0
  135. package/src/builtin-addons/internal-settings/buttons/app-info/backend.ts +7 -0
  136. package/src/builtin-addons/internal-settings/buttons/app-info/config.ts +13 -0
  137. package/src/builtin-addons/internal-settings/buttons/app-info/frontend.tsx +18 -0
  138. package/src/builtin-addons/internal-settings/buttons/brightness/common/frontend.tsx +50 -0
  139. package/src/builtin-addons/internal-settings/buttons/brightness-down/backend.ts +16 -0
  140. package/src/builtin-addons/internal-settings/buttons/brightness-down/config.ts +6 -0
  141. package/src/builtin-addons/internal-settings/buttons/brightness-down/frontend.tsx +8 -0
  142. package/src/builtin-addons/internal-settings/buttons/brightness-up/backend.ts +16 -0
  143. package/src/builtin-addons/internal-settings/buttons/brightness-up/config.ts +6 -0
  144. package/src/builtin-addons/internal-settings/buttons/brightness-up/frontend.tsx +8 -0
  145. package/src/builtin-addons/internal-settings/decks/settings.ts +25 -0
  146. package/src/builtin-addons/internal-settings/index.d.ts +4 -0
  147. package/src/builtin-addons/internal-settings/index.ts +53 -0
  148. package/src/builtin-addons/internal-settings/logo72x72.png +0 -0
  149. package/src/builtin-addons/internal-settings/sirenodeck.json +6 -0
  150. package/src/builtin-addons/media/README.md +45 -0
  151. package/src/builtin-addons/media/backend.ts +143 -0
  152. package/src/builtin-addons/media/buttons/media-player/backend.ts +19 -0
  153. package/src/builtin-addons/media/buttons/media-player/components/MediaStatus.tsx +25 -0
  154. package/src/builtin-addons/media/buttons/media-player/components/MediaSurface.tsx +52 -0
  155. package/src/builtin-addons/media/buttons/media-player/components/ProgressBar.tsx +21 -0
  156. package/src/builtin-addons/media/buttons/media-player/components/status-meta.ts +36 -0
  157. package/src/builtin-addons/media/buttons/media-player/config.ts +4 -0
  158. package/src/builtin-addons/media/buttons/media-player/frontend.tsx +29 -0
  159. package/src/builtin-addons/media/buttons/volume/common/frontend.tsx +52 -0
  160. package/src/builtin-addons/media/buttons/volume/mute/backend.ts +13 -0
  161. package/src/builtin-addons/media/buttons/volume/mute/config.ts +4 -0
  162. package/src/builtin-addons/media/buttons/volume/mute/frontend.tsx +26 -0
  163. package/src/builtin-addons/media/buttons/volume/volume-down/backend.ts +14 -0
  164. package/src/builtin-addons/media/buttons/volume/volume-down/config.ts +6 -0
  165. package/src/builtin-addons/media/buttons/volume/volume-down/frontend.tsx +10 -0
  166. package/src/builtin-addons/media/buttons/volume/volume-up/backend.ts +14 -0
  167. package/src/builtin-addons/media/buttons/volume/volume-up/config.ts +6 -0
  168. package/src/builtin-addons/media/buttons/volume/volume-up/frontend.tsx +8 -0
  169. package/src/builtin-addons/media/checks.ts +104 -0
  170. package/src/builtin-addons/media/executor.ts +36 -0
  171. package/src/builtin-addons/media/index.d.ts +5 -0
  172. package/src/builtin-addons/media/index.ts +53 -0
  173. package/src/builtin-addons/media/progress.ts +16 -0
  174. package/src/builtin-addons/media/providers/darwin.ts +391 -0
  175. package/src/builtin-addons/media/providers/index.ts +27 -0
  176. package/src/builtin-addons/media/providers/linux.ts +196 -0
  177. package/src/builtin-addons/media/providers/types.ts +48 -0
  178. package/src/builtin-addons/media/providers/windows.ts +121 -0
  179. package/src/builtin-addons/media/sirenodeck.json +6 -0
  180. package/src/builtin-addons/media/state.ts +19 -0
  181. package/src/builtin-addons/register-builtins.ts +65 -0
  182. package/src/builtin-addons/session/README.md +14 -0
  183. package/src/builtin-addons/session/buttons/session-info/backend.ts +8 -0
  184. package/src/builtin-addons/session/buttons/session-info/config.ts +4 -0
  185. package/src/builtin-addons/session/buttons/session-info/frontend.tsx +2 -0
  186. package/src/builtin-addons/session/buttons/time/backend.ts +7 -0
  187. package/src/builtin-addons/session/buttons/time/config.ts +6 -0
  188. package/src/builtin-addons/session/buttons/time/frontend.tsx +2 -0
  189. package/src/builtin-addons/session/index.d.ts +5 -0
  190. package/src/builtin-addons/session/index.ts +18 -0
  191. package/src/builtin-addons/session/sirenodeck.json +6 -0
  192. package/src/builtin-addons/system-status/README.md +169 -0
  193. package/src/builtin-addons/system-status/buttons/_shared.ts +131 -0
  194. package/src/builtin-addons/system-status/buttons/system-status/frontend.tsx +224 -0
  195. package/src/builtin-addons/system-status/buttons/system-status/schemas.ts +67 -0
  196. package/src/builtin-addons/system-status/domain/catalog.ts +53 -0
  197. package/src/builtin-addons/system-status/domain/chart-sampler.ts +58 -0
  198. package/src/builtin-addons/system-status/domain/display-metrics.ts +128 -0
  199. package/src/builtin-addons/system-status/domain/index.ts +10 -0
  200. package/src/builtin-addons/system-status/domain/live-metrics.ts +1021 -0
  201. package/src/builtin-addons/system-status/domain/metric-ids.ts +0 -0
  202. package/src/builtin-addons/system-status/index.ts +16 -0
  203. package/src/builtin-addons/system-status/manifest.ts +105 -0
  204. package/src/builtin-addons/system-status/shared/metrics-catalog.ts +316 -0
  205. package/src/builtin-addons/system-status/sirenodeck.json +6 -0
  206. package/src/builtin-addons/test-buildin/decks/action-buttons-test.ts +56 -0
  207. package/src/builtin-addons/test-buildin/index.d.ts +5 -0
  208. package/src/builtin-addons/test-buildin/index.ts +16 -0
  209. package/src/builtin-addons/value-display/README.md +59 -0
  210. package/src/builtin-addons/value-display/backend.ts +86 -0
  211. package/src/builtin-addons/value-display/buttons/value-display/backend.ts +17 -0
  212. package/src/builtin-addons/value-display/buttons/value-display/config.ts +36 -0
  213. package/src/builtin-addons/value-display/buttons/value-display/frontend.tsx +54 -0
  214. package/src/builtin-addons/value-display/index.d.ts +5 -0
  215. package/src/builtin-addons/value-display/index.ts +23 -0
  216. package/src/builtin-addons/value-display/sirenodeck.json +6 -0
  217. package/src/builtin-addons/weather/README.md +46 -0
  218. package/src/builtin-addons/weather/backend.ts +121 -0
  219. package/src/builtin-addons/weather/buttons/weather/backend.ts +27 -0
  220. package/src/builtin-addons/weather/buttons/weather/config.ts +29 -0
  221. package/src/builtin-addons/weather/buttons/weather/frontend.tsx +63 -0
  222. package/src/builtin-addons/weather/buttons/weather/icons.tsx +88 -0
  223. package/src/builtin-addons/weather/buttons/weather/pages.tsx +109 -0
  224. package/src/builtin-addons/weather/domain/codes.ts +35 -0
  225. package/src/builtin-addons/weather/domain/fetch.ts +47 -0
  226. package/src/builtin-addons/weather/index.d.ts +5 -0
  227. package/src/builtin-addons/weather/index.ts +23 -0
  228. package/src/builtin-addons/weather/poller.ts +70 -0
  229. package/src/builtin-addons/weather/provider/city-key.ts +9 -0
  230. package/src/builtin-addons/weather/provider/codes.ts +35 -0
  231. package/src/builtin-addons/weather/provider/fetch.ts +97 -0
  232. package/src/builtin-addons/weather/provider/geocode.ts +67 -0
  233. package/src/builtin-addons/weather/provider/types.ts +42 -0
  234. package/src/builtin-addons/weather/sirenodeck.json +6 -0
  235. package/src/cli/coding-agents-onboarding.ts +434 -0
  236. package/src/cli/commands/addon-decks.ts +441 -0
  237. package/src/cli/commands/addon-registry.ts +639 -0
  238. package/src/cli/commands/emulator-mode.ts +289 -0
  239. package/src/cli/commands/install.ts +128 -0
  240. package/src/cli/commands/logs.ts +74 -0
  241. package/src/cli/commands/network-bind.ts +111 -0
  242. package/src/cli/commands/pipeline/helpers.ts +75 -0
  243. package/src/cli/commands/pipeline/preflight.ts +20 -0
  244. package/src/cli/commands/port-identity.ts +152 -0
  245. package/src/cli/commands/reload.ts +37 -0
  246. package/src/cli/commands/restart.ts +139 -0
  247. package/src/cli/commands/run.ts +2270 -0
  248. package/src/cli/commands/service/install.ts +121 -0
  249. package/src/cli/commands/service/render-template.ts +158 -0
  250. package/src/cli/commands/service-manager.ts +184 -0
  251. package/src/cli/commands/spawn-daemon.ts +251 -0
  252. package/src/cli/commands/start.ts +1061 -0
  253. package/src/cli/commands/status.ts +110 -0
  254. package/src/cli/commands/stop.ts +121 -0
  255. package/src/cli/commands/subprocess-supervisor.ts +203 -0
  256. package/src/cli/commands/system-requirements.ts +699 -0
  257. package/src/cli/cwd.ts +28 -0
  258. package/src/cli/first-run.ts +149 -0
  259. package/src/cli/http-server.ts +244 -0
  260. package/src/cli/index.ts +355 -0
  261. package/src/cli/install-root.ts +26 -0
  262. package/src/cli/main.ts +194 -0
  263. package/src/cli/package-manager.ts +175 -0
  264. package/src/cli/probe-deps.ts +45 -0
  265. package/src/cli/prompt.ts +22 -0
  266. package/src/cli/startup-display.ts +452 -0
  267. package/src/config/builtin-icons.ts +57 -0
  268. package/src/config/config-diff.ts +54 -0
  269. package/src/config/discovery.ts +64 -0
  270. package/src/config/icon-resolver.ts +90 -0
  271. package/src/config/include-resolver.ts +202 -0
  272. package/src/config/index.ts +9 -0
  273. package/src/config/loader.ts +187 -0
  274. package/src/config/mutation.ts +514 -0
  275. package/src/config/reference-expander.ts +84 -0
  276. package/src/config/schemas.ts +223 -0
  277. package/src/config/validation.ts +254 -0
  278. package/src/core/asset-id.ts +21 -0
  279. package/src/core/gesture-state.ts +344 -0
  280. package/src/core/icon-asset-registry.ts +127 -0
  281. package/src/core/icon-source.ts +86 -0
  282. package/src/core/index.ts +30 -0
  283. package/src/core/mime.ts +16 -0
  284. package/src/core/pagination.ts +90 -0
  285. package/src/core/pub-sub.ts +126 -0
  286. package/src/core/settings.ts +153 -0
  287. package/src/core/store.ts +88 -0
  288. package/src/core/watcher.ts +73 -0
  289. package/src/deck/addon-handler-bridge.ts +516 -0
  290. package/src/deck/deck-config.ts +271 -0
  291. package/src/deck/editor-surfaces.ts +87 -0
  292. package/src/deck/host-context.ts +18 -0
  293. package/src/deck/index.ts +102 -0
  294. package/src/deck/macro-parse.ts +182 -0
  295. package/src/deck/methods.ts +348 -0
  296. package/src/deck/paginate-deck.ts +98 -0
  297. package/src/deck/position-buttons.ts +57 -0
  298. package/src/deck/runtime/index.ts +5 -0
  299. package/src/deck/runtime/runtime.ts +1060 -0
  300. package/src/deck/runtime-subscriptions.ts +45 -0
  301. package/src/deck/system-back-injection.ts +62 -0
  302. package/src/deck/system-buttons/registry.tsx +97 -0
  303. package/src/deck/system-buttons/types.ts +14 -0
  304. package/src/device/black-frame.ts +73 -0
  305. package/src/device/index.ts +13 -0
  306. package/src/device/linux-udev.ts +36 -0
  307. package/src/device/models.ts +89 -0
  308. package/src/device/reconnecting.ts +193 -0
  309. package/src/device/registry.ts +38 -0
  310. package/src/device/stream-deck.ts +155 -0
  311. package/src/index.ts +74 -0
  312. package/src/outputClient/emulator.ts +357 -0
  313. package/src/outputClient/index.ts +26 -0
  314. package/src/outputClient/open-browser.ts +26 -0
  315. package/src/outputClient/real.ts +393 -0
  316. package/src/outputClient/types.ts +83 -0
  317. package/src/render/browser-renderer.ts +240 -0
  318. package/src/render/buffer-hash.ts +20 -0
  319. package/src/render/editor-handler.ts +197 -0
  320. package/src/render/emulator-server.ts +128 -0
  321. package/src/render/icon-source-resolver.ts +69 -0
  322. package/src/render/index.ts +16 -0
  323. package/src/render/protocol.ts +61 -0
  324. package/src/render/push-raw-image.ts +100 -0
  325. package/src/render/screenshot-cadence.ts +87 -0
  326. package/src/render/state-publisher.ts +110 -0
  327. package/src/render/vite-server.ts +138 -0
  328. package/src/render/ws-bridge.ts +315 -0
  329. package/src/system/device-selection.ts +80 -0
  330. package/src/system/glob-match.ts +74 -0
  331. package/src/system/providers/active-app/darwin.ts +117 -0
  332. package/src/system/providers/active-app/linux.ts +245 -0
  333. package/src/system/providers/active-app/wayland-gnome.ts +303 -0
  334. package/src/system/providers/active-app/windows.ts +94 -0
  335. package/src/system/providers/active-app.ts +97 -0
  336. package/src/system/providers/clipboard/darwin.ts +65 -0
  337. package/src/system/providers/clipboard/linux.ts +163 -0
  338. package/src/system/providers/clipboard/windows.ts +94 -0
  339. package/src/system/providers/clipboard.ts +84 -0
  340. package/src/system/providers/error.ts +16 -0
  341. package/src/system/providers/key-macro/darwin.ts +221 -0
  342. package/src/system/providers/key-macro/linux.ts +651 -0
  343. package/src/system/providers/key-macro/parser.ts +215 -0
  344. package/src/system/providers/key-macro/windows.ts +476 -0
  345. package/src/system/providers/key-macro.ts +79 -0
  346. package/src/system/providers/notification/darwin.ts +36 -0
  347. package/src/system/providers/notification/linux.ts +115 -0
  348. package/src/system/providers/notification/windows.ts +72 -0
  349. package/src/system/providers/notification.ts +75 -0
  350. package/src/system/providers/session/darwin.ts +82 -0
  351. package/src/system/providers/session/linux.ts +175 -0
  352. package/src/system/providers/session/windows.ts +73 -0
  353. package/src/system/providers/session.ts +89 -0
  354. package/src/system/providers/shared.ts +65 -0
  355. package/src/system/requirements.ts +235 -0
  356. package/src/system/setup-wizard/config-seed.ts +30 -0
  357. package/src/system/setup-wizard/format.ts +275 -0
  358. package/src/system/setup-wizard/index.ts +45 -0
  359. package/src/system/setup-wizard/open-settings.ts +26 -0
  360. package/src/system/setup-wizard/plan.ts +230 -0
  361. package/src/system/setup-wizard/probe-cache.ts +34 -0
  362. package/src/system/setup-wizard/probe.ts +486 -0
  363. package/src/system/setup-wizard/runtime-features.ts +85 -0
  364. package/src/system/setup-wizard/sudo.ts +178 -0
  365. package/src/system/setup-wizard/types.ts +133 -0
  366. package/src/system/virtual-stream-deck.ts +67 -0
  367. package/src/themes/css.ts +210 -0
  368. package/src/themes/default/assets/IBMPlexSans-Italic-VariableFont_wdth,wght.ttf +0 -0
  369. package/src/themes/default/assets/IBMPlexSans-VariableFont_wdth,wght.ttf +0 -0
  370. package/src/themes/default/assets/IBM_Plex_Mono/IBMPlexMono-Bold.ttf +0 -0
  371. package/src/themes/default/assets/IBM_Plex_Mono/IBMPlexMono-BoldItalic.ttf +0 -0
  372. package/src/themes/default/assets/IBM_Plex_Mono/IBMPlexMono-ExtraLight.ttf +0 -0
  373. package/src/themes/default/assets/IBM_Plex_Mono/IBMPlexMono-ExtraLightItalic.ttf +0 -0
  374. package/src/themes/default/assets/IBM_Plex_Mono/IBMPlexMono-Italic.ttf +0 -0
  375. package/src/themes/default/assets/IBM_Plex_Mono/IBMPlexMono-Light.ttf +0 -0
  376. package/src/themes/default/assets/IBM_Plex_Mono/IBMPlexMono-LightItalic.ttf +0 -0
  377. package/src/themes/default/assets/IBM_Plex_Mono/IBMPlexMono-Medium.ttf +0 -0
  378. package/src/themes/default/assets/IBM_Plex_Mono/IBMPlexMono-MediumItalic.ttf +0 -0
  379. package/src/themes/default/assets/IBM_Plex_Mono/IBMPlexMono-Regular.ttf +0 -0
  380. package/src/themes/default/assets/IBM_Plex_Mono/IBMPlexMono-SemiBold.ttf +0 -0
  381. package/src/themes/default/assets/IBM_Plex_Mono/IBMPlexMono-SemiBoldItalic.ttf +0 -0
  382. package/src/themes/default/assets/IBM_Plex_Mono/IBMPlexMono-Thin.ttf +0 -0
  383. package/src/themes/default/assets/IBM_Plex_Mono/IBMPlexMono-ThinItalic.ttf +0 -0
  384. package/src/themes/default/assets/IBM_Plex_Mono/OFL.txt +93 -0
  385. package/src/themes/default/assets/OFL.txt +93 -0
  386. package/src/themes/default/assets/README.txt +106 -0
  387. package/src/themes/default/assets/static/IBMPlexSans-Bold.ttf +0 -0
  388. package/src/themes/default/assets/static/IBMPlexSans-BoldItalic.ttf +0 -0
  389. package/src/themes/default/assets/static/IBMPlexSans-ExtraLight.ttf +0 -0
  390. package/src/themes/default/assets/static/IBMPlexSans-ExtraLightItalic.ttf +0 -0
  391. package/src/themes/default/assets/static/IBMPlexSans-Italic.ttf +0 -0
  392. package/src/themes/default/assets/static/IBMPlexSans-Light.ttf +0 -0
  393. package/src/themes/default/assets/static/IBMPlexSans-LightItalic.ttf +0 -0
  394. package/src/themes/default/assets/static/IBMPlexSans-Medium.ttf +0 -0
  395. package/src/themes/default/assets/static/IBMPlexSans-MediumItalic.ttf +0 -0
  396. package/src/themes/default/assets/static/IBMPlexSans-Regular.ttf +0 -0
  397. package/src/themes/default/assets/static/IBMPlexSans-SemiBold.ttf +0 -0
  398. package/src/themes/default/assets/static/IBMPlexSans-SemiBoldItalic.ttf +0 -0
  399. package/src/themes/default/assets/static/IBMPlexSans-Thin.ttf +0 -0
  400. package/src/themes/default/assets/static/IBMPlexSans-ThinItalic.ttf +0 -0
  401. package/src/themes/default/assets/static/IBMPlexSans_Condensed-Bold.ttf +0 -0
  402. package/src/themes/default/assets/static/IBMPlexSans_Condensed-BoldItalic.ttf +0 -0
  403. package/src/themes/default/assets/static/IBMPlexSans_Condensed-ExtraLight.ttf +0 -0
  404. package/src/themes/default/assets/static/IBMPlexSans_Condensed-ExtraLightItalic.ttf +0 -0
  405. package/src/themes/default/assets/static/IBMPlexSans_Condensed-Italic.ttf +0 -0
  406. package/src/themes/default/assets/static/IBMPlexSans_Condensed-Light.ttf +0 -0
  407. package/src/themes/default/assets/static/IBMPlexSans_Condensed-LightItalic.ttf +0 -0
  408. package/src/themes/default/assets/static/IBMPlexSans_Condensed-Medium.ttf +0 -0
  409. package/src/themes/default/assets/static/IBMPlexSans_Condensed-MediumItalic.ttf +0 -0
  410. package/src/themes/default/assets/static/IBMPlexSans_Condensed-Regular.ttf +0 -0
  411. package/src/themes/default/assets/static/IBMPlexSans_Condensed-SemiBold.ttf +0 -0
  412. package/src/themes/default/assets/static/IBMPlexSans_Condensed-SemiBoldItalic.ttf +0 -0
  413. package/src/themes/default/assets/static/IBMPlexSans_Condensed-Thin.ttf +0 -0
  414. package/src/themes/default/assets/static/IBMPlexSans_Condensed-ThinItalic.ttf +0 -0
  415. package/src/themes/default/assets/static/IBMPlexSans_SemiCondensed-Bold.ttf +0 -0
  416. package/src/themes/default/assets/static/IBMPlexSans_SemiCondensed-BoldItalic.ttf +0 -0
  417. package/src/themes/default/assets/static/IBMPlexSans_SemiCondensed-ExtraLight.ttf +0 -0
  418. package/src/themes/default/assets/static/IBMPlexSans_SemiCondensed-ExtraLightItalic.ttf +0 -0
  419. package/src/themes/default/assets/static/IBMPlexSans_SemiCondensed-Italic.ttf +0 -0
  420. package/src/themes/default/assets/static/IBMPlexSans_SemiCondensed-Light.ttf +0 -0
  421. package/src/themes/default/assets/static/IBMPlexSans_SemiCondensed-LightItalic.ttf +0 -0
  422. package/src/themes/default/assets/static/IBMPlexSans_SemiCondensed-Medium.ttf +0 -0
  423. package/src/themes/default/assets/static/IBMPlexSans_SemiCondensed-MediumItalic.ttf +0 -0
  424. package/src/themes/default/assets/static/IBMPlexSans_SemiCondensed-Regular.ttf +0 -0
  425. package/src/themes/default/assets/static/IBMPlexSans_SemiCondensed-SemiBold.ttf +0 -0
  426. package/src/themes/default/assets/static/IBMPlexSans_SemiCondensed-SemiBoldItalic.ttf +0 -0
  427. package/src/themes/default/assets/static/IBMPlexSans_SemiCondensed-Thin.ttf +0 -0
  428. package/src/themes/default/assets/static/IBMPlexSans_SemiCondensed-ThinItalic.ttf +0 -0
  429. package/src/themes/default/components.css +81 -0
  430. package/src/themes/default/sirenodeck.json +170 -0
  431. package/src/themes/index.ts +14 -0
  432. package/src/themes/light/assets/IBMPlexSans-Italic-VariableFont_wdth,wght.ttf +0 -0
  433. package/src/themes/light/assets/IBMPlexSans-VariableFont_wdth,wght.ttf +0 -0
  434. package/src/themes/light/assets/IBM_Plex_Mono/IBMPlexMono-Bold.ttf +0 -0
  435. package/src/themes/light/assets/IBM_Plex_Mono/IBMPlexMono-Medium.ttf +0 -0
  436. package/src/themes/light/assets/IBM_Plex_Mono/IBMPlexMono-Regular.ttf +0 -0
  437. package/src/themes/light/assets/IBM_Plex_Mono/OFL.txt +93 -0
  438. package/src/themes/light/assets/OFL.txt +93 -0
  439. package/src/themes/light/assets/README.txt +106 -0
  440. package/src/themes/light/assets/static/IBMPlexSans-Light.ttf +0 -0
  441. package/src/themes/light/assets/static/IBMPlexSans-LightItalic.ttf +0 -0
  442. package/src/themes/light/assets/static/IBMPlexSans-Thin.ttf +0 -0
  443. package/src/themes/light/components.css +70 -0
  444. package/src/themes/light/sirenodeck.json +170 -0
  445. package/src/themes/loader.ts +343 -0
  446. package/src/themes/manifest.ts +164 -0
  447. package/src/themes/use-resolved-theme.tsx +32 -0
  448. package/src/ui/ButtonFrame.tsx +230 -0
  449. package/src/ui/contexts/AssetCacheContext.tsx +43 -0
  450. package/src/ui/index.ts +20 -0
  451. package/src/ui/primitives/Chip.tsx +53 -0
  452. package/src/ui/primitives/Icon.tsx +209 -0
  453. package/src/ui/primitives/Label.tsx +82 -0
  454. package/src/ui/primitives/ProgressBar.tsx +38 -0
  455. package/src/ui/primitives/TapIndicator.tsx +67 -0
  456. package/src/ui/primitives/Text.tsx +810 -0
  457. package/src/ui/primitives/index.ts +6 -0
  458. package/src/ui/surfaces/BarsSurface.tsx +211 -0
  459. package/src/ui/surfaces/IconLabelProgressSurface.tsx +133 -0
  460. package/src/ui/surfaces/IconLabelSurface.tsx +59 -0
  461. package/src/ui/surfaces/LabelValueListSurface.tsx +202 -0
  462. package/src/ui/surfaces/PaginatedSurface.tsx +164 -0
  463. package/src/ui/surfaces/SplitActionSurface.tsx +98 -0
  464. package/src/ui/surfaces/TemporaryErrorSurface.tsx +68 -0
  465. package/src/ui/surfaces/ValueChart.tsx +218 -0
  466. package/src/ui/surfaces/index.ts +32 -0
  467. package/src/ui/surfaces/paginated-schema.ts +22 -0
  468. package/src/ui/theme-presentation-builder.ts +184 -0
  469. package/src/ui/theme-presentation.tsx +146 -0
  470. package/src/ui/utils/cn.ts +12 -0
  471. package/src/util/cache-paths.ts +38 -0
  472. package/src/util/config-diff.ts +16 -0
  473. package/src/util/daemon-control.ts +85 -0
  474. package/src/util/daemon.ts +575 -0
  475. package/src/util/device-config.ts +56 -0
  476. package/src/util/errors.ts +13 -0
  477. package/src/util/log-reader.ts +114 -0
  478. package/src/util/log-tail.ts +103 -0
  479. package/src/util/logger.ts +417 -0
  480. package/src/util/process-title.ts +79 -0
  481. package/src/util/single-instance.ts +202 -0
  482. package/src/version.ts +10 -0
  483. package/src/vite/index.ts +6 -0
  484. package/src/vite/virtual-modules.ts +631 -0
  485. package/src/vite-env.d.ts +1 -0
@@ -0,0 +1,470 @@
1
+ import { open, readdir, stat } from "node:fs/promises"
2
+ import { basename, join } from "node:path"
3
+
4
+ import chokidar, { type FSWatcher } from "chokidar"
5
+
6
+ // ponytail: see packages/addons/app-shortcuts/src/index.ts for context.
7
+ import type { Agent, AgentProvider } from "../shared/state.js"
8
+ import {
9
+ deriveClaudeStatus,
10
+ type ClaudeJsonlEntry,
11
+ } from "../shared/claude-status.js"
12
+ import { readClaudeInstances, type ClaudeInstance } from "./claude-instances.js"
13
+
14
+ export const CLAUDE_LOGO = "addon://coding-agents/assets/claude-code.svg"
15
+
16
+ export interface ClaudeCodeProviderOptions {
17
+ readonly projectsDir?: string
18
+ readonly recentWindowMs?: number
19
+ /** Off in tests that only exercise transcript ingest. */
20
+ readonly readLeases?: boolean
21
+ }
22
+
23
+ const DEFAULT_PROJECTS_DIR = join(
24
+ process.env["HOME"] ?? "~",
25
+ ".claude",
26
+ "projects",
27
+ )
28
+
29
+ // ponytail: "live" window — sessions idle longer than this stay off the deck.
30
+ const RECENT_WINDOW_MS = 6 * 60 * 60 * 1000
31
+
32
+ // ponytail: the reporting machine had 136 MB across 105 transcripts, and the
33
+ // old ingest read EVERY file in full on startup and re-read a whole multi-MB
34
+ // file on every append. Enumerate by mtime, read only the tail, and cap the
35
+ // working set.
36
+ const MAX_SESSIONS = 32
37
+ const TAIL_BYTES = 256 * 1024
38
+ const TAIL_RETRY_BYTES = 1024 * 1024
39
+ const SCAN_DEBOUNCE_MS = 1_500
40
+
41
+ // A session transcript is named for its UUID. Subagent transcripts live in a
42
+ // `subagents/` subdirectory as `agent-<hex>.jsonl`, and abandoned ones are
43
+ // renamed `<uuid>.orphaned-<ts>-<hash>.jsonl`. This one pattern excludes both:
44
+ // 56 of those 105 files were subagents, and each would otherwise have been
45
+ // surfaced as its own agent tile — exactly the harness noise the summary is
46
+ // meant to aggregate away.
47
+ const SESSION_FILE_RE =
48
+ /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\.jsonl$/i
49
+
50
+ interface TailState {
51
+ size: number
52
+ entries: ClaudeJsonlEntry[]
53
+ }
54
+
55
+ export class ClaudeCodeProvider implements AgentProvider {
56
+ readonly id = "claude-code" as const
57
+ readonly displayName = "Claude Code"
58
+ readonly logoPath = CLAUDE_LOGO
59
+
60
+ readonly #projectsDir: string
61
+ readonly #recentWindowMs: number
62
+ readonly #leasesEnabled: boolean
63
+
64
+ constructor(opts: ClaudeCodeProviderOptions = {}) {
65
+ this.#projectsDir = opts.projectsDir ?? DEFAULT_PROJECTS_DIR
66
+ this.#recentWindowMs = opts.recentWindowMs ?? RECENT_WINDOW_MS
67
+ this.#leasesEnabled = opts.readLeases ?? true
68
+ }
69
+
70
+ async fetchSnapshot(_signal: AbortSignal): Promise<readonly Agent[]> {
71
+ // ponytail: this used to serve purely from a map that only subscribe()
72
+ // ever filled — and subscribe() was never called, because the host never
73
+ // wired `globalService.subscriptions`. One missing call silently produced
74
+ // "zero Claude Code sessions" forever. fetchSnapshot now scans on its own
75
+ // (debounced, since the poller runs every 2s) and subscribe() is only a
76
+ // latency optimisation, so neither can silently disable the other.
77
+ await this.#maybeScan()
78
+ const cutoff = Date.now() - this.#recentWindowMs
79
+ const fromTranscripts = [...this.#agents.values()].filter(
80
+ (a) => a.updatedAt >= cutoff,
81
+ )
82
+ const leases = this.#leasesEnabled
83
+ ? await readClaudeInstances({ prune: true })
84
+ : []
85
+ return mergeLeases(fromTranscripts, leases)
86
+ }
87
+
88
+ #lastScanAt = 0
89
+ #scanInFlight: Promise<void> | null = null
90
+
91
+ async #maybeScan(): Promise<void> {
92
+ if (this.#scanInFlight !== null) return this.#scanInFlight
93
+ if (Date.now() - this.#lastScanAt < SCAN_DEBOUNCE_MS) return
94
+ const run = this.#scan(() => undefined).finally(() => {
95
+ this.#lastScanAt = Date.now()
96
+ this.#scanInFlight = null
97
+ })
98
+ this.#scanInFlight = run
99
+ return run
100
+ }
101
+
102
+ subscribe(signal: AbortSignal, onChange: () => void): () => void {
103
+ const watcher: FSWatcher = chokidar.watch(this.#projectsDir, {
104
+ // ponytail: transcripts live at projects/<slug>/<uuid>.jsonl, so depth 2
105
+ // is enough. Depth 6 also indexed every subagents/, tool-results/ and
106
+ // memory/ directory — thousands of files chokidar had no use for.
107
+ ignored: (path) =>
108
+ path.endsWith(".lock") ||
109
+ path.includes("/subagents/") ||
110
+ path.includes("/tool-results/") ||
111
+ path.includes("/memory/"),
112
+ persistent: true,
113
+ ignoreInitial: false,
114
+ depth: 2,
115
+ })
116
+
117
+ let timer: NodeJS.Timeout | null = null
118
+
119
+ const schedule = (): void => {
120
+ // ponytail: this used to start a NEW timer per event, so a burst of N
121
+ // appends created N timers; only the first did any work and the rest
122
+ // still fired. Coalesce onto a single pending timer instead.
123
+ if (timer !== null) return
124
+ timer = setTimeout(() => {
125
+ timer = null
126
+ onChange()
127
+ }, 250)
128
+ }
129
+
130
+ void this.#scan(onChange)
131
+
132
+ const touch = (filePath: string): void => {
133
+ if (!isSessionTranscript(filePath)) return
134
+ void this.#ingestFile(filePath, schedule)
135
+ }
136
+
137
+ watcher.on("add", touch)
138
+ watcher.on("change", touch)
139
+ watcher.on("unlink", (filePath: string) => {
140
+ if (!isSessionTranscript(filePath)) return
141
+ this.#tails.delete(filePath)
142
+ if (this.#agents.delete(sessionIdFromPath(filePath))) schedule()
143
+ })
144
+
145
+ signal.addEventListener("abort", () => {
146
+ void watcher.close()
147
+ })
148
+
149
+ return () => {
150
+ if (timer !== null) clearTimeout(timer)
151
+ void watcher.close()
152
+ }
153
+ }
154
+
155
+ readonly #agents = new Map<string, Agent>()
156
+
157
+ async #scan(onChange: () => void): Promise<void> {
158
+ // ponytail: enumerate by NAME and MTIME only — never by opening files.
159
+ // On the reporting machine this takes the candidate set from 105 files
160
+ // (136 MB) to 9, before a single byte of transcript is read.
161
+ let found: Array<{ path: string; mtimeMs: number }> = []
162
+ try {
163
+ const entries = await readdir(this.#projectsDir, {
164
+ withFileTypes: true,
165
+ recursive: true,
166
+ })
167
+ const cutoff = Date.now() - this.#recentWindowMs
168
+ const stats = await Promise.all(
169
+ entries
170
+ .filter((e) => e.isFile() && isSessionTranscript(e.name))
171
+ .map(async (e) => {
172
+ const full = join(e.parentPath ?? this.#projectsDir, e.name)
173
+ // Belt and braces: a rename of the subagent layout must not start
174
+ // surfacing them as top-level agents.
175
+ if (full.includes("/subagents/")) return null
176
+ try {
177
+ const st = await stat(full)
178
+ return st.mtimeMs >= cutoff
179
+ ? { path: full, mtimeMs: st.mtimeMs }
180
+ : null
181
+ } catch {
182
+ return null
183
+ }
184
+ }),
185
+ )
186
+ found = stats.filter(
187
+ (x): x is { path: string; mtimeMs: number } => x !== null,
188
+ )
189
+ } catch {
190
+ return
191
+ }
192
+
193
+ found.sort((a, b) => b.mtimeMs - a.mtimeMs)
194
+ const live = found.slice(0, MAX_SESSIONS)
195
+ const liveIds = new Set(live.map((f) => sessionIdFromPath(f.path)))
196
+
197
+ for (const file of live) {
198
+ await this.#ingestFile(file.path, onChange, file.mtimeMs)
199
+ }
200
+
201
+ // Drop anything no longer live, and keep the tail cache bounded alongside.
202
+ for (const id of [...this.#agents.keys()]) {
203
+ if (!liveIds.has(id)) this.#agents.delete(id)
204
+ }
205
+ for (const path of [...this.#tails.keys()]) {
206
+ if (!live.some((f) => f.path === path)) this.#tails.delete(path)
207
+ }
208
+ }
209
+
210
+ readonly #tails = new Map<string, TailState>()
211
+
212
+ /**
213
+ * Reads only what is new. The first read takes the tail of the file; later
214
+ * reads take just the bytes appended since. A whole transcript is never read
215
+ * again — the largest here is 8 MB and grows while a session runs.
216
+ */
217
+ async #readNewEntries(
218
+ filePath: string,
219
+ size: number,
220
+ ): Promise<ClaudeJsonlEntry[] | null> {
221
+ const prev = this.#tails.get(filePath)
222
+ // Truncated or rotated → start over.
223
+ const from = prev !== undefined && size >= prev.size ? prev.size : null
224
+
225
+ const readFrom = async (offset: number): Promise<string | null> => {
226
+ const length = size - offset
227
+ if (length <= 0) return ""
228
+ const handle = await open(filePath, "r")
229
+ try {
230
+ const buf = Buffer.allocUnsafe(length)
231
+ await handle.read(buf, 0, length, offset)
232
+ return buf.toString("utf8")
233
+ } finally {
234
+ await handle.close()
235
+ }
236
+ }
237
+
238
+ try {
239
+ if (from !== null) {
240
+ const chunk = await readFrom(from)
241
+ if (chunk === null) return null
242
+ const appended = parseEntries(chunk)
243
+ const entries = [...prev!.entries, ...appended].slice(-MAX_ENTRIES)
244
+ this.#tails.set(filePath, { size, entries })
245
+ return entries
246
+ }
247
+
248
+ // First sight of this file: take the tail, dropping the leading partial
249
+ // line. Retry once wider if the window caught no assistant entry — a
250
+ // single tool result can be larger than the default tail.
251
+ for (const window of [TAIL_BYTES, TAIL_RETRY_BYTES]) {
252
+ const offset = Math.max(0, size - window)
253
+ const chunk = await readFrom(offset)
254
+ if (chunk === null) return null
255
+ const body = offset > 0 ? chunk.slice(chunk.indexOf("\n") + 1) : chunk
256
+ const entries = parseEntries(body).slice(-MAX_ENTRIES)
257
+ const usable =
258
+ offset === 0 || entries.some((e) => e.type === "assistant")
259
+ if (usable || window === TAIL_RETRY_BYTES) {
260
+ this.#tails.set(filePath, { size, entries })
261
+ return entries
262
+ }
263
+ }
264
+ return null
265
+ } catch {
266
+ return null
267
+ }
268
+ }
269
+
270
+ async #ingestFile(
271
+ filePath: string,
272
+ onChange: () => void,
273
+ mtimeMs?: number,
274
+ ): Promise<void> {
275
+ const sessionId = sessionIdFromPath(filePath)
276
+ try {
277
+ const st = await stat(filePath)
278
+ const entries = await this.#readNewEntries(filePath, st.size)
279
+ if (entries === null || entries.length === 0) return
280
+
281
+ // Pass mtime as the fallback so a transcript with no timestamps reports
282
+ // when the file last changed, rather than "now" — which made such files
283
+ // permanently live.
284
+ const derived = deriveClaudeStatus(
285
+ entries,
286
+ Date.now(),
287
+ mtimeMs ?? st.mtimeMs,
288
+ )
289
+ if (derived === null) return
290
+
291
+ const existing = this.#agents.get(sessionId)
292
+ const title = titleFromEntries(entries) ?? existing?.title ?? sessionId
293
+ const usage = contextTokensFromEntries(entries)
294
+ const agent: Agent = {
295
+ sessionId,
296
+ providerId: "claude-code",
297
+ title,
298
+ status: derived.status,
299
+ updatedAt: derived.updatedAt,
300
+ ...(derived.createdAt !== undefined
301
+ ? { createdAt: derived.createdAt }
302
+ : {}),
303
+ ...(derived.preview !== undefined
304
+ ? { lastMessagePreview: derived.preview }
305
+ : {}),
306
+ ...(usage !== undefined ? { contextTokens: usage } : {}),
307
+ ...(derived.cwd !== undefined
308
+ ? { directory: derived.cwd }
309
+ : existing?.directory !== undefined
310
+ ? { directory: existing.directory }
311
+ : {}),
312
+ }
313
+ this.#agents.set(sessionId, agent)
314
+ onChange()
315
+ } catch {
316
+ // file may be unreadable; skip
317
+ }
318
+ }
319
+ }
320
+
321
+ const MAX_ENTRIES = 400
322
+
323
+ const parseEntries = (raw: string): ClaudeJsonlEntry[] => {
324
+ const out: ClaudeJsonlEntry[] = []
325
+ for (const line of raw.split(/\r?\n/)) {
326
+ const trimmed = line.trim()
327
+ if (trimmed.length === 0) continue
328
+ try {
329
+ out.push(JSON.parse(trimmed) as ClaudeJsonlEntry)
330
+ } catch {
331
+ // skip malformed line
332
+ }
333
+ }
334
+ return out
335
+ }
336
+
337
+ /**
338
+ * Context size of the newest real assistant turn.
339
+ *
340
+ * ponytail: `costUSD` no longer exists in Claude Code 2.1.x transcripts, so the
341
+ * old cost sum was permanently zero. Real accounting lives on `message.usage`.
342
+ * Sidechain (subagent) entries are skipped so a subagent's context does not
343
+ * masquerade as the session's.
344
+ */
345
+ const contextTokensFromEntries = (
346
+ entries: readonly ClaudeJsonlEntry[],
347
+ ): number | undefined => {
348
+ for (let i = entries.length - 1; i >= 0; i -= 1) {
349
+ const e = entries[i] as
350
+ | (ClaudeJsonlEntry & {
351
+ isSidechain?: boolean
352
+ message?: { usage?: Record<string, unknown> }
353
+ })
354
+ | undefined
355
+ if (e?.type !== "assistant" || e.isSidechain === true) continue
356
+ const usage = e.message?.usage
357
+ if (usage === undefined) continue
358
+ const num = (k: string): number =>
359
+ typeof usage[k] === "number" ? (usage[k] as number) : 0
360
+ if (num("output_tokens") <= 0) continue
361
+ const total =
362
+ num("input_tokens") +
363
+ num("cache_creation_input_tokens") +
364
+ num("cache_read_input_tokens") +
365
+ num("output_tokens")
366
+ return total > 0 ? total : undefined
367
+ }
368
+ return undefined
369
+ }
370
+
371
+ /**
372
+ * True only for a top-level session transcript.
373
+ *
374
+ * Accepts `<uuid>.jsonl` and nothing else, which excludes subagent transcripts
375
+ * (`subagents/agent-<hex>.jsonl`) and abandoned ones
376
+ * (`<uuid>.orphaned-<ts>-<hash>.jsonl`) in a single rule.
377
+ */
378
+ const isSessionTranscript = (pathOrName: string): boolean => {
379
+ const base = pathOrName.split("/").pop() ?? pathOrName
380
+ return SESSION_FILE_RE.test(base)
381
+ }
382
+
383
+ const sessionIdFromPath = (filePath: string): string => {
384
+ const base = filePath.split("/").pop() ?? filePath
385
+ return base.replace(/\.jsonl$/, "")
386
+ }
387
+
388
+ const titleFromEntries = (
389
+ entries: readonly ClaudeJsonlEntry[],
390
+ ): string | undefined => {
391
+ for (const e of entries) {
392
+ const summary = (e as { summary?: unknown }).summary
393
+ if (typeof summary === "string" && summary.length > 0) return summary
394
+ }
395
+ for (const e of entries) {
396
+ if (e.type === "user") {
397
+ const content = flattenUserText(e.message?.content)
398
+ if (content) return content
399
+ }
400
+ }
401
+ return undefined
402
+ }
403
+
404
+ const flattenUserText = (content: unknown): string | undefined => {
405
+ if (typeof content === "string") {
406
+ return content.length > 80 ? `${content.slice(0, 79)}…` : content
407
+ }
408
+ if (Array.isArray(content)) {
409
+ for (const part of content) {
410
+ if (
411
+ part !== null &&
412
+ typeof part === "object" &&
413
+ "text" in part &&
414
+ typeof (part as { text: unknown }).text === "string"
415
+ ) {
416
+ const text = (part as { text: string }).text
417
+ return text.length > 80 ? `${text.slice(0, 79)}…` : text
418
+ }
419
+ }
420
+ }
421
+ return undefined
422
+ }
423
+
424
+ /**
425
+ * Folds hook-written leases over transcript-derived agents.
426
+ *
427
+ * ponytail: dedup MUST happen here, not in the frontend. The summary tile
428
+ * counts by flattening every provider's list, so emitting both a lease agent
429
+ * and a transcript agent for one session would double-count it.
430
+ *
431
+ * The lease wins anything it knows first-hand — status, directory, and the
432
+ * instance identity a transcript cannot supply. The transcript keeps what only
433
+ * it has: the title, the preview and the context size.
434
+ */
435
+ export const mergeLeases = (
436
+ fromTranscripts: readonly Agent[],
437
+ leases: readonly ClaudeInstance[],
438
+ ): readonly Agent[] => {
439
+ const bySession = new Map<string, Agent>()
440
+ for (const agent of fromTranscripts) bySession.set(agent.sessionId, agent)
441
+
442
+ for (const lease of leases) {
443
+ const existing = bySession.get(lease.sessionId)
444
+ if (existing === undefined) {
445
+ // A session whose transcript has not been flushed yet: surface it now
446
+ // rather than making the user wait for the first write.
447
+ bySession.set(lease.sessionId, {
448
+ sessionId: lease.sessionId,
449
+ instanceId: lease.instanceId,
450
+ providerId: "claude-code",
451
+ title: basename(lease.cwd) || "Claude Code",
452
+ status: lease.status,
453
+ directory: lease.cwd,
454
+ createdAt: lease.updatedAt,
455
+ updatedAt: lease.updatedAt,
456
+ ...(lease.pid !== null ? { pid: lease.pid } : {}),
457
+ })
458
+ continue
459
+ }
460
+ bySession.set(lease.sessionId, {
461
+ ...existing,
462
+ instanceId: lease.instanceId,
463
+ status: lease.status,
464
+ directory: lease.cwd,
465
+ updatedAt: Math.max(existing.updatedAt, lease.updatedAt),
466
+ ...(lease.pid !== null ? { pid: lease.pid } : {}),
467
+ })
468
+ }
469
+ return [...bySession.values()]
470
+ }
@@ -0,0 +1,184 @@
1
+ import { readFile, readdir, stat, unlink } from "node:fs/promises"
2
+ import { homedir } from "node:os"
3
+ import { join } from "node:path"
4
+
5
+ import type { AgentStatus } from "../shared/state.js"
6
+
7
+ /**
8
+ * A live Claude Code session, as reported by the hook script.
9
+ *
10
+ * ponytail: the OpenCode equivalent comes from a plugin injected into
11
+ * OpenCode's own config. Claude Code needs no plugin — it has first-class
12
+ * hooks, which are the supported way to be told about session state. Same
13
+ * lease directory and the same defensive parsing, so the two sources behave
14
+ * identically from the provider's point of view.
15
+ */
16
+ export interface ClaudeInstance {
17
+ readonly instanceId: string
18
+ readonly sessionId: string
19
+ readonly pid: number | null
20
+ readonly cwd: string
21
+ readonly status: AgentStatus
22
+ readonly updatedAt: number
23
+ readonly transcriptPath?: string
24
+ }
25
+
26
+ /**
27
+ * A `running`/`compacting` lease this old has missed its terminating event —
28
+ * a hook timed out, or the machine slept mid-turn. Such a lease is downgraded
29
+ * to `idle` rather than dropped, because the session itself is very likely
30
+ * still alive.
31
+ */
32
+ const TRANSIENT_LEASE_MAX_AGE_MS = 10 * 60 * 1000
33
+
34
+ /**
35
+ * Absolute ceiling. Unlike OpenCode, which heartbeats every 5s, hooks are
36
+ * event-driven: an idle session legitimately writes nothing for hours, so age
37
+ * alone cannot mean "dead". Liveness comes from the pid check; this only stops
38
+ * a lease surviving forever when the pid was never resolvable or got reused.
39
+ */
40
+ const LEASE_HARD_MAX_AGE_MS = 24 * 60 * 60 * 1000
41
+
42
+ export const claudeInstanceDir = (): string =>
43
+ join(
44
+ process.env["XDG_STATE_HOME"] ?? join(homedir(), ".local", "state"),
45
+ "sirenodeck",
46
+ "coding-agents",
47
+ )
48
+
49
+ export const claudeLeaseName = (sessionId: string): string =>
50
+ `claude-${sessionId}.json`
51
+
52
+ const processIsAlive = (pid: number): boolean => {
53
+ try {
54
+ process.kill(pid, 0)
55
+ return true
56
+ } catch {
57
+ return false
58
+ }
59
+ }
60
+
61
+ const parseStatus = (value: unknown): AgentStatus | null =>
62
+ value === "idle" ||
63
+ value === "running" ||
64
+ value === "waiting" ||
65
+ value === "waiting_for_human" ||
66
+ value === "error" ||
67
+ value === "compacting"
68
+ ? value
69
+ : null
70
+
71
+ const parseLease = (raw: string, now: number): ClaudeInstance | null => {
72
+ let value: Record<string, unknown>
73
+ try {
74
+ value = JSON.parse(raw) as Record<string, unknown>
75
+ } catch {
76
+ return null
77
+ }
78
+
79
+ const sessionId = value["sessionId"]
80
+ const cwd = value["cwd"]
81
+ const updatedAt = value["updatedAt"]
82
+ let status = parseStatus(value["state"])
83
+ if (
84
+ typeof sessionId !== "string" ||
85
+ sessionId.length === 0 ||
86
+ typeof cwd !== "string" ||
87
+ typeof updatedAt !== "number" ||
88
+ status === null
89
+ ) {
90
+ return null
91
+ }
92
+
93
+ const rawPid = value["pid"]
94
+ const pid =
95
+ typeof rawPid === "number" && Number.isInteger(rawPid) && rawPid > 0
96
+ ? rawPid
97
+ : null
98
+
99
+ const age = now - updatedAt
100
+ if (age > LEASE_HARD_MAX_AGE_MS) return null
101
+ // The hook could not find the claude process, so there is nothing to probe;
102
+ // fall back to the age ceiling above rather than suppressing the session.
103
+ if (pid !== null && !processIsAlive(pid)) return null
104
+
105
+ if (
106
+ (status === "running" || status === "compacting") &&
107
+ age > TRANSIENT_LEASE_MAX_AGE_MS
108
+ ) {
109
+ status = "idle"
110
+ }
111
+
112
+ const transcriptPath = value["transcriptPath"]
113
+ return {
114
+ instanceId: `claude:${sessionId}`,
115
+ sessionId,
116
+ pid,
117
+ cwd,
118
+ status,
119
+ updatedAt,
120
+ ...(typeof transcriptPath === "string" && transcriptPath.length > 0
121
+ ? { transcriptPath }
122
+ : {}),
123
+ }
124
+ }
125
+
126
+ export interface ReadClaudeInstancesOptions {
127
+ readonly dir?: string
128
+ readonly now?: number
129
+ /** Remove leases whose process is gone. The daemon is the only reader. */
130
+ readonly prune?: boolean
131
+ }
132
+
133
+ export const readClaudeInstances = async (
134
+ opts: ReadClaudeInstancesOptions = {},
135
+ ): Promise<readonly ClaudeInstance[]> => {
136
+ const dir = opts.dir ?? claudeInstanceDir()
137
+ const now = opts.now ?? Date.now()
138
+
139
+ let names: string[]
140
+ try {
141
+ names = (await readdir(dir)).filter(
142
+ (name) => name.startsWith("claude-") && name.endsWith(".json"),
143
+ )
144
+ } catch {
145
+ return []
146
+ }
147
+
148
+ const instances: ClaudeInstance[] = []
149
+ for (const name of names) {
150
+ const full = join(dir, name)
151
+ let instance: ClaudeInstance | null = null
152
+ try {
153
+ instance = parseLease(await readFile(full, "utf8"), now)
154
+ } catch {
155
+ // A session may replace or remove its lease while the dir is read.
156
+ continue
157
+ }
158
+ if (instance === null) {
159
+ if (opts.prune === true) {
160
+ try {
161
+ await unlink(full)
162
+ } catch {
163
+ // Already gone, or not ours to remove.
164
+ }
165
+ }
166
+ continue
167
+ }
168
+ // ponytail: hooks fire at turn boundaries, so between PreToolUse and Stop
169
+ // the lease goes quiet while the session is very much working. The
170
+ // transcript's mtime is a free heartbeat that keeps "last activity"
171
+ // honest without asking the hook to poll.
172
+ let updatedAt = instance.updatedAt
173
+ if (instance.transcriptPath !== undefined) {
174
+ try {
175
+ const st = await stat(instance.transcriptPath)
176
+ updatedAt = Math.max(updatedAt, st.mtimeMs)
177
+ } catch {
178
+ // Transcript moved or removed; the lease timestamp stands.
179
+ }
180
+ }
181
+ instances.push({ ...instance, updatedAt })
182
+ }
183
+ return instances.sort((a, b) => b.updatedAt - a.updatedAt)
184
+ }