@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.
- package/.next/BUILD_ID +1 -1
- package/.next/app-path-routes-manifest.json +7 -1
- package/.next/build-manifest.json +7 -8
- package/.next/fallback-build-manifest.json +3 -3
- package/.next/prerender-manifest.json +16 -12
- package/.next/required-server-files.js +5 -4
- package/.next/required-server-files.json +5 -4
- package/.next/routes-manifest.json +36 -0
- package/.next/server/app/_global-error/page/build-manifest.json +4 -5
- package/.next/server/app/_global-error/page.js +4 -4
- package/.next/server/app/_global-error/page.js.nft.json +1 -1
- package/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
- package/.next/server/app/_global-error.html +1 -1
- package/.next/server/app/_global-error.rsc +1 -1
- package/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
- package/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
- package/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
- package/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/.next/server/app/_not-found/page/build-manifest.json +4 -5
- package/.next/server/app/_not-found/page.js +5 -4
- package/.next/server/app/_not-found/page.js.nft.json +1 -1
- package/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/.next/server/app/admin/page/build-manifest.json +4 -5
- package/.next/server/app/admin/page.js +7 -5
- package/.next/server/app/admin/page.js.nft.json +1 -1
- package/.next/server/app/admin/page_client-reference-manifest.js +1 -1
- package/.next/server/app/api/codex-accounts/route.js +1 -1
- package/.next/server/app/api/codex-accounts/route.js.nft.json +1 -1
- package/.next/server/app/api/float/route/app-paths-manifest.json +3 -0
- package/.next/server/app/api/float/route/build-manifest.json +9 -0
- package/.next/server/app/api/float/route/server-reference-manifest.json +4 -0
- package/.next/server/app/api/float/route.js +8 -0
- package/.next/server/app/api/float/route.js.map +5 -0
- package/.next/server/app/api/float/route.js.nft.json +1 -0
- package/.next/server/app/api/float/route_client-reference-manifest.js +3 -0
- package/.next/server/app/api/import-sessions/route.js.nft.json +1 -1
- package/.next/server/app/api/sessions/[id]/tags/route.js +1 -1
- package/.next/server/app/api/sessions/[id]/tags/route.js.nft.json +1 -1
- package/.next/server/app/api/sessions/route.js +1 -1
- package/.next/server/app/api/sessions/route.js.nft.json +1 -1
- package/.next/server/app/api/settings/alerts/route/app-paths-manifest.json +3 -0
- package/.next/server/app/api/settings/alerts/route/build-manifest.json +9 -0
- package/.next/server/app/api/settings/alerts/route/server-reference-manifest.json +4 -0
- package/.next/server/app/api/settings/alerts/route.js +9 -0
- package/.next/server/app/api/settings/alerts/route.js.map +5 -0
- package/.next/server/app/api/settings/alerts/route.js.nft.json +1 -0
- package/.next/server/app/api/settings/alerts/route_client-reference-manifest.js +3 -0
- package/.next/server/app/api/settings/notify/route/app-paths-manifest.json +3 -0
- package/.next/server/app/api/settings/notify/route/build-manifest.json +9 -0
- package/.next/server/app/api/settings/notify/route/server-reference-manifest.json +4 -0
- package/.next/server/app/api/settings/notify/route.js +7 -0
- package/.next/server/app/api/settings/notify/route.js.map +5 -0
- package/.next/server/app/api/settings/notify/route.js.nft.json +1 -0
- package/.next/server/app/api/settings/notify/route_client-reference-manifest.js +3 -0
- package/.next/server/app/api/usage/route.js +1 -1
- package/.next/server/app/api/usage/route.js.nft.json +1 -1
- package/.next/server/app/float/page/app-paths-manifest.json +3 -0
- package/.next/server/app/float/page/build-manifest.json +16 -0
- package/.next/server/app/float/page/next-font-manifest.json +11 -0
- package/.next/server/app/float/page/react-loadable-manifest.json +1 -0
- package/.next/server/app/float/page/server-reference-manifest.json +4 -0
- package/.next/server/app/float/page.js +15 -0
- package/.next/server/app/float/page.js.map +5 -0
- package/.next/server/app/float/page.js.nft.json +1 -0
- package/.next/server/app/float/page_client-reference-manifest.js +3 -0
- package/.next/server/app/install.sh/route/app-paths-manifest.json +3 -0
- package/.next/server/app/install.sh/route/build-manifest.json +9 -0
- package/.next/server/app/install.sh/route/server-reference-manifest.json +4 -0
- package/.next/server/app/install.sh/route.js +6 -0
- package/.next/server/app/install.sh/route.js.map +5 -0
- package/.next/server/app/install.sh/route.js.nft.json +1 -0
- package/.next/server/app/install.sh/route_client-reference-manifest.js +3 -0
- package/.next/server/app/install.sh.body +108 -0
- package/.next/server/app/install.sh.meta +1 -0
- package/.next/server/app/page/build-manifest.json +4 -5
- package/.next/server/app/page.js +8 -5
- package/.next/server/app/page.js.nft.json +1 -1
- package/.next/server/app/page_client-reference-manifest.js +1 -1
- package/.next/server/app/settings/page/app-paths-manifest.json +3 -0
- package/.next/server/app/settings/page/build-manifest.json +16 -0
- package/.next/server/app/settings/page/next-font-manifest.json +11 -0
- package/.next/server/app/settings/page/react-loadable-manifest.json +1 -0
- package/.next/server/app/settings/page/server-reference-manifest.json +4 -0
- package/.next/server/app/settings/page.js +17 -0
- package/.next/server/app/settings/page.js.map +5 -0
- package/.next/server/app/settings/page.js.nft.json +1 -0
- package/.next/server/app/settings/page_client-reference-manifest.js +3 -0
- package/.next/server/app-paths-manifest.json +7 -1
- package/.next/server/chunks/[externals]__0_i~3ox._.js +3 -0
- package/.next/server/chunks/[externals]__0_i~3ox._.js.map +1 -0
- package/.next/server/chunks/[externals]__13p_1zh._.js +3 -0
- package/.next/server/chunks/[externals]__13p_1zh._.js.map +1 -0
- package/.next/server/chunks/[root-of-the-server]__01t2c3w._.js +38 -0
- package/.next/server/chunks/[root-of-the-server]__01t2c3w._.js.map +1 -0
- package/.next/server/chunks/{[root-of-the-server]__0g0u0lm._.js → [root-of-the-server]__024yzee._.js} +2 -2
- package/.next/server/chunks/{[root-of-the-server]__00q0o~z._.js → [root-of-the-server]__082iwfa._.js} +2 -2
- package/.next/server/chunks/[root-of-the-server]__0chedn~._.js +1 -1
- package/.next/server/chunks/[root-of-the-server]__0mwf0bn._.js +38 -0
- package/.next/server/chunks/[root-of-the-server]__0mwf0bn._.js.map +1 -0
- package/.next/server/chunks/{[root-of-the-server]__0-74syk._.js → [root-of-the-server]__0ru3_it._.js} +2 -2
- package/.next/server/chunks/[root-of-the-server]__0u6y_k1._.js +111 -0
- package/.next/server/chunks/[root-of-the-server]__0u6y_k1._.js.map +1 -0
- package/.next/server/chunks/{[root-of-the-server]__0866q87._.js → [root-of-the-server]__0xa4dzi._.js} +2 -2
- package/.next/server/chunks/[root-of-the-server]__13415c6._.js +96 -0
- package/.next/server/chunks/[root-of-the-server]__13415c6._.js.map +1 -0
- package/.next/server/chunks/[root-of-the-server]__13j_28o._.js +96 -0
- package/.next/server/chunks/[root-of-the-server]__13j_28o._.js.map +1 -0
- package/.next/server/chunks/_next-internal_server_app_api_float_route_actions_012j~jr.js +3 -0
- package/.next/server/chunks/_next-internal_server_app_api_float_route_actions_012j~jr.js.map +1 -0
- package/.next/server/chunks/_next-internal_server_app_api_settings_alerts_route_actions_0ydcyko.js +3 -0
- package/.next/server/chunks/_next-internal_server_app_api_settings_alerts_route_actions_0ydcyko.js.map +1 -0
- package/.next/server/chunks/_next-internal_server_app_api_settings_notify_route_actions_0-j35mb.js +3 -0
- package/.next/server/chunks/_next-internal_server_app_api_settings_notify_route_actions_0-j35mb.js.map +1 -0
- package/.next/server/chunks/_next-internal_server_app_install_sh_route_actions_0cj-6me.js +3 -0
- package/.next/server/chunks/_next-internal_server_app_install_sh_route_actions_0cj-6me.js.map +1 -0
- package/.next/server/chunks/node_modules_next_06f88ko._.js +19 -0
- package/.next/server/chunks/node_modules_next_06f88ko._.js.map +1 -0
- package/.next/server/chunks/node_modules_next_dist_esm_build_templates_app-route_03jj5jj.js +18 -0
- package/.next/server/chunks/node_modules_next_dist_esm_build_templates_app-route_03jj5jj.js.map +1 -0
- package/.next/server/chunks/src_12i1qk5._.js +3 -0
- package/.next/server/chunks/src_12i1qk5._.js.map +1 -0
- package/.next/server/chunks/src_lib_alerts_ticker_ts_0n6oy1d._.js +169 -0
- package/.next/server/chunks/src_lib_alerts_ticker_ts_0n6oy1d._.js.map +1 -0
- package/.next/server/chunks/ssr/[root-of-the-server]__03mt8o0._.js +3 -0
- package/.next/server/chunks/ssr/[root-of-the-server]__03mt8o0._.js.map +1 -0
- package/.next/server/chunks/ssr/{[root-of-the-server]__098zro9._.js → [root-of-the-server]__090jxzh._.js} +2 -2
- package/.next/server/chunks/ssr/[root-of-the-server]__090jxzh._.js.map +1 -0
- package/.next/server/chunks/ssr/{[root-of-the-server]__09hgk-7._.js → [root-of-the-server]__0c3j40u._.js} +2 -2
- package/.next/server/chunks/ssr/[root-of-the-server]__0c3j40u._.js.map +1 -0
- package/.next/server/chunks/ssr/[root-of-the-server]__0ot4nev._.js +3 -0
- package/.next/server/chunks/ssr/[root-of-the-server]__0ot4nev._.js.map +1 -0
- package/.next/server/chunks/ssr/[root-of-the-server]__0q_~l2m._.js +90 -0
- package/.next/server/chunks/ssr/[root-of-the-server]__0q_~l2m._.js.map +1 -0
- package/.next/server/chunks/ssr/{[root-of-the-server]__0cqes87._.js → [root-of-the-server]__0qt26ty._.js} +2 -2
- package/.next/server/chunks/ssr/[root-of-the-server]__0qt26ty._.js.map +1 -0
- package/.next/server/chunks/ssr/[root-of-the-server]__0s5.uhg._.js +55 -0
- package/.next/server/chunks/ssr/[root-of-the-server]__0s5.uhg._.js.map +1 -0
- package/.next/server/chunks/ssr/[root-of-the-server]__0x~-phx._.js +3 -0
- package/.next/server/chunks/ssr/[root-of-the-server]__0x~-phx._.js.map +1 -0
- package/.next/server/chunks/ssr/[root-of-the-server]__0y_zq8k._.js +3 -0
- package/.next/server/chunks/ssr/[root-of-the-server]__0y_zq8k._.js.map +1 -0
- package/.next/server/chunks/ssr/{[root-of-the-server]__08qr2ji._.js → [root-of-the-server]__0~953ob._.js} +2 -2
- package/.next/server/chunks/ssr/[root-of-the-server]__0~953ob._.js.map +1 -0
- package/.next/server/chunks/ssr/_0-9j34y._.js +70 -0
- package/.next/server/chunks/ssr/_0-9j34y._.js.map +1 -0
- package/.next/server/chunks/ssr/_0jxmm9h._.js +6 -0
- package/.next/server/chunks/ssr/_0jxmm9h._.js.map +1 -0
- package/.next/server/chunks/ssr/_0lfe3wr._.js +3 -0
- package/.next/server/chunks/ssr/_0lfe3wr._.js.map +1 -0
- package/.next/server/chunks/ssr/{node_modules_0i2xw~e._.js → _0nvprxh._.js} +5 -2
- package/.next/server/chunks/ssr/_0nvprxh._.js.map +1 -0
- package/.next/server/chunks/ssr/_0pugb10._.js +6 -0
- package/.next/server/chunks/ssr/_0pugb10._.js.map +1 -0
- package/.next/server/chunks/ssr/_next-internal_server_app_float_page_actions_0x6hm4p.js +3 -0
- package/.next/server/chunks/ssr/_next-internal_server_app_float_page_actions_0x6hm4p.js.map +1 -0
- package/.next/server/chunks/ssr/_next-internal_server_app_settings_page_actions_0mr68ai.js +3 -0
- package/.next/server/chunks/ssr/_next-internal_server_app_settings_page_actions_0mr68ai.js.map +1 -0
- package/.next/server/chunks/ssr/node_modules_@swc_helpers_cjs__interop_require_default_cjs_11~q6fv._.js +3 -0
- package/.next/server/chunks/ssr/node_modules_@swc_helpers_cjs__interop_require_default_cjs_11~q6fv._.js.map +1 -0
- package/.next/server/chunks/ssr/node_modules_next_dist_09jzzl8._.js +3 -0
- package/.next/server/chunks/ssr/node_modules_next_dist_09jzzl8._.js.map +1 -0
- package/.next/server/chunks/ssr/{node_modules_next_dist_0e1izl_._.js → node_modules_next_dist_0h9llsw._.js} +2 -2
- package/.next/server/chunks/ssr/node_modules_next_dist_0h9llsw._.js.map +1 -0
- package/.next/server/chunks/ssr/node_modules_next_dist_esm_build_templates_app-page_06b_a87.js +4 -0
- package/.next/server/chunks/ssr/node_modules_next_dist_esm_build_templates_app-page_06b_a87.js.map +1 -0
- package/.next/server/chunks/ssr/node_modules_next_dist_esm_build_templates_app-page_0p_u4px.js +4 -0
- package/.next/server/chunks/ssr/node_modules_next_dist_esm_build_templates_app-page_0p_u4px.js.map +1 -0
- package/.next/server/chunks/ssr/src_0urkups._.js +3 -0
- package/.next/server/chunks/ssr/src_0urkups._.js.map +1 -0
- package/.next/server/chunks/ssr/src_components_0ox9d~w._.js +3 -0
- package/.next/server/chunks/ssr/src_components_0ox9d~w._.js.map +1 -0
- package/.next/server/chunks/ssr/src_components_CodexAccountsPanel_tsx_0n9xjzi._.js +1 -1
- package/.next/server/chunks/ssr/src_components_CodexAccountsPanel_tsx_0n9xjzi._.js.map +1 -1
- package/.next/server/chunks/ssr/src_components_FloatingWidget_tsx_089.6oo._.js +3 -0
- package/.next/server/chunks/ssr/src_components_FloatingWidget_tsx_089.6oo._.js.map +1 -0
- package/.next/server/chunks/ssr/src_lib_i18n_client_tsx_07pysgz._.js +3 -0
- package/.next/server/chunks/ssr/src_lib_i18n_client_tsx_07pysgz._.js.map +1 -0
- package/.next/server/edge/chunks/_0-m5tdn._.js +3 -0
- package/.next/server/edge/chunks/_0-m5tdn._.js.map +1 -0
- package/.next/server/edge/chunks/node_modules_next_dist_esm_build_templates_edge-wrapper_0mz-5sp.js.map +1 -0
- package/.next/server/edge/chunks/turbopack-node_modules_next_dist_esm_build_templates_edge-wrapper_0mz-5sp.js +3 -0
- package/.next/server/functions-config-manifest.json +4 -1
- package/.next/server/instrumentation/middleware-manifest.json +12 -0
- package/.next/server/instrumentation.js +4 -0
- package/.next/server/instrumentation.js.map +5 -0
- package/.next/server/instrumentation.js.nft.json +1 -0
- package/.next/server/middleware-build-manifest.js +7 -8
- package/.next/server/next-font-manifest.js +1 -1
- package/.next/server/next-font-manifest.json +8 -0
- package/.next/server/pages/500.html +1 -1
- package/.next/server/pages-manifest.json +0 -1
- package/.next/server/server-reference-manifest.js +1 -1
- package/.next/server/server-reference-manifest.json +1 -1
- package/.next/static/chunks/0bymg5d-lqs0o.js +5 -0
- package/.next/static/chunks/0ebdqsutnp8y4.js +1 -0
- package/.next/static/chunks/0i3~mnai-l497.js +1 -0
- package/.next/static/chunks/0idj4tdmyr934.js +1 -0
- package/.next/static/chunks/10eoi4q2wk80z.js +1 -0
- package/.next/static/chunks/10refkmyp9rbl.css +3 -0
- package/.next/static/chunks/{07lhk_q6pmm3r.js → 10u3y4bw1ayzs.js} +1 -1
- package/.next/static/chunks/{00gq-v0e07i1q.js → 115dplafwys-z.js} +1 -1
- package/.next/static/chunks/12-9memveha-v.js +1 -0
- package/.next/static/chunks/163s8y.j70104.js +4 -0
- package/.next/static/chunks/turbopack-0k9twle9a8sh6.js +1 -0
- package/.next/types/routes.d.ts +8 -2
- package/.next/types/validator.ts +54 -0
- package/README.md +57 -17
- package/bin/vibemeter-float.swift +970 -0
- package/bin/vibemeter-notify.sh +172 -0
- package/bin/vibemeter.mjs +385 -7
- package/package.json +2 -1
- package/public/demo1.png +0 -0
- package/public/float-ball.png +0 -0
- package/public/float-collapsed.png +0 -0
- package/public/float-expanded.png +0 -0
- package/public/pay-alipay.jpg +0 -0
- package/.next/server/app/_not-found.html +0 -1
- package/.next/server/app/_not-found.meta +0 -16
- package/.next/server/app/_not-found.rsc +0 -16
- package/.next/server/app/_not-found.segments/_full.segment.rsc +0 -16
- package/.next/server/app/_not-found.segments/_head.segment.rsc +0 -6
- package/.next/server/app/_not-found.segments/_index.segment.rsc +0 -5
- package/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +0 -5
- package/.next/server/app/_not-found.segments/_not-found.segment.rsc +0 -5
- package/.next/server/app/_not-found.segments/_tree.segment.rsc +0 -2
- package/.next/server/chunks/[root-of-the-server]__0y68a5d._.js +0 -96
- package/.next/server/chunks/[root-of-the-server]__0y68a5d._.js.map +0 -1
- package/.next/server/chunks/ssr/[root-of-the-server]__054lv8d._.js +0 -3
- package/.next/server/chunks/ssr/[root-of-the-server]__054lv8d._.js.map +0 -1
- package/.next/server/chunks/ssr/[root-of-the-server]__08qr2ji._.js.map +0 -1
- package/.next/server/chunks/ssr/[root-of-the-server]__098zro9._.js.map +0 -1
- package/.next/server/chunks/ssr/[root-of-the-server]__09hgk-7._.js.map +0 -1
- package/.next/server/chunks/ssr/[root-of-the-server]__0cqes87._.js.map +0 -1
- package/.next/server/chunks/ssr/[root-of-the-server]__0l.drdn._.js +0 -3
- package/.next/server/chunks/ssr/[root-of-the-server]__0l.drdn._.js.map +0 -1
- package/.next/server/chunks/ssr/[root-of-the-server]__0v1zqf~._.js +0 -120
- package/.next/server/chunks/ssr/[root-of-the-server]__0v1zqf~._.js.map +0 -1
- package/.next/server/chunks/ssr/[root-of-the-server]__0v73tbn._.js +0 -3
- package/.next/server/chunks/ssr/[root-of-the-server]__0v73tbn._.js.map +0 -1
- package/.next/server/chunks/ssr/_0ye~8el._.js +0 -7
- package/.next/server/chunks/ssr/_0ye~8el._.js.map +0 -1
- package/.next/server/chunks/ssr/node_modules_0ck2~9g._.js +0 -3
- package/.next/server/chunks/ssr/node_modules_0ck2~9g._.js.map +0 -1
- package/.next/server/chunks/ssr/node_modules_0i2xw~e._.js.map +0 -1
- package/.next/server/chunks/ssr/node_modules_next_dist_0e1izl_._.js.map +0 -1
- package/.next/server/chunks/ssr/node_modules_next_dist_0gsjr7_._.js +0 -3
- package/.next/server/chunks/ssr/node_modules_next_dist_0gsjr7_._.js.map +0 -1
- package/.next/server/pages/404.html +0 -1
- package/.next/static/chunks/002~6040mndie.css +0 -3
- package/.next/static/chunks/02g3221oh~3le.js +0 -2
- package/.next/static/chunks/0erq0bmub6w_z.js +0 -2
- package/.next/static/chunks/0ljfidstam_7k.js +0 -1
- package/.next/static/chunks/0pqt~8bl3ukh4.js +0 -4
- package/.next/static/chunks/turbopack-14vio.b1b9i4l.js +0 -1
- /package/.next/server/chunks/{[root-of-the-server]__0g0u0lm._.js.map → [root-of-the-server]__024yzee._.js.map} +0 -0
- /package/.next/server/chunks/{[root-of-the-server]__00q0o~z._.js.map → [root-of-the-server]__082iwfa._.js.map} +0 -0
- /package/.next/server/chunks/{[root-of-the-server]__0-74syk._.js.map → [root-of-the-server]__0ru3_it._.js.map} +0 -0
- /package/.next/server/chunks/{[root-of-the-server]__0866q87._.js.map → [root-of-the-server]__0xa4dzi._.js.map} +0 -0
- /package/.next/static/{Ong62ufsHaRU36s0QK5ZZ → _Y03MiN6NI16Ms86q6vCJ}/_buildManifest.js +0 -0
- /package/.next/static/{Ong62ufsHaRU36s0QK5ZZ → _Y03MiN6NI16Ms86q6vCJ}/_clientMiddlewareManifest.js +0 -0
- /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
|
|
190
|
-
vibemeter install
|
|
191
|
-
vibemeter
|
|
192
|
-
vibemeter
|
|
193
|
-
vibemeter
|
|
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')
|
|
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.
|
|
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": {
|
package/public/demo1.png
ADDED
|
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,"Segoe UI",Roboto,Helvetica,Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji";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>
|