@hirra/vibemeter 0.1.2 → 0.1.4

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 (262) hide show
  1. package/.next/BUILD_ID +1 -1
  2. package/.next/app-path-routes-manifest.json +7 -1
  3. package/.next/build-manifest.json +7 -8
  4. package/.next/fallback-build-manifest.json +3 -3
  5. package/.next/prerender-manifest.json +16 -12
  6. package/.next/required-server-files.js +5 -4
  7. package/.next/required-server-files.json +5 -4
  8. package/.next/routes-manifest.json +36 -0
  9. package/.next/server/app/_global-error/page/build-manifest.json +4 -5
  10. package/.next/server/app/_global-error/page.js +4 -4
  11. package/.next/server/app/_global-error/page.js.nft.json +1 -1
  12. package/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
  13. package/.next/server/app/_global-error.html +1 -1
  14. package/.next/server/app/_global-error.rsc +1 -1
  15. package/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
  16. package/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  17. package/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  18. package/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  19. package/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  20. package/.next/server/app/_not-found/page/build-manifest.json +4 -5
  21. package/.next/server/app/_not-found/page.js +5 -4
  22. package/.next/server/app/_not-found/page.js.nft.json +1 -1
  23. package/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  24. package/.next/server/app/admin/page/build-manifest.json +4 -5
  25. package/.next/server/app/admin/page.js +7 -5
  26. package/.next/server/app/admin/page.js.nft.json +1 -1
  27. package/.next/server/app/admin/page_client-reference-manifest.js +1 -1
  28. package/.next/server/app/api/codex-accounts/route.js +1 -1
  29. package/.next/server/app/api/codex-accounts/route.js.nft.json +1 -1
  30. package/.next/server/app/api/float/route/app-paths-manifest.json +3 -0
  31. package/.next/server/app/api/float/route/build-manifest.json +9 -0
  32. package/.next/server/app/api/float/route/server-reference-manifest.json +4 -0
  33. package/.next/server/app/api/float/route.js +8 -0
  34. package/.next/server/app/api/float/route.js.map +5 -0
  35. package/.next/server/app/api/float/route.js.nft.json +1 -0
  36. package/.next/server/app/api/float/route_client-reference-manifest.js +3 -0
  37. package/.next/server/app/api/import-sessions/route.js.nft.json +1 -1
  38. package/.next/server/app/api/sessions/[id]/tags/route.js +1 -1
  39. package/.next/server/app/api/sessions/[id]/tags/route.js.nft.json +1 -1
  40. package/.next/server/app/api/sessions/route.js +1 -1
  41. package/.next/server/app/api/sessions/route.js.nft.json +1 -1
  42. package/.next/server/app/api/settings/alerts/route/app-paths-manifest.json +3 -0
  43. package/.next/server/app/api/settings/alerts/route/build-manifest.json +9 -0
  44. package/.next/server/app/api/settings/alerts/route/server-reference-manifest.json +4 -0
  45. package/.next/server/app/api/settings/alerts/route.js +9 -0
  46. package/.next/server/app/api/settings/alerts/route.js.map +5 -0
  47. package/.next/server/app/api/settings/alerts/route.js.nft.json +1 -0
  48. package/.next/server/app/api/settings/alerts/route_client-reference-manifest.js +3 -0
  49. package/.next/server/app/api/settings/notify/route/app-paths-manifest.json +3 -0
  50. package/.next/server/app/api/settings/notify/route/build-manifest.json +9 -0
  51. package/.next/server/app/api/settings/notify/route/server-reference-manifest.json +4 -0
  52. package/.next/server/app/api/settings/notify/route.js +7 -0
  53. package/.next/server/app/api/settings/notify/route.js.map +5 -0
  54. package/.next/server/app/api/settings/notify/route.js.nft.json +1 -0
  55. package/.next/server/app/api/settings/notify/route_client-reference-manifest.js +3 -0
  56. package/.next/server/app/api/usage/route.js +1 -1
  57. package/.next/server/app/api/usage/route.js.nft.json +1 -1
  58. package/.next/server/app/float/page/app-paths-manifest.json +3 -0
  59. package/.next/server/app/float/page/build-manifest.json +16 -0
  60. package/.next/server/app/float/page/next-font-manifest.json +11 -0
  61. package/.next/server/app/float/page/react-loadable-manifest.json +1 -0
  62. package/.next/server/app/float/page/server-reference-manifest.json +4 -0
  63. package/.next/server/app/float/page.js +15 -0
  64. package/.next/server/app/float/page.js.map +5 -0
  65. package/.next/server/app/float/page.js.nft.json +1 -0
  66. package/.next/server/app/float/page_client-reference-manifest.js +3 -0
  67. package/.next/server/app/install.sh/route/app-paths-manifest.json +3 -0
  68. package/.next/server/app/install.sh/route/build-manifest.json +9 -0
  69. package/.next/server/app/install.sh/route/server-reference-manifest.json +4 -0
  70. package/.next/server/app/install.sh/route.js +6 -0
  71. package/.next/server/app/install.sh/route.js.map +5 -0
  72. package/.next/server/app/install.sh/route.js.nft.json +1 -0
  73. package/.next/server/app/install.sh/route_client-reference-manifest.js +3 -0
  74. package/.next/server/app/install.sh.body +108 -0
  75. package/.next/server/app/install.sh.meta +1 -0
  76. package/.next/server/app/page/build-manifest.json +4 -5
  77. package/.next/server/app/page.js +8 -5
  78. package/.next/server/app/page.js.nft.json +1 -1
  79. package/.next/server/app/page_client-reference-manifest.js +1 -1
  80. package/.next/server/app/settings/page/app-paths-manifest.json +3 -0
  81. package/.next/server/app/settings/page/build-manifest.json +16 -0
  82. package/.next/server/app/settings/page/next-font-manifest.json +11 -0
  83. package/.next/server/app/settings/page/react-loadable-manifest.json +1 -0
  84. package/.next/server/app/settings/page/server-reference-manifest.json +4 -0
  85. package/.next/server/app/settings/page.js +17 -0
  86. package/.next/server/app/settings/page.js.map +5 -0
  87. package/.next/server/app/settings/page.js.nft.json +1 -0
  88. package/.next/server/app/settings/page_client-reference-manifest.js +3 -0
  89. package/.next/server/app-paths-manifest.json +7 -1
  90. package/.next/server/chunks/[externals]__0_i~3ox._.js +3 -0
  91. package/.next/server/chunks/[externals]__0_i~3ox._.js.map +1 -0
  92. package/.next/server/chunks/[externals]__13p_1zh._.js +3 -0
  93. package/.next/server/chunks/[externals]__13p_1zh._.js.map +1 -0
  94. package/.next/server/chunks/[root-of-the-server]__01t2c3w._.js +38 -0
  95. package/.next/server/chunks/[root-of-the-server]__01t2c3w._.js.map +1 -0
  96. package/.next/server/chunks/{[root-of-the-server]__0g0u0lm._.js → [root-of-the-server]__024yzee._.js} +2 -2
  97. package/.next/server/chunks/{[root-of-the-server]__00q0o~z._.js → [root-of-the-server]__082iwfa._.js} +2 -2
  98. package/.next/server/chunks/[root-of-the-server]__0chedn~._.js +1 -1
  99. package/.next/server/chunks/[root-of-the-server]__0mwf0bn._.js +38 -0
  100. package/.next/server/chunks/[root-of-the-server]__0mwf0bn._.js.map +1 -0
  101. package/.next/server/chunks/{[root-of-the-server]__0-74syk._.js → [root-of-the-server]__0ru3_it._.js} +2 -2
  102. package/.next/server/chunks/[root-of-the-server]__0u6y_k1._.js +111 -0
  103. package/.next/server/chunks/[root-of-the-server]__0u6y_k1._.js.map +1 -0
  104. package/.next/server/chunks/{[root-of-the-server]__0866q87._.js → [root-of-the-server]__0xa4dzi._.js} +2 -2
  105. package/.next/server/chunks/[root-of-the-server]__13415c6._.js +96 -0
  106. package/.next/server/chunks/[root-of-the-server]__13415c6._.js.map +1 -0
  107. package/.next/server/chunks/[root-of-the-server]__13j_28o._.js +96 -0
  108. package/.next/server/chunks/[root-of-the-server]__13j_28o._.js.map +1 -0
  109. package/.next/server/chunks/_next-internal_server_app_api_float_route_actions_012j~jr.js +3 -0
  110. package/.next/server/chunks/_next-internal_server_app_api_float_route_actions_012j~jr.js.map +1 -0
  111. package/.next/server/chunks/_next-internal_server_app_api_settings_alerts_route_actions_0ydcyko.js +3 -0
  112. package/.next/server/chunks/_next-internal_server_app_api_settings_alerts_route_actions_0ydcyko.js.map +1 -0
  113. package/.next/server/chunks/_next-internal_server_app_api_settings_notify_route_actions_0-j35mb.js +3 -0
  114. package/.next/server/chunks/_next-internal_server_app_api_settings_notify_route_actions_0-j35mb.js.map +1 -0
  115. package/.next/server/chunks/_next-internal_server_app_install_sh_route_actions_0cj-6me.js +3 -0
  116. package/.next/server/chunks/_next-internal_server_app_install_sh_route_actions_0cj-6me.js.map +1 -0
  117. package/.next/server/chunks/node_modules_next_06f88ko._.js +19 -0
  118. package/.next/server/chunks/node_modules_next_06f88ko._.js.map +1 -0
  119. package/.next/server/chunks/node_modules_next_dist_esm_build_templates_app-route_03jj5jj.js +18 -0
  120. package/.next/server/chunks/node_modules_next_dist_esm_build_templates_app-route_03jj5jj.js.map +1 -0
  121. package/.next/server/chunks/src_12i1qk5._.js +3 -0
  122. package/.next/server/chunks/src_12i1qk5._.js.map +1 -0
  123. package/.next/server/chunks/src_lib_alerts_ticker_ts_0n6oy1d._.js +169 -0
  124. package/.next/server/chunks/src_lib_alerts_ticker_ts_0n6oy1d._.js.map +1 -0
  125. package/.next/server/chunks/ssr/[root-of-the-server]__03mt8o0._.js +3 -0
  126. package/.next/server/chunks/ssr/[root-of-the-server]__03mt8o0._.js.map +1 -0
  127. package/.next/server/chunks/ssr/{[root-of-the-server]__098zro9._.js → [root-of-the-server]__090jxzh._.js} +2 -2
  128. package/.next/server/chunks/ssr/[root-of-the-server]__090jxzh._.js.map +1 -0
  129. package/.next/server/chunks/ssr/{[root-of-the-server]__09hgk-7._.js → [root-of-the-server]__0c3j40u._.js} +2 -2
  130. package/.next/server/chunks/ssr/[root-of-the-server]__0c3j40u._.js.map +1 -0
  131. package/.next/server/chunks/ssr/[root-of-the-server]__0ot4nev._.js +3 -0
  132. package/.next/server/chunks/ssr/[root-of-the-server]__0ot4nev._.js.map +1 -0
  133. package/.next/server/chunks/ssr/[root-of-the-server]__0q_~l2m._.js +90 -0
  134. package/.next/server/chunks/ssr/[root-of-the-server]__0q_~l2m._.js.map +1 -0
  135. package/.next/server/chunks/ssr/{[root-of-the-server]__0cqes87._.js → [root-of-the-server]__0qt26ty._.js} +2 -2
  136. package/.next/server/chunks/ssr/[root-of-the-server]__0qt26ty._.js.map +1 -0
  137. package/.next/server/chunks/ssr/[root-of-the-server]__0s5.uhg._.js +55 -0
  138. package/.next/server/chunks/ssr/[root-of-the-server]__0s5.uhg._.js.map +1 -0
  139. package/.next/server/chunks/ssr/[root-of-the-server]__0x~-phx._.js +3 -0
  140. package/.next/server/chunks/ssr/[root-of-the-server]__0x~-phx._.js.map +1 -0
  141. package/.next/server/chunks/ssr/[root-of-the-server]__0y_zq8k._.js +3 -0
  142. package/.next/server/chunks/ssr/[root-of-the-server]__0y_zq8k._.js.map +1 -0
  143. package/.next/server/chunks/ssr/{[root-of-the-server]__08qr2ji._.js → [root-of-the-server]__0~953ob._.js} +2 -2
  144. package/.next/server/chunks/ssr/[root-of-the-server]__0~953ob._.js.map +1 -0
  145. package/.next/server/chunks/ssr/_0-9j34y._.js +70 -0
  146. package/.next/server/chunks/ssr/_0-9j34y._.js.map +1 -0
  147. package/.next/server/chunks/ssr/_0jxmm9h._.js +6 -0
  148. package/.next/server/chunks/ssr/_0jxmm9h._.js.map +1 -0
  149. package/.next/server/chunks/ssr/_0lfe3wr._.js +3 -0
  150. package/.next/server/chunks/ssr/_0lfe3wr._.js.map +1 -0
  151. package/.next/server/chunks/ssr/{node_modules_0i2xw~e._.js → _0nvprxh._.js} +5 -2
  152. package/.next/server/chunks/ssr/_0nvprxh._.js.map +1 -0
  153. package/.next/server/chunks/ssr/_0pugb10._.js +6 -0
  154. package/.next/server/chunks/ssr/_0pugb10._.js.map +1 -0
  155. package/.next/server/chunks/ssr/_next-internal_server_app_float_page_actions_0x6hm4p.js +3 -0
  156. package/.next/server/chunks/ssr/_next-internal_server_app_float_page_actions_0x6hm4p.js.map +1 -0
  157. package/.next/server/chunks/ssr/_next-internal_server_app_settings_page_actions_0mr68ai.js +3 -0
  158. package/.next/server/chunks/ssr/_next-internal_server_app_settings_page_actions_0mr68ai.js.map +1 -0
  159. package/.next/server/chunks/ssr/node_modules_@swc_helpers_cjs__interop_require_default_cjs_11~q6fv._.js +3 -0
  160. package/.next/server/chunks/ssr/node_modules_@swc_helpers_cjs__interop_require_default_cjs_11~q6fv._.js.map +1 -0
  161. package/.next/server/chunks/ssr/node_modules_next_dist_09jzzl8._.js +3 -0
  162. package/.next/server/chunks/ssr/node_modules_next_dist_09jzzl8._.js.map +1 -0
  163. package/.next/server/chunks/ssr/{node_modules_next_dist_0e1izl_._.js → node_modules_next_dist_0h9llsw._.js} +2 -2
  164. package/.next/server/chunks/ssr/node_modules_next_dist_0h9llsw._.js.map +1 -0
  165. package/.next/server/chunks/ssr/node_modules_next_dist_esm_build_templates_app-page_06b_a87.js +4 -0
  166. package/.next/server/chunks/ssr/node_modules_next_dist_esm_build_templates_app-page_06b_a87.js.map +1 -0
  167. package/.next/server/chunks/ssr/node_modules_next_dist_esm_build_templates_app-page_0p_u4px.js +4 -0
  168. package/.next/server/chunks/ssr/node_modules_next_dist_esm_build_templates_app-page_0p_u4px.js.map +1 -0
  169. package/.next/server/chunks/ssr/src_0urkups._.js +3 -0
  170. package/.next/server/chunks/ssr/src_0urkups._.js.map +1 -0
  171. package/.next/server/chunks/ssr/src_components_0ox9d~w._.js +3 -0
  172. package/.next/server/chunks/ssr/src_components_0ox9d~w._.js.map +1 -0
  173. package/.next/server/chunks/ssr/src_components_CodexAccountsPanel_tsx_0n9xjzi._.js +1 -1
  174. package/.next/server/chunks/ssr/src_components_CodexAccountsPanel_tsx_0n9xjzi._.js.map +1 -1
  175. package/.next/server/chunks/ssr/src_components_FloatingWidget_tsx_089.6oo._.js +3 -0
  176. package/.next/server/chunks/ssr/src_components_FloatingWidget_tsx_089.6oo._.js.map +1 -0
  177. package/.next/server/chunks/ssr/src_lib_i18n_client_tsx_07pysgz._.js +3 -0
  178. package/.next/server/chunks/ssr/src_lib_i18n_client_tsx_07pysgz._.js.map +1 -0
  179. package/.next/server/edge/chunks/_0-m5tdn._.js +3 -0
  180. package/.next/server/edge/chunks/_0-m5tdn._.js.map +1 -0
  181. package/.next/server/edge/chunks/node_modules_next_dist_esm_build_templates_edge-wrapper_0mz-5sp.js.map +1 -0
  182. package/.next/server/edge/chunks/turbopack-node_modules_next_dist_esm_build_templates_edge-wrapper_0mz-5sp.js +3 -0
  183. package/.next/server/functions-config-manifest.json +4 -1
  184. package/.next/server/instrumentation/middleware-manifest.json +12 -0
  185. package/.next/server/instrumentation.js +4 -0
  186. package/.next/server/instrumentation.js.map +5 -0
  187. package/.next/server/instrumentation.js.nft.json +1 -0
  188. package/.next/server/middleware-build-manifest.js +7 -8
  189. package/.next/server/next-font-manifest.js +1 -1
  190. package/.next/server/next-font-manifest.json +8 -0
  191. package/.next/server/pages/500.html +1 -1
  192. package/.next/server/pages-manifest.json +0 -1
  193. package/.next/server/server-reference-manifest.js +1 -1
  194. package/.next/server/server-reference-manifest.json +1 -1
  195. package/.next/static/chunks/0bymg5d-lqs0o.js +5 -0
  196. package/.next/static/chunks/0ebdqsutnp8y4.js +1 -0
  197. package/.next/static/chunks/0i3~mnai-l497.js +1 -0
  198. package/.next/static/chunks/0idj4tdmyr934.js +1 -0
  199. package/.next/static/chunks/10eoi4q2wk80z.js +1 -0
  200. package/.next/static/chunks/10refkmyp9rbl.css +3 -0
  201. package/.next/static/chunks/{07lhk_q6pmm3r.js → 10u3y4bw1ayzs.js} +1 -1
  202. package/.next/static/chunks/{00gq-v0e07i1q.js → 115dplafwys-z.js} +1 -1
  203. package/.next/static/chunks/12-9memveha-v.js +1 -0
  204. package/.next/static/chunks/163s8y.j70104.js +4 -0
  205. package/.next/static/chunks/turbopack-0k9twle9a8sh6.js +1 -0
  206. package/.next/types/routes.d.ts +8 -2
  207. package/.next/types/validator.ts +54 -0
  208. package/README.md +57 -17
  209. package/bin/vibemeter-float.swift +970 -0
  210. package/bin/vibemeter-notify.sh +172 -0
  211. package/bin/vibemeter.mjs +385 -7
  212. package/package.json +2 -1
  213. package/public/demo1.png +0 -0
  214. package/public/float-ball.png +0 -0
  215. package/public/float-collapsed.png +0 -0
  216. package/public/float-expanded.png +0 -0
  217. package/public/pay-alipay.jpg +0 -0
  218. package/.next/server/app/_not-found.html +0 -1
  219. package/.next/server/app/_not-found.meta +0 -16
  220. package/.next/server/app/_not-found.rsc +0 -16
  221. package/.next/server/app/_not-found.segments/_full.segment.rsc +0 -16
  222. package/.next/server/app/_not-found.segments/_head.segment.rsc +0 -6
  223. package/.next/server/app/_not-found.segments/_index.segment.rsc +0 -5
  224. package/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +0 -5
  225. package/.next/server/app/_not-found.segments/_not-found.segment.rsc +0 -5
  226. package/.next/server/app/_not-found.segments/_tree.segment.rsc +0 -2
  227. package/.next/server/chunks/[root-of-the-server]__0y68a5d._.js +0 -96
  228. package/.next/server/chunks/[root-of-the-server]__0y68a5d._.js.map +0 -1
  229. package/.next/server/chunks/ssr/[root-of-the-server]__054lv8d._.js +0 -3
  230. package/.next/server/chunks/ssr/[root-of-the-server]__054lv8d._.js.map +0 -1
  231. package/.next/server/chunks/ssr/[root-of-the-server]__08qr2ji._.js.map +0 -1
  232. package/.next/server/chunks/ssr/[root-of-the-server]__098zro9._.js.map +0 -1
  233. package/.next/server/chunks/ssr/[root-of-the-server]__09hgk-7._.js.map +0 -1
  234. package/.next/server/chunks/ssr/[root-of-the-server]__0cqes87._.js.map +0 -1
  235. package/.next/server/chunks/ssr/[root-of-the-server]__0l.drdn._.js +0 -3
  236. package/.next/server/chunks/ssr/[root-of-the-server]__0l.drdn._.js.map +0 -1
  237. package/.next/server/chunks/ssr/[root-of-the-server]__0v1zqf~._.js +0 -120
  238. package/.next/server/chunks/ssr/[root-of-the-server]__0v1zqf~._.js.map +0 -1
  239. package/.next/server/chunks/ssr/[root-of-the-server]__0v73tbn._.js +0 -3
  240. package/.next/server/chunks/ssr/[root-of-the-server]__0v73tbn._.js.map +0 -1
  241. package/.next/server/chunks/ssr/_0ye~8el._.js +0 -7
  242. package/.next/server/chunks/ssr/_0ye~8el._.js.map +0 -1
  243. package/.next/server/chunks/ssr/node_modules_0ck2~9g._.js +0 -3
  244. package/.next/server/chunks/ssr/node_modules_0ck2~9g._.js.map +0 -1
  245. package/.next/server/chunks/ssr/node_modules_0i2xw~e._.js.map +0 -1
  246. package/.next/server/chunks/ssr/node_modules_next_dist_0e1izl_._.js.map +0 -1
  247. package/.next/server/chunks/ssr/node_modules_next_dist_0gsjr7_._.js +0 -3
  248. package/.next/server/chunks/ssr/node_modules_next_dist_0gsjr7_._.js.map +0 -1
  249. package/.next/server/pages/404.html +0 -1
  250. package/.next/static/chunks/002~6040mndie.css +0 -3
  251. package/.next/static/chunks/02g3221oh~3le.js +0 -2
  252. package/.next/static/chunks/0erq0bmub6w_z.js +0 -2
  253. package/.next/static/chunks/0ljfidstam_7k.js +0 -1
  254. package/.next/static/chunks/0pqt~8bl3ukh4.js +0 -4
  255. package/.next/static/chunks/turbopack-14vio.b1b9i4l.js +0 -1
  256. /package/.next/server/chunks/{[root-of-the-server]__0g0u0lm._.js.map → [root-of-the-server]__024yzee._.js.map} +0 -0
  257. /package/.next/server/chunks/{[root-of-the-server]__00q0o~z._.js.map → [root-of-the-server]__082iwfa._.js.map} +0 -0
  258. /package/.next/server/chunks/{[root-of-the-server]__0-74syk._.js.map → [root-of-the-server]__0ru3_it._.js.map} +0 -0
  259. /package/.next/server/chunks/{[root-of-the-server]__0866q87._.js.map → [root-of-the-server]__0xa4dzi._.js.map} +0 -0
  260. /package/.next/static/{Ong62ufsHaRU36s0QK5ZZ → _Y03MiN6NI16Ms86q6vCJ}/_buildManifest.js +0 -0
  261. /package/.next/static/{Ong62ufsHaRU36s0QK5ZZ → _Y03MiN6NI16Ms86q6vCJ}/_clientMiddlewareManifest.js +0 -0
  262. /package/.next/static/{Ong62ufsHaRU36s0QK5ZZ → _Y03MiN6NI16Ms86q6vCJ}/_ssgManifest.js +0 -0
@@ -0,0 +1,172 @@
1
+ #!/usr/bin/env bash
2
+ # vibemeter-notify — speak + display a macOS notification when Claude Code /
3
+ # Codex finishes. Driven by env vars so Vibemeter's settings page can rewrite
4
+ # the hook command without redeploying this script.
5
+ #
6
+ # Usage: vibemeter-notify <tool> <status>
7
+ # tool — "Claude", "Codex", ... (shown in the title + spoken)
8
+ # status — complete | needs_input | failed | <anything else>
9
+
10
+ set -euo pipefail
11
+
12
+ TOOL="${1:-AI}"
13
+ STATUS="${2:-complete}"
14
+ LOCALE="${VIBEMETER_NOTIFY_LOCALE:-zh}"
15
+ PROJECT="${VIBEMETER_NOTIFY_PROJECT:-$(basename "${PWD:-unknown}")}"
16
+ LOCK_DIR="${VIBEMETER_NOTIFY_LOCK_DIR:-${TMPDIR:-/tmp}/vibemeter-notify.lock}"
17
+ STATE_FILE="${VIBEMETER_NOTIFY_STATE_FILE:-${TMPDIR:-/tmp}/vibemeter-notify.last}"
18
+ DEDUPE_SECONDS="${VIBEMETER_NOTIFY_DEDUPE_SECONDS:-4}"
19
+
20
+ # Voice default depends on locale: Chinese voice for zh, system default for en
21
+ # (English say without -v uses the user's system voice, usually Alex / Samantha).
22
+ if [[ -n "${VIBEMETER_NOTIFY_VOICE:-}" ]]; then
23
+ VOICE="$VIBEMETER_NOTIFY_VOICE"
24
+ elif [[ "$LOCALE" == "en" ]]; then
25
+ VOICE="" # empty means: let `say` pick the system default
26
+ else
27
+ VOICE="Tingting"
28
+ fi
29
+
30
+ if [[ "$LOCALE" == "en" ]]; then
31
+ case "$STATUS" in
32
+ complete|done|success)
33
+ TITLE="$TOOL done"
34
+ BODY="$PROJECT finished"
35
+ SAY_SUFFIX="done"
36
+ ;;
37
+ needs_input|input|intervention|permission)
38
+ TITLE="$TOOL needs you"
39
+ BODY="$PROJECT needs your attention"
40
+ SAY_SUFFIX="needs you"
41
+ ;;
42
+ failed|fail|error)
43
+ TITLE="$TOOL may have failed"
44
+ BODY="$PROJECT may have failed"
45
+ SAY_SUFFIX="may have failed"
46
+ ;;
47
+ *)
48
+ TITLE="$TOOL notice"
49
+ BODY="$PROJECT $STATUS"
50
+ SAY_SUFFIX="$STATUS"
51
+ ;;
52
+ esac
53
+ SAY_TEXT="$TOOL [[slnc 80]]$PROJECT[[slnc 80]] $SAY_SUFFIX"
54
+ else
55
+ case "$STATUS" in
56
+ complete|done|success)
57
+ TITLE="$TOOL 完成"
58
+ BODY="$PROJECT 已完成"
59
+ SAY_SUFFIX="完成"
60
+ ;;
61
+ needs_input|input|intervention|permission)
62
+ TITLE="$TOOL 需要介入"
63
+ BODY="$PROJECT 需要你看一下"
64
+ SAY_SUFFIX="需要你看一下"
65
+ ;;
66
+ failed|fail|error)
67
+ TITLE="$TOOL 可能失败"
68
+ BODY="$PROJECT 可能失败了"
69
+ SAY_SUFFIX="可能失败"
70
+ ;;
71
+ *)
72
+ TITLE="$TOOL 通知"
73
+ BODY="$PROJECT $STATUS"
74
+ SAY_SUFFIX="$STATUS"
75
+ ;;
76
+ esac
77
+ SAY_TEXT="$TOOL [[slnc 80]]$PROJECT[[slnc 80]] $SAY_SUFFIX"
78
+ fi
79
+ KEY="$TOOL|$PROJECT|$STATUS"
80
+ LOCK_HELD=0
81
+
82
+ release_lock() {
83
+ if [[ "$LOCK_HELD" == "1" ]]; then
84
+ rmdir "$LOCK_DIR" 2>/dev/null || true
85
+ fi
86
+ }
87
+
88
+ acquire_lock() {
89
+ local i
90
+ for i in {1..150}; do
91
+ if mkdir "$LOCK_DIR" 2>/dev/null; then
92
+ LOCK_HELD=1
93
+ trap release_lock EXIT
94
+ return 0
95
+ fi
96
+ sleep 0.1
97
+ done
98
+ return 1
99
+ }
100
+
101
+ escape_osascript_string() {
102
+ printf '%s' "$1" | sed 's/\\/\\\\/g; s/"/\\"/g'
103
+ }
104
+
105
+ should_skip_duplicate() {
106
+ local now last_ts last_key
107
+ now="$(date +%s)"
108
+ if [[ -f "$STATE_FILE" ]]; then
109
+ IFS=$'\t' read -r last_ts last_key < "$STATE_FILE" || true
110
+ if [[ "${last_key:-}" == "$KEY" && "${last_ts:-0}" =~ ^[0-9]+$ ]]; then
111
+ if (( now - last_ts < DEDUPE_SECONDS )); then
112
+ return 0
113
+ fi
114
+ fi
115
+ fi
116
+ mkdir -p "$(dirname "$STATE_FILE")" 2>/dev/null || true
117
+ printf '%s\t%s\n' "$now" "$KEY" > "$STATE_FILE"
118
+ return 1
119
+ }
120
+
121
+ emit_dry_run() {
122
+ printf 'TITLE=%s\n' "$TITLE"
123
+ printf 'SUBTITLE=%s\n' "$PROJECT"
124
+ printf 'BODY=%s\n' "$BODY"
125
+ printf 'SAY=%s\n' "$SAY_TEXT"
126
+ }
127
+
128
+ main() {
129
+ acquire_lock || true
130
+
131
+ if should_skip_duplicate; then
132
+ if [[ "${VIBEMETER_NOTIFY_DRY_RUN:-0}" == "1" ]]; then
133
+ printf 'SKIPPED=duplicate\n'
134
+ fi
135
+ return 0
136
+ fi
137
+
138
+ if [[ "${VIBEMETER_NOTIFY_DRY_RUN:-0}" == "1" ]]; then
139
+ emit_dry_run
140
+ return 0
141
+ fi
142
+
143
+ local title subtitle body
144
+ title="$(escape_osascript_string "$TITLE")"
145
+ subtitle="$(escape_osascript_string "$PROJECT")"
146
+ body="$(escape_osascript_string "$BODY")"
147
+
148
+ if [[ "${VIBEMETER_NOTIFY_VISUAL:-1}" != "0" ]]; then
149
+ # Prefer the bundled Vibemeter binary so the banner is attributed to
150
+ # Vibemeter and can carry an app icon. Fall back to osascript when the
151
+ # bundle hasn't been built yet (e.g. user never ran `vibemeter float`).
152
+ local data_dir="${VIBEMETER_DATA_DIR:-$HOME/.vibemeter}"
153
+ local app_bin="$data_dir/Vibemeter.app/Contents/MacOS/Vibemeter"
154
+ if [[ -x "$app_bin" ]]; then
155
+ "$app_bin" --notify "$TITLE" "$BODY" "$TOOL" >/dev/null 2>&1 \
156
+ || osascript -e "display notification \"$body\" with title \"$title\" subtitle \"$subtitle\"" >/dev/null 2>&1 \
157
+ || true
158
+ else
159
+ osascript -e "display notification \"$body\" with title \"$title\" subtitle \"$subtitle\"" >/dev/null 2>&1 || true
160
+ fi
161
+ fi
162
+
163
+ if [[ "${VIBEMETER_NOTIFY_SOUND:-1}" != "0" ]]; then
164
+ if [[ -n "$VOICE" ]]; then
165
+ say -v "$VOICE" "$SAY_TEXT" >/dev/null 2>&1 || say "$SAY_TEXT" >/dev/null 2>&1 || true
166
+ else
167
+ say "$SAY_TEXT" >/dev/null 2>&1 || true
168
+ fi
169
+ fi
170
+ }
171
+
172
+ main "$@"
package/bin/vibemeter.mjs CHANGED
@@ -4,12 +4,14 @@
4
4
  *
5
5
  * vibemeter start the server in the foreground
6
6
  * vibemeter install register as a LaunchAgent so it boots on login (macOS)
7
+ * vibemeter float open the desktop floating widget
7
8
  * vibemeter uninstall remove the LaunchAgent
8
9
  * vibemeter status show whether the daemon is loaded / running
9
10
  */
10
11
 
11
12
  import { spawn, spawnSync } from 'node:child_process';
12
- import { existsSync, mkdirSync, writeFileSync, rmSync, readFileSync } from 'node:fs';
13
+ import { existsSync, mkdirSync, writeFileSync, rmSync, readFileSync, statSync, copyFileSync } from 'node:fs';
14
+ import { createInterface } from 'node:readline';
13
15
  import { fileURLToPath } from 'node:url';
14
16
  import { dirname, join, resolve as resolvePath } from 'node:path';
15
17
  import { homedir, platform } from 'node:os';
@@ -18,11 +20,16 @@ import { createRequire } from 'node:module';
18
20
  const __dirname = dirname(fileURLToPath(import.meta.url));
19
21
  const ROOT = join(__dirname, '..');
20
22
  const REQUIRE_HOOK = join(__dirname, 'require-hook.cjs');
23
+ const FLOAT_SWIFT = join(__dirname, 'vibemeter-float.swift');
24
+ const NOTIFY_SCRIPT = join(__dirname, 'vibemeter-notify.sh');
21
25
  const DEFAULT_PORT = 9527;
22
26
  const PORT = process.env.PORT ?? String(DEFAULT_PORT);
23
27
  const DATA_DIR = process.env.VIBEMETER_DATA_DIR ?? join(homedir(), '.vibemeter');
24
28
  const LABEL = 'com.hirra.vibemeter';
25
29
  const PLIST_PATH = join(homedir(), 'Library', 'LaunchAgents', `${LABEL}.plist`);
30
+ const CLAUDE_SETTINGS_PATH = join(homedir(), '.claude', 'settings.json');
31
+ const CODEX_CONFIG_PATH = join(homedir(), '.codex', 'config.toml');
32
+ const HOOK_MARKER = 'vibemeter-notify.sh';
26
33
 
27
34
  process.env.VIBEMETER_DATA_DIR = DATA_DIR;
28
35
  mkdirSync(DATA_DIR, { recursive: true });
@@ -169,6 +176,77 @@ function macStatus() {
169
176
  }
170
177
  }
171
178
 
179
+ // Compile the Swift binary into a minimal .app bundle so it has a stable
180
+ // CFBundleIdentifier — UNUserNotificationCenter refuses to deliver banners
181
+ // from unbundled binaries. The bundle also keeps the Dock icon hidden via
182
+ // LSUIElement and gives users a real "Vibemeter" name in the notification.
183
+ const APP_BUNDLE = join(DATA_DIR, 'Vibemeter.app');
184
+ const APP_BINARY = join(APP_BUNDLE, 'Contents', 'MacOS', 'Vibemeter');
185
+ const APP_INFO_PLIST = join(APP_BUNDLE, 'Contents', 'Info.plist');
186
+
187
+ function infoPlistXml() {
188
+ return `<?xml version="1.0" encoding="UTF-8"?>
189
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
190
+ <plist version="1.0">
191
+ <dict>
192
+ <key>CFBundleName</key><string>Vibemeter</string>
193
+ <key>CFBundleDisplayName</key><string>Vibemeter</string>
194
+ <key>CFBundleIdentifier</key><string>com.hirra.vibemeter</string>
195
+ <key>CFBundleExecutable</key><string>Vibemeter</string>
196
+ <key>CFBundlePackageType</key><string>APPL</string>
197
+ <key>CFBundleVersion</key><string>1</string>
198
+ <key>CFBundleShortVersionString</key><string>1.0</string>
199
+ <key>LSMinimumSystemVersion</key><string>11.0</string>
200
+ <key>LSUIElement</key><true/>
201
+ <key>NSHighResolutionCapable</key><true/>
202
+ </dict>
203
+ </plist>
204
+ `;
205
+ }
206
+
207
+ function resolveFloatBinary() {
208
+ if (!existsSync(FLOAT_SWIFT)) return null;
209
+ const stale = !existsSync(APP_BINARY) || statSync(APP_BINARY).mtimeMs < statSync(FLOAT_SWIFT).mtimeMs;
210
+ if (!stale && existsSync(APP_INFO_PLIST)) return APP_BINARY;
211
+
212
+ mkdirSync(dirname(APP_BINARY), { recursive: true });
213
+ writeFileSync(APP_INFO_PLIST, infoPlistXml());
214
+ const result = spawnSync('/usr/bin/swiftc', [FLOAT_SWIFT, '-o', APP_BINARY], {
215
+ stdio: 'inherit',
216
+ env: process.env,
217
+ });
218
+ return result.status === 0 ? APP_BINARY : null;
219
+ }
220
+
221
+ function openFloat() {
222
+ const url = `http://localhost:${PORT}/float`;
223
+ if (platform() === 'darwin') {
224
+ const binary = resolveFloatBinary();
225
+ const native = binary
226
+ ? spawn(binary, [url], {
227
+ detached: true,
228
+ stdio: 'ignore',
229
+ env: { ...process.env, VIBEMETER_DATA_DIR: DATA_DIR },
230
+ })
231
+ : null;
232
+ if (native) {
233
+ native.unref();
234
+ console.log(`✓ Opened Vibemeter floating window (${url})`);
235
+ return;
236
+ }
237
+
238
+ const app = spawnSync('open', ['-na', 'Google Chrome', '--args', `--app=${url}`, '--window-size=260,380'], { stdio: 'inherit' });
239
+ if (app.status === 0) return;
240
+ spawnSync('open', [url], { stdio: 'inherit' });
241
+ return;
242
+ }
243
+ if (platform() === 'linux') {
244
+ spawnSync('xdg-open', [url], { stdio: 'inherit' });
245
+ return;
246
+ }
247
+ console.log(url);
248
+ }
249
+
172
250
  function linuxInstallHint() {
173
251
  const scriptPath = resolvePath(fileURLToPath(import.meta.url));
174
252
  const unitPath = join(homedir(), '.config', 'systemd', 'user', 'vibemeter.service');
@@ -181,16 +259,289 @@ function linuxInstallHint() {
181
259
  console.log(`\n(target file: ${unitPath})`);
182
260
  }
183
261
 
262
+ // ── voice notifications ──────────────────────────────────────────────────
263
+
264
+ function timestampTag() {
265
+ const d = new Date();
266
+ const pad = (n) => String(n).padStart(2, '0');
267
+ return `${d.getFullYear()}${pad(d.getMonth() + 1)}${pad(d.getDate())}${pad(d.getHours())}${pad(d.getMinutes())}${pad(d.getSeconds())}`;
268
+ }
269
+
270
+ function backupOnce(path, tag) {
271
+ if (!existsSync(path)) return null;
272
+ const bak = `${path}.bak-vibemeter-${tag}`;
273
+ copyFileSync(path, bak);
274
+ return bak;
275
+ }
276
+
277
+ function readJsonSafe(path) {
278
+ try { return JSON.parse(readFileSync(path, 'utf8')); }
279
+ catch { return null; }
280
+ }
281
+
282
+ function writeJsonPretty(path, obj) {
283
+ mkdirSync(dirname(path), { recursive: true });
284
+ writeFileSync(path, JSON.stringify(obj, null, 2) + '\n');
285
+ }
286
+
287
+ function buildHookCommand(locale = 'zh') {
288
+ // Hook commands inherit a restricted PATH; absolute path is required.
289
+ // Locale env prefix is baked into the command so the bash speaker picks
290
+ // the right phrases without needing to read another config file.
291
+ return `VIBEMETER_NOTIFY_LOCALE=${shellQuote(locale)} ${shellQuote(NOTIFY_SCRIPT)} Claude`;
292
+ }
293
+
294
+ function shellQuote(s) {
295
+ if (/^[A-Za-z0-9_\-./]+$/.test(s)) return s;
296
+ return `'${String(s).replace(/'/g, `'\\''`)}'`;
297
+ }
298
+
299
+ function ensureClaudeHook(settings, eventName, statusArg, locale) {
300
+ if (!settings.hooks || typeof settings.hooks !== 'object') settings.hooks = {};
301
+ const list = Array.isArray(settings.hooks[eventName]) ? settings.hooks[eventName] : [];
302
+ const command = `${buildHookCommand(locale)} ${statusArg}`;
303
+ // Replace any existing Vibemeter hook so re-applying with a new locale
304
+ // updates the env prefix in place.
305
+ const filtered = list
306
+ .map((entry) => {
307
+ if (!Array.isArray(entry?.hooks)) return entry;
308
+ const kept = entry.hooks.filter((h) => !(typeof h?.command === 'string' && h.command.includes(HOOK_MARKER)));
309
+ return kept.length ? { ...entry, hooks: kept } : null;
310
+ })
311
+ .filter(Boolean);
312
+ filtered.push({ hooks: [{ type: 'command', command, async: true }] });
313
+ settings.hooks[eventName] = filtered;
314
+ return true;
315
+ }
316
+
317
+ function stripClaudeHook(settings, eventName) {
318
+ if (!settings.hooks || !Array.isArray(settings.hooks[eventName])) return false;
319
+ const before = settings.hooks[eventName];
320
+ const after = before
321
+ .map((entry) => {
322
+ if (!Array.isArray(entry?.hooks)) return entry;
323
+ const kept = entry.hooks.filter((h) => !(typeof h?.command === 'string' && h.command.includes(HOOK_MARKER)));
324
+ return kept.length ? { ...entry, hooks: kept } : null;
325
+ })
326
+ .filter(Boolean);
327
+ if (after.length === before.length && after.every((e, i) => e === before[i])) return false;
328
+ if (after.length === 0) delete settings.hooks[eventName];
329
+ else settings.hooks[eventName] = after;
330
+ return true;
331
+ }
332
+
333
+ function installClaudeHooks({ stop, notification, locale = 'zh' }) {
334
+ const settings = readJsonSafe(CLAUDE_SETTINGS_PATH) ?? {};
335
+ let changed = false;
336
+ if (stop) changed = ensureClaudeHook(settings, 'Stop', 'complete', locale) || changed;
337
+ if (notification) changed = ensureClaudeHook(settings, 'Notification', 'needs_input', locale) || changed;
338
+ if (!changed) return { changed: false, backup: null };
339
+ const backup = backupOnce(CLAUDE_SETTINGS_PATH, timestampTag());
340
+ writeJsonPretty(CLAUDE_SETTINGS_PATH, settings);
341
+ return { changed: true, backup };
342
+ }
343
+
344
+ function uninstallClaudeHooks() {
345
+ const settings = readJsonSafe(CLAUDE_SETTINGS_PATH);
346
+ if (!settings) return { changed: false, backup: null };
347
+ const removedStop = stripClaudeHook(settings, 'Stop');
348
+ const removedNotif = stripClaudeHook(settings, 'Notification');
349
+ if (!removedStop && !removedNotif) return { changed: false, backup: null };
350
+ if (settings.hooks && Object.keys(settings.hooks).length === 0) delete settings.hooks;
351
+ const backup = backupOnce(CLAUDE_SETTINGS_PATH, timestampTag());
352
+ writeJsonPretty(CLAUDE_SETTINGS_PATH, settings);
353
+ return { changed: true, backup };
354
+ }
355
+
356
+ function codexNotifyLine(locale = 'zh') {
357
+ // Codex's notify config is an exec array — locale prefix needs a sh -c wrapper.
358
+ return `notify = ["sh", "-c", ${JSON.stringify(`VIBEMETER_NOTIFY_LOCALE=${locale} ${NOTIFY_SCRIPT.replace(/'/g, `'\\''`)} Codex complete`)}]`;
359
+ }
360
+
361
+ function installCodexNotify(locale = 'zh') {
362
+ if (!existsSync(CODEX_CONFIG_PATH)) {
363
+ return { changed: false, backup: null, skipped: 'no-config' };
364
+ }
365
+ const original = readFileSync(CODEX_CONFIG_PATH, 'utf8');
366
+ const lines = original.split(/\r?\n/);
367
+ // Find top-level `notify = ...` (before any [section] header).
368
+ let notifyIndex = -1;
369
+ for (let i = 0; i < lines.length; i++) {
370
+ const line = lines[i];
371
+ if (/^\s*\[/.test(line)) break;
372
+ if (/^\s*notify\s*=/.test(line)) { notifyIndex = i; break; }
373
+ }
374
+ const want = codexNotifyLine(locale);
375
+ if (notifyIndex >= 0) {
376
+ if (lines[notifyIndex].trim() === want) return { changed: false, backup: null };
377
+ if (!lines[notifyIndex].includes(HOOK_MARKER)) {
378
+ return { changed: false, backup: null, skipped: 'foreign-notify', existing: lines[notifyIndex] };
379
+ }
380
+ lines[notifyIndex] = want;
381
+ } else {
382
+ // Insert before the first [section] header, or at the top if none.
383
+ let insertAt = lines.findIndex((l) => /^\s*\[/.test(l));
384
+ if (insertAt < 0) insertAt = lines.length;
385
+ lines.splice(insertAt, 0, want);
386
+ }
387
+ const backup = backupOnce(CODEX_CONFIG_PATH, timestampTag());
388
+ writeFileSync(CODEX_CONFIG_PATH, lines.join('\n'));
389
+ return { changed: true, backup };
390
+ }
391
+
392
+ function uninstallCodexNotify() {
393
+ if (!existsSync(CODEX_CONFIG_PATH)) return { changed: false, backup: null };
394
+ const original = readFileSync(CODEX_CONFIG_PATH, 'utf8');
395
+ const lines = original.split(/\r?\n/);
396
+ let removed = false;
397
+ for (let i = 0; i < lines.length; i++) {
398
+ if (/^\s*\[/.test(lines[i])) break;
399
+ if (/^\s*notify\s*=/.test(lines[i]) && lines[i].includes(HOOK_MARKER)) {
400
+ lines.splice(i, 1);
401
+ removed = true;
402
+ break;
403
+ }
404
+ }
405
+ if (!removed) return { changed: false, backup: null };
406
+ const backup = backupOnce(CODEX_CONFIG_PATH, timestampTag());
407
+ writeFileSync(CODEX_CONFIG_PATH, lines.join('\n'));
408
+ return { changed: true, backup };
409
+ }
410
+
411
+ function notifyInstall(opts = {}) {
412
+ const { stop = true, notification = false, codex = true, locale = process.env.VIBEMETER_NOTIFY_LOCALE || 'zh' } = opts;
413
+ if (!existsSync(NOTIFY_SCRIPT)) {
414
+ console.error(`Notify script missing: ${NOTIFY_SCRIPT}`);
415
+ process.exit(1);
416
+ }
417
+ // Build the .app bundle eagerly so the very first hook firing can use the
418
+ // native notification path instead of falling back to osascript.
419
+ if (platform() === 'darwin') resolveFloatBinary();
420
+ const cl = installClaudeHooks({ stop, notification, locale });
421
+ const cx = codex ? installCodexNotify(locale) : { changed: false, backup: null, skipped: 'disabled' };
422
+ console.log('');
423
+ console.log(' Vibemeter voice notifications');
424
+ if (cl.changed) console.log(` ✓ Claude Code hooks updated (${CLAUDE_SETTINGS_PATH})`);
425
+ else console.log(` · Claude Code hooks already in place`);
426
+ if (cl.backup) console.log(` backup: ${cl.backup}`);
427
+ if (cx.changed) console.log(` ✓ Codex notify updated (${CODEX_CONFIG_PATH})`);
428
+ else if (cx.skipped === 'no-config') console.log(` · Codex config not found — skipped`);
429
+ else if (cx.skipped === 'foreign-notify') console.log(` ! Codex notify already set to: ${cx.existing} — left as-is`);
430
+ else if (cx.skipped === 'disabled') console.log(` · Codex notify left unchanged`);
431
+ else console.log(` · Codex notify already pointing to Vibemeter`);
432
+ if (cx.backup) console.log(` backup: ${cx.backup}`);
433
+ console.log('');
434
+ console.log(' Open http://localhost:' + PORT + '/settings to change voice or disable.');
435
+ }
436
+
437
+ function notifyUninstall() {
438
+ const cl = uninstallClaudeHooks();
439
+ const cx = uninstallCodexNotify();
440
+ console.log('');
441
+ if (cl.changed) console.log(` ✓ Removed Vibemeter hooks from ${CLAUDE_SETTINGS_PATH}`);
442
+ else console.log(` · No Vibemeter hooks in ${CLAUDE_SETTINGS_PATH}`);
443
+ if (cl.backup) console.log(` backup: ${cl.backup}`);
444
+ if (cx.changed) console.log(` ✓ Removed Vibemeter notify from ${CODEX_CONFIG_PATH}`);
445
+ else console.log(` · No Vibemeter notify in ${CODEX_CONFIG_PATH}`);
446
+ if (cx.backup) console.log(` backup: ${cx.backup}`);
447
+ console.log('');
448
+ }
449
+
450
+ function notifyStatusObject() {
451
+ const settings = readJsonSafe(CLAUDE_SETTINGS_PATH);
452
+ const hasClaudeHook = (event) => {
453
+ const list = settings?.hooks?.[event];
454
+ if (!Array.isArray(list)) return false;
455
+ return list.some((entry) =>
456
+ Array.isArray(entry?.hooks) && entry.hooks.some((h) => typeof h?.command === 'string' && h.command.includes(HOOK_MARKER)),
457
+ );
458
+ };
459
+ let codexInstalled = false;
460
+ let codexForeign = null;
461
+ if (existsSync(CODEX_CONFIG_PATH)) {
462
+ const lines = readFileSync(CODEX_CONFIG_PATH, 'utf8').split(/\r?\n/);
463
+ for (const line of lines) {
464
+ if (/^\s*\[/.test(line)) break;
465
+ if (/^\s*notify\s*=/.test(line)) {
466
+ if (line.includes(HOOK_MARKER)) codexInstalled = true;
467
+ else codexForeign = line.trim();
468
+ break;
469
+ }
470
+ }
471
+ }
472
+ return {
473
+ scriptPath: NOTIFY_SCRIPT,
474
+ claudeSettingsPath: CLAUDE_SETTINGS_PATH,
475
+ codexConfigPath: CODEX_CONFIG_PATH,
476
+ claudeStop: hasClaudeHook('Stop'),
477
+ claudeNotification: hasClaudeHook('Notification'),
478
+ codex: codexInstalled,
479
+ codexForeign,
480
+ codexConfigExists: existsSync(CODEX_CONFIG_PATH),
481
+ };
482
+ }
483
+
484
+ function notifyStatus() {
485
+ const s = notifyStatusObject();
486
+ console.log('');
487
+ console.log(` Voice notification status`);
488
+ console.log(` ───────────────────────────`);
489
+ console.log(` Claude Stop hook: ${s.claudeStop ? '✓ installed' : '· not installed'}`);
490
+ console.log(` Claude Notification hook: ${s.claudeNotification ? '✓ installed' : '· not installed'}`);
491
+ console.log(` Codex notify: ${s.codex ? '✓ installed' : s.codexForeign ? `! foreign (${s.codexForeign})` : s.codexConfigExists ? '· not installed' : '· no codex config'}`);
492
+ console.log(` Script: ${s.scriptPath}`);
493
+ console.log('');
494
+ }
495
+
496
+ async function promptYesNo(question, defaultYes = true) {
497
+ if (!process.stdin.isTTY) return defaultYes;
498
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
499
+ const suffix = defaultYes ? ' [Y/n] ' : ' [y/N] ';
500
+ return new Promise((resolve) => {
501
+ rl.question(question + suffix, (answer) => {
502
+ rl.close();
503
+ const a = answer.trim().toLowerCase();
504
+ if (!a) resolve(defaultYes);
505
+ else resolve(a === 'y' || a === 'yes');
506
+ });
507
+ });
508
+ }
509
+
510
+ async function offerNotifyDuringInstall() {
511
+ if (platform() !== 'darwin') return;
512
+ const s = notifyStatusObject();
513
+ if (s.claudeStop && s.codex) return; // already wired
514
+ if (!process.stdin.isTTY) {
515
+ console.log('');
516
+ console.log(' Voice notifications available — visit http://localhost:' + PORT + '/settings to enable.');
517
+ return;
518
+ }
519
+ console.log('');
520
+ const enable = await promptYesNo('Enable voice notifications for Claude Code + Codex?', true);
521
+ if (!enable) {
522
+ console.log(' Skipped. You can enable later from http://localhost:' + PORT + '/settings');
523
+ return;
524
+ }
525
+ notifyInstall({ stop: true, notification: false, codex: true });
526
+ }
527
+
528
+ // ── end voice notifications ──────────────────────────────────────────────
529
+
184
530
  function help() {
185
531
  console.log(`
186
532
  Vibemeter — local AI coding dashboard
187
533
 
188
534
  Usage:
189
- vibemeter start the server in the foreground (Ctrl-C to stop)
190
- vibemeter install run on login + keep alive (macOS LaunchAgent)
191
- vibemeter uninstall remove the auto-start config
192
- vibemeter status show whether the daemon is loaded + tail logs
193
- vibemeter help this message
535
+ vibemeter start the server in the foreground (Ctrl-C to stop)
536
+ vibemeter install run on login + keep alive (macOS LaunchAgent)
537
+ vibemeter float open the desktop floating widget
538
+ vibemeter uninstall remove the auto-start config
539
+ vibemeter status show whether the daemon is loaded + tail logs
540
+ vibemeter notify <t> <s> speak a notification (used by hooks)
541
+ vibemeter notify-install wire Vibemeter into Claude Code + Codex
542
+ vibemeter notify-uninstall remove Vibemeter from Claude Code + Codex
543
+ vibemeter notify-status show which voice hooks are installed
544
+ vibemeter help this message
194
545
 
195
546
  Environment:
196
547
  PORT default ${DEFAULT_PORT}
@@ -205,10 +556,37 @@ switch (cmd) {
205
556
  await start();
206
557
  break;
207
558
  case 'install':
208
- if (platform() === 'darwin') macInstall();
559
+ if (platform() === 'darwin') {
560
+ macInstall();
561
+ await offerNotifyDuringInstall();
562
+ }
209
563
  else if (platform() === 'linux') linuxInstallHint();
210
564
  else { console.error('Auto-start install not implemented for this platform yet.'); process.exit(1); }
211
565
  break;
566
+ case 'notify': {
567
+ if (platform() !== 'darwin') {
568
+ console.error('Voice notifications are macOS-only right now.');
569
+ process.exit(1);
570
+ }
571
+ const tool = process.argv[3] ?? 'AI';
572
+ const status = process.argv[4] ?? 'complete';
573
+ const r = spawnSync(NOTIFY_SCRIPT, [tool, status], { stdio: 'inherit' });
574
+ process.exit(r.status ?? 0);
575
+ }
576
+ case 'notify-install':
577
+ if (platform() !== 'darwin') { console.error('macOS only.'); process.exit(1); }
578
+ notifyInstall({ stop: true, notification: false, codex: true });
579
+ break;
580
+ case 'notify-uninstall':
581
+ if (platform() !== 'darwin') { console.error('macOS only.'); process.exit(1); }
582
+ notifyUninstall();
583
+ break;
584
+ case 'notify-status':
585
+ notifyStatus();
586
+ break;
587
+ case 'float':
588
+ openFloat();
589
+ break;
212
590
  case 'uninstall':
213
591
  if (platform() === 'darwin') macUninstall();
214
592
  else { console.error('Auto-start uninstall not implemented for this platform yet.'); process.exit(1); }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hirra/vibemeter",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
4
4
  "description": "Local-first dashboard for your AI coding life — Claude Code, Codex, Cursor session tracking with usage windows, spending, heatmaps, and achievements.",
5
5
  "keywords": [
6
6
  "claude-code",
@@ -38,6 +38,7 @@
38
38
  "start": "next start",
39
39
  "lint": "eslint",
40
40
  "typecheck": "tsc --noEmit",
41
+ "deploy:marketing": "bash scripts/deploy-marketing.sh",
41
42
  "prepack": "rm -rf .next && next build && rm -rf .next/cache .next/dev .next/diagnostics .next/trace .next/trace-build .next/turbopack"
42
43
  },
43
44
  "engines": {
Binary file
Binary file
Binary file
Binary file
Binary file
@@ -1 +0,0 @@
1
- <!DOCTYPE html><html lang="en" class="geist_a71539c9-module__T19VSG__variable geist_mono_8d43a2aa-module__8Li5zG__variable h-full antialiased"><head><meta charSet="utf-8"/><meta name="viewport" content="width=device-width, initial-scale=1"/><link rel="stylesheet" href="/_next/static/chunks/002~6040mndie.css" data-precedence="next"/><link rel="preload" as="script" fetchPriority="low" href="/_next/static/chunks/00gq-v0e07i1q.js"/><script src="/_next/static/chunks/0pqt~8bl3ukh4.js" async=""></script><script src="/_next/static/chunks/02g3221oh~3le.js" async=""></script><script src="/_next/static/chunks/07lhk_q6pmm3r.js" async=""></script><script src="/_next/static/chunks/turbopack-14vio.b1b9i4l.js" async=""></script><script src="/_next/static/chunks/01xlw8hd842-c.js" async=""></script><script src="/_next/static/chunks/0d3shmwh5_nmn.js" async=""></script><meta name="robots" content="noindex"/><meta name="next-size-adjust" content=""/><title>404: This page could not be found.</title><title>Vibemeter</title><meta name="description" content="Measure your AI coding vibe — local-first dashboard for Claude Code, Codex, and Cursor"/><link rel="icon" href="/favicon.ico?favicon.0x3dzn~oxb6tn.ico" sizes="256x256" type="image/x-icon"/><script src="/_next/static/chunks/03~yq9q893hmn.js" noModule=""></script></head><body class="min-h-full flex flex-col"><div hidden=""><!--$--><!--/$--></div><div style="font-family:system-ui,&quot;Segoe UI&quot;,Roboto,Helvetica,Arial,sans-serif,&quot;Apple Color Emoji&quot;,&quot;Segoe UI Emoji&quot;;height:100vh;text-align:center;display:flex;flex-direction:column;align-items:center;justify-content:center"><div><style>body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}</style><h1 class="next-error-h1" style="display:inline-block;margin:0 20px 0 0;padding:0 23px 0 0;font-size:24px;font-weight:500;vertical-align:top;line-height:49px">404</h1><div style="display:inline-block"><h2 style="font-size:14px;font-weight:400;line-height:49px;margin:0">This page could not be found.</h2></div></div></div><!--$--><!--/$--><script src="/_next/static/chunks/00gq-v0e07i1q.js" id="_R_" async=""></script><script>(self.__next_f=self.__next_f||[]).push([0])</script><script>self.__next_f.push([1,"1:\"$Sreact.fragment\"\n2:I[39756,[\"/_next/static/chunks/01xlw8hd842-c.js\",\"/_next/static/chunks/0d3shmwh5_nmn.js\"],\"default\"]\n3:I[37457,[\"/_next/static/chunks/01xlw8hd842-c.js\",\"/_next/static/chunks/0d3shmwh5_nmn.js\"],\"default\"]\n4:I[97367,[\"/_next/static/chunks/01xlw8hd842-c.js\",\"/_next/static/chunks/0d3shmwh5_nmn.js\"],\"OutletBoundary\"]\n5:\"$Sreact.suspense\"\n8:I[97367,[\"/_next/static/chunks/01xlw8hd842-c.js\",\"/_next/static/chunks/0d3shmwh5_nmn.js\"],\"ViewportBoundary\"]\na:I[97367,[\"/_next/static/chunks/01xlw8hd842-c.js\",\"/_next/static/chunks/0d3shmwh5_nmn.js\"],\"MetadataBoundary\"]\nc:I[68027,[\"/_next/static/chunks/01xlw8hd842-c.js\",\"/_next/static/chunks/0d3shmwh5_nmn.js\"],\"default\",1]\n:HL[\"/_next/static/chunks/002~6040mndie.css\",\"style\"]\n"])</script><script>self.__next_f.push([1,"0:{\"P\":null,\"c\":[\"\",\"_not-found\"],\"q\":\"\",\"i\":false,\"f\":[[[\"\",{\"children\":[\"/_not-found\",{\"children\":[\"__PAGE__\",{}]}]},\"$undefined\",\"$undefined\",16],[[\"$\",\"$1\",\"c\",{\"children\":[[[\"$\",\"link\",\"0\",{\"rel\":\"stylesheet\",\"href\":\"/_next/static/chunks/002~6040mndie.css\",\"precedence\":\"next\",\"crossOrigin\":\"$undefined\",\"nonce\":\"$undefined\"}],[\"$\",\"script\",\"script-0\",{\"src\":\"/_next/static/chunks/01xlw8hd842-c.js\",\"async\":true,\"nonce\":\"$undefined\"}],[\"$\",\"script\",\"script-1\",{\"src\":\"/_next/static/chunks/0d3shmwh5_nmn.js\",\"async\":true,\"nonce\":\"$undefined\"}]],[\"$\",\"html\",null,{\"lang\":\"en\",\"className\":\"geist_a71539c9-module__T19VSG__variable geist_mono_8d43a2aa-module__8Li5zG__variable h-full antialiased\",\"children\":[\"$\",\"body\",null,{\"className\":\"min-h-full flex flex-col\",\"children\":[\"$\",\"$L2\",null,{\"parallelRouterKey\":\"children\",\"error\":\"$undefined\",\"errorStyles\":\"$undefined\",\"errorScripts\":\"$undefined\",\"template\":[\"$\",\"$L3\",null,{}],\"templateStyles\":\"$undefined\",\"templateScripts\":\"$undefined\",\"notFound\":[[[\"$\",\"title\",null,{\"children\":\"404: This page could not be found.\"}],[\"$\",\"div\",null,{\"style\":{\"fontFamily\":\"system-ui,\\\"Segoe UI\\\",Roboto,Helvetica,Arial,sans-serif,\\\"Apple Color Emoji\\\",\\\"Segoe UI Emoji\\\"\",\"height\":\"100vh\",\"textAlign\":\"center\",\"display\":\"flex\",\"flexDirection\":\"column\",\"alignItems\":\"center\",\"justifyContent\":\"center\"},\"children\":[\"$\",\"div\",null,{\"children\":[[\"$\",\"style\",null,{\"dangerouslySetInnerHTML\":{\"__html\":\"body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}\"}}],[\"$\",\"h1\",null,{\"className\":\"next-error-h1\",\"style\":{\"display\":\"inline-block\",\"margin\":\"0 20px 0 0\",\"padding\":\"0 23px 0 0\",\"fontSize\":24,\"fontWeight\":500,\"verticalAlign\":\"top\",\"lineHeight\":\"49px\"},\"children\":404}],[\"$\",\"div\",null,{\"style\":{\"display\":\"inline-block\"},\"children\":[\"$\",\"h2\",null,{\"style\":{\"fontSize\":14,\"fontWeight\":400,\"lineHeight\":\"49px\",\"margin\":0},\"children\":\"This page could not be found.\"}]}]]}]}]],[]],\"forbidden\":\"$undefined\",\"unauthorized\":\"$undefined\"}]}]}]]}],{\"children\":[[\"$\",\"$1\",\"c\",{\"children\":[null,[\"$\",\"$L2\",null,{\"parallelRouterKey\":\"children\",\"error\":\"$undefined\",\"errorStyles\":\"$undefined\",\"errorScripts\":\"$undefined\",\"template\":[\"$\",\"$L3\",null,{}],\"templateStyles\":\"$undefined\",\"templateScripts\":\"$undefined\",\"notFound\":\"$undefined\",\"forbidden\":\"$undefined\",\"unauthorized\":\"$undefined\"}]]}],{\"children\":[[\"$\",\"$1\",\"c\",{\"children\":[[[\"$\",\"title\",null,{\"children\":\"404: This page could not be found.\"}],[\"$\",\"div\",null,{\"style\":\"$0:f:0:1:0:props:children:1:props:children:props:children:props:notFound:0:1:props:style\",\"children\":[\"$\",\"div\",null,{\"children\":[[\"$\",\"style\",null,{\"dangerouslySetInnerHTML\":{\"__html\":\"body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}\"}}],[\"$\",\"h1\",null,{\"className\":\"next-error-h1\",\"style\":\"$0:f:0:1:0:props:children:1:props:children:props:children:props:notFound:0:1:props:children:props:children:1:props:style\",\"children\":404}],[\"$\",\"div\",null,{\"style\":\"$0:f:0:1:0:props:children:1:props:children:props:children:props:notFound:0:1:props:children:props:children:2:props:style\",\"children\":[\"$\",\"h2\",null,{\"style\":\"$0:f:0:1:0:props:children:1:props:children:props:children:props:notFound:0:1:props:children:props:children:2:props:children:props:style\",\"children\":\"This page could not be found.\"}]}]]}]}]],null,[\"$\",\"$L4\",null,{\"children\":[\"$\",\"$5\",null,{\"name\":\"Next.MetadataOutlet\",\"children\":\"$@6\"}]}]]}],{},null,false,null]},null,false,\"$@7\"]},null,false,null],[\"$\",\"$1\",\"h\",{\"children\":[[\"$\",\"meta\",null,{\"name\":\"robots\",\"content\":\"noindex\"}],[\"$\",\"$L8\",null,{\"children\":\"$L9\"}],[\"$\",\"div\",null,{\"hidden\":true,\"children\":[\"$\",\"$La\",null,{\"children\":[\"$\",\"$5\",null,{\"name\":\"Next.Metadata\",\"children\":\"$Lb\"}]}]}],[\"$\",\"meta\",null,{\"name\":\"next-size-adjust\",\"content\":\"\"}]]}],false]],\"m\":\"$undefined\",\"G\":[\"$c\",[[\"$\",\"link\",\"0\",{\"rel\":\"stylesheet\",\"href\":\"/_next/static/chunks/002~6040mndie.css\",\"precedence\":\"next\",\"crossOrigin\":\"$undefined\",\"nonce\":\"$undefined\"}]]],\"S\":true,\"h\":null,\"s\":\"$undefined\",\"l\":\"$undefined\",\"p\":\"$undefined\",\"d\":\"$undefined\",\"b\":\"Ong62ufsHaRU36s0QK5ZZ\"}\n"])</script><script>self.__next_f.push([1,"d:[]\n7:\"$Wd\"\n"])</script><script>self.__next_f.push([1,"9:[[\"$\",\"meta\",\"0\",{\"charSet\":\"utf-8\"}],[\"$\",\"meta\",\"1\",{\"name\":\"viewport\",\"content\":\"width=device-width, initial-scale=1\"}]]\n"])</script><script>self.__next_f.push([1,"e:I[27201,[\"/_next/static/chunks/01xlw8hd842-c.js\",\"/_next/static/chunks/0d3shmwh5_nmn.js\"],\"IconMark\"]\n6:null\nb:[[\"$\",\"title\",\"0\",{\"children\":\"Vibemeter\"}],[\"$\",\"meta\",\"1\",{\"name\":\"description\",\"content\":\"Measure your AI coding vibe — local-first dashboard for Claude Code, Codex, and Cursor\"}],[\"$\",\"link\",\"2\",{\"rel\":\"icon\",\"href\":\"/favicon.ico?favicon.0x3dzn~oxb6tn.ico\",\"sizes\":\"256x256\",\"type\":\"image/x-icon\"}],[\"$\",\"$Le\",\"3\",{}]]\n"])</script></body></html>