@10et/cli 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (1401) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +79 -0
  3. package/clawdbot-plugin/clawdbot.plugin.json +20 -0
  4. package/clawdbot-plugin/index.js +555 -0
  5. package/clawdbot-plugin/index.ts +582 -0
  6. package/clawdbot-skill/README.md +328 -0
  7. package/clawdbot-skill/SKILL.md +59 -0
  8. package/clawdbot-skill/index.ts +683 -0
  9. package/clawdbot-skill/package.json +28 -0
  10. package/clawdbot-skill/skill.json +19 -0
  11. package/dist/commands/agent.d.ts +7 -0
  12. package/dist/commands/agent.d.ts.map +1 -0
  13. package/dist/commands/agent.js +170 -0
  14. package/dist/commands/agent.js.map +1 -0
  15. package/dist/commands/agents.d.ts +5 -0
  16. package/dist/commands/agents.d.ts.map +1 -0
  17. package/dist/commands/agents.js +400 -0
  18. package/dist/commands/agents.js.map +1 -0
  19. package/dist/commands/ci-setup.d.ts +5 -0
  20. package/dist/commands/ci-setup.d.ts.map +1 -0
  21. package/dist/commands/ci-setup.js +82 -0
  22. package/dist/commands/ci-setup.js.map +1 -0
  23. package/dist/commands/clawdbot.d.ts +11 -0
  24. package/dist/commands/clawdbot.d.ts.map +1 -0
  25. package/dist/commands/clawdbot.js +215 -0
  26. package/dist/commands/clawdbot.js.map +1 -0
  27. package/dist/commands/context-hub.d.ts +22 -0
  28. package/dist/commands/context-hub.d.ts.map +1 -0
  29. package/dist/commands/context-hub.js +3505 -0
  30. package/dist/commands/context-hub.js.map +1 -0
  31. package/dist/commands/deploy.d.ts +5 -0
  32. package/dist/commands/deploy.d.ts.map +1 -0
  33. package/dist/commands/deploy.js +371 -0
  34. package/dist/commands/deploy.js.map +1 -0
  35. package/dist/commands/digest.d.ts +12 -0
  36. package/dist/commands/digest.d.ts.map +1 -0
  37. package/dist/commands/digest.js +128 -0
  38. package/dist/commands/digest.js.map +1 -0
  39. package/dist/commands/doctor.d.ts +8 -0
  40. package/dist/commands/doctor.d.ts.map +1 -0
  41. package/dist/commands/doctor.js +265 -0
  42. package/dist/commands/doctor.js.map +1 -0
  43. package/dist/commands/eval.d.ts +46 -0
  44. package/dist/commands/eval.d.ts.map +1 -0
  45. package/dist/commands/eval.js +427 -0
  46. package/dist/commands/eval.js.map +1 -0
  47. package/dist/commands/feedback.d.ts +2 -0
  48. package/dist/commands/feedback.d.ts.map +1 -0
  49. package/dist/commands/feedback.js +179 -0
  50. package/dist/commands/feedback.js.map +1 -0
  51. package/dist/commands/findings.d.ts +13 -0
  52. package/dist/commands/findings.d.ts.map +1 -0
  53. package/dist/commands/findings.js +203 -0
  54. package/dist/commands/findings.js.map +1 -0
  55. package/dist/commands/flows.d.ts +10 -0
  56. package/dist/commands/flows.d.ts.map +1 -0
  57. package/dist/commands/flows.js +423 -0
  58. package/dist/commands/flows.js.map +1 -0
  59. package/dist/commands/gtm-process-update.d.ts +10 -0
  60. package/dist/commands/gtm-process-update.d.ts.map +1 -0
  61. package/dist/commands/gtm-process-update.js +101 -0
  62. package/dist/commands/gtm-process-update.js.map +1 -0
  63. package/dist/commands/hooks.d.ts +13 -0
  64. package/dist/commands/hooks.d.ts.map +1 -0
  65. package/dist/commands/hooks.js +304 -0
  66. package/dist/commands/hooks.js.map +1 -0
  67. package/dist/commands/hud.d.ts +4 -0
  68. package/dist/commands/hud.d.ts.map +1 -0
  69. package/dist/commands/hud.js +300 -0
  70. package/dist/commands/hud.js.map +1 -0
  71. package/dist/commands/ide.d.ts +28 -0
  72. package/dist/commands/ide.d.ts.map +1 -0
  73. package/dist/commands/ide.js +628 -0
  74. package/dist/commands/ide.js.map +1 -0
  75. package/dist/commands/improve.d.ts +11 -0
  76. package/dist/commands/improve.d.ts.map +1 -0
  77. package/dist/commands/improve.js +77 -0
  78. package/dist/commands/improve.js.map +1 -0
  79. package/dist/commands/init-from-service.d.ts +15 -0
  80. package/dist/commands/init-from-service.d.ts.map +1 -0
  81. package/dist/commands/init-from-service.js +540 -0
  82. package/dist/commands/init-from-service.js.map +1 -0
  83. package/dist/commands/init.d.ts +15 -0
  84. package/dist/commands/init.d.ts.map +1 -0
  85. package/dist/commands/init.js +940 -0
  86. package/dist/commands/init.js.map +1 -0
  87. package/dist/commands/kanban.d.ts +34 -0
  88. package/dist/commands/kanban.d.ts.map +1 -0
  89. package/dist/commands/kanban.js +225 -0
  90. package/dist/commands/kanban.js.map +1 -0
  91. package/dist/commands/linear.d.ts +41 -0
  92. package/dist/commands/linear.d.ts.map +1 -0
  93. package/dist/commands/linear.js +740 -0
  94. package/dist/commands/linear.js.map +1 -0
  95. package/dist/commands/login.d.ts +12 -0
  96. package/dist/commands/login.d.ts.map +1 -0
  97. package/dist/commands/login.js +780 -0
  98. package/dist/commands/login.js.map +1 -0
  99. package/dist/commands/memory.d.ts +38 -0
  100. package/dist/commands/memory.d.ts.map +1 -0
  101. package/dist/commands/memory.js +229 -0
  102. package/dist/commands/memory.js.map +1 -0
  103. package/dist/commands/migrate-services.d.ts +8 -0
  104. package/dist/commands/migrate-services.d.ts.map +1 -0
  105. package/dist/commands/migrate-services.js +182 -0
  106. package/dist/commands/migrate-services.js.map +1 -0
  107. package/dist/commands/migrate-tenet.d.ts +25 -0
  108. package/dist/commands/migrate-tenet.d.ts.map +1 -0
  109. package/dist/commands/migrate-tenet.js +252 -0
  110. package/dist/commands/migrate-tenet.js.map +1 -0
  111. package/dist/commands/onboard.d.ts +24 -0
  112. package/dist/commands/onboard.d.ts.map +1 -0
  113. package/dist/commands/onboard.js +873 -0
  114. package/dist/commands/onboard.js.map +1 -0
  115. package/dist/commands/openclaw.d.ts +59 -0
  116. package/dist/commands/openclaw.d.ts.map +1 -0
  117. package/dist/commands/openclaw.js +775 -0
  118. package/dist/commands/openclaw.js.map +1 -0
  119. package/dist/commands/orchestrate.d.ts +14 -0
  120. package/dist/commands/orchestrate.d.ts.map +1 -0
  121. package/dist/commands/orchestrate.js +270 -0
  122. package/dist/commands/orchestrate.js.map +1 -0
  123. package/dist/commands/organize.d.ts +16 -0
  124. package/dist/commands/organize.d.ts.map +1 -0
  125. package/dist/commands/organize.js +334 -0
  126. package/dist/commands/organize.js.map +1 -0
  127. package/dist/commands/peter.d.ts +21 -0
  128. package/dist/commands/peter.d.ts.map +1 -0
  129. package/dist/commands/peter.js +2778 -0
  130. package/dist/commands/peter.js.map +1 -0
  131. package/dist/commands/pi-fleet.d.ts +18 -0
  132. package/dist/commands/pi-fleet.d.ts.map +1 -0
  133. package/dist/commands/pi-fleet.js +382 -0
  134. package/dist/commands/pi-fleet.js.map +1 -0
  135. package/dist/commands/pi.d.ts +25 -0
  136. package/dist/commands/pi.d.ts.map +1 -0
  137. package/dist/commands/pi.js +218 -0
  138. package/dist/commands/pi.js.map +1 -0
  139. package/dist/commands/pivot.d.ts +28 -0
  140. package/dist/commands/pivot.d.ts.map +1 -0
  141. package/dist/commands/pivot.js +216 -0
  142. package/dist/commands/pivot.js.map +1 -0
  143. package/dist/commands/portfolio.d.ts +11 -0
  144. package/dist/commands/portfolio.d.ts.map +1 -0
  145. package/dist/commands/portfolio.js +239 -0
  146. package/dist/commands/portfolio.js.map +1 -0
  147. package/dist/commands/predict.d.ts +25 -0
  148. package/dist/commands/predict.d.ts.map +1 -0
  149. package/dist/commands/predict.js +234 -0
  150. package/dist/commands/predict.js.map +1 -0
  151. package/dist/commands/profile.d.ts +46 -0
  152. package/dist/commands/profile.d.ts.map +1 -0
  153. package/dist/commands/profile.js +498 -0
  154. package/dist/commands/profile.js.map +1 -0
  155. package/dist/commands/ralph.d.ts +11 -0
  156. package/dist/commands/ralph.d.ts.map +1 -0
  157. package/dist/commands/ralph.js +102 -0
  158. package/dist/commands/ralph.js.map +1 -0
  159. package/dist/commands/repair.d.ts +7 -0
  160. package/dist/commands/repair.d.ts.map +1 -0
  161. package/dist/commands/repair.js +356 -0
  162. package/dist/commands/repair.js.map +1 -0
  163. package/dist/commands/scope.d.ts +8 -0
  164. package/dist/commands/scope.d.ts.map +1 -0
  165. package/dist/commands/scope.js +503 -0
  166. package/dist/commands/scope.js.map +1 -0
  167. package/dist/commands/service-agent.d.ts +16 -0
  168. package/dist/commands/service-agent.d.ts.map +1 -0
  169. package/dist/commands/service-agent.js +375 -0
  170. package/dist/commands/service-agent.js.map +1 -0
  171. package/dist/commands/service-manager.d.ts +12 -0
  172. package/dist/commands/service-manager.d.ts.map +1 -0
  173. package/dist/commands/service-manager.js +969 -0
  174. package/dist/commands/service-manager.js.map +1 -0
  175. package/dist/commands/service-validate.d.ts +12 -0
  176. package/dist/commands/service-validate.d.ts.map +1 -0
  177. package/dist/commands/service-validate.js +619 -0
  178. package/dist/commands/service-validate.js.map +1 -0
  179. package/dist/commands/services-create.d.ts +15 -0
  180. package/dist/commands/services-create.d.ts.map +1 -0
  181. package/dist/commands/services-create.js +1800 -0
  182. package/dist/commands/services-create.js.map +1 -0
  183. package/dist/commands/services-scan.d.ts +13 -0
  184. package/dist/commands/services-scan.d.ts.map +1 -0
  185. package/dist/commands/services-scan.js +251 -0
  186. package/dist/commands/services-scan.js.map +1 -0
  187. package/dist/commands/services-sync-agents.d.ts +23 -0
  188. package/dist/commands/services-sync-agents.d.ts.map +1 -0
  189. package/dist/commands/services-sync-agents.js +207 -0
  190. package/dist/commands/services-sync-agents.js.map +1 -0
  191. package/dist/commands/services.d.ts +19 -0
  192. package/dist/commands/services.d.ts.map +1 -0
  193. package/dist/commands/services.js +906 -0
  194. package/dist/commands/services.js.map +1 -0
  195. package/dist/commands/session.d.ts +7 -0
  196. package/dist/commands/session.d.ts.map +1 -0
  197. package/dist/commands/session.js +597 -0
  198. package/dist/commands/session.js.map +1 -0
  199. package/dist/commands/setup.d.ts +12 -0
  200. package/dist/commands/setup.d.ts.map +1 -0
  201. package/dist/commands/setup.js +727 -0
  202. package/dist/commands/setup.js.map +1 -0
  203. package/dist/commands/skills.d.ts +31 -0
  204. package/dist/commands/skills.d.ts.map +1 -0
  205. package/dist/commands/skills.js +314 -0
  206. package/dist/commands/skills.js.map +1 -0
  207. package/dist/commands/start.d.ts +25 -0
  208. package/dist/commands/start.d.ts.map +1 -0
  209. package/dist/commands/start.js +251 -0
  210. package/dist/commands/start.js.map +1 -0
  211. package/dist/commands/status.d.ts +2 -0
  212. package/dist/commands/status.d.ts.map +1 -0
  213. package/dist/commands/status.js +163 -0
  214. package/dist/commands/status.js.map +1 -0
  215. package/dist/commands/synopsis.d.ts +54 -0
  216. package/dist/commands/synopsis.d.ts.map +1 -0
  217. package/dist/commands/synopsis.js +277 -0
  218. package/dist/commands/synopsis.js.map +1 -0
  219. package/dist/commands/telemetry-monitor.d.ts +11 -0
  220. package/dist/commands/telemetry-monitor.d.ts.map +1 -0
  221. package/dist/commands/telemetry-monitor.js +224 -0
  222. package/dist/commands/telemetry-monitor.js.map +1 -0
  223. package/dist/commands/telemetry-test.d.ts +11 -0
  224. package/dist/commands/telemetry-test.d.ts.map +1 -0
  225. package/dist/commands/telemetry-test.js +67 -0
  226. package/dist/commands/telemetry-test.js.map +1 -0
  227. package/dist/commands/tenet-agents.d.ts +13 -0
  228. package/dist/commands/tenet-agents.d.ts.map +1 -0
  229. package/dist/commands/tenet-agents.js +191 -0
  230. package/dist/commands/tenet-agents.js.map +1 -0
  231. package/dist/commands/tenet-setup.d.ts +20 -0
  232. package/dist/commands/tenet-setup.d.ts.map +1 -0
  233. package/dist/commands/tenet-setup.js +135 -0
  234. package/dist/commands/tenet-setup.js.map +1 -0
  235. package/dist/commands/train.d.ts +51 -0
  236. package/dist/commands/train.d.ts.map +1 -0
  237. package/dist/commands/train.js +692 -0
  238. package/dist/commands/train.js.map +1 -0
  239. package/dist/commands/update.d.ts +12 -0
  240. package/dist/commands/update.d.ts.map +1 -0
  241. package/dist/commands/update.js +559 -0
  242. package/dist/commands/update.js.map +1 -0
  243. package/dist/commands/validate-settings.d.ts +37 -0
  244. package/dist/commands/validate-settings.d.ts.map +1 -0
  245. package/dist/commands/validate-settings.js +197 -0
  246. package/dist/commands/validate-settings.js.map +1 -0
  247. package/dist/commands/verify.d.ts +14 -0
  248. package/dist/commands/verify.d.ts.map +1 -0
  249. package/dist/commands/verify.js +304 -0
  250. package/dist/commands/verify.js.map +1 -0
  251. package/dist/commands/viz.d.ts +40 -0
  252. package/dist/commands/viz.d.ts.map +1 -0
  253. package/dist/commands/viz.js +877 -0
  254. package/dist/commands/viz.js.map +1 -0
  255. package/dist/commands/voice.d.ts +409 -0
  256. package/dist/commands/voice.d.ts.map +1 -0
  257. package/dist/commands/voice.js +4765 -0
  258. package/dist/commands/voice.js.map +1 -0
  259. package/dist/commands/whoami.d.ts +2 -0
  260. package/dist/commands/whoami.d.ts.map +1 -0
  261. package/dist/commands/whoami.js +24 -0
  262. package/dist/commands/whoami.js.map +1 -0
  263. package/dist/dashboard/index.d.ts +11 -0
  264. package/dist/dashboard/index.d.ts.map +1 -0
  265. package/dist/dashboard/index.js +70 -0
  266. package/dist/dashboard/index.js.map +1 -0
  267. package/dist/dashboard-static/assets/index-BVrmW-ZI.js +154 -0
  268. package/dist/dashboard-static/assets/index-DtruPD44.css +1 -0
  269. package/dist/dashboard-static/index.html +16 -0
  270. package/dist/index.d.ts +4 -0
  271. package/dist/index.d.ts.map +1 -0
  272. package/dist/index.js +1707 -0
  273. package/dist/index.js.map +1 -0
  274. package/dist/lib/advanced-setup.d.ts +78 -0
  275. package/dist/lib/advanced-setup.d.ts.map +1 -0
  276. package/dist/lib/advanced-setup.js +433 -0
  277. package/dist/lib/advanced-setup.js.map +1 -0
  278. package/dist/lib/agent-config.d.ts +86 -0
  279. package/dist/lib/agent-config.d.ts.map +1 -0
  280. package/dist/lib/agent-config.js +281 -0
  281. package/dist/lib/agent-config.js.map +1 -0
  282. package/dist/lib/agent-generator.d.ts +36 -0
  283. package/dist/lib/agent-generator.d.ts.map +1 -0
  284. package/dist/lib/agent-generator.js +400 -0
  285. package/dist/lib/agent-generator.js.map +1 -0
  286. package/dist/lib/agent-guards.d.ts +67 -0
  287. package/dist/lib/agent-guards.d.ts.map +1 -0
  288. package/dist/lib/agent-guards.js +229 -0
  289. package/dist/lib/agent-guards.js.map +1 -0
  290. package/dist/lib/agent-manifest.d.ts +35 -0
  291. package/dist/lib/agent-manifest.d.ts.map +1 -0
  292. package/dist/lib/agent-manifest.js +75 -0
  293. package/dist/lib/agent-manifest.js.map +1 -0
  294. package/dist/lib/agent-runtime-api.d.ts +32 -0
  295. package/dist/lib/agent-runtime-api.d.ts.map +1 -0
  296. package/dist/lib/agent-runtime-api.js +270 -0
  297. package/dist/lib/agent-runtime-api.js.map +1 -0
  298. package/dist/lib/agent-session.d.ts +104 -0
  299. package/dist/lib/agent-session.d.ts.map +1 -0
  300. package/dist/lib/agent-session.js +954 -0
  301. package/dist/lib/agent-session.js.map +1 -0
  302. package/dist/lib/build-supervisor.d.ts +44 -0
  303. package/dist/lib/build-supervisor.d.ts.map +1 -0
  304. package/dist/lib/build-supervisor.js +79 -0
  305. package/dist/lib/build-supervisor.js.map +1 -0
  306. package/dist/lib/connectors/index.d.ts +19 -0
  307. package/dist/lib/connectors/index.d.ts.map +1 -0
  308. package/dist/lib/connectors/index.js +23 -0
  309. package/dist/lib/connectors/index.js.map +1 -0
  310. package/dist/lib/counterfactual-engine.d.ts +136 -0
  311. package/dist/lib/counterfactual-engine.d.ts.map +1 -0
  312. package/dist/lib/counterfactual-engine.js +417 -0
  313. package/dist/lib/counterfactual-engine.js.map +1 -0
  314. package/dist/lib/counterfactual-training-bridge.d.ts +114 -0
  315. package/dist/lib/counterfactual-training-bridge.d.ts.map +1 -0
  316. package/dist/lib/counterfactual-training-bridge.js +322 -0
  317. package/dist/lib/counterfactual-training-bridge.js.map +1 -0
  318. package/dist/lib/discovery-agent.d.ts +48 -0
  319. package/dist/lib/discovery-agent.d.ts.map +1 -0
  320. package/dist/lib/discovery-agent.js +111 -0
  321. package/dist/lib/discovery-agent.js.map +1 -0
  322. package/dist/lib/domain/engine.d.ts +11 -0
  323. package/dist/lib/domain/engine.d.ts.map +1 -0
  324. package/dist/lib/domain/engine.js +23 -0
  325. package/dist/lib/domain/engine.js.map +1 -0
  326. package/dist/lib/domain/index.d.ts +3 -0
  327. package/dist/lib/domain/index.d.ts.map +1 -0
  328. package/dist/lib/domain/index.js +3 -0
  329. package/dist/lib/domain/index.js.map +1 -0
  330. package/dist/lib/domain/template-loader.d.ts +9 -0
  331. package/dist/lib/domain/template-loader.d.ts.map +1 -0
  332. package/dist/lib/domain/template-loader.js +29 -0
  333. package/dist/lib/domain/template-loader.js.map +1 -0
  334. package/dist/lib/domain/types.d.ts +29 -0
  335. package/dist/lib/domain/types.d.ts.map +1 -0
  336. package/dist/lib/domain/types.js +2 -0
  337. package/dist/lib/domain/types.js.map +1 -0
  338. package/dist/lib/dynamics-model.d.ts +107 -0
  339. package/dist/lib/dynamics-model.d.ts.map +1 -0
  340. package/dist/lib/dynamics-model.js +363 -0
  341. package/dist/lib/dynamics-model.js.map +1 -0
  342. package/dist/lib/eval-snapshot.d.ts +47 -0
  343. package/dist/lib/eval-snapshot.d.ts.map +1 -0
  344. package/dist/lib/eval-snapshot.js +326 -0
  345. package/dist/lib/eval-snapshot.js.map +1 -0
  346. package/dist/lib/eval-store.d.ts +20 -0
  347. package/dist/lib/eval-store.d.ts.map +1 -0
  348. package/dist/lib/eval-store.js +209 -0
  349. package/dist/lib/eval-store.js.map +1 -0
  350. package/dist/lib/findings-engine.d.ts +51 -0
  351. package/dist/lib/findings-engine.d.ts.map +1 -0
  352. package/dist/lib/findings-engine.js +338 -0
  353. package/dist/lib/findings-engine.js.map +1 -0
  354. package/dist/lib/flow-engine.d.ts +57 -0
  355. package/dist/lib/flow-engine.d.ts.map +1 -0
  356. package/dist/lib/flow-engine.js +717 -0
  357. package/dist/lib/flow-engine.js.map +1 -0
  358. package/dist/lib/gtm-generator.d.ts +29 -0
  359. package/dist/lib/gtm-generator.d.ts.map +1 -0
  360. package/dist/lib/gtm-generator.js +250 -0
  361. package/dist/lib/gtm-generator.js.map +1 -0
  362. package/dist/lib/hook-transformer.d.ts +11 -0
  363. package/dist/lib/hook-transformer.d.ts.map +1 -0
  364. package/dist/lib/hook-transformer.js +74 -0
  365. package/dist/lib/hook-transformer.js.map +1 -0
  366. package/dist/lib/hub-client.d.ts +81 -0
  367. package/dist/lib/hub-client.d.ts.map +1 -0
  368. package/dist/lib/hub-client.js +73 -0
  369. package/dist/lib/hub-client.js.map +1 -0
  370. package/dist/lib/hub-health.d.ts +40 -0
  371. package/dist/lib/hub-health.d.ts.map +1 -0
  372. package/dist/lib/hub-health.js +101 -0
  373. package/dist/lib/hub-health.js.map +1 -0
  374. package/dist/lib/ide-panes.d.ts +58 -0
  375. package/dist/lib/ide-panes.d.ts.map +1 -0
  376. package/dist/lib/ide-panes.js +508 -0
  377. package/dist/lib/ide-panes.js.map +1 -0
  378. package/dist/lib/invariant-monitor.d.ts +54 -0
  379. package/dist/lib/invariant-monitor.d.ts.map +1 -0
  380. package/dist/lib/invariant-monitor.js +487 -0
  381. package/dist/lib/invariant-monitor.js.map +1 -0
  382. package/dist/lib/journal-analyzer.d.ts +71 -0
  383. package/dist/lib/journal-analyzer.d.ts.map +1 -0
  384. package/dist/lib/journal-analyzer.js +306 -0
  385. package/dist/lib/journal-analyzer.js.map +1 -0
  386. package/dist/lib/kanban-github.d.ts +81 -0
  387. package/dist/lib/kanban-github.d.ts.map +1 -0
  388. package/dist/lib/kanban-github.js +318 -0
  389. package/dist/lib/kanban-github.js.map +1 -0
  390. package/dist/lib/kanban.d.ts +131 -0
  391. package/dist/lib/kanban.d.ts.map +1 -0
  392. package/dist/lib/kanban.js +340 -0
  393. package/dist/lib/kanban.js.map +1 -0
  394. package/dist/lib/kuva.d.ts +45 -0
  395. package/dist/lib/kuva.d.ts.map +1 -0
  396. package/dist/lib/kuva.js +131 -0
  397. package/dist/lib/kuva.js.map +1 -0
  398. package/dist/lib/linear-client.d.ts +73 -0
  399. package/dist/lib/linear-client.d.ts.map +1 -0
  400. package/dist/lib/linear-client.js +112 -0
  401. package/dist/lib/linear-client.js.map +1 -0
  402. package/dist/lib/linear-id-map.d.ts +20 -0
  403. package/dist/lib/linear-id-map.d.ts.map +1 -0
  404. package/dist/lib/linear-id-map.js +59 -0
  405. package/dist/lib/linear-id-map.js.map +1 -0
  406. package/dist/lib/linear-kanban.d.ts +66 -0
  407. package/dist/lib/linear-kanban.d.ts.map +1 -0
  408. package/dist/lib/linear-kanban.js +175 -0
  409. package/dist/lib/linear-kanban.js.map +1 -0
  410. package/dist/lib/linear-webhook.d.ts +50 -0
  411. package/dist/lib/linear-webhook.d.ts.map +1 -0
  412. package/dist/lib/linear-webhook.js +92 -0
  413. package/dist/lib/linear-webhook.js.map +1 -0
  414. package/dist/lib/map-event-bus.d.ts +50 -0
  415. package/dist/lib/map-event-bus.d.ts.map +1 -0
  416. package/dist/lib/map-event-bus.js +366 -0
  417. package/dist/lib/map-event-bus.js.map +1 -0
  418. package/dist/lib/memory-db.d.ts +136 -0
  419. package/dist/lib/memory-db.d.ts.map +1 -0
  420. package/dist/lib/memory-db.js +429 -0
  421. package/dist/lib/memory-db.js.map +1 -0
  422. package/dist/lib/memory-indexer.d.ts +61 -0
  423. package/dist/lib/memory-indexer.d.ts.map +1 -0
  424. package/dist/lib/memory-indexer.js +418 -0
  425. package/dist/lib/memory-indexer.js.map +1 -0
  426. package/dist/lib/memory-search.d.ts +185 -0
  427. package/dist/lib/memory-search.d.ts.map +1 -0
  428. package/dist/lib/memory-search.js +678 -0
  429. package/dist/lib/memory-search.js.map +1 -0
  430. package/dist/lib/meta-orchestrator.d.ts +141 -0
  431. package/dist/lib/meta-orchestrator.d.ts.map +1 -0
  432. package/dist/lib/meta-orchestrator.js +552 -0
  433. package/dist/lib/meta-orchestrator.js.map +1 -0
  434. package/dist/lib/model-pricing.d.ts +11 -0
  435. package/dist/lib/model-pricing.d.ts.map +1 -0
  436. package/dist/lib/model-pricing.js +27 -0
  437. package/dist/lib/model-pricing.js.map +1 -0
  438. package/dist/lib/onboarding.d.ts +40 -0
  439. package/dist/lib/onboarding.d.ts.map +1 -0
  440. package/dist/lib/onboarding.js +213 -0
  441. package/dist/lib/onboarding.js.map +1 -0
  442. package/dist/lib/openclaw-registry.d.ts +48 -0
  443. package/dist/lib/openclaw-registry.d.ts.map +1 -0
  444. package/dist/lib/openclaw-registry.js +181 -0
  445. package/dist/lib/openclaw-registry.js.map +1 -0
  446. package/dist/lib/openclaw-sdk.d.ts +115 -0
  447. package/dist/lib/openclaw-sdk.d.ts.map +1 -0
  448. package/dist/lib/openclaw-sdk.js +220 -0
  449. package/dist/lib/openclaw-sdk.js.map +1 -0
  450. package/dist/lib/peer-agent-generator.d.ts +44 -0
  451. package/dist/lib/peer-agent-generator.d.ts.map +1 -0
  452. package/dist/lib/peer-agent-generator.js +310 -0
  453. package/dist/lib/peer-agent-generator.js.map +1 -0
  454. package/dist/lib/peter-parker-bridge.d.ts +70 -0
  455. package/dist/lib/peter-parker-bridge.d.ts.map +1 -0
  456. package/dist/lib/peter-parker-bridge.js +345 -0
  457. package/dist/lib/peter-parker-bridge.js.map +1 -0
  458. package/dist/lib/peter-parker-config.d.ts +13 -0
  459. package/dist/lib/peter-parker-config.d.ts.map +1 -0
  460. package/dist/lib/peter-parker-config.js +86 -0
  461. package/dist/lib/peter-parker-config.js.map +1 -0
  462. package/dist/lib/physical-world-model.d.ts +50 -0
  463. package/dist/lib/physical-world-model.d.ts.map +1 -0
  464. package/dist/lib/physical-world-model.js +251 -0
  465. package/dist/lib/physical-world-model.js.map +1 -0
  466. package/dist/lib/pi-sky/bridge.d.ts +55 -0
  467. package/dist/lib/pi-sky/bridge.d.ts.map +1 -0
  468. package/dist/lib/pi-sky/bridge.js +264 -0
  469. package/dist/lib/pi-sky/bridge.js.map +1 -0
  470. package/dist/lib/pi-sky/cost-monitor.d.ts +21 -0
  471. package/dist/lib/pi-sky/cost-monitor.d.ts.map +1 -0
  472. package/dist/lib/pi-sky/cost-monitor.js +126 -0
  473. package/dist/lib/pi-sky/cost-monitor.js.map +1 -0
  474. package/dist/lib/pi-sky/eval-sweep.d.ts +27 -0
  475. package/dist/lib/pi-sky/eval-sweep.d.ts.map +1 -0
  476. package/dist/lib/pi-sky/eval-sweep.js +141 -0
  477. package/dist/lib/pi-sky/eval-sweep.js.map +1 -0
  478. package/dist/lib/pi-sky/event-router.d.ts +32 -0
  479. package/dist/lib/pi-sky/event-router.d.ts.map +1 -0
  480. package/dist/lib/pi-sky/event-router.js +176 -0
  481. package/dist/lib/pi-sky/event-router.js.map +1 -0
  482. package/dist/lib/pi-sky/experiment.d.ts +9 -0
  483. package/dist/lib/pi-sky/experiment.d.ts.map +1 -0
  484. package/dist/lib/pi-sky/experiment.js +83 -0
  485. package/dist/lib/pi-sky/experiment.js.map +1 -0
  486. package/dist/lib/pi-sky/index.d.ts +16 -0
  487. package/dist/lib/pi-sky/index.d.ts.map +1 -0
  488. package/dist/lib/pi-sky/index.js +16 -0
  489. package/dist/lib/pi-sky/index.js.map +1 -0
  490. package/dist/lib/pi-sky/stratus-gate.d.ts +28 -0
  491. package/dist/lib/pi-sky/stratus-gate.d.ts.map +1 -0
  492. package/dist/lib/pi-sky/stratus-gate.js +61 -0
  493. package/dist/lib/pi-sky/stratus-gate.js.map +1 -0
  494. package/dist/lib/pi-sky/swarm.d.ts +28 -0
  495. package/dist/lib/pi-sky/swarm.d.ts.map +1 -0
  496. package/dist/lib/pi-sky/swarm.js +208 -0
  497. package/dist/lib/pi-sky/swarm.js.map +1 -0
  498. package/dist/lib/pi-sky/types.d.ts +139 -0
  499. package/dist/lib/pi-sky/types.d.ts.map +1 -0
  500. package/dist/lib/pi-sky/types.js +2 -0
  501. package/dist/lib/pi-sky/types.js.map +1 -0
  502. package/dist/lib/pi-sky/voice-bridge.d.ts +20 -0
  503. package/dist/lib/pi-sky/voice-bridge.d.ts.map +1 -0
  504. package/dist/lib/pi-sky/voice-bridge.js +91 -0
  505. package/dist/lib/pi-sky/voice-bridge.js.map +1 -0
  506. package/dist/lib/planning-loop.d.ts +157 -0
  507. package/dist/lib/planning-loop.d.ts.map +1 -0
  508. package/dist/lib/planning-loop.js +537 -0
  509. package/dist/lib/planning-loop.js.map +1 -0
  510. package/dist/lib/policy-head.d.ts +53 -0
  511. package/dist/lib/policy-head.d.ts.map +1 -0
  512. package/dist/lib/policy-head.js +400 -0
  513. package/dist/lib/policy-head.js.map +1 -0
  514. package/dist/lib/predictor.d.ts +109 -0
  515. package/dist/lib/predictor.d.ts.map +1 -0
  516. package/dist/lib/predictor.js +433 -0
  517. package/dist/lib/predictor.js.map +1 -0
  518. package/dist/lib/replay-buffer.d.ts +93 -0
  519. package/dist/lib/replay-buffer.d.ts.map +1 -0
  520. package/dist/lib/replay-buffer.js +302 -0
  521. package/dist/lib/replay-buffer.js.map +1 -0
  522. package/dist/lib/resource-optimizer-middleware.d.ts +39 -0
  523. package/dist/lib/resource-optimizer-middleware.d.ts.map +1 -0
  524. package/dist/lib/resource-optimizer-middleware.js +228 -0
  525. package/dist/lib/resource-optimizer-middleware.js.map +1 -0
  526. package/dist/lib/resource-optimizer.d.ts +71 -0
  527. package/dist/lib/resource-optimizer.d.ts.map +1 -0
  528. package/dist/lib/resource-optimizer.js +228 -0
  529. package/dist/lib/resource-optimizer.js.map +1 -0
  530. package/dist/lib/rewards/index.d.ts +14 -0
  531. package/dist/lib/rewards/index.d.ts.map +1 -0
  532. package/dist/lib/rewards/index.js +15 -0
  533. package/dist/lib/rewards/index.js.map +1 -0
  534. package/dist/lib/rl-manager.d.ts +74 -0
  535. package/dist/lib/rl-manager.d.ts.map +1 -0
  536. package/dist/lib/rl-manager.js +245 -0
  537. package/dist/lib/rl-manager.js.map +1 -0
  538. package/dist/lib/sentinel-rl.d.ts +97 -0
  539. package/dist/lib/sentinel-rl.d.ts.map +1 -0
  540. package/dist/lib/sentinel-rl.js +430 -0
  541. package/dist/lib/sentinel-rl.js.map +1 -0
  542. package/dist/lib/service-analyzer.d.ts +76 -0
  543. package/dist/lib/service-analyzer.d.ts.map +1 -0
  544. package/dist/lib/service-analyzer.js +704 -0
  545. package/dist/lib/service-analyzer.js.map +1 -0
  546. package/dist/lib/service-dependencies.d.ts +44 -0
  547. package/dist/lib/service-dependencies.d.ts.map +1 -0
  548. package/dist/lib/service-dependencies.js +314 -0
  549. package/dist/lib/service-dependencies.js.map +1 -0
  550. package/dist/lib/service-detector.d.ts +61 -0
  551. package/dist/lib/service-detector.d.ts.map +1 -0
  552. package/dist/lib/service-detector.js +541 -0
  553. package/dist/lib/service-detector.js.map +1 -0
  554. package/dist/lib/service-gtm.d.ts +208 -0
  555. package/dist/lib/service-gtm.d.ts.map +1 -0
  556. package/dist/lib/service-gtm.js +1006 -0
  557. package/dist/lib/service-gtm.js.map +1 -0
  558. package/dist/lib/service-mcp-base.d.ts +103 -0
  559. package/dist/lib/service-mcp-base.d.ts.map +1 -0
  560. package/dist/lib/service-mcp-base.js +263 -0
  561. package/dist/lib/service-mcp-base.js.map +1 -0
  562. package/dist/lib/service-questionnaire.d.ts +11 -0
  563. package/dist/lib/service-questionnaire.d.ts.map +1 -0
  564. package/dist/lib/service-questionnaire.js +89 -0
  565. package/dist/lib/service-questionnaire.js.map +1 -0
  566. package/dist/lib/service-utils.d.ts +103 -0
  567. package/dist/lib/service-utils.d.ts.map +1 -0
  568. package/dist/lib/service-utils.js +385 -0
  569. package/dist/lib/service-utils.js.map +1 -0
  570. package/dist/lib/session-lock.d.ts +61 -0
  571. package/dist/lib/session-lock.d.ts.map +1 -0
  572. package/dist/lib/session-lock.js +438 -0
  573. package/dist/lib/session-lock.js.map +1 -0
  574. package/dist/lib/setup/agent-generator.d.ts +25 -0
  575. package/dist/lib/setup/agent-generator.d.ts.map +1 -0
  576. package/dist/lib/setup/agent-generator.js +444 -0
  577. package/dist/lib/setup/agent-generator.js.map +1 -0
  578. package/dist/lib/setup/context-analyzer.d.ts +16 -0
  579. package/dist/lib/setup/context-analyzer.d.ts.map +1 -0
  580. package/dist/lib/setup/context-analyzer.js +112 -0
  581. package/dist/lib/setup/context-analyzer.js.map +1 -0
  582. package/dist/lib/setup/doc-auditor.d.ts +54 -0
  583. package/dist/lib/setup/doc-auditor.d.ts.map +1 -0
  584. package/dist/lib/setup/doc-auditor.js +629 -0
  585. package/dist/lib/setup/doc-auditor.js.map +1 -0
  586. package/dist/lib/setup/domain-generator.d.ts +7 -0
  587. package/dist/lib/setup/domain-generator.d.ts.map +1 -0
  588. package/dist/lib/setup/domain-generator.js +58 -0
  589. package/dist/lib/setup/domain-generator.js.map +1 -0
  590. package/dist/lib/setup/flow-generator.d.ts +10 -0
  591. package/dist/lib/setup/flow-generator.d.ts.map +1 -0
  592. package/dist/lib/setup/flow-generator.js +113 -0
  593. package/dist/lib/setup/flow-generator.js.map +1 -0
  594. package/dist/lib/setup/invariant-bridge.d.ts +91 -0
  595. package/dist/lib/setup/invariant-bridge.d.ts.map +1 -0
  596. package/dist/lib/setup/invariant-bridge.js +384 -0
  597. package/dist/lib/setup/invariant-bridge.js.map +1 -0
  598. package/dist/lib/setup/smart-eval-generator.d.ts +38 -0
  599. package/dist/lib/setup/smart-eval-generator.d.ts.map +1 -0
  600. package/dist/lib/setup/smart-eval-generator.js +378 -0
  601. package/dist/lib/setup/smart-eval-generator.js.map +1 -0
  602. package/dist/lib/setup/smart-recommender.d.ts +63 -0
  603. package/dist/lib/setup/smart-recommender.d.ts.map +1 -0
  604. package/dist/lib/setup/smart-recommender.js +329 -0
  605. package/dist/lib/setup/smart-recommender.js.map +1 -0
  606. package/dist/lib/setup/spec-generator.d.ts +99 -0
  607. package/dist/lib/setup/spec-generator.d.ts.map +1 -0
  608. package/dist/lib/setup/spec-generator.js +784 -0
  609. package/dist/lib/setup/spec-generator.js.map +1 -0
  610. package/dist/lib/setup/starter-intelligence.d.ts +25 -0
  611. package/dist/lib/setup/starter-intelligence.d.ts.map +1 -0
  612. package/dist/lib/setup/starter-intelligence.js +309 -0
  613. package/dist/lib/setup/starter-intelligence.js.map +1 -0
  614. package/dist/lib/setup/violation-agent-generator.d.ts +32 -0
  615. package/dist/lib/setup/violation-agent-generator.d.ts.map +1 -0
  616. package/dist/lib/setup/violation-agent-generator.js +255 -0
  617. package/dist/lib/setup/violation-agent-generator.js.map +1 -0
  618. package/dist/lib/skill-generator.d.ts +21 -0
  619. package/dist/lib/skill-generator.d.ts.map +1 -0
  620. package/dist/lib/skill-generator.js +253 -0
  621. package/dist/lib/skill-generator.js.map +1 -0
  622. package/dist/lib/state-capture.d.ts +36 -0
  623. package/dist/lib/state-capture.d.ts.map +1 -0
  624. package/dist/lib/state-capture.js +541 -0
  625. package/dist/lib/state-capture.js.map +1 -0
  626. package/dist/lib/stealth-onboarding.d.ts +40 -0
  627. package/dist/lib/stealth-onboarding.d.ts.map +1 -0
  628. package/dist/lib/stealth-onboarding.js +213 -0
  629. package/dist/lib/stealth-onboarding.js.map +1 -0
  630. package/dist/lib/storage/cloud.d.ts +27 -0
  631. package/dist/lib/storage/cloud.d.ts.map +1 -0
  632. package/dist/lib/storage/cloud.js +75 -0
  633. package/dist/lib/storage/cloud.js.map +1 -0
  634. package/dist/lib/storage/index.d.ts +15 -0
  635. package/dist/lib/storage/index.d.ts.map +1 -0
  636. package/dist/lib/storage/index.js +15 -0
  637. package/dist/lib/storage/index.js.map +1 -0
  638. package/dist/lib/storage/interface.d.ts +56 -0
  639. package/dist/lib/storage/interface.d.ts.map +1 -0
  640. package/dist/lib/storage/interface.js +2 -0
  641. package/dist/lib/storage/interface.js.map +1 -0
  642. package/dist/lib/storage/local.d.ts +26 -0
  643. package/dist/lib/storage/local.d.ts.map +1 -0
  644. package/dist/lib/storage/local.js +164 -0
  645. package/dist/lib/storage/local.js.map +1 -0
  646. package/dist/lib/stratus-client.d.ts +178 -0
  647. package/dist/lib/stratus-client.d.ts.map +1 -0
  648. package/dist/lib/stratus-client.js +739 -0
  649. package/dist/lib/stratus-client.js.map +1 -0
  650. package/dist/lib/stratus-rollout-test.d.ts +10 -0
  651. package/dist/lib/stratus-rollout-test.d.ts.map +1 -0
  652. package/dist/lib/stratus-rollout-test.js +412 -0
  653. package/dist/lib/stratus-rollout-test.js.map +1 -0
  654. package/dist/lib/surface-agent.d.ts +78 -0
  655. package/dist/lib/surface-agent.d.ts.map +1 -0
  656. package/dist/lib/surface-agent.js +105 -0
  657. package/dist/lib/surface-agent.js.map +1 -0
  658. package/dist/lib/surface-coordination-example.d.ts +30 -0
  659. package/dist/lib/surface-coordination-example.d.ts.map +1 -0
  660. package/dist/lib/surface-coordination-example.js +164 -0
  661. package/dist/lib/surface-coordination-example.js.map +1 -0
  662. package/dist/lib/telemetry/physical-world-collector.d.ts +15 -0
  663. package/dist/lib/telemetry/physical-world-collector.d.ts.map +1 -0
  664. package/dist/lib/telemetry/physical-world-collector.js +177 -0
  665. package/dist/lib/telemetry/physical-world-collector.js.map +1 -0
  666. package/dist/lib/telemetry/training-bridge.d.ts +51 -0
  667. package/dist/lib/telemetry/training-bridge.d.ts.map +1 -0
  668. package/dist/lib/telemetry/training-bridge.js +185 -0
  669. package/dist/lib/telemetry/training-bridge.js.map +1 -0
  670. package/dist/lib/telemetry-agent-v2.d.ts +128 -0
  671. package/dist/lib/telemetry-agent-v2.d.ts.map +1 -0
  672. package/dist/lib/telemetry-agent-v2.js +1043 -0
  673. package/dist/lib/telemetry-agent-v2.js.map +1 -0
  674. package/dist/lib/telemetry-agent.d.ts +57 -0
  675. package/dist/lib/telemetry-agent.d.ts.map +1 -0
  676. package/dist/lib/telemetry-agent.js +289 -0
  677. package/dist/lib/telemetry-agent.js.map +1 -0
  678. package/dist/lib/telemetry-digest.d.ts +10 -0
  679. package/dist/lib/telemetry-digest.d.ts.map +1 -0
  680. package/dist/lib/telemetry-digest.js +381 -0
  681. package/dist/lib/telemetry-digest.js.map +1 -0
  682. package/dist/lib/telemetry.d.ts +37 -0
  683. package/dist/lib/telemetry.d.ts.map +1 -0
  684. package/dist/lib/telemetry.js +376 -0
  685. package/dist/lib/telemetry.js.map +1 -0
  686. package/dist/lib/tenet-board-agent.d.ts +52 -0
  687. package/dist/lib/tenet-board-agent.d.ts.map +1 -0
  688. package/dist/lib/tenet-board-agent.js +226 -0
  689. package/dist/lib/tenet-board-agent.js.map +1 -0
  690. package/dist/lib/tenet-ide-agent.d.ts +40 -0
  691. package/dist/lib/tenet-ide-agent.d.ts.map +1 -0
  692. package/dist/lib/tenet-ide-agent.js +199 -0
  693. package/dist/lib/tenet-ide-agent.js.map +1 -0
  694. package/dist/lib/text-preprocessing.d.ts +83 -0
  695. package/dist/lib/text-preprocessing.d.ts.map +1 -0
  696. package/dist/lib/text-preprocessing.js +261 -0
  697. package/dist/lib/text-preprocessing.js.map +1 -0
  698. package/dist/lib/tool-schemas.d.ts +35 -0
  699. package/dist/lib/tool-schemas.d.ts.map +1 -0
  700. package/dist/lib/tool-schemas.js +246 -0
  701. package/dist/lib/tool-schemas.js.map +1 -0
  702. package/dist/lib/training-buffer.d.ts +86 -0
  703. package/dist/lib/training-buffer.d.ts.map +1 -0
  704. package/dist/lib/training-buffer.js +139 -0
  705. package/dist/lib/training-buffer.js.map +1 -0
  706. package/dist/lib/training-tuples.d.ts +33 -0
  707. package/dist/lib/training-tuples.d.ts.map +1 -0
  708. package/dist/lib/training-tuples.js +273 -0
  709. package/dist/lib/training-tuples.js.map +1 -0
  710. package/dist/lib/trajectory-loader.d.ts +82 -0
  711. package/dist/lib/trajectory-loader.d.ts.map +1 -0
  712. package/dist/lib/trajectory-loader.js +406 -0
  713. package/dist/lib/trajectory-loader.js.map +1 -0
  714. package/dist/lib/tuple-miner.d.ts +30 -0
  715. package/dist/lib/tuple-miner.d.ts.map +1 -0
  716. package/dist/lib/tuple-miner.js +427 -0
  717. package/dist/lib/tuple-miner.js.map +1 -0
  718. package/dist/lib/vm-backend.d.ts +72 -0
  719. package/dist/lib/vm-backend.d.ts.map +1 -0
  720. package/dist/lib/vm-backend.js +175 -0
  721. package/dist/lib/vm-backend.js.map +1 -0
  722. package/dist/lib/workspace/backend.d.ts +53 -0
  723. package/dist/lib/workspace/backend.d.ts.map +1 -0
  724. package/dist/lib/workspace/backend.js +37 -0
  725. package/dist/lib/workspace/backend.js.map +1 -0
  726. package/dist/lib/workspace/cmux-adapter.d.ts +46 -0
  727. package/dist/lib/workspace/cmux-adapter.d.ts.map +1 -0
  728. package/dist/lib/workspace/cmux-adapter.js +261 -0
  729. package/dist/lib/workspace/cmux-adapter.js.map +1 -0
  730. package/dist/lib/workspace/data-pipeline.d.ts +35 -0
  731. package/dist/lib/workspace/data-pipeline.d.ts.map +1 -0
  732. package/dist/lib/workspace/data-pipeline.js +494 -0
  733. package/dist/lib/workspace/data-pipeline.js.map +1 -0
  734. package/dist/lib/workspace/engine.d.ts +65 -0
  735. package/dist/lib/workspace/engine.d.ts.map +1 -0
  736. package/dist/lib/workspace/engine.js +407 -0
  737. package/dist/lib/workspace/engine.js.map +1 -0
  738. package/dist/lib/workspace/notifications.d.ts +14 -0
  739. package/dist/lib/workspace/notifications.d.ts.map +1 -0
  740. package/dist/lib/workspace/notifications.js +41 -0
  741. package/dist/lib/workspace/notifications.js.map +1 -0
  742. package/dist/lib/workspace/sidebar-runner.d.ts +13 -0
  743. package/dist/lib/workspace/sidebar-runner.d.ts.map +1 -0
  744. package/dist/lib/workspace/sidebar-runner.js +419 -0
  745. package/dist/lib/workspace/sidebar-runner.js.map +1 -0
  746. package/dist/lib/workspace/surface-registry.d.ts +49 -0
  747. package/dist/lib/workspace/surface-registry.d.ts.map +1 -0
  748. package/dist/lib/workspace/surface-registry.js +225 -0
  749. package/dist/lib/workspace/surface-registry.js.map +1 -0
  750. package/dist/lib/workspace/surface-type.d.ts +153 -0
  751. package/dist/lib/workspace/surface-type.d.ts.map +1 -0
  752. package/dist/lib/workspace/surface-type.js +9 -0
  753. package/dist/lib/workspace/surface-type.js.map +1 -0
  754. package/dist/lib/workspace/surfaces/agent-overview.d.ts +16 -0
  755. package/dist/lib/workspace/surfaces/agent-overview.d.ts.map +1 -0
  756. package/dist/lib/workspace/surfaces/agent-overview.js +116 -0
  757. package/dist/lib/workspace/surfaces/agent-overview.js.map +1 -0
  758. package/dist/lib/workspace/surfaces/agent.d.ts +16 -0
  759. package/dist/lib/workspace/surfaces/agent.d.ts.map +1 -0
  760. package/dist/lib/workspace/surfaces/agent.js +112 -0
  761. package/dist/lib/workspace/surfaces/agent.js.map +1 -0
  762. package/dist/lib/workspace/surfaces/claude.d.ts +15 -0
  763. package/dist/lib/workspace/surfaces/claude.d.ts.map +1 -0
  764. package/dist/lib/workspace/surfaces/claude.js +23 -0
  765. package/dist/lib/workspace/surfaces/claude.js.map +1 -0
  766. package/dist/lib/workspace/surfaces/dashboard.d.ts +21 -0
  767. package/dist/lib/workspace/surfaces/dashboard.d.ts.map +1 -0
  768. package/dist/lib/workspace/surfaces/dashboard.js +32 -0
  769. package/dist/lib/workspace/surfaces/dashboard.js.map +1 -0
  770. package/dist/lib/workspace/surfaces/eval.d.ts +15 -0
  771. package/dist/lib/workspace/surfaces/eval.d.ts.map +1 -0
  772. package/dist/lib/workspace/surfaces/eval.js +42 -0
  773. package/dist/lib/workspace/surfaces/eval.js.map +1 -0
  774. package/dist/lib/workspace/surfaces/event-stream.d.ts +16 -0
  775. package/dist/lib/workspace/surfaces/event-stream.d.ts.map +1 -0
  776. package/dist/lib/workspace/surfaces/event-stream.js +40 -0
  777. package/dist/lib/workspace/surfaces/event-stream.js.map +1 -0
  778. package/dist/lib/workspace/surfaces/flow.d.ts +16 -0
  779. package/dist/lib/workspace/surfaces/flow.d.ts.map +1 -0
  780. package/dist/lib/workspace/surfaces/flow.js +49 -0
  781. package/dist/lib/workspace/surfaces/flow.js.map +1 -0
  782. package/dist/lib/workspace/surfaces/index.d.ts +19 -0
  783. package/dist/lib/workspace/surfaces/index.d.ts.map +1 -0
  784. package/dist/lib/workspace/surfaces/index.js +19 -0
  785. package/dist/lib/workspace/surfaces/index.js.map +1 -0
  786. package/dist/lib/workspace/surfaces/kanban.d.ts +15 -0
  787. package/dist/lib/workspace/surfaces/kanban.d.ts.map +1 -0
  788. package/dist/lib/workspace/surfaces/kanban.js +43 -0
  789. package/dist/lib/workspace/surfaces/kanban.js.map +1 -0
  790. package/dist/lib/workspace/surfaces/physical-world.d.ts +15 -0
  791. package/dist/lib/workspace/surfaces/physical-world.d.ts.map +1 -0
  792. package/dist/lib/workspace/surfaces/physical-world.js +37 -0
  793. package/dist/lib/workspace/surfaces/physical-world.js.map +1 -0
  794. package/dist/lib/workspace/surfaces/portfolio.d.ts +16 -0
  795. package/dist/lib/workspace/surfaces/portfolio.d.ts.map +1 -0
  796. package/dist/lib/workspace/surfaces/portfolio.js +102 -0
  797. package/dist/lib/workspace/surfaces/portfolio.js.map +1 -0
  798. package/dist/lib/workspace/surfaces/service.d.ts +16 -0
  799. package/dist/lib/workspace/surfaces/service.d.ts.map +1 -0
  800. package/dist/lib/workspace/surfaces/service.js +45 -0
  801. package/dist/lib/workspace/surfaces/service.js.map +1 -0
  802. package/dist/lib/workspace/surfaces/shell.d.ts +15 -0
  803. package/dist/lib/workspace/surfaces/shell.d.ts.map +1 -0
  804. package/dist/lib/workspace/surfaces/shell.js +19 -0
  805. package/dist/lib/workspace/surfaces/shell.js.map +1 -0
  806. package/dist/lib/workspace/surfaces/sidebar.d.ts +22 -0
  807. package/dist/lib/workspace/surfaces/sidebar.d.ts.map +1 -0
  808. package/dist/lib/workspace/surfaces/sidebar.js +94 -0
  809. package/dist/lib/workspace/surfaces/sidebar.js.map +1 -0
  810. package/dist/lib/workspace/surfaces/telemetry.d.ts +16 -0
  811. package/dist/lib/workspace/surfaces/telemetry.d.ts.map +1 -0
  812. package/dist/lib/workspace/surfaces/telemetry.js +48 -0
  813. package/dist/lib/workspace/surfaces/telemetry.js.map +1 -0
  814. package/dist/lib/workspace/surfaces/topology.d.ts +15 -0
  815. package/dist/lib/workspace/surfaces/topology.d.ts.map +1 -0
  816. package/dist/lib/workspace/surfaces/topology.js +19 -0
  817. package/dist/lib/workspace/surfaces/topology.js.map +1 -0
  818. package/dist/lib/workspace/surfaces/training.d.ts +16 -0
  819. package/dist/lib/workspace/surfaces/training.d.ts.map +1 -0
  820. package/dist/lib/workspace/surfaces/training.js +22 -0
  821. package/dist/lib/workspace/surfaces/training.js.map +1 -0
  822. package/dist/lib/workspace/tmux-adapter.d.ts +30 -0
  823. package/dist/lib/workspace/tmux-adapter.d.ts.map +1 -0
  824. package/dist/lib/workspace/tmux-adapter.js +137 -0
  825. package/dist/lib/workspace/tmux-adapter.js.map +1 -0
  826. package/dist/lib/workspace/tmux-sidebar.d.ts +14 -0
  827. package/dist/lib/workspace/tmux-sidebar.d.ts.map +1 -0
  828. package/dist/lib/workspace/tmux-sidebar.js +230 -0
  829. package/dist/lib/workspace/tmux-sidebar.js.map +1 -0
  830. package/dist/lib/world-model-store.d.ts +172 -0
  831. package/dist/lib/world-model-store.d.ts.map +1 -0
  832. package/dist/lib/world-model-store.js +487 -0
  833. package/dist/lib/world-model-store.js.map +1 -0
  834. package/dist/mcp/context-hub-mcp.d.ts +11 -0
  835. package/dist/mcp/context-hub-mcp.d.ts.map +1 -0
  836. package/dist/mcp/context-hub-mcp.js +797 -0
  837. package/dist/mcp/context-hub-mcp.js.map +1 -0
  838. package/dist/mcp/service-mcp-server.d.ts +12 -0
  839. package/dist/mcp/service-mcp-server.d.ts.map +1 -0
  840. package/dist/mcp/service-mcp-server.js +434 -0
  841. package/dist/mcp/service-mcp-server.js.map +1 -0
  842. package/dist/mcp/service-peer-mcp.d.ts +36 -0
  843. package/dist/mcp/service-peer-mcp.d.ts.map +1 -0
  844. package/dist/mcp/service-peer-mcp.js +220 -0
  845. package/dist/mcp/service-peer-mcp.js.map +1 -0
  846. package/dist/mcp/service-registry-mcp.d.ts +13 -0
  847. package/dist/mcp/service-registry-mcp.d.ts.map +1 -0
  848. package/dist/mcp/service-registry-mcp.js +330 -0
  849. package/dist/mcp/service-registry-mcp.js.map +1 -0
  850. package/dist/telegram/voice.d.ts +146 -0
  851. package/dist/telegram/voice.d.ts.map +1 -0
  852. package/dist/telegram/voice.js +351 -0
  853. package/dist/telegram/voice.js.map +1 -0
  854. package/dist/types/eval.d.ts +18 -0
  855. package/dist/types/eval.d.ts.map +1 -0
  856. package/dist/types/eval.js +5 -0
  857. package/dist/types/eval.js.map +1 -0
  858. package/dist/types/flows.d.ts +72 -0
  859. package/dist/types/flows.d.ts.map +1 -0
  860. package/dist/types/flows.js +10 -0
  861. package/dist/types/flows.js.map +1 -0
  862. package/dist/types/ide.d.ts +49 -0
  863. package/dist/types/ide.d.ts.map +1 -0
  864. package/dist/types/ide.js +5 -0
  865. package/dist/types/ide.js.map +1 -0
  866. package/dist/types/journal.d.ts +133 -0
  867. package/dist/types/journal.d.ts.map +1 -0
  868. package/dist/types/journal.js +59 -0
  869. package/dist/types/journal.js.map +1 -0
  870. package/dist/types/map.d.ts +42 -0
  871. package/dist/types/map.d.ts.map +1 -0
  872. package/dist/types/map.js +39 -0
  873. package/dist/types/map.js.map +1 -0
  874. package/dist/types/physical-world-model.d.ts +65 -0
  875. package/dist/types/physical-world-model.d.ts.map +1 -0
  876. package/dist/types/physical-world-model.js +43 -0
  877. package/dist/types/physical-world-model.js.map +1 -0
  878. package/dist/types/platform-digest.d.ts +228 -0
  879. package/dist/types/platform-digest.d.ts.map +1 -0
  880. package/dist/types/platform-digest.js +5 -0
  881. package/dist/types/platform-digest.js.map +1 -0
  882. package/dist/types/skills.d.ts +44 -0
  883. package/dist/types/skills.d.ts.map +1 -0
  884. package/dist/types/skills.js +5 -0
  885. package/dist/types/skills.js.map +1 -0
  886. package/dist/types/telemetry-digest.d.ts +75 -0
  887. package/dist/types/telemetry-digest.d.ts.map +1 -0
  888. package/dist/types/telemetry-digest.js +5 -0
  889. package/dist/types/telemetry-digest.js.map +1 -0
  890. package/dist/types/telemetry.d.ts +107 -0
  891. package/dist/types/telemetry.d.ts.map +1 -0
  892. package/dist/types/telemetry.js +5 -0
  893. package/dist/types/telemetry.js.map +1 -0
  894. package/dist/types/world-model.d.ts +478 -0
  895. package/dist/types/world-model.d.ts.map +1 -0
  896. package/dist/types/world-model.js +87 -0
  897. package/dist/types/world-model.js.map +1 -0
  898. package/dist/ui/banner.d.ts +18 -0
  899. package/dist/ui/banner.d.ts.map +1 -0
  900. package/dist/ui/banner.js +323 -0
  901. package/dist/ui/banner.js.map +1 -0
  902. package/dist/ui/context-hub-logs.d.ts +10 -0
  903. package/dist/ui/context-hub-logs.d.ts.map +1 -0
  904. package/dist/ui/context-hub-logs.js +175 -0
  905. package/dist/ui/context-hub-logs.js.map +1 -0
  906. package/dist/ui/event-dashboard.d.ts +12 -0
  907. package/dist/ui/event-dashboard.d.ts.map +1 -0
  908. package/dist/ui/event-dashboard.js +342 -0
  909. package/dist/ui/event-dashboard.js.map +1 -0
  910. package/dist/ui/index.d.ts +8 -0
  911. package/dist/ui/index.d.ts.map +1 -0
  912. package/dist/ui/index.js +8 -0
  913. package/dist/ui/index.js.map +1 -0
  914. package/dist/ui/prompts.d.ts +52 -0
  915. package/dist/ui/prompts.d.ts.map +1 -0
  916. package/dist/ui/prompts.js +72 -0
  917. package/dist/ui/prompts.js.map +1 -0
  918. package/dist/ui/service-dashboard.d.ts +11 -0
  919. package/dist/ui/service-dashboard.d.ts.map +1 -0
  920. package/dist/ui/service-dashboard.js +357 -0
  921. package/dist/ui/service-dashboard.js.map +1 -0
  922. package/dist/ui/services-manager.d.ts +11 -0
  923. package/dist/ui/services-manager.d.ts.map +1 -0
  924. package/dist/ui/services-manager.js +507 -0
  925. package/dist/ui/services-manager.js.map +1 -0
  926. package/dist/ui/theme.d.ts +82 -0
  927. package/dist/ui/theme.d.ts.map +1 -0
  928. package/dist/ui/theme.js +142 -0
  929. package/dist/ui/theme.js.map +1 -0
  930. package/dist/utils/auth-guard.d.ts +66 -0
  931. package/dist/utils/auth-guard.d.ts.map +1 -0
  932. package/dist/utils/auth-guard.js +347 -0
  933. package/dist/utils/auth-guard.js.map +1 -0
  934. package/dist/utils/auth-status.d.ts +21 -0
  935. package/dist/utils/auth-status.d.ts.map +1 -0
  936. package/dist/utils/auth-status.js +53 -0
  937. package/dist/utils/auth-status.js.map +1 -0
  938. package/dist/utils/claude-md-generator.d.ts +10 -0
  939. package/dist/utils/claude-md-generator.d.ts.map +1 -0
  940. package/dist/utils/claude-md-generator.js +215 -0
  941. package/dist/utils/claude-md-generator.js.map +1 -0
  942. package/dist/utils/context-hub-port.d.ts +33 -0
  943. package/dist/utils/context-hub-port.d.ts.map +1 -0
  944. package/dist/utils/context-hub-port.js +118 -0
  945. package/dist/utils/context-hub-port.js.map +1 -0
  946. package/dist/utils/ensure-context-hub.d.ts +20 -0
  947. package/dist/utils/ensure-context-hub.d.ts.map +1 -0
  948. package/dist/utils/ensure-context-hub.js +66 -0
  949. package/dist/utils/ensure-context-hub.js.map +1 -0
  950. package/dist/utils/ensure-project.d.ts +12 -0
  951. package/dist/utils/ensure-project.d.ts.map +1 -0
  952. package/dist/utils/ensure-project.js +81 -0
  953. package/dist/utils/ensure-project.js.map +1 -0
  954. package/dist/utils/git.d.ts +73 -0
  955. package/dist/utils/git.d.ts.map +1 -0
  956. package/dist/utils/git.js +222 -0
  957. package/dist/utils/git.js.map +1 -0
  958. package/dist/utils/github-auth.d.ts +54 -0
  959. package/dist/utils/github-auth.d.ts.map +1 -0
  960. package/dist/utils/github-auth.js +376 -0
  961. package/dist/utils/github-auth.js.map +1 -0
  962. package/dist/utils/github-repo.d.ts +30 -0
  963. package/dist/utils/github-repo.d.ts.map +1 -0
  964. package/dist/utils/github-repo.js +219 -0
  965. package/dist/utils/github-repo.js.map +1 -0
  966. package/dist/utils/jfl-config.d.ts +30 -0
  967. package/dist/utils/jfl-config.d.ts.map +1 -0
  968. package/dist/utils/jfl-config.js +153 -0
  969. package/dist/utils/jfl-config.js.map +1 -0
  970. package/dist/utils/jfl-migration.d.ts +29 -0
  971. package/dist/utils/jfl-migration.d.ts.map +1 -0
  972. package/dist/utils/jfl-migration.js +142 -0
  973. package/dist/utils/jfl-migration.js.map +1 -0
  974. package/dist/utils/jfl-paths.d.ts +71 -0
  975. package/dist/utils/jfl-paths.d.ts.map +1 -0
  976. package/dist/utils/jfl-paths.js +157 -0
  977. package/dist/utils/jfl-paths.js.map +1 -0
  978. package/dist/utils/platform-auth.d.ts +81 -0
  979. package/dist/utils/platform-auth.d.ts.map +1 -0
  980. package/dist/utils/platform-auth.js +192 -0
  981. package/dist/utils/platform-auth.js.map +1 -0
  982. package/dist/utils/project-config.d.ts +43 -0
  983. package/dist/utils/project-config.d.ts.map +1 -0
  984. package/dist/utils/project-config.js +97 -0
  985. package/dist/utils/project-config.js.map +1 -0
  986. package/dist/utils/provenance.d.ts +65 -0
  987. package/dist/utils/provenance.d.ts.map +1 -0
  988. package/dist/utils/provenance.js +213 -0
  989. package/dist/utils/provenance.js.map +1 -0
  990. package/dist/utils/settings-validator.d.ts +74 -0
  991. package/dist/utils/settings-validator.d.ts.map +1 -0
  992. package/dist/utils/settings-validator.js +241 -0
  993. package/dist/utils/settings-validator.js.map +1 -0
  994. package/dist/utils/skill-registry.d.ts +49 -0
  995. package/dist/utils/skill-registry.d.ts.map +1 -0
  996. package/dist/utils/skill-registry.js +192 -0
  997. package/dist/utils/skill-registry.js.map +1 -0
  998. package/dist/utils/tenet-env.d.ts +34 -0
  999. package/dist/utils/tenet-env.d.ts.map +1 -0
  1000. package/dist/utils/tenet-env.js +42 -0
  1001. package/dist/utils/tenet-env.js.map +1 -0
  1002. package/dist/utils/wallet.d.ts +62 -0
  1003. package/dist/utils/wallet.d.ts.map +1 -0
  1004. package/dist/utils/wallet.js +253 -0
  1005. package/dist/utils/wallet.js.map +1 -0
  1006. package/dist/utils/x402-client.d.ts +86 -0
  1007. package/dist/utils/x402-client.d.ts.map +1 -0
  1008. package/dist/utils/x402-client.js +266 -0
  1009. package/dist/utils/x402-client.js.map +1 -0
  1010. package/package.json +105 -0
  1011. package/packages/pi/AGENTS.md +112 -0
  1012. package/packages/pi/assets/boot.mp3 +0 -0
  1013. package/packages/pi/dist/agent-grid.d.ts +24 -0
  1014. package/packages/pi/dist/agent-grid.d.ts.map +1 -0
  1015. package/packages/pi/dist/agent-grid.js +162 -0
  1016. package/packages/pi/dist/agent-grid.js.map +1 -0
  1017. package/packages/pi/dist/agent-names.d.ts +43 -0
  1018. package/packages/pi/dist/agent-names.d.ts.map +1 -0
  1019. package/packages/pi/dist/agent-names.js +156 -0
  1020. package/packages/pi/dist/agent-names.js.map +1 -0
  1021. package/packages/pi/dist/autoresearch.d.ts +15 -0
  1022. package/packages/pi/dist/autoresearch.d.ts.map +1 -0
  1023. package/packages/pi/dist/autoresearch.js +372 -0
  1024. package/packages/pi/dist/autoresearch.js.map +1 -0
  1025. package/packages/pi/dist/bookmarks.d.ts +15 -0
  1026. package/packages/pi/dist/bookmarks.d.ts.map +1 -0
  1027. package/packages/pi/dist/bookmarks.js +77 -0
  1028. package/packages/pi/dist/bookmarks.js.map +1 -0
  1029. package/packages/pi/dist/context.d.ts +17 -0
  1030. package/packages/pi/dist/context.d.ts.map +1 -0
  1031. package/packages/pi/dist/context.js +152 -0
  1032. package/packages/pi/dist/context.js.map +1 -0
  1033. package/packages/pi/dist/crm-tool.d.ts +12 -0
  1034. package/packages/pi/dist/crm-tool.d.ts.map +1 -0
  1035. package/packages/pi/dist/crm-tool.js +58 -0
  1036. package/packages/pi/dist/crm-tool.js.map +1 -0
  1037. package/packages/pi/dist/eval-tool.d.ts +11 -0
  1038. package/packages/pi/dist/eval-tool.d.ts.map +1 -0
  1039. package/packages/pi/dist/eval-tool.js +188 -0
  1040. package/packages/pi/dist/eval-tool.js.map +1 -0
  1041. package/packages/pi/dist/eval.d.ts +12 -0
  1042. package/packages/pi/dist/eval.d.ts.map +1 -0
  1043. package/packages/pi/dist/eval.js +43 -0
  1044. package/packages/pi/dist/eval.js.map +1 -0
  1045. package/packages/pi/dist/footer.d.ts +20 -0
  1046. package/packages/pi/dist/footer.d.ts.map +1 -0
  1047. package/packages/pi/dist/footer.js +222 -0
  1048. package/packages/pi/dist/footer.js.map +1 -0
  1049. package/packages/pi/dist/header.d.ts +17 -0
  1050. package/packages/pi/dist/header.d.ts.map +1 -0
  1051. package/packages/pi/dist/header.js +156 -0
  1052. package/packages/pi/dist/header.js.map +1 -0
  1053. package/packages/pi/dist/hub-resolver.d.ts +11 -0
  1054. package/packages/pi/dist/hub-resolver.d.ts.map +1 -0
  1055. package/packages/pi/dist/hub-resolver.js +58 -0
  1056. package/packages/pi/dist/hub-resolver.js.map +1 -0
  1057. package/packages/pi/dist/hub-tools.d.ts +14 -0
  1058. package/packages/pi/dist/hub-tools.d.ts.map +1 -0
  1059. package/packages/pi/dist/hub-tools.js +266 -0
  1060. package/packages/pi/dist/hub-tools.js.map +1 -0
  1061. package/packages/pi/dist/hud-tool.d.ts +17 -0
  1062. package/packages/pi/dist/hud-tool.d.ts.map +1 -0
  1063. package/packages/pi/dist/hud-tool.js +297 -0
  1064. package/packages/pi/dist/hud-tool.js.map +1 -0
  1065. package/packages/pi/dist/index.d.ts +12 -0
  1066. package/packages/pi/dist/index.d.ts.map +1 -0
  1067. package/packages/pi/dist/index.js +556 -0
  1068. package/packages/pi/dist/index.js.map +1 -0
  1069. package/packages/pi/dist/jfl-resolve.d.ts +29 -0
  1070. package/packages/pi/dist/jfl-resolve.d.ts.map +1 -0
  1071. package/packages/pi/dist/jfl-resolve.js +89 -0
  1072. package/packages/pi/dist/jfl-resolve.js.map +1 -0
  1073. package/packages/pi/dist/journal.d.ts +23 -0
  1074. package/packages/pi/dist/journal.d.ts.map +1 -0
  1075. package/packages/pi/dist/journal.js +250 -0
  1076. package/packages/pi/dist/journal.js.map +1 -0
  1077. package/packages/pi/dist/map-bridge.d.ts +20 -0
  1078. package/packages/pi/dist/map-bridge.d.ts.map +1 -0
  1079. package/packages/pi/dist/map-bridge.js +181 -0
  1080. package/packages/pi/dist/map-bridge.js.map +1 -0
  1081. package/packages/pi/dist/memory-tool.d.ts +11 -0
  1082. package/packages/pi/dist/memory-tool.d.ts.map +1 -0
  1083. package/packages/pi/dist/memory-tool.js +162 -0
  1084. package/packages/pi/dist/memory-tool.js.map +1 -0
  1085. package/packages/pi/dist/notifications.d.ts +15 -0
  1086. package/packages/pi/dist/notifications.d.ts.map +1 -0
  1087. package/packages/pi/dist/notifications.js +65 -0
  1088. package/packages/pi/dist/notifications.js.map +1 -0
  1089. package/packages/pi/dist/onboarding-v1.d.ts +15 -0
  1090. package/packages/pi/dist/onboarding-v1.d.ts.map +1 -0
  1091. package/packages/pi/dist/onboarding-v1.js +417 -0
  1092. package/packages/pi/dist/onboarding-v1.js.map +1 -0
  1093. package/packages/pi/dist/onboarding-v2.d.ts +18 -0
  1094. package/packages/pi/dist/onboarding-v2.d.ts.map +1 -0
  1095. package/packages/pi/dist/onboarding-v2.js +402 -0
  1096. package/packages/pi/dist/onboarding-v2.js.map +1 -0
  1097. package/packages/pi/dist/onboarding-v3.d.ts +13 -0
  1098. package/packages/pi/dist/onboarding-v3.d.ts.map +1 -0
  1099. package/packages/pi/dist/onboarding-v3.js +581 -0
  1100. package/packages/pi/dist/onboarding-v3.js.map +1 -0
  1101. package/packages/pi/dist/peter-parker.d.ts +12 -0
  1102. package/packages/pi/dist/peter-parker.d.ts.map +1 -0
  1103. package/packages/pi/dist/peter-parker.js +162 -0
  1104. package/packages/pi/dist/peter-parker.js.map +1 -0
  1105. package/packages/pi/dist/pivot-tool.d.ts +11 -0
  1106. package/packages/pi/dist/pivot-tool.d.ts.map +1 -0
  1107. package/packages/pi/dist/pivot-tool.js +56 -0
  1108. package/packages/pi/dist/pivot-tool.js.map +1 -0
  1109. package/packages/pi/dist/policy-head-tool.d.ts +15 -0
  1110. package/packages/pi/dist/policy-head-tool.d.ts.map +1 -0
  1111. package/packages/pi/dist/policy-head-tool.js +220 -0
  1112. package/packages/pi/dist/policy-head-tool.js.map +1 -0
  1113. package/packages/pi/dist/portfolio-bridge.d.ts +12 -0
  1114. package/packages/pi/dist/portfolio-bridge.d.ts.map +1 -0
  1115. package/packages/pi/dist/portfolio-bridge.js +81 -0
  1116. package/packages/pi/dist/portfolio-bridge.js.map +1 -0
  1117. package/packages/pi/dist/service-skills.d.ts +15 -0
  1118. package/packages/pi/dist/service-skills.d.ts.map +1 -0
  1119. package/packages/pi/dist/service-skills.js +198 -0
  1120. package/packages/pi/dist/service-skills.js.map +1 -0
  1121. package/packages/pi/dist/session.d.ts +28 -0
  1122. package/packages/pi/dist/session.d.ts.map +1 -0
  1123. package/packages/pi/dist/session.js +649 -0
  1124. package/packages/pi/dist/session.js.map +1 -0
  1125. package/packages/pi/dist/shortcuts.d.ts +11 -0
  1126. package/packages/pi/dist/shortcuts.d.ts.map +1 -0
  1127. package/packages/pi/dist/shortcuts.js +231 -0
  1128. package/packages/pi/dist/shortcuts.js.map +1 -0
  1129. package/packages/pi/dist/startup-briefing.d.ts +13 -0
  1130. package/packages/pi/dist/startup-briefing.d.ts.map +1 -0
  1131. package/packages/pi/dist/startup-briefing.js +432 -0
  1132. package/packages/pi/dist/startup-briefing.js.map +1 -0
  1133. package/packages/pi/dist/stratus-bridge.d.ts +14 -0
  1134. package/packages/pi/dist/stratus-bridge.d.ts.map +1 -0
  1135. package/packages/pi/dist/stratus-bridge.js +104 -0
  1136. package/packages/pi/dist/stratus-bridge.js.map +1 -0
  1137. package/packages/pi/dist/subway-mesh.d.ts +88 -0
  1138. package/packages/pi/dist/subway-mesh.d.ts.map +1 -0
  1139. package/packages/pi/dist/subway-mesh.js +813 -0
  1140. package/packages/pi/dist/subway-mesh.js.map +1 -0
  1141. package/packages/pi/dist/synopsis-tool.d.ts +12 -0
  1142. package/packages/pi/dist/synopsis-tool.d.ts.map +1 -0
  1143. package/packages/pi/dist/synopsis-tool.js +84 -0
  1144. package/packages/pi/dist/synopsis-tool.js.map +1 -0
  1145. package/packages/pi/dist/tool-renderers.d.ts +55 -0
  1146. package/packages/pi/dist/tool-renderers.d.ts.map +1 -0
  1147. package/packages/pi/dist/tool-renderers.js +349 -0
  1148. package/packages/pi/dist/tool-renderers.js.map +1 -0
  1149. package/packages/pi/dist/training-buffer-tool.d.ts +16 -0
  1150. package/packages/pi/dist/training-buffer-tool.d.ts.map +1 -0
  1151. package/packages/pi/dist/training-buffer-tool.js +319 -0
  1152. package/packages/pi/dist/training-buffer-tool.js.map +1 -0
  1153. package/packages/pi/dist/types.d.ts +195 -0
  1154. package/packages/pi/dist/types.d.ts.map +1 -0
  1155. package/packages/pi/dist/types.js +11 -0
  1156. package/packages/pi/dist/types.js.map +1 -0
  1157. package/packages/pi/extensions/agent-grid.ts +191 -0
  1158. package/packages/pi/extensions/agent-names.ts +178 -0
  1159. package/packages/pi/extensions/autoresearch.ts +428 -0
  1160. package/packages/pi/extensions/bookmarks.ts +85 -0
  1161. package/packages/pi/extensions/context.ts +158 -0
  1162. package/packages/pi/extensions/crm-tool.ts +61 -0
  1163. package/packages/pi/extensions/eval-tool.ts +224 -0
  1164. package/packages/pi/extensions/eval.ts +61 -0
  1165. package/packages/pi/extensions/footer.ts +239 -0
  1166. package/packages/pi/extensions/header.ts +171 -0
  1167. package/packages/pi/extensions/hub-resolver.ts +63 -0
  1168. package/packages/pi/extensions/hub-tools.ts +267 -0
  1169. package/packages/pi/extensions/hud-tool.ts +294 -0
  1170. package/packages/pi/extensions/index.ts +601 -0
  1171. package/packages/pi/extensions/jfl-resolve.ts +98 -0
  1172. package/packages/pi/extensions/journal.ts +309 -0
  1173. package/packages/pi/extensions/map-bridge.ts +209 -0
  1174. package/packages/pi/extensions/memory-tool.ts +170 -0
  1175. package/packages/pi/extensions/notifications.ts +73 -0
  1176. package/packages/pi/extensions/onboarding-v1.ts +455 -0
  1177. package/packages/pi/extensions/onboarding-v2.ts +374 -0
  1178. package/packages/pi/extensions/onboarding-v3.ts +686 -0
  1179. package/packages/pi/extensions/peter-parker.ts +203 -0
  1180. package/packages/pi/extensions/pivot-tool.ts +59 -0
  1181. package/packages/pi/extensions/policy-head-tool.ts +277 -0
  1182. package/packages/pi/extensions/portfolio-bridge.ts +89 -0
  1183. package/packages/pi/extensions/service-skills.ts +219 -0
  1184. package/packages/pi/extensions/session.ts +684 -0
  1185. package/packages/pi/extensions/shortcuts.ts +259 -0
  1186. package/packages/pi/extensions/startup-briefing.ts +482 -0
  1187. package/packages/pi/extensions/stratus-bridge.ts +116 -0
  1188. package/packages/pi/extensions/subway-mesh.ts +893 -0
  1189. package/packages/pi/extensions/synopsis-tool.ts +88 -0
  1190. package/packages/pi/extensions/tool-renderers.ts +366 -0
  1191. package/packages/pi/extensions/training-buffer-tool.ts +376 -0
  1192. package/packages/pi/extensions/types.ts +169 -0
  1193. package/packages/pi/package-lock.json +346 -0
  1194. package/packages/pi/package.json +42 -0
  1195. package/packages/pi/skills/agent-browser/SKILL.md +116 -0
  1196. package/packages/pi/skills/brand-architect/SKILL.md +240 -0
  1197. package/packages/pi/skills/brand-architect/config.yaml +137 -0
  1198. package/packages/pi/skills/campaign-hud/config.yaml +112 -0
  1199. package/packages/pi/skills/content-creator/SKILL.md +294 -0
  1200. package/packages/pi/skills/context/SKILL.md +65 -0
  1201. package/packages/pi/skills/debug/MULTI_AGENT.md +360 -0
  1202. package/packages/pi/skills/debug/SKILL.md +554 -0
  1203. package/packages/pi/skills/end/SKILL.md +1790 -0
  1204. package/packages/pi/skills/eval/SKILL.md +75 -0
  1205. package/packages/pi/skills/fly-deploy/SKILL.md +676 -0
  1206. package/packages/pi/skills/founder-video/SKILL.md +467 -0
  1207. package/packages/pi/skills/hud/SKILL.md +160 -0
  1208. package/packages/pi/skills/orchestrate/SKILL.md +74 -0
  1209. package/packages/pi/skills/pi-agents/SKILL.md +78 -0
  1210. package/packages/pi/skills/pivot/SKILL.md +91 -0
  1211. package/packages/pi/skills/react-best-practices/AGENTS.md +2249 -0
  1212. package/packages/pi/skills/react-best-practices/README.md +123 -0
  1213. package/packages/pi/skills/react-best-practices/SKILL.md +125 -0
  1214. package/packages/pi/skills/react-best-practices/metadata.json +15 -0
  1215. package/packages/pi/skills/react-best-practices/rules/_sections.md +46 -0
  1216. package/packages/pi/skills/react-best-practices/rules/_template.md +28 -0
  1217. package/packages/pi/skills/react-best-practices/rules/advanced-event-handler-refs.md +55 -0
  1218. package/packages/pi/skills/react-best-practices/rules/advanced-use-latest.md +49 -0
  1219. package/packages/pi/skills/react-best-practices/rules/async-api-routes.md +38 -0
  1220. package/packages/pi/skills/react-best-practices/rules/async-defer-await.md +80 -0
  1221. package/packages/pi/skills/react-best-practices/rules/async-dependencies.md +36 -0
  1222. package/packages/pi/skills/react-best-practices/rules/async-parallel.md +28 -0
  1223. package/packages/pi/skills/react-best-practices/rules/async-suspense-boundaries.md +99 -0
  1224. package/packages/pi/skills/react-best-practices/rules/bundle-barrel-imports.md +59 -0
  1225. package/packages/pi/skills/react-best-practices/rules/bundle-conditional.md +31 -0
  1226. package/packages/pi/skills/react-best-practices/rules/bundle-defer-third-party.md +49 -0
  1227. package/packages/pi/skills/react-best-practices/rules/bundle-dynamic-imports.md +35 -0
  1228. package/packages/pi/skills/react-best-practices/rules/bundle-preload.md +50 -0
  1229. package/packages/pi/skills/react-best-practices/rules/client-event-listeners.md +74 -0
  1230. package/packages/pi/skills/react-best-practices/rules/client-swr-dedup.md +56 -0
  1231. package/packages/pi/skills/react-best-practices/rules/js-batch-dom-css.md +82 -0
  1232. package/packages/pi/skills/react-best-practices/rules/js-cache-function-results.md +80 -0
  1233. package/packages/pi/skills/react-best-practices/rules/js-cache-property-access.md +28 -0
  1234. package/packages/pi/skills/react-best-practices/rules/js-cache-storage.md +70 -0
  1235. package/packages/pi/skills/react-best-practices/rules/js-combine-iterations.md +32 -0
  1236. package/packages/pi/skills/react-best-practices/rules/js-early-exit.md +50 -0
  1237. package/packages/pi/skills/react-best-practices/rules/js-hoist-regexp.md +45 -0
  1238. package/packages/pi/skills/react-best-practices/rules/js-index-maps.md +37 -0
  1239. package/packages/pi/skills/react-best-practices/rules/js-length-check-first.md +49 -0
  1240. package/packages/pi/skills/react-best-practices/rules/js-min-max-loop.md +82 -0
  1241. package/packages/pi/skills/react-best-practices/rules/js-set-map-lookups.md +24 -0
  1242. package/packages/pi/skills/react-best-practices/rules/js-tosorted-immutable.md +57 -0
  1243. package/packages/pi/skills/react-best-practices/rules/rendering-activity.md +26 -0
  1244. package/packages/pi/skills/react-best-practices/rules/rendering-animate-svg-wrapper.md +47 -0
  1245. package/packages/pi/skills/react-best-practices/rules/rendering-conditional-render.md +40 -0
  1246. package/packages/pi/skills/react-best-practices/rules/rendering-content-visibility.md +38 -0
  1247. package/packages/pi/skills/react-best-practices/rules/rendering-hoist-jsx.md +46 -0
  1248. package/packages/pi/skills/react-best-practices/rules/rendering-hydration-no-flicker.md +82 -0
  1249. package/packages/pi/skills/react-best-practices/rules/rendering-svg-precision.md +28 -0
  1250. package/packages/pi/skills/react-best-practices/rules/rerender-defer-reads.md +39 -0
  1251. package/packages/pi/skills/react-best-practices/rules/rerender-dependencies.md +45 -0
  1252. package/packages/pi/skills/react-best-practices/rules/rerender-derived-state.md +29 -0
  1253. package/packages/pi/skills/react-best-practices/rules/rerender-functional-setstate.md +74 -0
  1254. package/packages/pi/skills/react-best-practices/rules/rerender-lazy-state-init.md +58 -0
  1255. package/packages/pi/skills/react-best-practices/rules/rerender-memo.md +44 -0
  1256. package/packages/pi/skills/react-best-practices/rules/rerender-transitions.md +40 -0
  1257. package/packages/pi/skills/react-best-practices/rules/server-after-nonblocking.md +73 -0
  1258. package/packages/pi/skills/react-best-practices/rules/server-cache-lru.md +41 -0
  1259. package/packages/pi/skills/react-best-practices/rules/server-cache-react.md +26 -0
  1260. package/packages/pi/skills/react-best-practices/rules/server-parallel-fetching.md +79 -0
  1261. package/packages/pi/skills/react-best-practices/rules/server-serialization.md +38 -0
  1262. package/packages/pi/skills/remotion-best-practices/SKILL.md +43 -0
  1263. package/packages/pi/skills/remotion-best-practices/rules/3d.md +86 -0
  1264. package/packages/pi/skills/remotion-best-practices/rules/animations.md +29 -0
  1265. package/packages/pi/skills/remotion-best-practices/rules/assets/charts-bar-chart.tsx +173 -0
  1266. package/packages/pi/skills/remotion-best-practices/rules/assets/text-animations-typewriter.tsx +100 -0
  1267. package/packages/pi/skills/remotion-best-practices/rules/assets/text-animations-word-highlight.tsx +108 -0
  1268. package/packages/pi/skills/remotion-best-practices/rules/assets.md +78 -0
  1269. package/packages/pi/skills/remotion-best-practices/rules/audio.md +172 -0
  1270. package/packages/pi/skills/remotion-best-practices/rules/calculate-metadata.md +104 -0
  1271. package/packages/pi/skills/remotion-best-practices/rules/can-decode.md +75 -0
  1272. package/packages/pi/skills/remotion-best-practices/rules/charts.md +58 -0
  1273. package/packages/pi/skills/remotion-best-practices/rules/compositions.md +146 -0
  1274. package/packages/pi/skills/remotion-best-practices/rules/display-captions.md +126 -0
  1275. package/packages/pi/skills/remotion-best-practices/rules/extract-frames.md +229 -0
  1276. package/packages/pi/skills/remotion-best-practices/rules/fonts.md +152 -0
  1277. package/packages/pi/skills/remotion-best-practices/rules/get-audio-duration.md +58 -0
  1278. package/packages/pi/skills/remotion-best-practices/rules/get-video-dimensions.md +68 -0
  1279. package/packages/pi/skills/remotion-best-practices/rules/get-video-duration.md +58 -0
  1280. package/packages/pi/skills/remotion-best-practices/rules/gifs.md +138 -0
  1281. package/packages/pi/skills/remotion-best-practices/rules/images.md +130 -0
  1282. package/packages/pi/skills/remotion-best-practices/rules/import-srt-captions.md +67 -0
  1283. package/packages/pi/skills/remotion-best-practices/rules/lottie.md +68 -0
  1284. package/packages/pi/skills/remotion-best-practices/rules/measuring-dom-nodes.md +35 -0
  1285. package/packages/pi/skills/remotion-best-practices/rules/measuring-text.md +143 -0
  1286. package/packages/pi/skills/remotion-best-practices/rules/sequencing.md +106 -0
  1287. package/packages/pi/skills/remotion-best-practices/rules/tailwind.md +11 -0
  1288. package/packages/pi/skills/remotion-best-practices/rules/text-animations.md +20 -0
  1289. package/packages/pi/skills/remotion-best-practices/rules/timing.md +179 -0
  1290. package/packages/pi/skills/remotion-best-practices/rules/transcribe-captions.md +19 -0
  1291. package/packages/pi/skills/remotion-best-practices/rules/transitions.md +122 -0
  1292. package/packages/pi/skills/remotion-best-practices/rules/trimming.md +53 -0
  1293. package/packages/pi/skills/remotion-best-practices/rules/videos.md +171 -0
  1294. package/packages/pi/skills/search/SKILL.md +220 -0
  1295. package/packages/pi/skills/spec/SKILL.md +377 -0
  1296. package/packages/pi/skills/startup/SKILL.md +315 -0
  1297. package/packages/pi/skills/subway-browser/SKILL.md +292 -0
  1298. package/packages/pi/skills/viz/SKILL.md +204 -0
  1299. package/packages/pi/skills/web-architect/SKILL.md +309 -0
  1300. package/packages/pi/skills/x-algorithm/SKILL.md +305 -0
  1301. package/packages/pi/teams/dev-team.yaml +63 -0
  1302. package/packages/pi/teams/gtm-team.yaml +79 -0
  1303. package/packages/pi/themes/jfl.theme.json +76 -0
  1304. package/packages/pi/tsconfig.json +21 -0
  1305. package/scripts/__pycache__/train-policy-head.cpython-314.pyc +0 -0
  1306. package/scripts/collect-tuples.sh +124 -0
  1307. package/scripts/commit-gtm.sh +56 -0
  1308. package/scripts/commit-product.sh +68 -0
  1309. package/scripts/context-query.sh +45 -0
  1310. package/scripts/destroy-fleet.sh +37 -0
  1311. package/scripts/generate-changesets.sh +113 -0
  1312. package/scripts/jfl-ide.sh +48 -0
  1313. package/scripts/migrate-to-branch-sessions.sh +201 -0
  1314. package/scripts/postinstall.js +146 -0
  1315. package/scripts/pp-branch-pr.sh +133 -0
  1316. package/scripts/pp-branch-pr.sh.bak +115 -0
  1317. package/scripts/session/auto-commit.sh +297 -0
  1318. package/scripts/session/fix-tracked-logs.sh +97 -0
  1319. package/scripts/session/jfl-doctor.sh +707 -0
  1320. package/scripts/session/session-cleanup.sh +292 -0
  1321. package/scripts/session/session-end.sh +198 -0
  1322. package/scripts/session/session-init.sh +356 -0
  1323. package/scripts/session/session-init.sh.backup +292 -0
  1324. package/scripts/session/session-sync.sh +192 -0
  1325. package/scripts/session/test-context-preservation.sh +160 -0
  1326. package/scripts/session/test-critical-infrastructure.sh +293 -0
  1327. package/scripts/session/test-experience-level.sh +336 -0
  1328. package/scripts/session/test-session-cleanup.sh +268 -0
  1329. package/scripts/session/test-session-sync.sh +320 -0
  1330. package/scripts/setup-branch-protection.sh +106 -0
  1331. package/scripts/spawn-fleet.sh +144 -0
  1332. package/scripts/telemetry-dashboard.sh +44 -0
  1333. package/scripts/test-map-eventbus.sh +357 -0
  1334. package/scripts/test-onboarding.sh +121 -0
  1335. package/scripts/test-planning-loop-e2e.ts +181 -0
  1336. package/scripts/test-server-inference.ts +49 -0
  1337. package/scripts/test-state-sensitivity.ts +32 -0
  1338. package/scripts/train/requirements.txt +5 -0
  1339. package/scripts/train/train-policy-head.py +477 -0
  1340. package/scripts/train/v2/__pycache__/dataset.cpython-314.pyc +0 -0
  1341. package/scripts/train/v2/__pycache__/eval.cpython-314.pyc +0 -0
  1342. package/scripts/train/v2/__pycache__/generate_data.cpython-314.pyc +0 -0
  1343. package/scripts/train/v2/__pycache__/infer.cpython-314.pyc +0 -0
  1344. package/scripts/train/v2/__pycache__/model.cpython-314.pyc +0 -0
  1345. package/scripts/train/v2/__pycache__/precompute.cpython-314.pyc +0 -0
  1346. package/scripts/train/v2/__pycache__/train.cpython-314.pyc +0 -0
  1347. package/scripts/train/v2/__pycache__/transform_buffer.cpython-314.pyc +0 -0
  1348. package/scripts/train/v2/__pycache__/validate_data.cpython-314.pyc +0 -0
  1349. package/scripts/train/v2/benchmark.py +661 -0
  1350. package/scripts/train/v2/dataset.py +81 -0
  1351. package/scripts/train/v2/domain.json +66 -0
  1352. package/scripts/train/v2/eval.py +196 -0
  1353. package/scripts/train/v2/generate_balanced.py +439 -0
  1354. package/scripts/train/v2/generate_data.py +219 -0
  1355. package/scripts/train/v2/generate_hard_negatives.py +219 -0
  1356. package/scripts/train/v2/infer.py +301 -0
  1357. package/scripts/train/v2/infer_server.py +224 -0
  1358. package/scripts/train/v2/model.py +112 -0
  1359. package/scripts/train/v2/online_train.py +576 -0
  1360. package/scripts/train/v2/precompute.py +150 -0
  1361. package/scripts/train/v2/train.py +302 -0
  1362. package/scripts/train/v2/transform_buffer.py +227 -0
  1363. package/scripts/train/v2/validate_data.py +115 -0
  1364. package/scripts/train-policy-head.py +434 -0
  1365. package/scripts/vm-swarm/README.md +301 -0
  1366. package/scripts/vm-swarm/collect-tuples.sh +331 -0
  1367. package/scripts/vm-swarm/create-base-template.sh +339 -0
  1368. package/scripts/vm-swarm/kill-fleet.sh +204 -0
  1369. package/scripts/vm-swarm/monitor-fleet.sh +346 -0
  1370. package/scripts/vm-swarm/spawn-fleet.sh +304 -0
  1371. package/scripts/voice-start.sh +156 -0
  1372. package/scripts/voice-stop.sh +33 -0
  1373. package/scripts/where-am-i.sh +78 -0
  1374. package/templates/QUICKSTART_SKILL_TO_PRODUCT.md +242 -0
  1375. package/templates/brand/BRAND_BRIEF.md +124 -0
  1376. package/templates/brand/BRAND_DECISIONS.md +168 -0
  1377. package/templates/brand/BRAND_GUIDELINES.md +251 -0
  1378. package/templates/brand/VOICE_AND_TONE.md +146 -0
  1379. package/templates/brand/global.css +240 -0
  1380. package/templates/collaboration/CONTRIBUTOR.md +74 -0
  1381. package/templates/collaboration/CRM.md +97 -0
  1382. package/templates/collaboration/TASKS.md +83 -0
  1383. package/templates/dating/FUNNEL.md +29 -0
  1384. package/templates/dating/REWARDS.md +24 -0
  1385. package/templates/dating/SIGNALS.md +18 -0
  1386. package/templates/dating/anti_patterns.md +23 -0
  1387. package/templates/dating/connectors/index.md +16 -0
  1388. package/templates/dating/modes.md +21 -0
  1389. package/templates/dating/psychology.md +18 -0
  1390. package/templates/default/README.md +19 -0
  1391. package/templates/service-agent/.claude/settings.json +32 -0
  1392. package/templates/service-agent/CLAUDE.md +334 -0
  1393. package/templates/service-agent/knowledge/ARCHITECTURE.md +115 -0
  1394. package/templates/service-agent/knowledge/DEPLOYMENT.md +199 -0
  1395. package/templates/service-agent/knowledge/RUNBOOK.md +412 -0
  1396. package/templates/service-agent/knowledge/SERVICE_SPEC.md +77 -0
  1397. package/templates/service-mcp-template.js +325 -0
  1398. package/templates/strategic/NARRATIVE.md +114 -0
  1399. package/templates/strategic/ROADMAP.md +128 -0
  1400. package/templates/strategic/THESIS.md +108 -0
  1401. package/templates/strategic/VISION.md +74 -0
@@ -0,0 +1,4765 @@
1
+ import chalk from "chalk";
2
+ import ora from "ora";
3
+ import inquirer from "inquirer";
4
+ import { existsSync, mkdirSync, statSync, createWriteStream, unlinkSync, renameSync, readFileSync, writeFileSync, openSync, closeSync } from "fs";
5
+ import { join } from "path";
6
+ import { platform } from "os";
7
+ import { createHash } from "crypto";
8
+ import https from "https";
9
+ import http from "http";
10
+ import { EventEmitter } from "events";
11
+ import WebSocket from "ws";
12
+ import { spawn, execSync } from "child_process";
13
+ // @ts-ignore - node-global-key-listener doesn't have type definitions
14
+ import { GlobalKeyboardListener } from "node-global-key-listener";
15
+ import { TENET_VOICE_DEBUG } from "../utils/tenet-env.js";
16
+ // VAD Configuration
17
+ const VAD_SILENCE_THRESHOLD_DB = -40; // dB threshold for silence detection
18
+ const VAD_SILENCE_DURATION_MS = 1500; // Stop after 1.5 seconds of silence
19
+ // =============================================================================
20
+ // Voice Error Handling System
21
+ // =============================================================================
22
+ /**
23
+ * Error types for voice-related failures
24
+ */
25
+ export var VoiceErrorType;
26
+ (function (VoiceErrorType) {
27
+ VoiceErrorType["SERVER_NOT_RUNNING"] = "SERVER_NOT_RUNNING";
28
+ VoiceErrorType["MIC_UNAVAILABLE"] = "MIC_UNAVAILABLE";
29
+ VoiceErrorType["CONNECTION_DROPPED"] = "CONNECTION_DROPPED";
30
+ VoiceErrorType["TRANSCRIPTION_EMPTY"] = "TRANSCRIPTION_EMPTY";
31
+ VoiceErrorType["PERMISSION_DENIED"] = "PERMISSION_DENIED";
32
+ VoiceErrorType["MODEL_NOT_FOUND"] = "MODEL_NOT_FOUND";
33
+ VoiceErrorType["AUTH_FAILED"] = "AUTH_FAILED";
34
+ VoiceErrorType["SOX_NOT_INSTALLED"] = "SOX_NOT_INSTALLED";
35
+ VoiceErrorType["RECORDING_FAILED"] = "RECORDING_FAILED";
36
+ VoiceErrorType["TIMEOUT"] = "TIMEOUT";
37
+ VoiceErrorType["ACCESSIBILITY_NOT_GRANTED"] = "ACCESSIBILITY_NOT_GRANTED";
38
+ VoiceErrorType["PLATFORM_NOT_SUPPORTED"] = "PLATFORM_NOT_SUPPORTED";
39
+ VoiceErrorType["WAYLAND_NOT_SUPPORTED"] = "WAYLAND_NOT_SUPPORTED";
40
+ })(VoiceErrorType || (VoiceErrorType = {}));
41
+ /**
42
+ * Error messages and recovery suggestions for each error type
43
+ */
44
+ const VOICE_ERROR_MESSAGES = {
45
+ [VoiceErrorType.SERVER_NOT_RUNNING]: {
46
+ message: "Voice server is not running.",
47
+ suggestions: [
48
+ "Start the server with: jfl voice server start",
49
+ "Check if another process is using the port",
50
+ ],
51
+ },
52
+ [VoiceErrorType.MIC_UNAVAILABLE]: {
53
+ message: "Microphone not available.",
54
+ suggestions: [
55
+ platform() === "darwin"
56
+ ? "Check Settings > Privacy & Security > Microphone"
57
+ : platform() === "win32"
58
+ ? "Check Settings > Privacy > Microphone"
59
+ : "Check your audio settings and permissions",
60
+ "Ensure your microphone is connected and enabled",
61
+ "Try selecting a different device with: jfl voice devices",
62
+ ],
63
+ },
64
+ [VoiceErrorType.CONNECTION_DROPPED]: {
65
+ message: "Connection to voice server lost.",
66
+ suggestions: [
67
+ "Check if the server is still running: jfl voice server status",
68
+ "Restart the server: jfl voice server start",
69
+ "Check your network connection",
70
+ ],
71
+ },
72
+ [VoiceErrorType.TRANSCRIPTION_EMPTY]: {
73
+ message: "No speech detected.",
74
+ suggestions: [
75
+ "Try speaking louder or more clearly",
76
+ "Move closer to your microphone",
77
+ "Check your microphone is working: jfl voice test",
78
+ "Ensure background noise is minimized",
79
+ ],
80
+ },
81
+ [VoiceErrorType.PERMISSION_DENIED]: {
82
+ message: "Microphone permission denied.",
83
+ suggestions: platform() === "darwin"
84
+ ? [
85
+ "Open System Settings > Privacy & Security > Microphone",
86
+ "Grant permission to Terminal (or your terminal app)",
87
+ "You may need to restart your terminal after granting permission",
88
+ ]
89
+ : platform() === "win32"
90
+ ? [
91
+ "Open Settings > Privacy > Microphone",
92
+ "Enable 'Allow apps to access your microphone'",
93
+ "Ensure your terminal app is allowed",
94
+ ]
95
+ : [
96
+ "Check your system's audio permissions",
97
+ "On some systems, run: sudo usermod -aG audio $USER",
98
+ "Then log out and log back in",
99
+ ],
100
+ },
101
+ [VoiceErrorType.MODEL_NOT_FOUND]: {
102
+ message: "Whisper model not found.",
103
+ suggestions: [
104
+ "Download a model with: jfl voice model download base",
105
+ "List available models: jfl voice model list",
106
+ "Run setup wizard: jfl voice setup",
107
+ ],
108
+ },
109
+ [VoiceErrorType.AUTH_FAILED]: {
110
+ message: "Authentication failed. Server token may have changed.",
111
+ suggestions: [
112
+ "Restart the voice server: jfl voice server start",
113
+ "If the issue persists, delete ~/.jfl/voice-server.token and restart",
114
+ ],
115
+ },
116
+ [VoiceErrorType.SOX_NOT_INSTALLED]: {
117
+ message: "Audio recording tool (sox) not found.",
118
+ suggestions: platform() === "darwin"
119
+ ? ["Install with: brew install sox"]
120
+ : platform() === "win32"
121
+ ? ["Install with: choco install sox.portable"]
122
+ : [
123
+ "Install with: sudo apt-get install sox libsox-fmt-all",
124
+ "Or for Fedora/RHEL: sudo dnf install sox",
125
+ ],
126
+ },
127
+ [VoiceErrorType.RECORDING_FAILED]: {
128
+ message: "Failed to start recording.",
129
+ suggestions: [
130
+ "Check microphone connection",
131
+ "Try a different audio device: jfl voice devices",
132
+ "Check if another application is using the microphone",
133
+ ],
134
+ },
135
+ [VoiceErrorType.TIMEOUT]: {
136
+ message: "Operation timed out.",
137
+ suggestions: [
138
+ "The server may be overloaded - try again",
139
+ "Check server status: jfl voice server status",
140
+ "For large audio files, the model may need more time",
141
+ ],
142
+ },
143
+ [VoiceErrorType.ACCESSIBILITY_NOT_GRANTED]: {
144
+ message: "Accessibility permission not granted.",
145
+ suggestions: [
146
+ "Open System Settings > Privacy & Security > Accessibility",
147
+ "Add your terminal app (Terminal, iTerm2, etc.) to the allowed list",
148
+ "Toggle the permission off and on if already added",
149
+ "You may need to restart your terminal after granting permission",
150
+ ],
151
+ },
152
+ [VoiceErrorType.PLATFORM_NOT_SUPPORTED]: {
153
+ message: "Global hotkey is not supported on this platform.",
154
+ suggestions: [
155
+ "Use 'jfl voice' for manual recording",
156
+ "Supported platforms: macOS, Linux (X11), Windows 10/11",
157
+ ],
158
+ },
159
+ [VoiceErrorType.WAYLAND_NOT_SUPPORTED]: {
160
+ message: "Global hotkey is not supported on Wayland.",
161
+ suggestions: [
162
+ "Use 'jfl voice' for manual recording instead",
163
+ "Switch to an X11 session for hotkey support",
164
+ "Or use an X11-based desktop environment (GNOME on X11, KDE on X11)",
165
+ ],
166
+ },
167
+ };
168
+ /**
169
+ * Custom error class for voice-related errors
170
+ */
171
+ export class VoiceError extends Error {
172
+ type;
173
+ originalError;
174
+ context;
175
+ recoverable;
176
+ audioBuffer; // Preserved audio for retry
177
+ constructor(type, options) {
178
+ const errorInfo = VOICE_ERROR_MESSAGES[type];
179
+ super(errorInfo.message);
180
+ this.name = "VoiceError";
181
+ this.type = type;
182
+ this.originalError = options?.originalError;
183
+ this.context = options?.context;
184
+ this.recoverable = options?.recoverable ?? false;
185
+ this.audioBuffer = options?.audioBuffer;
186
+ // Maintain proper stack trace
187
+ if (Error.captureStackTrace) {
188
+ Error.captureStackTrace(this, VoiceError);
189
+ }
190
+ }
191
+ /**
192
+ * Get user-friendly suggestions for recovering from this error
193
+ */
194
+ getSuggestions() {
195
+ return VOICE_ERROR_MESSAGES[this.type].suggestions;
196
+ }
197
+ /**
198
+ * Get full error message including original error if present
199
+ */
200
+ getFullMessage() {
201
+ let msg = this.message;
202
+ if (this.originalError) {
203
+ msg += ` (${this.originalError.message})`;
204
+ }
205
+ return msg;
206
+ }
207
+ }
208
+ /**
209
+ * Display a voice error with formatted output and suggestions
210
+ */
211
+ export function handleVoiceError(error, spinner) {
212
+ // Stop spinner if active
213
+ if (spinner) {
214
+ spinner.stop();
215
+ }
216
+ // Handle VoiceError with full formatting
217
+ if (error instanceof VoiceError) {
218
+ console.log();
219
+ console.log(chalk.red(` ✗ ${error.getFullMessage()}`));
220
+ console.log();
221
+ const suggestions = error.getSuggestions();
222
+ if (suggestions.length > 0) {
223
+ console.log(chalk.yellow(" Suggestions:"));
224
+ for (const suggestion of suggestions) {
225
+ console.log(chalk.gray(` - ${suggestion}`));
226
+ }
227
+ console.log();
228
+ }
229
+ // Show additional context if in debug mode
230
+ if (process.env.DEBUG && error.context) {
231
+ console.log(chalk.gray(" Debug context:"));
232
+ console.log(chalk.gray(` ${JSON.stringify(error.context, null, 2)}`));
233
+ console.log();
234
+ }
235
+ // Indicate if the error is recoverable
236
+ if (error.recoverable && error.audioBuffer) {
237
+ console.log(chalk.cyan(" Audio has been preserved. You can retry the transcription."));
238
+ console.log();
239
+ }
240
+ }
241
+ else {
242
+ // Handle generic errors
243
+ console.log();
244
+ console.log(chalk.red(` ✗ Error: ${error.message}`));
245
+ console.log();
246
+ // Try to infer error type from message and provide suggestions
247
+ const inferredSuggestions = inferErrorSuggestions(error);
248
+ if (inferredSuggestions.length > 0) {
249
+ console.log(chalk.yellow(" Suggestions:"));
250
+ for (const suggestion of inferredSuggestions) {
251
+ console.log(chalk.gray(` - ${suggestion}`));
252
+ }
253
+ console.log();
254
+ }
255
+ }
256
+ }
257
+ /**
258
+ * Try to infer helpful suggestions from a generic error
259
+ */
260
+ function inferErrorSuggestions(error) {
261
+ const message = error.message.toLowerCase();
262
+ if (message.includes("enoent") || message.includes("not found")) {
263
+ if (message.includes("sock") || message.includes("socket")) {
264
+ return VOICE_ERROR_MESSAGES[VoiceErrorType.SERVER_NOT_RUNNING].suggestions;
265
+ }
266
+ if (message.includes("model") || message.includes("ggml")) {
267
+ return VOICE_ERROR_MESSAGES[VoiceErrorType.MODEL_NOT_FOUND].suggestions;
268
+ }
269
+ if (message.includes("sox") || message.includes("rec")) {
270
+ return VOICE_ERROR_MESSAGES[VoiceErrorType.SOX_NOT_INSTALLED].suggestions;
271
+ }
272
+ }
273
+ if (message.includes("permission") || message.includes("denied") || message.includes("access")) {
274
+ return VOICE_ERROR_MESSAGES[VoiceErrorType.PERMISSION_DENIED].suggestions;
275
+ }
276
+ if (message.includes("auth") || message.includes("token") || message.includes("401")) {
277
+ return VOICE_ERROR_MESSAGES[VoiceErrorType.AUTH_FAILED].suggestions;
278
+ }
279
+ if (message.includes("timeout") || message.includes("timed out")) {
280
+ return VOICE_ERROR_MESSAGES[VoiceErrorType.TIMEOUT].suggestions;
281
+ }
282
+ if (message.includes("connection") || message.includes("connect") || message.includes("econnrefused")) {
283
+ return VOICE_ERROR_MESSAGES[VoiceErrorType.CONNECTION_DROPPED].suggestions;
284
+ }
285
+ if (message.includes("microphone") || message.includes("mic") || message.includes("audio")) {
286
+ return VOICE_ERROR_MESSAGES[VoiceErrorType.MIC_UNAVAILABLE].suggestions;
287
+ }
288
+ return [];
289
+ }
290
+ /**
291
+ * Create appropriate VoiceError based on error analysis
292
+ */
293
+ export function createVoiceError(error, context, audioBuffer) {
294
+ const message = error.message.toLowerCase();
295
+ // Determine error type from message patterns
296
+ let type;
297
+ if (message.includes("socket not found") || message.includes("voice server socket")) {
298
+ type = VoiceErrorType.SERVER_NOT_RUNNING;
299
+ }
300
+ else if (message.includes("auth") || message.includes("token") || message.includes("unauthorized")) {
301
+ type = VoiceErrorType.AUTH_FAILED;
302
+ }
303
+ else if (message.includes("permission") || message.includes("denied")) {
304
+ type = VoiceErrorType.PERMISSION_DENIED;
305
+ }
306
+ else if (message.includes("model") && (message.includes("not found") || message.includes("missing"))) {
307
+ type = VoiceErrorType.MODEL_NOT_FOUND;
308
+ }
309
+ else if (message.includes("no speech") || message.includes("empty transcript")) {
310
+ type = VoiceErrorType.TRANSCRIPTION_EMPTY;
311
+ }
312
+ else if (message.includes("connection") || message.includes("disconnect")) {
313
+ type = VoiceErrorType.CONNECTION_DROPPED;
314
+ }
315
+ else if (message.includes("sox") || message.includes("no audio recording tool")) {
316
+ type = VoiceErrorType.SOX_NOT_INSTALLED;
317
+ }
318
+ else if (message.includes("microphone") || message.includes("audio device")) {
319
+ type = VoiceErrorType.MIC_UNAVAILABLE;
320
+ }
321
+ else if (message.includes("timeout")) {
322
+ type = VoiceErrorType.TIMEOUT;
323
+ }
324
+ else if (message.includes("record")) {
325
+ type = VoiceErrorType.RECORDING_FAILED;
326
+ }
327
+ else {
328
+ // Default to recording failed for unknown errors
329
+ type = VoiceErrorType.RECORDING_FAILED;
330
+ }
331
+ // Determine if recoverable (has audio buffer for retry)
332
+ const recoverable = audioBuffer !== undefined && audioBuffer.length > 0;
333
+ return new VoiceError(type, {
334
+ originalError: error,
335
+ context,
336
+ recoverable,
337
+ audioBuffer,
338
+ });
339
+ }
340
+ /**
341
+ * Check if server is running and return appropriate error if not
342
+ * Checks both TCP port 9090 and Unix socket for backwards compatibility
343
+ */
344
+ export function checkServerRunning() {
345
+ // Check if server PID file exists and process is running
346
+ const pidPath = join(JFL_PATHS.data, "voice-server.pid");
347
+ if (existsSync(pidPath)) {
348
+ try {
349
+ const pid = parseInt(readFileSync(pidPath, "utf-8").trim(), 10);
350
+ // Check if process is running by sending signal 0
351
+ process.kill(pid, 0);
352
+ return null; // Server is running
353
+ }
354
+ catch {
355
+ // PID file exists but process not running
356
+ }
357
+ }
358
+ // Fallback: check Unix socket for backwards compatibility
359
+ const socketPath = getVoiceSocketPath();
360
+ if (existsSync(socketPath)) {
361
+ return null;
362
+ }
363
+ return new VoiceError(VoiceErrorType.SERVER_NOT_RUNNING);
364
+ }
365
+ /**
366
+ * Check if auth token exists and return appropriate error if not
367
+ */
368
+ export function checkAuthToken() {
369
+ const token = readAuthToken();
370
+ if (!token) {
371
+ return new VoiceError(VoiceErrorType.AUTH_FAILED, {
372
+ context: { reason: "Token file not found or empty" },
373
+ });
374
+ }
375
+ return null;
376
+ }
377
+ /**
378
+ * Check if a model is available and return appropriate error if not
379
+ */
380
+ export function checkModelAvailable(modelName) {
381
+ const model = modelName || getCurrentDefaultModel();
382
+ if (!isModelDownloaded(model)) {
383
+ return new VoiceError(VoiceErrorType.MODEL_NOT_FOUND, {
384
+ context: { modelName: model },
385
+ });
386
+ }
387
+ return null;
388
+ }
389
+ // Whisper model definitions
390
+ // Source: https://huggingface.co/ggerganov/whisper.cpp
391
+ const WHISPER_MODELS = {
392
+ tiny: {
393
+ name: "tiny",
394
+ displayName: "Tiny",
395
+ size: "75 MB",
396
+ sizeBytes: 75_000_000,
397
+ description: "Fastest, lowest accuracy. Good for quick testing.",
398
+ url: "https://huggingface.co/ggerganov/whisper.cpp/resolve/main/ggml-tiny.bin",
399
+ sha256: "be07e048e1e599ad46341c8d2a135645097a538221678b7acdd1b1919c6e1b21",
400
+ },
401
+ "tiny.en": {
402
+ name: "tiny.en",
403
+ displayName: "Tiny (English)",
404
+ size: "75 MB",
405
+ sizeBytes: 75_000_000,
406
+ description: "English-only tiny model. Slightly better for English.",
407
+ url: "https://huggingface.co/ggerganov/whisper.cpp/resolve/main/ggml-tiny.en.bin",
408
+ sha256: "921e4cf8b0c2c68d26b626b8b0adfe5f188ccd0e42f74ea3a3c4a02313978c93",
409
+ },
410
+ base: {
411
+ name: "base",
412
+ displayName: "Base",
413
+ size: "142 MB",
414
+ sizeBytes: 142_000_000,
415
+ description: "Balanced speed/accuracy. Recommended default.",
416
+ url: "https://huggingface.co/ggerganov/whisper.cpp/resolve/main/ggml-base.bin",
417
+ sha256: "60ed5bc3dd14eea856493d334349b405782ddcaf0028d4b5df4088345fba2efe",
418
+ },
419
+ "base.en": {
420
+ name: "base.en",
421
+ displayName: "Base (English)",
422
+ size: "142 MB",
423
+ sizeBytes: 142_000_000,
424
+ description: "English-only base model.",
425
+ url: "https://huggingface.co/ggerganov/whisper.cpp/resolve/main/ggml-base.en.bin",
426
+ sha256: "a03779c86df3323075f5e796b3f6af1e6faa6a45b5eb1ef6c3fba57b4ccd0f66",
427
+ },
428
+ small: {
429
+ name: "small",
430
+ displayName: "Small",
431
+ size: "466 MB",
432
+ sizeBytes: 466_000_000,
433
+ description: "Higher accuracy, slower. Good for important transcriptions.",
434
+ url: "https://huggingface.co/ggerganov/whisper.cpp/resolve/main/ggml-small.bin",
435
+ sha256: "1be3a9b2063867b937e64e2ec7483364a79917e157fa98c5d94b5c1c8c6f0da8",
436
+ },
437
+ "small.en": {
438
+ name: "small.en",
439
+ displayName: "Small (English)",
440
+ size: "466 MB",
441
+ sizeBytes: 466_000_000,
442
+ description: "English-only small model.",
443
+ url: "https://huggingface.co/ggerganov/whisper.cpp/resolve/main/ggml-small.en.bin",
444
+ sha256: "db8a495a91d927739e50b3fc1830cbe8b5d3ce7c499c1ab2c1a4d508d4f5bede",
445
+ },
446
+ };
447
+ const DEFAULT_MODEL = "base";
448
+ // Voice-specific data paths (use XDG data directory)
449
+ import { JFL_PATHS } from "../utils/jfl-paths.js";
450
+ // Get models directory
451
+ function getModelsDir() {
452
+ return join(JFL_PATHS.data, "models");
453
+ }
454
+ // Get voice config path
455
+ function getVoiceConfigPath() {
456
+ return join(JFL_PATHS.config, "voice.yaml");
457
+ }
458
+ // Get voice socket path
459
+ function getVoiceSocketPath() {
460
+ return join(JFL_PATHS.data, "voice.sock");
461
+ }
462
+ // Get voice server token path
463
+ function getVoiceTokenPath() {
464
+ return join(JFL_PATHS.data, "voice-server.token");
465
+ }
466
+ // Get voice daemon PID file path
467
+ function getVoiceDaemonPidPath() {
468
+ return join(JFL_PATHS.data, "voice-daemon.pid");
469
+ }
470
+ // Get voice daemon log file path
471
+ function getVoiceDaemonLogPath() {
472
+ return join(JFL_PATHS.data, "voice-daemon.log");
473
+ }
474
+ // Read auth token from file
475
+ export function readAuthToken() {
476
+ const tokenPath = getVoiceTokenPath();
477
+ if (!existsSync(tokenPath)) {
478
+ return null;
479
+ }
480
+ try {
481
+ return readFileSync(tokenPath, "utf-8").trim();
482
+ }
483
+ catch {
484
+ return null;
485
+ }
486
+ }
487
+ // Ensure directories exist
488
+ function ensureDirectories() {
489
+ const modelsDir = getModelsDir();
490
+ // Ensure data and config directories exist
491
+ if (!existsSync(JFL_PATHS.data)) {
492
+ mkdirSync(JFL_PATHS.data, { recursive: true, mode: 0o700 });
493
+ }
494
+ if (!existsSync(JFL_PATHS.config)) {
495
+ mkdirSync(JFL_PATHS.config, { recursive: true, mode: 0o700 });
496
+ }
497
+ if (!existsSync(modelsDir)) {
498
+ mkdirSync(modelsDir, { mode: 0o755 });
499
+ }
500
+ }
501
+ // Get path for a model file
502
+ function getModelPath(modelName) {
503
+ return join(getModelsDir(), `ggml-${modelName}.bin`);
504
+ }
505
+ // Get path for partial download file
506
+ function getPartialPath(modelName) {
507
+ return join(getModelsDir(), `ggml-${modelName}.bin.partial`);
508
+ }
509
+ // Check if model is downloaded
510
+ function isModelDownloaded(modelName) {
511
+ const modelPath = getModelPath(modelName);
512
+ if (!existsSync(modelPath))
513
+ return false;
514
+ const model = WHISPER_MODELS[modelName];
515
+ if (!model)
516
+ return false;
517
+ // Check file size is approximately correct (within 1%)
518
+ const stats = statSync(modelPath);
519
+ const expectedSize = model.sizeBytes;
520
+ const tolerance = expectedSize * 0.01;
521
+ return Math.abs(stats.size - expectedSize) < tolerance;
522
+ }
523
+ // Get current default model from config
524
+ function getCurrentDefaultModel() {
525
+ const configPath = getVoiceConfigPath();
526
+ if (!existsSync(configPath))
527
+ return DEFAULT_MODEL;
528
+ try {
529
+ const content = readFileSync(configPath, "utf-8");
530
+ const match = content.match(/^model:\s*(\S+)/m);
531
+ return match ? match[1] : DEFAULT_MODEL;
532
+ }
533
+ catch {
534
+ return DEFAULT_MODEL;
535
+ }
536
+ }
537
+ // Set default model in config
538
+ function setDefaultModel(modelName) {
539
+ const configPath = getVoiceConfigPath();
540
+ ensureDirectories();
541
+ let content = "";
542
+ if (existsSync(configPath)) {
543
+ content = readFileSync(configPath, "utf-8");
544
+ if (content.match(/^model:/m)) {
545
+ content = content.replace(/^model:\s*\S+/m, `model: ${modelName}`);
546
+ }
547
+ else {
548
+ content = `model: ${modelName}\n${content}`;
549
+ }
550
+ }
551
+ else {
552
+ content = `# JFL Voice Configuration\nmodel: ${modelName}\n`;
553
+ }
554
+ writeFileSync(configPath, content, { mode: 0o644 });
555
+ }
556
+ // Format bytes to human readable
557
+ function formatBytes(bytes) {
558
+ if (bytes < 1024)
559
+ return `${bytes} B`;
560
+ if (bytes < 1024 * 1024)
561
+ return `${(bytes / 1024).toFixed(1)} KB`;
562
+ if (bytes < 1024 * 1024 * 1024)
563
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
564
+ return `${(bytes / (1024 * 1024 * 1024)).toFixed(1)} GB`;
565
+ }
566
+ // Download file with progress and resume support
567
+ async function downloadFile(url, destPath, partialPath, expectedSize, onProgress) {
568
+ return new Promise((resolve, reject) => {
569
+ // Check for existing partial download
570
+ let startByte = 0;
571
+ if (existsSync(partialPath)) {
572
+ const stats = statSync(partialPath);
573
+ startByte = stats.size;
574
+ }
575
+ const headers = {};
576
+ if (startByte > 0) {
577
+ headers["Range"] = `bytes=${startByte}-`;
578
+ }
579
+ const protocol = url.startsWith("https") ? https : http;
580
+ const request = protocol.get(url, { headers }, (response) => {
581
+ // Handle redirects
582
+ if (response.statusCode === 301 || response.statusCode === 302 || response.statusCode === 307) {
583
+ const redirectUrl = response.headers.location;
584
+ if (redirectUrl) {
585
+ downloadFile(redirectUrl, destPath, partialPath, expectedSize, onProgress)
586
+ .then(resolve)
587
+ .catch(reject);
588
+ return;
589
+ }
590
+ }
591
+ // Handle 416 Range Not Satisfiable (file already complete)
592
+ if (response.statusCode === 416) {
593
+ if (existsSync(partialPath)) {
594
+ renameSync(partialPath, destPath);
595
+ }
596
+ resolve();
597
+ return;
598
+ }
599
+ // Check for successful response
600
+ if (response.statusCode !== 200 && response.statusCode !== 206) {
601
+ reject(new Error(`Download failed with status ${response.statusCode}`));
602
+ return;
603
+ }
604
+ const totalSize = response.statusCode === 206
605
+ ? expectedSize
606
+ : parseInt(response.headers["content-length"] || "0", 10);
607
+ const fileStream = createWriteStream(partialPath, {
608
+ flags: startByte > 0 ? "a" : "w",
609
+ mode: 0o644,
610
+ });
611
+ let downloaded = startByte;
612
+ response.on("data", (chunk) => {
613
+ downloaded += chunk.length;
614
+ onProgress(downloaded, totalSize || expectedSize);
615
+ });
616
+ response.pipe(fileStream);
617
+ fileStream.on("finish", () => {
618
+ fileStream.close();
619
+ // Move partial to final destination
620
+ renameSync(partialPath, destPath);
621
+ resolve();
622
+ });
623
+ fileStream.on("error", (err) => {
624
+ fileStream.close();
625
+ reject(err);
626
+ });
627
+ response.on("error", (err) => {
628
+ fileStream.close();
629
+ reject(err);
630
+ });
631
+ });
632
+ request.on("error", (err) => {
633
+ reject(err);
634
+ });
635
+ });
636
+ }
637
+ // Verify downloaded file SHA256
638
+ function verifyChecksum(filePath, expectedHash) {
639
+ const fileBuffer = readFileSync(filePath);
640
+ const hash = createHash("sha256").update(fileBuffer).digest("hex");
641
+ return hash === expectedHash;
642
+ }
643
+ /**
644
+ * VoiceClient - Manages WebSocket connection to the whisper server
645
+ *
646
+ * Features:
647
+ * - Connects to Unix socket at ~/.jfl/voice.sock
648
+ * - Authenticates with token from ~/.jfl/voice-server.token
649
+ * - Streams 16-bit PCM audio at 16kHz mono
650
+ * - Receives partial and final transcripts
651
+ * - Auto-reconnects with exponential backoff
652
+ *
653
+ * Usage:
654
+ * ```typescript
655
+ * const client = new VoiceClient()
656
+ *
657
+ * client.onTranscript((text, isFinal) => {
658
+ * if (isFinal) console.log("Final:", text)
659
+ * else console.log("Partial:", text)
660
+ * })
661
+ *
662
+ * client.onError((error) => {
663
+ * console.error("Error:", error.message)
664
+ * })
665
+ *
666
+ * await client.connect()
667
+ *
668
+ * // Send audio chunks (16-bit PCM, 16kHz mono)
669
+ * client.sendAudio(audioBuffer)
670
+ *
671
+ * // When done
672
+ * client.disconnect()
673
+ * ```
674
+ */
675
+ export class VoiceClient extends EventEmitter {
676
+ ws = null;
677
+ serverUrl;
678
+ authToken;
679
+ state = "disconnected";
680
+ reconnectAttempts = 0;
681
+ reconnectTimer = null;
682
+ shouldReconnect = false;
683
+ // Configuration
684
+ maxReconnectAttempts;
685
+ initialReconnectDelay;
686
+ maxReconnectDelay;
687
+ // Callback holders
688
+ transcriptCallbacks = [];
689
+ errorCallbacks = [];
690
+ connectionCallbacks = [];
691
+ constructor(options = {}) {
692
+ super();
693
+ // Prefer direct TCP connection over Unix socket for better binary data handling
694
+ this.serverUrl = options.serverUrl ?? "ws://127.0.0.1:9090";
695
+ this.authToken = options.authToken ?? null;
696
+ this.maxReconnectAttempts = options.maxReconnectAttempts ?? 5;
697
+ this.initialReconnectDelay = options.initialReconnectDelay ?? 1000;
698
+ this.maxReconnectDelay = options.maxReconnectDelay ?? 30000;
699
+ }
700
+ /**
701
+ * Get the current connection state
702
+ */
703
+ getState() {
704
+ return this.state;
705
+ }
706
+ /**
707
+ * Check if connected
708
+ */
709
+ isConnected() {
710
+ return this.state === "connected" && this.ws?.readyState === WebSocket.OPEN;
711
+ }
712
+ /**
713
+ * Connect to the whisper server
714
+ *
715
+ * @throws VoiceError if auth token is missing (AUTH_FAILED)
716
+ * @throws VoiceError if connection fails
717
+ */
718
+ async connect() {
719
+ if (this.state === "connected" || this.state === "connecting") {
720
+ return;
721
+ }
722
+ // Get auth token if not provided
723
+ if (!this.authToken) {
724
+ this.authToken = readAuthToken();
725
+ }
726
+ if (!this.authToken) {
727
+ throw new VoiceError(VoiceErrorType.AUTH_FAILED, {
728
+ context: { tokenPath: getVoiceTokenPath(), reason: "Token not found" },
729
+ });
730
+ }
731
+ this.setState("connecting");
732
+ this.shouldReconnect = true;
733
+ this.reconnectAttempts = 0;
734
+ return this.doConnect();
735
+ }
736
+ /**
737
+ * Internal connection method
738
+ */
739
+ doConnect() {
740
+ return new Promise((resolve, reject) => {
741
+ try {
742
+ // Create WebSocket connection via TCP
743
+ // Server expects token as query parameter for WebSocket connections
744
+ const url = `${this.serverUrl}/?token=${this.authToken}`;
745
+ this.ws = new WebSocket(url);
746
+ const connectionTimeout = setTimeout(() => {
747
+ if (this.state === "connecting") {
748
+ this.ws?.terminate();
749
+ const error = new Error("Connection timeout");
750
+ this.handleError(error);
751
+ reject(error);
752
+ }
753
+ }, 10000);
754
+ this.ws.on("open", () => {
755
+ clearTimeout(connectionTimeout);
756
+ this.setState("connected");
757
+ this.reconnectAttempts = 0;
758
+ this.emit("connected");
759
+ resolve();
760
+ });
761
+ this.ws.on("message", (data) => {
762
+ this.handleMessage(data);
763
+ });
764
+ this.ws.on("error", (error) => {
765
+ clearTimeout(connectionTimeout);
766
+ this.handleError(error);
767
+ if (this.state === "connecting") {
768
+ reject(error);
769
+ }
770
+ });
771
+ this.ws.on("close", (code, reason) => {
772
+ clearTimeout(connectionTimeout);
773
+ this.handleClose(code, reason.toString());
774
+ });
775
+ }
776
+ catch (error) {
777
+ this.setState("disconnected");
778
+ const err = error instanceof Error ? error : new Error(String(error));
779
+ this.handleError(err);
780
+ reject(err);
781
+ }
782
+ });
783
+ }
784
+ /**
785
+ * Disconnect from the whisper server
786
+ */
787
+ disconnect() {
788
+ this.shouldReconnect = false;
789
+ this.clearReconnectTimer();
790
+ if (this.ws) {
791
+ // Remove listeners to prevent reconnection attempts
792
+ this.ws.removeAllListeners();
793
+ if (this.ws.readyState === WebSocket.OPEN ||
794
+ this.ws.readyState === WebSocket.CONNECTING) {
795
+ this.ws.close(1000, "Client disconnect");
796
+ }
797
+ this.ws = null;
798
+ }
799
+ this.setState("disconnected");
800
+ this.emit("disconnected");
801
+ }
802
+ /**
803
+ * Send audio data to the whisper server
804
+ *
805
+ * @param buffer - 16-bit PCM audio data at 16kHz mono
806
+ * @throws Error if not connected
807
+ */
808
+ sendAudio(buffer) {
809
+ if (!this.isConnected()) {
810
+ throw new Error("Not connected to voice server");
811
+ }
812
+ if (!this.ws) {
813
+ throw new Error("WebSocket is null");
814
+ }
815
+ // WORKAROUND: Node.js 25 + ws library bug
816
+ // Buffer.concat() creates buffers that don't send properly via WebSocket
817
+ // Writing to a temp file and reading back creates a buffer that works
818
+ // This is a known issue with certain Buffer pooling/sharing behaviors
819
+ const tempPath = `/tmp/.voice-send-${Date.now()}.tmp`;
820
+ try {
821
+ if (TENET_VOICE_DEBUG) {
822
+ console.log(`[VoiceClient] Writing ${buffer.length} bytes to temp file: ${tempPath}`);
823
+ }
824
+ writeFileSync(tempPath, buffer);
825
+ const sendBuffer = readFileSync(tempPath);
826
+ unlinkSync(tempPath);
827
+ if (TENET_VOICE_DEBUG) {
828
+ console.log(`[VoiceClient] sendAudio: ${sendBuffer.length} bytes (via temp file)`);
829
+ console.log(`[VoiceClient] Buffer.isBuffer: ${Buffer.isBuffer(sendBuffer)}`);
830
+ console.log(`[VoiceClient] ws.readyState: ${this.ws.readyState} (OPEN=1)`);
831
+ console.log(`[VoiceClient] ws.bufferedAmount before: ${this.ws.bufferedAmount}`);
832
+ }
833
+ // Send binary audio data - Buffer is automatically sent as binary
834
+ // Use callback to catch send errors
835
+ this.ws.send(sendBuffer, (err) => {
836
+ if (err) {
837
+ console.error(`[VoiceClient] send error:`, err);
838
+ }
839
+ else if (TENET_VOICE_DEBUG) {
840
+ console.log(`[VoiceClient] send callback: success`);
841
+ console.log(`[VoiceClient] ws.bufferedAmount after: ${this.ws?.bufferedAmount}`);
842
+ }
843
+ });
844
+ }
845
+ catch (e) {
846
+ console.error(`[VoiceClient] Error in temp file workaround:`, e);
847
+ // Clean up temp file on error
848
+ try {
849
+ unlinkSync(tempPath);
850
+ }
851
+ catch { }
852
+ throw e;
853
+ }
854
+ if (TENET_VOICE_DEBUG) {
855
+ console.log(`[VoiceClient] ws.bufferedAmount after: ${this.ws.bufferedAmount}`);
856
+ }
857
+ }
858
+ /**
859
+ * Send end-of-audio signal to get final transcript
860
+ */
861
+ endAudio() {
862
+ if (!this.isConnected() || !this.ws) {
863
+ return;
864
+ }
865
+ // Send a JSON message indicating end of audio stream
866
+ this.ws.send(JSON.stringify({ type: "end_audio" }));
867
+ }
868
+ /**
869
+ * Register callback for transcript events
870
+ *
871
+ * @param callback - Called with transcript text and whether it's final
872
+ */
873
+ onTranscript(callback) {
874
+ this.transcriptCallbacks.push(callback);
875
+ }
876
+ /**
877
+ * Register callback for error events
878
+ *
879
+ * @param callback - Called with error
880
+ */
881
+ onError(callback) {
882
+ this.errorCallbacks.push(callback);
883
+ }
884
+ /**
885
+ * Register callback for connection state changes
886
+ *
887
+ * @param callback - Called with new connection state
888
+ */
889
+ onConnectionStateChange(callback) {
890
+ this.connectionCallbacks.push(callback);
891
+ }
892
+ /**
893
+ * Remove all callbacks
894
+ */
895
+ removeAllCallbacks() {
896
+ this.transcriptCallbacks = [];
897
+ this.errorCallbacks = [];
898
+ this.connectionCallbacks = [];
899
+ }
900
+ // ---------------------------------------------------------------------------
901
+ // Private methods
902
+ // ---------------------------------------------------------------------------
903
+ setState(state) {
904
+ if (this.state !== state) {
905
+ this.state = state;
906
+ for (const callback of this.connectionCallbacks) {
907
+ try {
908
+ callback(state);
909
+ }
910
+ catch (e) {
911
+ // Ignore callback errors
912
+ }
913
+ }
914
+ }
915
+ }
916
+ handleMessage(data) {
917
+ try {
918
+ // Parse JSON message
919
+ const message = JSON.parse(data.toString());
920
+ // Debug logging
921
+ if (TENET_VOICE_DEBUG) {
922
+ console.log(`[VoiceClient] Received: ${message.type}`, JSON.stringify(message));
923
+ }
924
+ if (message.type === "error") {
925
+ const errorMsg = message;
926
+ const error = new Error(errorMsg.error);
927
+ this.handleError(error);
928
+ return;
929
+ }
930
+ if (message.type === "partial" || message.type === "final") {
931
+ const transcript = message;
932
+ const isFinal = transcript.type === "final";
933
+ for (const callback of this.transcriptCallbacks) {
934
+ try {
935
+ callback(transcript.text, isFinal);
936
+ }
937
+ catch (e) {
938
+ // Ignore callback errors
939
+ }
940
+ }
941
+ this.emit("transcript", transcript.text, isFinal);
942
+ }
943
+ }
944
+ catch (e) {
945
+ // Failed to parse message - could be binary data or malformed JSON
946
+ if (TENET_VOICE_DEBUG) {
947
+ console.error("Failed to parse server message:", e);
948
+ }
949
+ }
950
+ }
951
+ handleError(error) {
952
+ // Notify error callbacks
953
+ for (const callback of this.errorCallbacks) {
954
+ try {
955
+ callback(error);
956
+ }
957
+ catch (e) {
958
+ // Ignore callback errors
959
+ }
960
+ }
961
+ this.emit("error", error);
962
+ }
963
+ handleClose(code, reason) {
964
+ const wasConnected = this.state === "connected";
965
+ this.ws = null;
966
+ if (process.env.DEBUG) {
967
+ console.log(`WebSocket closed: code=${code}, reason=${reason}`);
968
+ }
969
+ // Check if we should attempt to reconnect
970
+ if (this.shouldReconnect && wasConnected) {
971
+ this.scheduleReconnect();
972
+ }
973
+ else {
974
+ this.setState("disconnected");
975
+ }
976
+ this.emit("close", code, reason);
977
+ }
978
+ scheduleReconnect() {
979
+ if (this.reconnectAttempts >= this.maxReconnectAttempts) {
980
+ const error = new Error(`Failed to reconnect after ${this.maxReconnectAttempts} attempts`);
981
+ this.handleError(error);
982
+ this.setState("disconnected");
983
+ this.emit("reconnect_failed");
984
+ return;
985
+ }
986
+ this.setState("reconnecting");
987
+ this.reconnectAttempts++;
988
+ // Calculate delay with exponential backoff
989
+ const delay = Math.min(this.initialReconnectDelay * Math.pow(2, this.reconnectAttempts - 1), this.maxReconnectDelay);
990
+ if (process.env.DEBUG) {
991
+ console.log(`Reconnecting in ${delay}ms (attempt ${this.reconnectAttempts}/${this.maxReconnectAttempts})`);
992
+ }
993
+ this.emit("reconnecting", this.reconnectAttempts, delay);
994
+ this.reconnectTimer = setTimeout(async () => {
995
+ if (!this.shouldReconnect) {
996
+ return;
997
+ }
998
+ try {
999
+ await this.doConnect();
1000
+ this.emit("reconnected");
1001
+ }
1002
+ catch (error) {
1003
+ // doConnect will schedule another reconnect attempt on failure
1004
+ // through handleClose callback
1005
+ }
1006
+ }, delay);
1007
+ }
1008
+ clearReconnectTimer() {
1009
+ if (this.reconnectTimer) {
1010
+ clearTimeout(this.reconnectTimer);
1011
+ this.reconnectTimer = null;
1012
+ }
1013
+ }
1014
+ }
1015
+ /**
1016
+ * AudioRecorder - Cross-platform audio recording with sample rate conversion
1017
+ *
1018
+ * Records audio from the system microphone and outputs 16-bit PCM at 16kHz mono.
1019
+ * Works on macOS, Linux, and Windows using sox/rec/arecord backends.
1020
+ *
1021
+ * Prerequisites:
1022
+ * - macOS: `brew install sox`
1023
+ * - Linux: `sudo apt-get install sox libsox-fmt-all` or `alsa-utils`
1024
+ * - Windows: `choco install sox.portable`
1025
+ *
1026
+ * Usage:
1027
+ * ```typescript
1028
+ * const recorder = new AudioRecorder({ sampleRate: 16000 })
1029
+ *
1030
+ * recorder.on('data', (chunk: Buffer) => {
1031
+ * // Process 16-bit PCM audio data
1032
+ * voiceClient.sendAudio(chunk)
1033
+ * })
1034
+ *
1035
+ * recorder.on('error', (error: Error) => {
1036
+ * console.error('Recording error:', error)
1037
+ * })
1038
+ *
1039
+ * await recorder.start()
1040
+ *
1041
+ * // Later...
1042
+ * recorder.stop()
1043
+ * ```
1044
+ */
1045
+ export class AudioRecorder extends EventEmitter {
1046
+ state = "idle";
1047
+ process = null;
1048
+ device;
1049
+ targetSampleRate;
1050
+ recorderBackend;
1051
+ currentPlatform;
1052
+ disconnectCheckInterval = null;
1053
+ constructor(options = {}) {
1054
+ super();
1055
+ this.device = options.device;
1056
+ this.targetSampleRate = options.sampleRate ?? 16000;
1057
+ this.currentPlatform = platform();
1058
+ this.recorderBackend = this.selectRecorder(options.recorder ?? "auto");
1059
+ }
1060
+ /**
1061
+ * List available audio input devices
1062
+ *
1063
+ * @returns Promise resolving to array of available audio devices
1064
+ */
1065
+ static async listDevices() {
1066
+ const currentPlatform = platform();
1067
+ try {
1068
+ if (currentPlatform === "darwin") {
1069
+ return await AudioRecorder.listDevicesMacOS();
1070
+ }
1071
+ else if (currentPlatform === "linux") {
1072
+ return await AudioRecorder.listDevicesLinux();
1073
+ }
1074
+ else if (currentPlatform === "win32") {
1075
+ return await AudioRecorder.listDevicesWindows();
1076
+ }
1077
+ else {
1078
+ throw new Error(`Unsupported platform: ${currentPlatform}`);
1079
+ }
1080
+ }
1081
+ catch (error) {
1082
+ // If listing fails, return empty array with a warning
1083
+ console.warn("Failed to list audio devices:", error);
1084
+ return [];
1085
+ }
1086
+ }
1087
+ /**
1088
+ * List audio devices on macOS using sox
1089
+ */
1090
+ static async listDevicesMacOS() {
1091
+ const devices = [];
1092
+ try {
1093
+ // On macOS, sox -d uses coreaudio which lists devices differently
1094
+ // Try using system_profiler for detailed device list
1095
+ const output = execSync("system_profiler SPAudioDataType -json 2>/dev/null", { encoding: "utf-8", timeout: 5000 });
1096
+ const data = JSON.parse(output);
1097
+ const audioData = data.SPAudioDataType;
1098
+ if (audioData && Array.isArray(audioData)) {
1099
+ for (const device of audioData) {
1100
+ // Each audio device group may have input devices
1101
+ const name = device._name || "Unknown Device";
1102
+ const items = device._items || [];
1103
+ for (const item of items) {
1104
+ if (item.coreaudio_input_source) {
1105
+ devices.push({
1106
+ id: String(item.coreaudio_device_input || name),
1107
+ name: String(item._name || name),
1108
+ isDefault: item.coreaudio_default_audio_input_device === "yes",
1109
+ });
1110
+ }
1111
+ }
1112
+ // Also check the main device entry
1113
+ if (device.coreaudio_input_source) {
1114
+ devices.push({
1115
+ id: String(device.coreaudio_device_input || name),
1116
+ name: String(name),
1117
+ isDefault: device.coreaudio_default_audio_input_device === "yes",
1118
+ });
1119
+ }
1120
+ }
1121
+ }
1122
+ }
1123
+ catch {
1124
+ // Fallback: try using sox to list devices if available
1125
+ try {
1126
+ // Sox on macOS with coreaudio can list devices
1127
+ const soxOutput = execSync("rec -q --list-devices 2>&1 || true", {
1128
+ encoding: "utf-8",
1129
+ timeout: 5000,
1130
+ });
1131
+ // Parse sox output for device names
1132
+ const lines = soxOutput.split("\n");
1133
+ for (const line of lines) {
1134
+ const match = line.match(/^\s*(\d+)\s+(.+)$/);
1135
+ if (match) {
1136
+ devices.push({
1137
+ id: match[1],
1138
+ name: match[2].trim(),
1139
+ isDefault: match[1] === "0",
1140
+ });
1141
+ }
1142
+ }
1143
+ }
1144
+ catch {
1145
+ // Last resort: add a default device entry
1146
+ devices.push({
1147
+ id: "default",
1148
+ name: "Default Input Device",
1149
+ isDefault: true,
1150
+ });
1151
+ }
1152
+ }
1153
+ // If no devices found, add default
1154
+ if (devices.length === 0) {
1155
+ devices.push({
1156
+ id: "default",
1157
+ name: "Default Input Device",
1158
+ isDefault: true,
1159
+ });
1160
+ }
1161
+ return devices;
1162
+ }
1163
+ /**
1164
+ * List audio devices on Linux using arecord or pactl
1165
+ */
1166
+ static async listDevicesLinux() {
1167
+ const devices = [];
1168
+ // Try PulseAudio/PipeWire first
1169
+ try {
1170
+ const pactlOutput = execSync("pactl list short sources 2>/dev/null", {
1171
+ encoding: "utf-8",
1172
+ timeout: 5000,
1173
+ });
1174
+ const lines = pactlOutput.trim().split("\n");
1175
+ for (const line of lines) {
1176
+ const parts = line.split("\t");
1177
+ if (parts.length >= 2) {
1178
+ const id = parts[1];
1179
+ // Skip monitor devices (output monitors)
1180
+ if (id.includes(".monitor"))
1181
+ continue;
1182
+ devices.push({
1183
+ id,
1184
+ name: id,
1185
+ isDefault: id.includes("@DEFAULT_SOURCE@") || devices.length === 0,
1186
+ });
1187
+ }
1188
+ }
1189
+ if (devices.length > 0) {
1190
+ return devices;
1191
+ }
1192
+ }
1193
+ catch {
1194
+ // PulseAudio not available, try ALSA
1195
+ }
1196
+ // Try ALSA
1197
+ try {
1198
+ const arecordOutput = execSync("arecord -l 2>/dev/null", {
1199
+ encoding: "utf-8",
1200
+ timeout: 5000,
1201
+ });
1202
+ const lines = arecordOutput.split("\n");
1203
+ for (const line of lines) {
1204
+ // Parse lines like: "card 0: PCH [HDA Intel PCH], device 0: ALC892 Analog [ALC892 Analog]"
1205
+ const match = line.match(/^card\s+(\d+):\s+([^,]+),\s+device\s+(\d+):\s+(.+)$/);
1206
+ if (match) {
1207
+ const cardNum = match[1];
1208
+ const deviceNum = match[3];
1209
+ const deviceName = match[4].trim();
1210
+ devices.push({
1211
+ id: `hw:${cardNum},${deviceNum}`,
1212
+ name: `${match[2].trim()} - ${deviceName}`,
1213
+ isDefault: devices.length === 0,
1214
+ });
1215
+ }
1216
+ }
1217
+ }
1218
+ catch {
1219
+ // ALSA not available
1220
+ }
1221
+ // Fallback to default
1222
+ if (devices.length === 0) {
1223
+ devices.push({
1224
+ id: "default",
1225
+ name: "Default Input Device",
1226
+ isDefault: true,
1227
+ });
1228
+ }
1229
+ return devices;
1230
+ }
1231
+ /**
1232
+ * List audio devices on Windows using sox or powershell
1233
+ */
1234
+ static async listDevicesWindows() {
1235
+ const devices = [];
1236
+ try {
1237
+ // Use PowerShell to list audio input devices
1238
+ const psCommand = `
1239
+ Get-WmiObject Win32_SoundDevice |
1240
+ Where-Object { $_.Status -eq 'OK' } |
1241
+ Select-Object DeviceID, Name |
1242
+ ConvertTo-Json
1243
+ `;
1244
+ const output = execSync(`powershell -Command "${psCommand}"`, {
1245
+ encoding: "utf-8",
1246
+ timeout: 10000,
1247
+ });
1248
+ const data = JSON.parse(output);
1249
+ const deviceList = Array.isArray(data) ? data : [data];
1250
+ for (let i = 0; i < deviceList.length; i++) {
1251
+ const device = deviceList[i];
1252
+ if (device && device.Name) {
1253
+ devices.push({
1254
+ id: device.DeviceID || String(i),
1255
+ name: device.Name,
1256
+ isDefault: i === 0,
1257
+ });
1258
+ }
1259
+ }
1260
+ }
1261
+ catch {
1262
+ // Fallback to default
1263
+ }
1264
+ if (devices.length === 0) {
1265
+ devices.push({
1266
+ id: "-1",
1267
+ name: "Default Input Device",
1268
+ isDefault: true,
1269
+ });
1270
+ }
1271
+ return devices;
1272
+ }
1273
+ /**
1274
+ * Select the appropriate recorder backend based on platform and availability
1275
+ */
1276
+ selectRecorder(preference) {
1277
+ if (preference !== "auto") {
1278
+ // Verify the requested backend is available
1279
+ if (this.isRecorderAvailable(preference)) {
1280
+ return preference;
1281
+ }
1282
+ console.warn(`Recorder '${preference}' not available, falling back to auto-detection`);
1283
+ }
1284
+ // Auto-detect based on platform
1285
+ if (this.currentPlatform === "darwin") {
1286
+ // macOS: prefer rec (comes with sox), then sox
1287
+ if (this.isRecorderAvailable("rec"))
1288
+ return "rec";
1289
+ if (this.isRecorderAvailable("sox"))
1290
+ return "sox";
1291
+ }
1292
+ else if (this.currentPlatform === "linux") {
1293
+ // Linux: prefer arecord (ALSA), then sox
1294
+ if (this.isRecorderAvailable("arecord"))
1295
+ return "arecord";
1296
+ if (this.isRecorderAvailable("sox"))
1297
+ return "sox";
1298
+ if (this.isRecorderAvailable("rec"))
1299
+ return "rec";
1300
+ }
1301
+ else if (this.currentPlatform === "win32") {
1302
+ // Windows: prefer sox
1303
+ if (this.isRecorderAvailable("sox"))
1304
+ return "sox";
1305
+ if (this.isRecorderAvailable("rec"))
1306
+ return "rec";
1307
+ }
1308
+ // Default fallback - throw VoiceError
1309
+ throw new VoiceError(VoiceErrorType.SOX_NOT_INSTALLED);
1310
+ }
1311
+ /**
1312
+ * Check if a recorder binary is available
1313
+ */
1314
+ isRecorderAvailable(recorder) {
1315
+ try {
1316
+ const command = this.currentPlatform === "win32" ? `where ${recorder}` : `which ${recorder}`;
1317
+ execSync(command, { stdio: "ignore", timeout: 3000 });
1318
+ return true;
1319
+ }
1320
+ catch {
1321
+ return false;
1322
+ }
1323
+ }
1324
+ /**
1325
+ * Check if currently recording
1326
+ */
1327
+ isRecording() {
1328
+ return this.state === "recording";
1329
+ }
1330
+ /**
1331
+ * Get current recording state
1332
+ */
1333
+ getState() {
1334
+ return this.state;
1335
+ }
1336
+ /**
1337
+ * Start recording audio
1338
+ *
1339
+ * @throws Error if already recording
1340
+ * @throws Error if recorder is not available
1341
+ */
1342
+ async start() {
1343
+ if (this.state === "recording") {
1344
+ throw new Error("Already recording");
1345
+ }
1346
+ this.state = "recording";
1347
+ try {
1348
+ const args = this.buildRecorderArgs();
1349
+ const command = this.recorderBackend === "arecord" ? "arecord" : this.recorderBackend;
1350
+ if (process.env.DEBUG) {
1351
+ console.log(`Starting recorder: ${command} ${args.join(" ")}`);
1352
+ }
1353
+ this.process = spawn(command, args, {
1354
+ stdio: ["ignore", "pipe", "pipe"],
1355
+ });
1356
+ if (!this.process.stdout) {
1357
+ throw new Error("Failed to open audio stream");
1358
+ }
1359
+ // Set up stdout as the audio data stream
1360
+ const stdout = this.process.stdout;
1361
+ // Buffer for collecting data
1362
+ let audioBuffer = Buffer.alloc(0);
1363
+ const chunkSize = 3200; // 100ms of 16kHz 16-bit mono audio
1364
+ stdout.on("data", (chunk) => {
1365
+ // Accumulate data - ensure chunk is a proper Buffer (Node may emit Uint8Array)
1366
+ audioBuffer = Buffer.concat([audioBuffer, Buffer.from(chunk)]);
1367
+ // Emit complete chunks
1368
+ while (audioBuffer.length >= chunkSize) {
1369
+ // IMPORTANT: Use Buffer.from() to create an independent copy, not subarray()
1370
+ // subarray() creates a view that shares the underlying ArrayBuffer
1371
+ // which can cause issues when the buffer is sent over WebSocket
1372
+ const emitChunk = Buffer.from(audioBuffer.subarray(0, chunkSize));
1373
+ audioBuffer = audioBuffer.subarray(chunkSize);
1374
+ this.emit("data", emitChunk);
1375
+ }
1376
+ });
1377
+ stdout.on("end", () => {
1378
+ // Emit any remaining data (as a copy)
1379
+ if (audioBuffer.length > 0) {
1380
+ this.emit("data", Buffer.from(audioBuffer));
1381
+ }
1382
+ this.handleProcessEnd();
1383
+ });
1384
+ stdout.on("error", (error) => {
1385
+ this.handleError(error);
1386
+ });
1387
+ // Handle stderr for warnings/errors
1388
+ if (this.process.stderr) {
1389
+ this.process.stderr.on("data", (data) => {
1390
+ const message = data.toString();
1391
+ // Only emit as error if it contains actual error indicators
1392
+ if (message.toLowerCase().includes("error") ||
1393
+ message.toLowerCase().includes("fail") ||
1394
+ message.toLowerCase().includes("cannot")) {
1395
+ this.handleError(new Error(`Recorder error: ${message}`));
1396
+ }
1397
+ else if (process.env.DEBUG) {
1398
+ console.warn("Recorder stderr:", message);
1399
+ }
1400
+ });
1401
+ }
1402
+ // Handle process errors
1403
+ this.process.on("error", (error) => {
1404
+ this.handleError(error);
1405
+ });
1406
+ this.process.on("exit", (code, signal) => {
1407
+ if (this.state === "recording") {
1408
+ // Unexpected exit
1409
+ if (code !== 0 && code !== null) {
1410
+ this.handleError(new Error(`Recorder exited with code ${code}`));
1411
+ }
1412
+ else if (signal) {
1413
+ this.handleError(new Error(`Recorder killed by signal ${signal}`));
1414
+ }
1415
+ else {
1416
+ this.handleProcessEnd();
1417
+ }
1418
+ }
1419
+ });
1420
+ // Start monitoring for device disconnect
1421
+ this.startDisconnectMonitor();
1422
+ }
1423
+ catch (error) {
1424
+ this.state = "error";
1425
+ throw error;
1426
+ }
1427
+ }
1428
+ /**
1429
+ * Stop recording
1430
+ */
1431
+ stop() {
1432
+ if (this.state !== "recording") {
1433
+ return;
1434
+ }
1435
+ this.state = "stopping";
1436
+ this.stopDisconnectMonitor();
1437
+ if (this.process) {
1438
+ // Send SIGTERM for graceful shutdown
1439
+ try {
1440
+ this.process.kill("SIGTERM");
1441
+ }
1442
+ catch {
1443
+ // Process may have already exited
1444
+ }
1445
+ // Force kill after timeout
1446
+ const killTimeout = setTimeout(() => {
1447
+ if (this.process) {
1448
+ try {
1449
+ this.process.kill("SIGKILL");
1450
+ }
1451
+ catch {
1452
+ // Ignore
1453
+ }
1454
+ }
1455
+ }, 1000);
1456
+ this.process.once("exit", () => {
1457
+ clearTimeout(killTimeout);
1458
+ this.process = null;
1459
+ this.state = "idle";
1460
+ this.emit("stopped");
1461
+ });
1462
+ }
1463
+ else {
1464
+ this.state = "idle";
1465
+ this.emit("stopped");
1466
+ }
1467
+ }
1468
+ /**
1469
+ * Build command-line arguments for the recorder
1470
+ */
1471
+ buildRecorderArgs() {
1472
+ const args = [];
1473
+ if (this.recorderBackend === "arecord") {
1474
+ // ALSA arecord arguments
1475
+ args.push("-f", "S16_LE", // 16-bit signed little-endian
1476
+ "-r", String(this.targetSampleRate), "-c", "1", // Mono
1477
+ "-t", "raw", // Raw PCM output
1478
+ "-q" // Quiet mode
1479
+ );
1480
+ if (this.device) {
1481
+ args.push("-D", this.device);
1482
+ }
1483
+ }
1484
+ else if (this.recorderBackend === "sox" || this.recorderBackend === "rec") {
1485
+ // sox/rec arguments for recording
1486
+ // Format: rec [input-options] output-file [effects]
1487
+ // On macOS, hardware often can't record at 16kHz directly, so we record
1488
+ // at native rate and use sox's 'rate' effect to resample to target
1489
+ args.push("-q", // Quiet
1490
+ "-c", "1", // Mono
1491
+ "-b", "16", // 16-bit
1492
+ "-e", "signed-integer", // Signed integer encoding
1493
+ "-t", "raw", // Raw PCM output
1494
+ "-", // Output to stdout
1495
+ "rate", String(this.targetSampleRate) // Resample to target rate
1496
+ );
1497
+ if (this.device) {
1498
+ // Device specification differs by platform
1499
+ if (this.currentPlatform === "darwin") {
1500
+ // macOS: use -d for default device or specify device
1501
+ args.unshift("-d", this.device);
1502
+ }
1503
+ else if (this.currentPlatform === "linux") {
1504
+ // Linux: AUDIODEV environment or -d flag
1505
+ args.unshift("-d", this.device);
1506
+ }
1507
+ else if (this.currentPlatform === "win32") {
1508
+ // Windows: use -t waveaudio with device number
1509
+ args.unshift("-t", "waveaudio", this.device);
1510
+ }
1511
+ }
1512
+ else {
1513
+ // Default device - rec uses system default automatically on macOS
1514
+ if (this.currentPlatform === "linux") {
1515
+ // Linux: pulseaudio or alsa default
1516
+ args.unshift("-d");
1517
+ }
1518
+ else if (this.currentPlatform === "win32") {
1519
+ args.unshift("-t", "waveaudio", "-1");
1520
+ }
1521
+ // macOS: no flag needed, rec uses coreaudio default automatically
1522
+ }
1523
+ }
1524
+ return args;
1525
+ }
1526
+ /**
1527
+ * Start monitoring for device disconnect
1528
+ */
1529
+ startDisconnectMonitor() {
1530
+ // Check every 2 seconds if the process is still healthy
1531
+ this.disconnectCheckInterval = setInterval(() => {
1532
+ if (this.state === "recording" && this.process) {
1533
+ // Check if process is still running
1534
+ try {
1535
+ // Sending signal 0 checks if process exists without affecting it
1536
+ process.kill(this.process.pid, 0);
1537
+ }
1538
+ catch {
1539
+ // Process doesn't exist
1540
+ this.handleError(new Error("Audio device disconnected or recorder stopped unexpectedly"));
1541
+ this.stop();
1542
+ }
1543
+ }
1544
+ }, 2000);
1545
+ }
1546
+ /**
1547
+ * Stop disconnect monitoring
1548
+ */
1549
+ stopDisconnectMonitor() {
1550
+ if (this.disconnectCheckInterval) {
1551
+ clearInterval(this.disconnectCheckInterval);
1552
+ this.disconnectCheckInterval = null;
1553
+ }
1554
+ }
1555
+ /**
1556
+ * Handle process end
1557
+ */
1558
+ handleProcessEnd() {
1559
+ this.stopDisconnectMonitor();
1560
+ if (this.state === "recording") {
1561
+ this.state = "idle";
1562
+ this.emit("end");
1563
+ }
1564
+ }
1565
+ /**
1566
+ * Handle recording error
1567
+ */
1568
+ handleError(error) {
1569
+ this.stopDisconnectMonitor();
1570
+ this.state = "error";
1571
+ this.emit("error", error);
1572
+ }
1573
+ }
1574
+ // List models command
1575
+ export async function listModelsCommand() {
1576
+ console.log(chalk.bold("\n🎤 JFL Voice - Available Models\n"));
1577
+ ensureDirectories();
1578
+ const currentDefault = getCurrentDefaultModel();
1579
+ console.log(chalk.gray("Models are downloaded from Hugging Face (ggerganov/whisper.cpp)\n"));
1580
+ const modelNames = Object.keys(WHISPER_MODELS);
1581
+ // Table header
1582
+ console.log(chalk.gray(" ") +
1583
+ chalk.bold("Model".padEnd(14)) +
1584
+ chalk.bold("Size".padEnd(10)) +
1585
+ chalk.bold("Status".padEnd(14)) +
1586
+ chalk.bold("Description"));
1587
+ console.log(chalk.gray(" " + "─".repeat(70)));
1588
+ for (const name of modelNames) {
1589
+ const model = WHISPER_MODELS[name];
1590
+ const isDownloaded = isModelDownloaded(name);
1591
+ const isDefault = name === currentDefault;
1592
+ const statusIcon = isDownloaded ? chalk.green("✓") : chalk.gray("○");
1593
+ const defaultMarker = isDefault ? chalk.cyan(" (default)") : "";
1594
+ const status = isDownloaded ? chalk.green("downloaded") + defaultMarker : chalk.gray("not downloaded");
1595
+ console.log(` ${statusIcon} ` +
1596
+ chalk.white(name.padEnd(12)) +
1597
+ chalk.gray(model.size.padEnd(10)) +
1598
+ status.padEnd(24) +
1599
+ chalk.gray(model.description));
1600
+ }
1601
+ console.log();
1602
+ console.log(chalk.cyan("Commands:"));
1603
+ console.log(" jfl voice model download <name> Download a model");
1604
+ console.log(" jfl voice model default <name> Set default model");
1605
+ console.log();
1606
+ }
1607
+ // Download model command
1608
+ export async function downloadModelCommand(modelName, options) {
1609
+ console.log(chalk.bold("\n🎤 JFL Voice - Download Model\n"));
1610
+ // Validate model name
1611
+ if (!(modelName in WHISPER_MODELS)) {
1612
+ console.log(chalk.red(`Unknown model: ${modelName}`));
1613
+ console.log(chalk.gray("\nAvailable models:"));
1614
+ for (const name of Object.keys(WHISPER_MODELS)) {
1615
+ console.log(` - ${name}`);
1616
+ }
1617
+ return;
1618
+ }
1619
+ const model = WHISPER_MODELS[modelName];
1620
+ ensureDirectories();
1621
+ const modelPath = getModelPath(modelName);
1622
+ const partialPath = getPartialPath(modelName);
1623
+ // Check if already downloaded
1624
+ if (isModelDownloaded(modelName) && !options?.force) {
1625
+ console.log(chalk.green(`✓ Model '${modelName}' is already downloaded`));
1626
+ console.log(chalk.gray(` Location: ${modelPath}`));
1627
+ return;
1628
+ }
1629
+ // Check for partial download
1630
+ let resuming = false;
1631
+ if (existsSync(partialPath)) {
1632
+ const stats = statSync(partialPath);
1633
+ const percent = ((stats.size / model.sizeBytes) * 100).toFixed(1);
1634
+ console.log(chalk.yellow(`Found incomplete download (${percent}% complete)`));
1635
+ const { resume } = await inquirer.prompt([
1636
+ {
1637
+ type: "confirm",
1638
+ name: "resume",
1639
+ message: "Resume download?",
1640
+ default: true,
1641
+ },
1642
+ ]);
1643
+ if (!resume) {
1644
+ unlinkSync(partialPath);
1645
+ }
1646
+ else {
1647
+ resuming = true;
1648
+ }
1649
+ }
1650
+ console.log(chalk.gray(`Model: ${model.displayName}`));
1651
+ console.log(chalk.gray(`Size: ${model.size}`));
1652
+ console.log(chalk.gray(`URL: ${model.url}`));
1653
+ if (resuming) {
1654
+ console.log(chalk.cyan("Resuming download..."));
1655
+ }
1656
+ console.log();
1657
+ const spinner = ora({
1658
+ text: "Starting download...",
1659
+ prefixText: " ",
1660
+ }).start();
1661
+ const startTime = Date.now();
1662
+ let lastUpdate = 0;
1663
+ try {
1664
+ await downloadFile(model.url, modelPath, partialPath, model.sizeBytes, (downloaded, total) => {
1665
+ const now = Date.now();
1666
+ // Update at most every 100ms to avoid flickering
1667
+ if (now - lastUpdate < 100)
1668
+ return;
1669
+ lastUpdate = now;
1670
+ const percent = ((downloaded / total) * 100).toFixed(1);
1671
+ const elapsed = (now - startTime) / 1000;
1672
+ const speed = downloaded / elapsed;
1673
+ const remaining = (total - downloaded) / speed;
1674
+ spinner.text = `Downloading: ${percent}% (${formatBytes(downloaded)}/${formatBytes(total)}) - ${formatBytes(speed)}/s - ${remaining.toFixed(0)}s remaining`;
1675
+ });
1676
+ spinner.text = "Verifying checksum...";
1677
+ // Verify checksum
1678
+ if (!verifyChecksum(modelPath, model.sha256)) {
1679
+ spinner.fail("Checksum verification failed");
1680
+ console.log(chalk.red("\nThe downloaded file is corrupted."));
1681
+ console.log(chalk.gray("Try downloading again with: jfl voice model download " + modelName + " --force"));
1682
+ unlinkSync(modelPath);
1683
+ return;
1684
+ }
1685
+ spinner.succeed("Download complete!");
1686
+ const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
1687
+ console.log();
1688
+ console.log(chalk.green(`✓ Model '${modelName}' downloaded successfully`));
1689
+ console.log(chalk.gray(` Location: ${modelPath}`));
1690
+ console.log(chalk.gray(` Time: ${elapsed}s`));
1691
+ // Offer to set as default if base model
1692
+ if (modelName === "base" && getCurrentDefaultModel() !== "base") {
1693
+ const { setDefault } = await inquirer.prompt([
1694
+ {
1695
+ type: "confirm",
1696
+ name: "setDefault",
1697
+ message: "Set 'base' as your default model?",
1698
+ default: true,
1699
+ },
1700
+ ]);
1701
+ if (setDefault) {
1702
+ setDefaultModel("base");
1703
+ console.log(chalk.green("✓ Default model set to 'base'"));
1704
+ }
1705
+ }
1706
+ }
1707
+ catch (error) {
1708
+ spinner.fail("Download failed");
1709
+ console.error(chalk.red(error));
1710
+ console.log(chalk.gray("\nYou can resume the download by running the same command again."));
1711
+ }
1712
+ console.log();
1713
+ }
1714
+ // Set default model command
1715
+ export async function setDefaultModelCommand(modelName) {
1716
+ // Validate model name
1717
+ if (!(modelName in WHISPER_MODELS)) {
1718
+ console.log(chalk.red(`Unknown model: ${modelName}`));
1719
+ console.log(chalk.gray("\nAvailable models:"));
1720
+ for (const name of Object.keys(WHISPER_MODELS)) {
1721
+ console.log(` - ${name}`);
1722
+ }
1723
+ return;
1724
+ }
1725
+ // Check if model is downloaded
1726
+ if (!isModelDownloaded(modelName)) {
1727
+ console.log(chalk.yellow(`Model '${modelName}' is not downloaded yet.`));
1728
+ const { download } = await inquirer.prompt([
1729
+ {
1730
+ type: "confirm",
1731
+ name: "download",
1732
+ message: "Download it now?",
1733
+ default: true,
1734
+ },
1735
+ ]);
1736
+ if (download) {
1737
+ await downloadModelCommand(modelName);
1738
+ }
1739
+ return;
1740
+ }
1741
+ setDefaultModel(modelName);
1742
+ console.log(chalk.green(`✓ Default model set to '${modelName}'`));
1743
+ }
1744
+ // List audio devices command
1745
+ export async function listDevicesCommand() {
1746
+ console.log(chalk.bold("\n🎤 JFL Voice - Audio Devices\n"));
1747
+ const spinner = ora({
1748
+ text: "Detecting audio devices...",
1749
+ prefixText: " ",
1750
+ }).start();
1751
+ try {
1752
+ const devices = await AudioRecorder.listDevices();
1753
+ spinner.stop();
1754
+ if (devices.length === 0) {
1755
+ console.log(chalk.yellow(" No audio input devices found."));
1756
+ console.log();
1757
+ console.log(chalk.gray(" Make sure you have a microphone connected and permissions granted."));
1758
+ console.log();
1759
+ return;
1760
+ }
1761
+ console.log(chalk.gray(` Found ${devices.length} audio input device${devices.length > 1 ? "s" : ""}:\n`));
1762
+ // Table header
1763
+ console.log(chalk.gray(" ") +
1764
+ chalk.bold("Device".padEnd(40)) +
1765
+ chalk.bold("ID".padEnd(30)) +
1766
+ chalk.bold("Default"));
1767
+ console.log(chalk.gray(" " + "─".repeat(75)));
1768
+ for (const device of devices) {
1769
+ const defaultMarker = device.isDefault ? chalk.green("✓") : chalk.gray("-");
1770
+ const deviceName = String(device.name || "Unknown").substring(0, 38).padEnd(40);
1771
+ const deviceId = String(device.id || "default").substring(0, 28).padEnd(30);
1772
+ console.log(` ${deviceName}` +
1773
+ chalk.gray(deviceId) +
1774
+ defaultMarker);
1775
+ }
1776
+ console.log();
1777
+ console.log(chalk.cyan("Usage:"));
1778
+ console.log(" jfl voice test Test with default device");
1779
+ console.log(" jfl voice test --device <id> Test with specific device");
1780
+ console.log();
1781
+ }
1782
+ catch (error) {
1783
+ spinner.fail("Failed to list devices");
1784
+ console.error(chalk.red(` ${error}`));
1785
+ console.log();
1786
+ }
1787
+ }
1788
+ // Voice test command - records audio and sends to whisper server for transcription
1789
+ export async function voiceTestCommand(options) {
1790
+ console.log(chalk.bold("\n🎤 JFL Voice - Test\n"));
1791
+ const duration = 3; // Fixed 3 second recording
1792
+ // Step 1: Check if whisper server is running
1793
+ const serverError = checkServerRunning();
1794
+ if (serverError) {
1795
+ handleVoiceError(serverError);
1796
+ return;
1797
+ }
1798
+ // Step 2: Check for auth token
1799
+ const authError = checkAuthToken();
1800
+ if (authError) {
1801
+ handleVoiceError(authError);
1802
+ return;
1803
+ }
1804
+ const authToken = readAuthToken();
1805
+ const socketPath = getVoiceSocketPath();
1806
+ // Step 3: Check for sox/rec availability
1807
+ let recorder;
1808
+ try {
1809
+ recorder = new AudioRecorder({
1810
+ device: options?.device,
1811
+ sampleRate: 16000,
1812
+ });
1813
+ }
1814
+ catch (error) {
1815
+ if (error instanceof VoiceError) {
1816
+ handleVoiceError(error);
1817
+ }
1818
+ else {
1819
+ handleVoiceError(createVoiceError(error instanceof Error ? error : new Error(String(error))));
1820
+ }
1821
+ return;
1822
+ }
1823
+ console.log(chalk.gray(" Recording for 3 seconds..."));
1824
+ console.log(chalk.gray(" Speak clearly into your microphone!\n"));
1825
+ // Collect audio data
1826
+ const audioChunks = [];
1827
+ let peakLevel = 0;
1828
+ recorder.on("data", (chunk) => {
1829
+ audioChunks.push(chunk);
1830
+ // Calculate peak level from 16-bit samples
1831
+ for (let i = 0; i < chunk.length - 1; i += 2) {
1832
+ const sample = chunk.readInt16LE(i);
1833
+ const level = Math.abs(sample) / 32768;
1834
+ if (level > peakLevel) {
1835
+ peakLevel = level;
1836
+ }
1837
+ }
1838
+ });
1839
+ let recordingError = null;
1840
+ recorder.on("error", (error) => {
1841
+ recordingError = error;
1842
+ });
1843
+ // Step 4: Start recording with countdown
1844
+ try {
1845
+ await recorder.start();
1846
+ }
1847
+ catch (error) {
1848
+ const voiceError = new VoiceError(VoiceErrorType.RECORDING_FAILED, {
1849
+ originalError: error instanceof Error ? error : new Error(String(error)),
1850
+ context: { device: options?.device },
1851
+ });
1852
+ handleVoiceError(voiceError);
1853
+ return;
1854
+ }
1855
+ // Countdown display
1856
+ for (let i = duration; i > 0; i--) {
1857
+ process.stdout.write(` Recording... ${chalk.cyan(String(i))}...\r`);
1858
+ await new Promise((resolve) => setTimeout(resolve, 1000));
1859
+ }
1860
+ process.stdout.write(" \r"); // Clear countdown line
1861
+ recorder.stop();
1862
+ // Wait for recorder to fully stop
1863
+ await new Promise((resolve) => {
1864
+ const checkStopped = () => {
1865
+ if (recorder.getState() === "idle") {
1866
+ resolve();
1867
+ }
1868
+ else {
1869
+ setTimeout(checkStopped, 50);
1870
+ }
1871
+ };
1872
+ setTimeout(checkStopped, 100);
1873
+ });
1874
+ if (recordingError !== null) {
1875
+ const voiceError = new VoiceError(VoiceErrorType.RECORDING_FAILED, {
1876
+ originalError: recordingError,
1877
+ context: { device: options?.device },
1878
+ });
1879
+ handleVoiceError(voiceError);
1880
+ return;
1881
+ }
1882
+ // Check if we got any audio
1883
+ const totalBytes = audioChunks.reduce((sum, chunk) => sum + chunk.length, 0);
1884
+ if (totalBytes === 0) {
1885
+ const voiceError = new VoiceError(VoiceErrorType.MIC_UNAVAILABLE, {
1886
+ context: { totalBytes: 0, device: options?.device },
1887
+ });
1888
+ handleVoiceError(voiceError);
1889
+ return;
1890
+ }
1891
+ // Check audio levels
1892
+ if (peakLevel < 0.01) {
1893
+ // Low audio but not an error, just a warning
1894
+ console.log(chalk.yellow(" ⚠ Very low audio level detected"));
1895
+ console.log(chalk.gray(" Suggestion: Try speaking louder or check microphone"));
1896
+ console.log();
1897
+ }
1898
+ // Combine all audio chunks and normalize
1899
+ const audioBuffer = normalizeAudio(Buffer.concat(audioChunks));
1900
+ // Step 5: Connect to whisper server and send audio
1901
+ console.log(chalk.gray(" Transcribing..."));
1902
+ const client = new VoiceClient({
1903
+ socketPath,
1904
+ authToken,
1905
+ maxReconnectAttempts: 1, // Don't retry for test
1906
+ });
1907
+ let transcription = "";
1908
+ let transcriptionReceived = false;
1909
+ let transcriptionError = null;
1910
+ client.onTranscript((text, isFinal) => {
1911
+ // Store the latest transcription (partial or final)
1912
+ // Filter out blank audio markers
1913
+ if (text && text.trim() && !text.includes("[BLANK_AUDIO]")) {
1914
+ transcription = text;
1915
+ }
1916
+ if (isFinal) {
1917
+ transcriptionReceived = true;
1918
+ }
1919
+ });
1920
+ client.onError((error) => {
1921
+ transcriptionError = error;
1922
+ });
1923
+ try {
1924
+ await client.connect();
1925
+ // Send audio data and wait for it to be sent
1926
+ client.sendAudio(audioBuffer);
1927
+ // Wait for data to fully transmit before sending end signal
1928
+ // WebSocket bufferedAmount=0 doesn't guarantee the server received it
1929
+ await new Promise((resolve) => setTimeout(resolve, 500));
1930
+ // Signal end of audio
1931
+ client.endAudio();
1932
+ // Wait for transcription with timeout
1933
+ const timeout = 10000; // 10 seconds
1934
+ const startTime = Date.now();
1935
+ while (!transcriptionReceived && !transcriptionError) {
1936
+ if (Date.now() - startTime > timeout) {
1937
+ // If we have a partial transcription, use it instead of erroring
1938
+ if (transcription) {
1939
+ break;
1940
+ }
1941
+ transcriptionError = new VoiceError(VoiceErrorType.TIMEOUT, {
1942
+ context: { timeout, operation: "transcription" },
1943
+ recoverable: true,
1944
+ audioBuffer,
1945
+ });
1946
+ break;
1947
+ }
1948
+ await new Promise((resolve) => setTimeout(resolve, 100));
1949
+ }
1950
+ client.disconnect();
1951
+ }
1952
+ catch (error) {
1953
+ client.disconnect();
1954
+ const voiceError = error instanceof VoiceError
1955
+ ? error
1956
+ : createVoiceError(error instanceof Error ? error : new Error(String(error)), { operation: "connect" }, audioBuffer);
1957
+ handleVoiceError(voiceError);
1958
+ return;
1959
+ }
1960
+ // Step 6: Display results
1961
+ console.log();
1962
+ if (transcriptionError) {
1963
+ const voiceError = transcriptionError instanceof VoiceError
1964
+ ? transcriptionError
1965
+ : createVoiceError(transcriptionError, { operation: "transcription" }, audioBuffer);
1966
+ handleVoiceError(voiceError);
1967
+ return;
1968
+ }
1969
+ if (!transcription || transcription.trim() === "") {
1970
+ const voiceError = new VoiceError(VoiceErrorType.TRANSCRIPTION_EMPTY, {
1971
+ context: { audioLength: totalBytes, peakLevel },
1972
+ recoverable: true,
1973
+ audioBuffer,
1974
+ });
1975
+ handleVoiceError(voiceError);
1976
+ return;
1977
+ }
1978
+ // Success! Show the transcription
1979
+ console.log(chalk.green(" ✓ Transcription successful!\n"));
1980
+ console.log(chalk.white(" You said:"));
1981
+ console.log(chalk.cyan(` "${transcription.trim()}"`));
1982
+ console.log();
1983
+ // Show audio stats
1984
+ const durationActual = totalBytes / (16000 * 2); // 16kHz, 16-bit
1985
+ console.log(chalk.gray(` Audio: ${durationActual.toFixed(1)}s, peak level ${(peakLevel * 100).toFixed(0)}%`));
1986
+ console.log();
1987
+ }
1988
+ // Test audio recording command (without transcription, for debugging)
1989
+ export async function testRecordingCommand(options) {
1990
+ console.log(chalk.bold("\n🎤 JFL Voice - Recording Test\n"));
1991
+ const duration = options?.duration ?? 5;
1992
+ // Check for sox/rec availability
1993
+ try {
1994
+ const recorder = new AudioRecorder({
1995
+ device: options?.device,
1996
+ sampleRate: 16000,
1997
+ });
1998
+ console.log(chalk.gray(` Recording for ${duration} seconds...`));
1999
+ console.log(chalk.gray(` Format: 16-bit PCM, 16kHz, mono`));
2000
+ if (options?.device) {
2001
+ console.log(chalk.gray(` Device: ${options.device}`));
2002
+ }
2003
+ else {
2004
+ console.log(chalk.gray(` Device: default`));
2005
+ }
2006
+ console.log();
2007
+ let totalBytes = 0;
2008
+ let chunkCount = 0;
2009
+ let peakLevel = 0;
2010
+ recorder.on("data", (chunk) => {
2011
+ totalBytes += chunk.length;
2012
+ chunkCount++;
2013
+ // Calculate peak level from 16-bit samples
2014
+ for (let i = 0; i < chunk.length - 1; i += 2) {
2015
+ const sample = chunk.readInt16LE(i);
2016
+ const level = Math.abs(sample) / 32768;
2017
+ if (level > peakLevel) {
2018
+ peakLevel = level;
2019
+ }
2020
+ }
2021
+ });
2022
+ recorder.on("error", (error) => {
2023
+ console.error(chalk.red(`\n Recording error: ${error.message}`));
2024
+ });
2025
+ const spinner = ora({
2026
+ text: "Recording...",
2027
+ prefixText: " ",
2028
+ }).start();
2029
+ await recorder.start();
2030
+ // Record for specified duration
2031
+ await new Promise((resolve) => {
2032
+ const startTime = Date.now();
2033
+ const updateInterval = setInterval(() => {
2034
+ const elapsed = (Date.now() - startTime) / 1000;
2035
+ const remaining = Math.max(0, duration - elapsed);
2036
+ // Create a visual level meter
2037
+ const levelBars = Math.round(peakLevel * 20);
2038
+ const meter = "█".repeat(levelBars) + "░".repeat(20 - levelBars);
2039
+ spinner.text = `Recording... ${remaining.toFixed(1)}s remaining [${meter}]`;
2040
+ if (elapsed >= duration) {
2041
+ clearInterval(updateInterval);
2042
+ recorder.stop();
2043
+ resolve();
2044
+ }
2045
+ }, 100);
2046
+ });
2047
+ spinner.succeed("Recording complete!");
2048
+ console.log();
2049
+ // Show stats
2050
+ const durationActual = totalBytes / (16000 * 2); // 16kHz, 16-bit
2051
+ console.log(chalk.gray(" Statistics:"));
2052
+ console.log(` - Duration: ${durationActual.toFixed(2)}s`);
2053
+ console.log(` - Data received: ${formatBytes(totalBytes)}`);
2054
+ console.log(` - Chunks: ${chunkCount}`);
2055
+ console.log(` - Peak level: ${(peakLevel * 100).toFixed(1)}%`);
2056
+ if (peakLevel < 0.01) {
2057
+ console.log();
2058
+ console.log(chalk.yellow(" ⚠️ No audio detected. Check your microphone:"));
2059
+ console.log(chalk.gray(" - Is the microphone connected and enabled?"));
2060
+ console.log(chalk.gray(" - Does the application have microphone permission?"));
2061
+ console.log(chalk.gray(" - Try speaking louder or moving closer to the mic."));
2062
+ }
2063
+ else if (peakLevel < 0.1) {
2064
+ console.log();
2065
+ console.log(chalk.yellow(" ⚠️ Audio level is low. Consider:"));
2066
+ console.log(chalk.gray(" - Speaking louder"));
2067
+ console.log(chalk.gray(" - Increasing microphone gain"));
2068
+ console.log(chalk.gray(" - Moving closer to the microphone"));
2069
+ }
2070
+ else {
2071
+ console.log();
2072
+ console.log(chalk.green(" ✓ Audio input is working correctly!"));
2073
+ }
2074
+ console.log();
2075
+ }
2076
+ catch (error) {
2077
+ if (error instanceof VoiceError) {
2078
+ handleVoiceError(error);
2079
+ }
2080
+ else {
2081
+ handleVoiceError(createVoiceError(error instanceof Error ? error : new Error(String(error))));
2082
+ }
2083
+ }
2084
+ }
2085
+ // Default preview configuration
2086
+ const DEFAULT_PREVIEW_CONFIG = {
2087
+ timeout: 2.5, // seconds
2088
+ };
2089
+ // Default security configuration
2090
+ const DEFAULT_SECURITY_CONFIG = {
2091
+ maxRecordingDuration: 60, // 60 seconds max recording
2092
+ clipboardClearDelay: 5, // Clear clipboard 5 seconds after paste
2093
+ };
2094
+ // Default hotkey configuration
2095
+ const DEFAULT_HOTKEY_CONFIG = {
2096
+ mode: "auto",
2097
+ holdThreshold: 300, // ms
2098
+ };
2099
+ // Read voice config from YAML file
2100
+ function readVoiceConfig() {
2101
+ const configPath = getVoiceConfigPath();
2102
+ if (!existsSync(configPath)) {
2103
+ return null;
2104
+ }
2105
+ try {
2106
+ const content = readFileSync(configPath, "utf-8");
2107
+ const config = {
2108
+ model: "base",
2109
+ device: "default",
2110
+ sampleRate: 16000,
2111
+ autoStart: false,
2112
+ hotkey: { ...DEFAULT_HOTKEY_CONFIG },
2113
+ preview: { ...DEFAULT_PREVIEW_CONFIG },
2114
+ security: { ...DEFAULT_SECURITY_CONFIG },
2115
+ };
2116
+ // Parse YAML manually (simple key: value format)
2117
+ // Supports nested hotkey, preview, and security sections
2118
+ const lines = content.split("\n");
2119
+ let currentSection = "none";
2120
+ for (const line of lines) {
2121
+ const trimmed = line.trim();
2122
+ if (trimmed.startsWith("#") || trimmed === "")
2123
+ continue;
2124
+ // Check if entering a section
2125
+ if (trimmed === "hotkey:") {
2126
+ currentSection = "hotkey";
2127
+ continue;
2128
+ }
2129
+ if (trimmed === "preview:") {
2130
+ currentSection = "preview";
2131
+ continue;
2132
+ }
2133
+ if (trimmed === "security:") {
2134
+ currentSection = "security";
2135
+ continue;
2136
+ }
2137
+ // Check if leaving section (new top-level key)
2138
+ if (!line.startsWith(" ") && !line.startsWith("\t") && trimmed.includes(":")) {
2139
+ currentSection = "none";
2140
+ }
2141
+ if (!trimmed.includes(":"))
2142
+ continue;
2143
+ const [key, ...valueParts] = trimmed.split(":");
2144
+ const value = valueParts.join(":").trim();
2145
+ if (currentSection === "hotkey") {
2146
+ // Parse hotkey sub-keys
2147
+ if (key === "mode" && (value === "auto" || value === "tap" || value === "hold")) {
2148
+ config.hotkey.mode = value;
2149
+ }
2150
+ else if (key === "holdThreshold") {
2151
+ config.hotkey.holdThreshold = parseInt(value, 10) || 300;
2152
+ }
2153
+ }
2154
+ else if (currentSection === "preview") {
2155
+ // Parse preview sub-keys
2156
+ if (key === "timeout") {
2157
+ const parsed = parseFloat(value);
2158
+ // Validate: 0 (disabled) or 1-10 seconds
2159
+ if (!isNaN(parsed) && (parsed === 0 || (parsed >= 1 && parsed <= 10))) {
2160
+ config.preview.timeout = parsed;
2161
+ }
2162
+ }
2163
+ }
2164
+ else if (currentSection === "security") {
2165
+ // Parse security sub-keys (VS-SEC-3)
2166
+ if (key === "maxRecordingDuration") {
2167
+ const parsed = parseInt(value, 10);
2168
+ // Validate: 10-300 seconds (reasonable bounds)
2169
+ if (!isNaN(parsed) && parsed >= 10 && parsed <= 300) {
2170
+ config.security.maxRecordingDuration = parsed;
2171
+ }
2172
+ }
2173
+ else if (key === "clipboardClearDelay") {
2174
+ const parsed = parseInt(value, 10);
2175
+ // Validate: 1-60 seconds
2176
+ if (!isNaN(parsed) && parsed >= 1 && parsed <= 60) {
2177
+ config.security.clipboardClearDelay = parsed;
2178
+ }
2179
+ }
2180
+ }
2181
+ else {
2182
+ // Parse top-level keys
2183
+ if (key === "model")
2184
+ config.model = value;
2185
+ else if (key === "device")
2186
+ config.device = value;
2187
+ else if (key === "sampleRate")
2188
+ config.sampleRate = parseInt(value, 10) || 16000;
2189
+ else if (key === "autoStart")
2190
+ config.autoStart = value === "true";
2191
+ }
2192
+ }
2193
+ return config;
2194
+ }
2195
+ catch {
2196
+ return null;
2197
+ }
2198
+ }
2199
+ // Write voice config to YAML file
2200
+ function writeVoiceConfig(config) {
2201
+ ensureDirectories();
2202
+ const configPath = getVoiceConfigPath();
2203
+ // Ensure config sections have defaults if not provided
2204
+ const hotkeyConfig = config.hotkey || DEFAULT_HOTKEY_CONFIG;
2205
+ const previewConfig = config.preview || DEFAULT_PREVIEW_CONFIG;
2206
+ const securityConfig = config.security || DEFAULT_SECURITY_CONFIG;
2207
+ const content = `# JFL Voice Configuration
2208
+ # Generated by: jfl voice setup
2209
+ # Re-run setup to change settings: jfl voice setup
2210
+
2211
+ model: ${config.model}
2212
+ device: ${config.device}
2213
+ sampleRate: ${config.sampleRate}
2214
+ autoStart: ${config.autoStart}
2215
+
2216
+ # Hotkey settings for voice hotkey mode
2217
+ # mode: auto (smart detection), tap (tap-to-toggle), or hold (hold-to-talk)
2218
+ # holdThreshold: ms to hold before entering hold-to-talk mode (default: 300)
2219
+ hotkey:
2220
+ mode: ${hotkeyConfig.mode}
2221
+ holdThreshold: ${hotkeyConfig.holdThreshold}
2222
+
2223
+ # Preview settings for transcript review before sending
2224
+ # timeout: seconds to wait before auto-sending (1-10, or 0 to disable auto-send)
2225
+ # When preview is shown: Enter=send immediately, Esc=cancel, any other key=edit mode
2226
+ preview:
2227
+ timeout: ${previewConfig.timeout}
2228
+
2229
+ # Security settings for clipboard hygiene and recording limits (VS-SEC-3)
2230
+ # maxRecordingDuration: seconds before auto-stop (10-300, default: 60)
2231
+ # clipboardClearDelay: seconds after paste before clearing clipboard (1-60, default: 5)
2232
+ security:
2233
+ maxRecordingDuration: ${securityConfig.maxRecordingDuration}
2234
+ clipboardClearDelay: ${securityConfig.clipboardClearDelay}
2235
+ `;
2236
+ writeFileSync(configPath, content, { mode: 0o644 });
2237
+ }
2238
+ // Check microphone permissions on macOS
2239
+ async function checkMicPermissions() {
2240
+ if (platform() !== "darwin") {
2241
+ return { granted: true, message: "Permissions check not required on this platform" };
2242
+ }
2243
+ try {
2244
+ // On macOS, try a quick recording test to trigger permission prompt
2245
+ // If sox can record, permissions are granted
2246
+ execSync("rec -q -r 16000 -c 1 -b 16 -e signed-integer -t raw - trim 0 0.1 2>/dev/null | head -c 1", {
2247
+ timeout: 5000,
2248
+ stdio: ["ignore", "pipe", "ignore"],
2249
+ });
2250
+ return { granted: true, message: "Microphone access granted" };
2251
+ }
2252
+ catch (error) {
2253
+ // Check if it's a permission error
2254
+ try {
2255
+ // Try to check system preferences
2256
+ const result = execSync("defaults read com.apple.controlcenter 'NSStatusItem Visible Microphone' 2>/dev/null || echo 'unknown'", { encoding: "utf-8", timeout: 3000 }).trim();
2257
+ if (result === "unknown") {
2258
+ return {
2259
+ granted: false,
2260
+ message: "Unable to determine microphone permission status. Try recording to trigger permission prompt.",
2261
+ };
2262
+ }
2263
+ }
2264
+ catch {
2265
+ // Ignore
2266
+ }
2267
+ return {
2268
+ granted: false,
2269
+ message: "Microphone access may not be granted. Go to System Preferences > Privacy & Security > Microphone",
2270
+ };
2271
+ }
2272
+ }
2273
+ // Test audio device with a short recording
2274
+ async function testAudioDevice(device, durationSecs = 3) {
2275
+ return new Promise((resolve) => {
2276
+ try {
2277
+ const recorder = new AudioRecorder({
2278
+ device: device === "default" ? undefined : device,
2279
+ sampleRate: 16000,
2280
+ });
2281
+ let totalBytes = 0;
2282
+ let peakLevel = 0;
2283
+ let hasError = false;
2284
+ recorder.on("data", (chunk) => {
2285
+ totalBytes += chunk.length;
2286
+ // Calculate peak level from 16-bit samples
2287
+ for (let i = 0; i < chunk.length - 1; i += 2) {
2288
+ const sample = chunk.readInt16LE(i);
2289
+ const level = Math.abs(sample) / 32768;
2290
+ if (level > peakLevel) {
2291
+ peakLevel = level;
2292
+ }
2293
+ }
2294
+ });
2295
+ recorder.on("error", (error) => {
2296
+ hasError = true;
2297
+ recorder.stop();
2298
+ resolve({ success: false, peakLevel: 0, error: error.message });
2299
+ });
2300
+ recorder.start().then(() => {
2301
+ // Record for specified duration
2302
+ setTimeout(() => {
2303
+ recorder.stop();
2304
+ if (!hasError) {
2305
+ resolve({
2306
+ success: totalBytes > 0,
2307
+ peakLevel,
2308
+ error: totalBytes === 0 ? "No audio data received" : undefined,
2309
+ });
2310
+ }
2311
+ }, durationSecs * 1000);
2312
+ }).catch((error) => {
2313
+ resolve({ success: false, peakLevel: 0, error: String(error) });
2314
+ });
2315
+ }
2316
+ catch (error) {
2317
+ resolve({ success: false, peakLevel: 0, error: String(error) });
2318
+ }
2319
+ });
2320
+ }
2321
+ // First-time setup wizard
2322
+ export async function voiceSetupCommand() {
2323
+ console.log(chalk.bold("\n🎤 JFL Voice - Setup Wizard\n"));
2324
+ // Check for existing config
2325
+ const existingConfig = readVoiceConfig();
2326
+ if (existingConfig) {
2327
+ console.log(chalk.gray(" Existing configuration found:"));
2328
+ console.log(chalk.gray(` Model: ${existingConfig.model}`));
2329
+ console.log(chalk.gray(` Device: ${existingConfig.device}`));
2330
+ console.log(chalk.gray(` Sample Rate: ${existingConfig.sampleRate}Hz`));
2331
+ console.log(chalk.gray(` Auto Start: ${existingConfig.autoStart}`));
2332
+ console.log();
2333
+ const { reconfigure } = await inquirer.prompt([
2334
+ {
2335
+ type: "confirm",
2336
+ name: "reconfigure",
2337
+ message: "Reconfigure voice settings?",
2338
+ default: false,
2339
+ },
2340
+ ]);
2341
+ if (!reconfigure) {
2342
+ console.log(chalk.gray("\n Setup cancelled. Using existing configuration.\n"));
2343
+ return;
2344
+ }
2345
+ console.log();
2346
+ }
2347
+ console.log(chalk.gray(" This wizard will help you set up voice input for JFL."));
2348
+ console.log(chalk.gray(" You'll configure:"));
2349
+ console.log(chalk.gray(" 1. Whisper model (for speech-to-text)"));
2350
+ console.log(chalk.gray(" 2. Microphone device"));
2351
+ console.log(chalk.gray(" 3. Test the audio pipeline"));
2352
+ console.log();
2353
+ ensureDirectories();
2354
+ // ============================================================================
2355
+ // Step 1: Select and download whisper model
2356
+ // ============================================================================
2357
+ console.log(chalk.cyan.bold(" Step 1: Select Whisper Model\n"));
2358
+ // Build model choices with download status
2359
+ const modelChoices = [
2360
+ {
2361
+ name: `${chalk.white("tiny")} (75 MB) - Fastest, lowest accuracy ${isModelDownloaded("tiny") ? chalk.green("[downloaded]") : ""}`,
2362
+ value: "tiny",
2363
+ },
2364
+ {
2365
+ name: `${chalk.white("tiny.en")} (75 MB) - English-only tiny ${isModelDownloaded("tiny.en") ? chalk.green("[downloaded]") : ""}`,
2366
+ value: "tiny.en",
2367
+ },
2368
+ {
2369
+ name: `${chalk.white("base")} (142 MB) - Balanced, recommended ${isModelDownloaded("base") ? chalk.green("[downloaded]") : chalk.yellow("[recommended]")}`,
2370
+ value: "base",
2371
+ },
2372
+ {
2373
+ name: `${chalk.white("base.en")} (142 MB) - English-only base ${isModelDownloaded("base.en") ? chalk.green("[downloaded]") : ""}`,
2374
+ value: "base.en",
2375
+ },
2376
+ {
2377
+ name: `${chalk.white("small")} (466 MB) - Higher accuracy, slower ${isModelDownloaded("small") ? chalk.green("[downloaded]") : ""}`,
2378
+ value: "small",
2379
+ },
2380
+ {
2381
+ name: `${chalk.white("small.en")} (466 MB) - English-only small ${isModelDownloaded("small.en") ? chalk.green("[downloaded]") : ""}`,
2382
+ value: "small.en",
2383
+ },
2384
+ ];
2385
+ const { selectedModel } = await inquirer.prompt([
2386
+ {
2387
+ type: "list",
2388
+ name: "selectedModel",
2389
+ message: "Select a whisper model:",
2390
+ choices: modelChoices,
2391
+ default: "base",
2392
+ },
2393
+ ]);
2394
+ // Download model if needed
2395
+ if (!isModelDownloaded(selectedModel)) {
2396
+ console.log();
2397
+ const { confirmDownload } = await inquirer.prompt([
2398
+ {
2399
+ type: "confirm",
2400
+ name: "confirmDownload",
2401
+ message: `Download ${selectedModel} model (${WHISPER_MODELS[selectedModel].size})?`,
2402
+ default: true,
2403
+ },
2404
+ ]);
2405
+ if (!confirmDownload) {
2406
+ console.log(chalk.yellow("\n Model not downloaded. Setup incomplete.\n"));
2407
+ console.log(chalk.gray(" Run 'jfl voice setup' again when ready.\n"));
2408
+ return;
2409
+ }
2410
+ // Download the model
2411
+ await downloadModelCommand(selectedModel);
2412
+ // Verify download succeeded
2413
+ if (!isModelDownloaded(selectedModel)) {
2414
+ console.log(chalk.red("\n Model download failed. Setup incomplete.\n"));
2415
+ return;
2416
+ }
2417
+ }
2418
+ else {
2419
+ console.log(chalk.green(`\n ✓ Model '${selectedModel}' is already downloaded.\n`));
2420
+ }
2421
+ // ============================================================================
2422
+ // Step 2: Select microphone device
2423
+ // ============================================================================
2424
+ console.log(chalk.cyan.bold(" Step 2: Select Microphone\n"));
2425
+ const spinner = ora({
2426
+ text: "Detecting audio devices...",
2427
+ prefixText: " ",
2428
+ }).start();
2429
+ const devices = await AudioRecorder.listDevices();
2430
+ spinner.stop();
2431
+ if (devices.length === 0) {
2432
+ console.log(chalk.yellow(" No audio input devices found."));
2433
+ console.log(chalk.gray(" Make sure you have a microphone connected.\n"));
2434
+ const { continueWithDefault } = await inquirer.prompt([
2435
+ {
2436
+ type: "confirm",
2437
+ name: "continueWithDefault",
2438
+ message: "Continue with default device anyway?",
2439
+ default: true,
2440
+ },
2441
+ ]);
2442
+ if (!continueWithDefault) {
2443
+ console.log(chalk.yellow("\n Setup cancelled.\n"));
2444
+ return;
2445
+ }
2446
+ }
2447
+ let selectedDevice = "default";
2448
+ if (devices.length > 0) {
2449
+ // Build device choices
2450
+ const deviceChoices = devices.map((device) => ({
2451
+ name: `${device.name}${device.isDefault ? chalk.cyan(" (system default)") : ""}`,
2452
+ value: device.id,
2453
+ }));
2454
+ // Add "default" option at the top
2455
+ deviceChoices.unshift({
2456
+ name: `${chalk.white("default")} - Use system default device`,
2457
+ value: "default",
2458
+ });
2459
+ const { device } = await inquirer.prompt([
2460
+ {
2461
+ type: "list",
2462
+ name: "device",
2463
+ message: "Select microphone:",
2464
+ choices: deviceChoices,
2465
+ default: "default",
2466
+ },
2467
+ ]);
2468
+ selectedDevice = device;
2469
+ }
2470
+ // ============================================================================
2471
+ // Step 3: Check permissions (macOS)
2472
+ // ============================================================================
2473
+ if (platform() === "darwin") {
2474
+ console.log(chalk.cyan.bold("\n Step 3: Check Permissions\n"));
2475
+ const permSpinner = ora({
2476
+ text: "Checking microphone permissions...",
2477
+ prefixText: " ",
2478
+ }).start();
2479
+ const permStatus = await checkMicPermissions();
2480
+ if (permStatus.granted) {
2481
+ permSpinner.succeed("Microphone permission granted");
2482
+ }
2483
+ else {
2484
+ permSpinner.warn("Microphone permission may be needed");
2485
+ console.log(chalk.yellow(`\n ${permStatus.message}`));
2486
+ console.log();
2487
+ const { openPrefs } = await inquirer.prompt([
2488
+ {
2489
+ type: "confirm",
2490
+ name: "openPrefs",
2491
+ message: "Open System Preferences > Privacy & Security?",
2492
+ default: true,
2493
+ },
2494
+ ]);
2495
+ if (openPrefs) {
2496
+ try {
2497
+ execSync("open x-apple.systempreferences:com.apple.preference.security?Privacy_Microphone", {
2498
+ stdio: "ignore",
2499
+ });
2500
+ console.log(chalk.gray("\n Grant microphone access to Terminal (or your terminal app).\n"));
2501
+ const { permGranted } = await inquirer.prompt([
2502
+ {
2503
+ type: "confirm",
2504
+ name: "permGranted",
2505
+ message: "Did you grant microphone permission?",
2506
+ default: true,
2507
+ },
2508
+ ]);
2509
+ if (!permGranted) {
2510
+ console.log(chalk.yellow("\n You may need to grant permission for voice input to work.\n"));
2511
+ }
2512
+ }
2513
+ catch {
2514
+ console.log(chalk.gray(" Could not open System Preferences automatically."));
2515
+ console.log(chalk.gray(" Please manually go to System Preferences > Privacy & Security > Microphone\n"));
2516
+ }
2517
+ }
2518
+ }
2519
+ }
2520
+ // ============================================================================
2521
+ // Step 4: Test the microphone
2522
+ // ============================================================================
2523
+ console.log(chalk.cyan.bold("\n Step 4: Test Microphone\n"));
2524
+ const { runTest } = await inquirer.prompt([
2525
+ {
2526
+ type: "confirm",
2527
+ name: "runTest",
2528
+ message: "Test the microphone for 3 seconds?",
2529
+ default: true,
2530
+ },
2531
+ ]);
2532
+ let testPassed = false;
2533
+ if (runTest) {
2534
+ console.log(chalk.gray("\n Recording for 3 seconds... Speak into your microphone!\n"));
2535
+ const testSpinner = ora({
2536
+ text: "Recording...",
2537
+ prefixText: " ",
2538
+ }).start();
2539
+ const result = await testAudioDevice(selectedDevice, 3);
2540
+ if (result.success) {
2541
+ testSpinner.succeed("Recording complete!");
2542
+ console.log();
2543
+ // Show level meter
2544
+ const levelBars = Math.round(result.peakLevel * 20);
2545
+ const meter = "█".repeat(levelBars) + "░".repeat(20 - levelBars);
2546
+ console.log(chalk.gray(` Peak level: [${meter}] ${(result.peakLevel * 100).toFixed(1)}%`));
2547
+ if (result.peakLevel < 0.01) {
2548
+ console.log(chalk.yellow("\n ⚠️ No audio detected. Check your microphone connection."));
2549
+ }
2550
+ else if (result.peakLevel < 0.1) {
2551
+ console.log(chalk.yellow("\n ⚠️ Audio level is low. Consider increasing microphone gain."));
2552
+ testPassed = true;
2553
+ }
2554
+ else {
2555
+ console.log(chalk.green("\n ✓ Audio input is working correctly!"));
2556
+ testPassed = true;
2557
+ }
2558
+ }
2559
+ else {
2560
+ testSpinner.fail("Test failed");
2561
+ console.log(chalk.red(`\n Error: ${result.error}`));
2562
+ console.log(chalk.gray("\n Make sure you have sox installed:"));
2563
+ console.log(chalk.gray(" macOS: brew install sox"));
2564
+ console.log(chalk.gray(" Linux: sudo apt-get install sox libsox-fmt-all"));
2565
+ console.log(chalk.gray(" Windows: choco install sox.portable"));
2566
+ }
2567
+ // Option to retry with different device
2568
+ if (!testPassed && devices.length > 1) {
2569
+ console.log();
2570
+ const { tryAnother } = await inquirer.prompt([
2571
+ {
2572
+ type: "confirm",
2573
+ name: "tryAnother",
2574
+ message: "Try a different device?",
2575
+ default: true,
2576
+ },
2577
+ ]);
2578
+ if (tryAnother) {
2579
+ // Let them pick again
2580
+ const remainingDevices = devices.filter((d) => d.id !== selectedDevice);
2581
+ const retryChoices = remainingDevices.map((device) => ({
2582
+ name: `${device.name}${device.isDefault ? chalk.cyan(" (system default)") : ""}`,
2583
+ value: device.id,
2584
+ }));
2585
+ if (retryChoices.length > 0) {
2586
+ const { retryDevice } = await inquirer.prompt([
2587
+ {
2588
+ type: "list",
2589
+ name: "retryDevice",
2590
+ message: "Select another device:",
2591
+ choices: retryChoices,
2592
+ },
2593
+ ]);
2594
+ selectedDevice = retryDevice;
2595
+ console.log(chalk.gray("\n Recording for 3 seconds with new device...\n"));
2596
+ const retrySpinner = ora({
2597
+ text: "Recording...",
2598
+ prefixText: " ",
2599
+ }).start();
2600
+ const retryResult = await testAudioDevice(selectedDevice, 3);
2601
+ if (retryResult.success && retryResult.peakLevel >= 0.01) {
2602
+ retrySpinner.succeed("Recording complete!");
2603
+ const levelBars2 = Math.round(retryResult.peakLevel * 20);
2604
+ const meter2 = "█".repeat(levelBars2) + "░".repeat(20 - levelBars2);
2605
+ console.log(chalk.gray(`\n Peak level: [${meter2}] ${(retryResult.peakLevel * 100).toFixed(1)}%`));
2606
+ console.log(chalk.green("\n ✓ Audio input is working with this device!"));
2607
+ testPassed = true;
2608
+ }
2609
+ else {
2610
+ retrySpinner.fail("Test failed with this device too");
2611
+ }
2612
+ }
2613
+ }
2614
+ }
2615
+ }
2616
+ else {
2617
+ console.log(chalk.gray("\n Skipping microphone test."));
2618
+ testPassed = true; // Assume it works if user skips
2619
+ }
2620
+ // ============================================================================
2621
+ // Step 5: Save configuration
2622
+ // ============================================================================
2623
+ console.log(chalk.cyan.bold("\n Step 5: Save Configuration\n"));
2624
+ // Ask about auto-start preference
2625
+ const { autoStart } = await inquirer.prompt([
2626
+ {
2627
+ type: "confirm",
2628
+ name: "autoStart",
2629
+ message: "Auto-start whisper server when using voice? (recommended)",
2630
+ default: false,
2631
+ },
2632
+ ]);
2633
+ // Build final config (preserve existing hotkey, preview, and security settings if any)
2634
+ const existingVoiceConfig = readVoiceConfig();
2635
+ const config = {
2636
+ model: selectedModel,
2637
+ device: selectedDevice,
2638
+ sampleRate: 16000,
2639
+ autoStart,
2640
+ hotkey: existingVoiceConfig?.hotkey || { ...DEFAULT_HOTKEY_CONFIG },
2641
+ preview: existingVoiceConfig?.preview || { ...DEFAULT_PREVIEW_CONFIG },
2642
+ security: existingVoiceConfig?.security || { ...DEFAULT_SECURITY_CONFIG },
2643
+ };
2644
+ // Save config
2645
+ writeVoiceConfig(config);
2646
+ setDefaultModel(selectedModel);
2647
+ console.log(chalk.green("\n ✓ Configuration saved to ~/.jfl/voice.yaml"));
2648
+ console.log();
2649
+ console.log(chalk.gray(" Configuration:"));
2650
+ console.log(chalk.gray(` Model: ${config.model}`));
2651
+ console.log(chalk.gray(` Device: ${config.device}`));
2652
+ console.log(chalk.gray(` Sample Rate: ${config.sampleRate}Hz`));
2653
+ console.log(chalk.gray(` Auto Start: ${config.autoStart}`));
2654
+ // ============================================================================
2655
+ // Summary
2656
+ // ============================================================================
2657
+ console.log(chalk.bold("\n ✅ Setup Complete!\n"));
2658
+ if (testPassed) {
2659
+ console.log(chalk.green(" Voice input is ready to use."));
2660
+ }
2661
+ else {
2662
+ console.log(chalk.yellow(" Voice input configured, but microphone test did not pass."));
2663
+ console.log(chalk.gray(" Run 'jfl voice test' to troubleshoot."));
2664
+ }
2665
+ console.log();
2666
+ console.log(chalk.cyan(" Next steps:"));
2667
+ console.log(chalk.gray(" jfl voice test Test microphone again"));
2668
+ console.log(chalk.gray(" jfl voice server start Start the whisper server"));
2669
+ console.log(chalk.gray(" jfl voice setup Re-run this wizard"));
2670
+ console.log();
2671
+ }
2672
+ // =============================================================================
2673
+ // Voice Slash Command - /voice with VAD
2674
+ // =============================================================================
2675
+ /** Show voice command help */
2676
+ function showVoiceHelp() {
2677
+ console.log(chalk.bold("\n🎤 JFL Voice\n"));
2678
+ console.log(chalk.gray("Voice input for JFL CLI with automatic silence detection.\n"));
2679
+ console.log(chalk.cyan("Quick Start:"));
2680
+ console.log(" jfl voice Start recording (stops on silence)");
2681
+ console.log(" jfl voice record Same as above");
2682
+ console.log(chalk.cyan("\nModel Management:"));
2683
+ console.log(" jfl voice model list List available models");
2684
+ console.log(" jfl voice model download <name> Download a model");
2685
+ console.log(" jfl voice model default <name> Set default model");
2686
+ console.log(chalk.cyan("\nHotkey Mode (macOS):"));
2687
+ console.log(" jfl voice hotkey Start global hotkey listener");
2688
+ console.log(" jfl voice hotkey --mode <mode> Set hotkey mode: auto, tap, or hold");
2689
+ console.log(chalk.gray(" Ctrl+Shift+Space triggers recording"));
2690
+ console.log(chalk.gray(" auto: Tap to toggle, or hold to talk"));
2691
+ console.log(chalk.gray(" tap: Tap to start/stop recording"));
2692
+ console.log(chalk.gray(" hold: Hold to record, release to stop"));
2693
+ console.log(chalk.gray(" Requires Accessibility permission"));
2694
+ console.log(chalk.cyan("\nDaemon Mode (macOS):"));
2695
+ console.log(" jfl voice daemon start Start hotkey listener in background");
2696
+ console.log(" jfl voice daemon stop Stop the background daemon");
2697
+ console.log(" jfl voice daemon status Show daemon status and uptime");
2698
+ console.log(" jfl voice daemon start --mode <m> Start daemon with mode: auto, tap, hold");
2699
+ console.log(chalk.gray(" Daemon survives terminal close"));
2700
+ console.log(chalk.gray(" PID stored in ~/.jfl/voice-daemon.pid"));
2701
+ console.log(chalk.cyan("\nAudio Input:"));
2702
+ console.log(" jfl voice devices List audio input devices");
2703
+ console.log(" jfl voice test Test voice input (record + transcribe)");
2704
+ console.log(" jfl voice test --device <id> Test with specific device");
2705
+ console.log(" jfl voice recording Test recording only (no transcription)");
2706
+ console.log(" jfl voice recording --duration N Record for N seconds (default: 5)");
2707
+ console.log(chalk.cyan("\nServer Commands: (coming soon)"));
2708
+ console.log(chalk.gray(" jfl voice server start Start whisper server"));
2709
+ console.log(chalk.gray(" jfl voice server stop Stop whisper server"));
2710
+ console.log(chalk.gray(" jfl voice server status Show server status"));
2711
+ console.log(chalk.cyan("\nSetup:"));
2712
+ console.log(" jfl voice setup First-time setup wizard");
2713
+ console.log(" jfl voice help Show this help");
2714
+ console.log();
2715
+ }
2716
+ /** Convert linear amplitude to dB */
2717
+ function amplitudeToDb(amplitude) {
2718
+ if (amplitude <= 0)
2719
+ return -100;
2720
+ return 20 * Math.log10(amplitude);
2721
+ }
2722
+ /** Calculate peak amplitude from 16-bit PCM audio chunk */
2723
+ function calculatePeakAmplitude(chunk) {
2724
+ let peak = 0;
2725
+ for (let i = 0; i < chunk.length - 1; i += 2) {
2726
+ const sample = chunk.readInt16LE(i);
2727
+ const amplitude = Math.abs(sample) / 32768;
2728
+ if (amplitude > peak) {
2729
+ peak = amplitude;
2730
+ }
2731
+ }
2732
+ return peak;
2733
+ }
2734
+ // =============================================================================
2735
+ // VS-012: Waveform Visualization
2736
+ // =============================================================================
2737
+ /** Unicode block characters for waveform visualization (sorted by height) */
2738
+ const WAVEFORM_BLOCKS = ["▁", "▂", "▃", "▄", "▅", "▆", "▇", "█"];
2739
+ /** Rolling buffer size for waveform display */
2740
+ const WAVEFORM_BUFFER_SIZE = 7;
2741
+ /** Rolling buffer to store recent audio levels for waveform display */
2742
+ let waveformBuffer = [];
2743
+ /**
2744
+ * Map a dB level to a waveform character
2745
+ * @param dbLevel - Audio level in dB (typically -60 to 0)
2746
+ * @returns A Unicode block character representing the level
2747
+ */
2748
+ function dbToWaveformChar(dbLevel) {
2749
+ // Map dB range (-60 to 0) to character index (0 to 7)
2750
+ // -60 dB or below = lowest bar, 0 dB = highest bar
2751
+ const minDb = -60;
2752
+ const maxDb = 0;
2753
+ const clampedDb = Math.max(minDb, Math.min(maxDb, dbLevel));
2754
+ // Normalize to 0-1 range
2755
+ const normalized = (clampedDb - minDb) / (maxDb - minDb);
2756
+ // Map to character index
2757
+ const index = Math.floor(normalized * (WAVEFORM_BLOCKS.length - 1));
2758
+ return WAVEFORM_BLOCKS[index];
2759
+ }
2760
+ /**
2761
+ * Add a level to the waveform buffer
2762
+ * @param dbLevel - Audio level in dB
2763
+ */
2764
+ function addToWaveformBuffer(dbLevel) {
2765
+ waveformBuffer.push(dbLevel);
2766
+ if (waveformBuffer.length > WAVEFORM_BUFFER_SIZE) {
2767
+ waveformBuffer.shift();
2768
+ }
2769
+ }
2770
+ /**
2771
+ * Reset the waveform buffer (call at start of new recording)
2772
+ */
2773
+ function resetWaveformBuffer() {
2774
+ waveformBuffer = [];
2775
+ }
2776
+ /**
2777
+ * Render the waveform visualization from the rolling buffer
2778
+ * @returns A string like "▁▃▅▇▅▃▁" representing recent audio levels
2779
+ */
2780
+ function renderWaveform() {
2781
+ if (waveformBuffer.length === 0) {
2782
+ // Return minimal bars when no data yet
2783
+ return WAVEFORM_BLOCKS[0].repeat(WAVEFORM_BUFFER_SIZE);
2784
+ }
2785
+ // Pad with low values if buffer isn't full yet
2786
+ const paddedBuffer = [...waveformBuffer];
2787
+ while (paddedBuffer.length < WAVEFORM_BUFFER_SIZE) {
2788
+ paddedBuffer.unshift(-60); // Pad with silence at the start
2789
+ }
2790
+ return paddedBuffer.map(db => dbToWaveformChar(db)).join("");
2791
+ }
2792
+ /**
2793
+ * Check if terminal supports Unicode waveform characters
2794
+ * @returns true if waveform should be displayed
2795
+ */
2796
+ function supportsWaveform() {
2797
+ // Check for dumb terminal
2798
+ if (process.env.TERM === "dumb") {
2799
+ return false;
2800
+ }
2801
+ // Check for Windows cmd.exe (not PowerShell or Windows Terminal)
2802
+ if (process.platform === "win32" && !process.env.WT_SESSION && !process.env.TERM_PROGRAM) {
2803
+ return false;
2804
+ }
2805
+ return true;
2806
+ }
2807
+ /** Copy text to clipboard (cross-platform) */
2808
+ function copyToClipboard(text) {
2809
+ const currentPlatform = platform();
2810
+ try {
2811
+ if (currentPlatform === "darwin") {
2812
+ execSync("pbcopy", { input: text, stdio: ["pipe", "ignore", "ignore"] });
2813
+ return true;
2814
+ }
2815
+ else if (currentPlatform === "linux") {
2816
+ // Try xclip first, then xsel
2817
+ try {
2818
+ execSync("xclip -selection clipboard", { input: text, stdio: ["pipe", "ignore", "ignore"] });
2819
+ return true;
2820
+ }
2821
+ catch {
2822
+ try {
2823
+ execSync("xsel --clipboard --input", { input: text, stdio: ["pipe", "ignore", "ignore"] });
2824
+ return true;
2825
+ }
2826
+ catch {
2827
+ return false;
2828
+ }
2829
+ }
2830
+ }
2831
+ else if (currentPlatform === "win32") {
2832
+ execSync("clip", { input: text, stdio: ["pipe", "ignore", "ignore"] });
2833
+ return true;
2834
+ }
2835
+ return false;
2836
+ }
2837
+ catch {
2838
+ return false;
2839
+ }
2840
+ }
2841
+ /**
2842
+ * Clear the clipboard contents (VS-SEC-3: clipboard hygiene)
2843
+ * Uses osascript on macOS to set clipboard to empty string
2844
+ * Returns true on success, false on failure
2845
+ */
2846
+ function clearClipboard() {
2847
+ const currentPlatform = platform();
2848
+ try {
2849
+ if (currentPlatform === "darwin") {
2850
+ // Use osascript to clear clipboard on macOS
2851
+ execSync(`osascript -e 'set the clipboard to ""'`, { stdio: ["pipe", "ignore", "ignore"] });
2852
+ return true;
2853
+ }
2854
+ else if (currentPlatform === "linux") {
2855
+ // Try xclip first, then xsel
2856
+ try {
2857
+ execSync("xclip -selection clipboard", { input: "", stdio: ["pipe", "ignore", "ignore"] });
2858
+ return true;
2859
+ }
2860
+ catch {
2861
+ try {
2862
+ execSync("xsel --clipboard --input", { input: "", stdio: ["pipe", "ignore", "ignore"] });
2863
+ return true;
2864
+ }
2865
+ catch {
2866
+ return false;
2867
+ }
2868
+ }
2869
+ }
2870
+ else if (currentPlatform === "win32") {
2871
+ // Use PowerShell to clear clipboard on Windows
2872
+ execSync("powershell.exe -command \"Set-Clipboard -Value ''\"", { stdio: ["pipe", "ignore", "ignore"] });
2873
+ return true;
2874
+ }
2875
+ return false;
2876
+ }
2877
+ catch {
2878
+ return false;
2879
+ }
2880
+ }
2881
+ /**
2882
+ * Schedule clipboard clearing after a delay (VS-SEC-3)
2883
+ * @param delaySeconds Seconds to wait before clearing (default: 5)
2884
+ * @returns Timer reference for potential cancellation
2885
+ */
2886
+ function scheduleClipboardClear(delaySeconds = 5) {
2887
+ return setTimeout(() => {
2888
+ const cleared = clearClipboard();
2889
+ if (process.env.DEBUG && cleared) {
2890
+ console.log(chalk.gray(" [debug] Clipboard cleared for security"));
2891
+ }
2892
+ }, delaySeconds * 1000);
2893
+ }
2894
+ /**
2895
+ * Securely zero out a Buffer's contents (VS-SEC-3: buffer hygiene)
2896
+ * Overwrites the buffer with zeros to prevent sensitive audio data from lingering in memory
2897
+ * @param buffer The Buffer to zero out
2898
+ */
2899
+ function zeroBuffer(buffer) {
2900
+ if (buffer && buffer.length > 0) {
2901
+ buffer.fill(0);
2902
+ }
2903
+ }
2904
+ /**
2905
+ * Securely zero out an array of Buffers (VS-SEC-3)
2906
+ * @param buffers Array of Buffers to zero out
2907
+ */
2908
+ function zeroBuffers(buffers) {
2909
+ for (const buffer of buffers) {
2910
+ zeroBuffer(buffer);
2911
+ }
2912
+ // Clear the array reference
2913
+ buffers.length = 0;
2914
+ }
2915
+ /**
2916
+ * Normalize audio buffer to improve transcription quality.
2917
+ * Many microphones record at very low levels which causes blank transcriptions.
2918
+ * This normalizes to a target RMS for better whisper recognition.
2919
+ *
2920
+ * @param audioBuffer - 16-bit PCM audio buffer
2921
+ * @returns Normalized buffer (may be the same buffer if no normalization needed)
2922
+ */
2923
+ function normalizeAudio(audioBuffer) {
2924
+ const samples = audioBuffer.length / 2;
2925
+ let sumSquares = 0;
2926
+ let maxSample = 0;
2927
+ for (let i = 0; i < samples; i++) {
2928
+ const sample = audioBuffer.readInt16LE(i * 2);
2929
+ sumSquares += sample * sample;
2930
+ if (Math.abs(sample) > maxSample)
2931
+ maxSample = Math.abs(sample);
2932
+ }
2933
+ const rms = Math.sqrt(sumSquares / samples);
2934
+ // Target RMS of 3000 for good whisper recognition
2935
+ const targetRms = 3000;
2936
+ if (rms > 0 && rms < targetRms) {
2937
+ const maxGain = 32767 / (maxSample || 1);
2938
+ const desiredGain = targetRms / rms;
2939
+ // Allow up to 3x the clipping-safe gain, accepting some peak clipping
2940
+ const gain = Math.min(desiredGain, maxGain * 3);
2941
+ if (gain > 1.2) {
2942
+ const amplified = Buffer.alloc(audioBuffer.length);
2943
+ for (let i = 0; i < samples; i++) {
2944
+ let sample = audioBuffer.readInt16LE(i * 2);
2945
+ sample = Math.round(Math.max(-32768, Math.min(32767, sample * gain)));
2946
+ amplified.writeInt16LE(sample, i * 2);
2947
+ }
2948
+ if (TENET_VOICE_DEBUG) {
2949
+ console.log(`[Voice] Audio normalized: gain=${gain.toFixed(2)}x, rms=${rms.toFixed(0)}->${(rms * gain).toFixed(0)}`);
2950
+ }
2951
+ return amplified;
2952
+ }
2953
+ }
2954
+ if (TENET_VOICE_DEBUG && rms > 0) {
2955
+ console.log(`[Voice] Audio level ok: rms=${rms.toFixed(0)}, max=${maxSample}`);
2956
+ }
2957
+ return audioBuffer;
2958
+ }
2959
+ /**
2960
+ * Get the name of the currently focused application
2961
+ * - macOS: via osascript
2962
+ * - Linux: via xdotool (X11 only)
2963
+ * - Windows: via PowerShell
2964
+ * VS-010/VS-011: Cross-platform focused app detection
2965
+ * Returns null if unable to determine
2966
+ */
2967
+ function getFocusedApp() {
2968
+ const currentPlatform = platform();
2969
+ try {
2970
+ if (currentPlatform === "darwin") {
2971
+ const result = execSync(`osascript -e 'tell application "System Events" to get name of first application process whose frontmost is true'`, { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] });
2972
+ return result.trim();
2973
+ }
2974
+ else if (currentPlatform === "linux") {
2975
+ // VS-010: Get focused window on Linux X11 using xdotool
2976
+ const windowId = execSync(`xdotool getactivewindow`, { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }).trim();
2977
+ if (windowId) {
2978
+ const windowName = execSync(`xdotool getwindowname ${windowId}`, { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }).trim();
2979
+ return windowName || null;
2980
+ }
2981
+ return null;
2982
+ }
2983
+ else if (currentPlatform === "win32") {
2984
+ // VS-011: Get focused window on Windows using PowerShell
2985
+ const result = execSync(`powershell.exe -command "(Get-Process | Where-Object {$_.MainWindowHandle -eq (Add-Type -MemberDefinition '[DllImport(\\\"user32.dll\\\")]public static extern IntPtr GetForegroundWindow();' -Name 'Win32' -Namespace 'Native' -PassThru)::GetForegroundWindow()}).MainWindowTitle"`, { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] });
2986
+ return result.trim() || null;
2987
+ }
2988
+ return null;
2989
+ }
2990
+ catch {
2991
+ return null;
2992
+ }
2993
+ }
2994
+ /**
2995
+ * Get the platform-appropriate paste shortcut string for display
2996
+ * VS-010/VS-011: Cross-platform paste shortcut labels
2997
+ */
2998
+ function getPasteShortcut() {
2999
+ const currentPlatform = platform();
3000
+ if (currentPlatform === "darwin") {
3001
+ return "Cmd+V";
3002
+ }
3003
+ else if (currentPlatform === "linux") {
3004
+ return "Ctrl+Shift+V";
3005
+ }
3006
+ else {
3007
+ return "Ctrl+V";
3008
+ }
3009
+ }
3010
+ /**
3011
+ * Simulate paste keystroke
3012
+ * - macOS: Cmd+V via osascript
3013
+ * - Linux: Ctrl+Shift+V via xdotool (X11 only)
3014
+ * - Windows: Ctrl+V via PowerShell SendKeys
3015
+ * VS-010/VS-011: Cross-platform paste simulation
3016
+ * Returns true on success, false on failure
3017
+ */
3018
+ function simulatePaste() {
3019
+ const currentPlatform = platform();
3020
+ try {
3021
+ if (currentPlatform === "darwin") {
3022
+ execSync(`osascript -e 'tell application "System Events" to keystroke "v" using command down'`, { stdio: ["pipe", "ignore", "ignore"] });
3023
+ return true;
3024
+ }
3025
+ else if (currentPlatform === "linux") {
3026
+ // VS-010: Linux paste via xdotool (X11 only)
3027
+ // Use Ctrl+Shift+V for terminal compatibility
3028
+ execSync(`xdotool key --clearmodifiers ctrl+shift+v`, { stdio: ["pipe", "ignore", "ignore"] });
3029
+ return true;
3030
+ }
3031
+ else if (currentPlatform === "win32") {
3032
+ // VS-011: Windows paste via PowerShell SendKeys
3033
+ // ^v is Ctrl+V in SendKeys notation
3034
+ execSync(`powershell.exe -command "Add-Type -AssemblyName System.Windows.Forms; [System.Windows.Forms.SendKeys]::SendWait('^v')"`, { stdio: ["pipe", "ignore", "ignore"] });
3035
+ return true;
3036
+ }
3037
+ return false;
3038
+ }
3039
+ catch {
3040
+ return false;
3041
+ }
3042
+ }
3043
+ /**
3044
+ * Show a desktop notification with title and message
3045
+ * - macOS: via osascript
3046
+ * - Linux: via notify-send (libnotify)
3047
+ * - Windows: via PowerShell toast notification
3048
+ * VS-010/VS-011: Cross-platform notification support
3049
+ * Returns true on success, false on failure
3050
+ */
3051
+ function showNotification(title, message) {
3052
+ const currentPlatform = platform();
3053
+ try {
3054
+ if (currentPlatform === "darwin") {
3055
+ // Escape backslashes and double quotes for AppleScript strings
3056
+ const escapedMessage = message.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
3057
+ const escapedTitle = title.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
3058
+ execSync(`osascript -e 'display notification "${escapedMessage}" with title "${escapedTitle}"'`, { stdio: ["pipe", "ignore", "ignore"] });
3059
+ return true;
3060
+ }
3061
+ else if (currentPlatform === "linux") {
3062
+ // VS-010: Linux notification via notify-send (part of libnotify)
3063
+ // Escape single quotes for shell
3064
+ const escapedMessage = message.replace(/'/g, "'\\''");
3065
+ const escapedTitle = title.replace(/'/g, "'\\''");
3066
+ execSync(`notify-send '${escapedTitle}' '${escapedMessage}'`, { stdio: ["pipe", "ignore", "ignore"] });
3067
+ return true;
3068
+ }
3069
+ else if (currentPlatform === "win32") {
3070
+ // VS-011: Windows toast notification via PowerShell
3071
+ // Escape for PowerShell string
3072
+ const escapedMessage = message.replace(/'/g, "''").replace(/`/g, "``");
3073
+ const escapedTitle = title.replace(/'/g, "''").replace(/`/g, "``");
3074
+ execSync(`powershell.exe -command "[Windows.UI.Notifications.ToastNotificationManager, Windows.UI.Notifications, ContentType = WindowsRuntime] | Out-Null; $xml = [Windows.UI.Notifications.ToastNotificationManager]::GetTemplateContent([Windows.UI.Notifications.ToastTemplateType]::ToastText02); $texts = $xml.GetElementsByTagName('text'); $texts[0].AppendChild($xml.CreateTextNode('${escapedTitle}')); $texts[1].AppendChild($xml.CreateTextNode('${escapedMessage}')); [Windows.UI.Notifications.ToastNotificationManager]::CreateToastNotifier('JFL Voice').Show([Windows.UI.Notifications.ToastNotification]::new($xml))"`, { stdio: ["pipe", "ignore", "ignore"] });
3075
+ return true;
3076
+ }
3077
+ return false;
3078
+ }
3079
+ catch {
3080
+ return false;
3081
+ }
3082
+ }
3083
+ // =============================================================================
3084
+ // VS-UX-2: System Recording Indicator
3085
+ // =============================================================================
3086
+ /**
3087
+ * Show system recording indicator via desktop notification
3088
+ * This provides a visual indicator that recording is active, even when terminal is not visible
3089
+ * VS-010/VS-011: Now works on macOS, Linux, and Windows
3090
+ * Returns true on success, false on failure
3091
+ */
3092
+ export function showRecordingIndicator() {
3093
+ return showNotification("Voice Recording", "Recording started... Press Ctrl+Shift+Space to stop");
3094
+ }
3095
+ /**
3096
+ * Hide system recording indicator (show stopped notification)
3097
+ * VS-010/VS-011: Now works on macOS, Linux, and Windows
3098
+ * Returns true on success, false on failure
3099
+ */
3100
+ export function hideRecordingIndicator(reason) {
3101
+ const messages = {
3102
+ stopped: "Recording stopped",
3103
+ cancelled: "Recording cancelled",
3104
+ completed: "Recording complete - transcribing...",
3105
+ };
3106
+ const message = messages[reason || "stopped"] || "Recording stopped";
3107
+ return showNotification("Voice Recording", message);
3108
+ }
3109
+ /**
3110
+ * Preview transcript with configurable auto-send countdown
3111
+ *
3112
+ * Behavior:
3113
+ * - Shows transcript with countdown (if timeout > 0)
3114
+ * - Enter: send immediately
3115
+ * - Esc: cancel entirely
3116
+ * - Any other key: pause countdown and enter edit mode
3117
+ * - In edit mode: user can modify text, Enter to send, Esc to cancel
3118
+ * - If countdown reaches 0: send automatically
3119
+ *
3120
+ * @param transcript - The transcribed text to preview
3121
+ * @param timeoutSeconds - Countdown duration (0 = disabled, require explicit Enter)
3122
+ * @returns PreviewResult indicating user action and final text
3123
+ */
3124
+ async function previewTranscript(transcript, timeoutSeconds) {
3125
+ return new Promise((resolve) => {
3126
+ // Set up raw mode for immediate key detection
3127
+ const stdin = process.stdin;
3128
+ const stdout = process.stdout;
3129
+ // If stdin is not a TTY (e.g., running as daemon), skip preview and auto-send
3130
+ if (!stdin.isTTY) {
3131
+ resolve({ action: "send", text: transcript });
3132
+ return;
3133
+ }
3134
+ // Store original mode to restore later
3135
+ const wasRaw = stdin.isRaw;
3136
+ stdin.setRawMode(true);
3137
+ stdin.resume();
3138
+ stdin.setEncoding("utf8");
3139
+ let currentText = transcript;
3140
+ let countdownValue = timeoutSeconds;
3141
+ let countdownInterval = null;
3142
+ let inEditMode = false;
3143
+ let editBuffer = "";
3144
+ let cursorPos = 0;
3145
+ // Helper to clear the current line and write new content
3146
+ const clearAndWrite = (text) => {
3147
+ stdout.write("\r\x1b[K" + text);
3148
+ };
3149
+ // Helper to show the preview line
3150
+ const showPreview = () => {
3151
+ if (inEditMode) {
3152
+ // Edit mode: show editable text with cursor
3153
+ const before = editBuffer.slice(0, cursorPos);
3154
+ const cursor = editBuffer[cursorPos] || " ";
3155
+ const after = editBuffer.slice(cursorPos + 1);
3156
+ clearAndWrite(chalk.gray(" Edit: ") +
3157
+ chalk.cyan(before) +
3158
+ chalk.bgCyan.black(cursor) +
3159
+ chalk.cyan(after) +
3160
+ chalk.gray(" [Enter=send, Esc=cancel]"));
3161
+ }
3162
+ else if (timeoutSeconds === 0) {
3163
+ // No countdown - require explicit action
3164
+ clearAndWrite(chalk.gray(" ") +
3165
+ chalk.cyan(`"${currentText}"`) +
3166
+ chalk.gray(" [Enter=send, Esc=cancel, any key=edit]"));
3167
+ }
3168
+ else {
3169
+ // Show countdown
3170
+ const countdownDisplay = countdownValue.toFixed(1);
3171
+ clearAndWrite(chalk.gray(" ") +
3172
+ chalk.cyan(`"${currentText}"`) +
3173
+ chalk.yellow(` Sending in ${countdownDisplay}s...`) +
3174
+ chalk.gray(" [Enter=send, Esc=cancel, any key=edit]"));
3175
+ }
3176
+ };
3177
+ // Cleanup function
3178
+ const cleanup = () => {
3179
+ if (countdownInterval) {
3180
+ clearInterval(countdownInterval);
3181
+ countdownInterval = null;
3182
+ }
3183
+ stdin.setRawMode(wasRaw || false);
3184
+ stdin.removeListener("data", onData);
3185
+ stdout.write("\n");
3186
+ };
3187
+ // Start countdown if enabled
3188
+ if (timeoutSeconds > 0) {
3189
+ countdownInterval = setInterval(() => {
3190
+ countdownValue -= 0.1;
3191
+ if (countdownValue <= 0) {
3192
+ cleanup();
3193
+ resolve({ action: "send", text: currentText });
3194
+ }
3195
+ else {
3196
+ showPreview();
3197
+ }
3198
+ }, 100);
3199
+ }
3200
+ // Handle key input
3201
+ const onData = (key) => {
3202
+ // Handle special keys
3203
+ const keyCode = key.charCodeAt(0);
3204
+ if (inEditMode) {
3205
+ // Edit mode key handling
3206
+ if (key === "\r" || key === "\n") {
3207
+ // Enter - send the edited text
3208
+ cleanup();
3209
+ resolve({ action: "send", text: editBuffer });
3210
+ }
3211
+ else if (key === "\x1b") {
3212
+ // Check for escape sequences (arrow keys, etc.)
3213
+ // Simple escape = cancel
3214
+ // Arrow keys come as \x1b[A, \x1b[B, \x1b[C, \x1b[D
3215
+ // We'll handle simple escape for now
3216
+ cleanup();
3217
+ resolve({ action: "cancel" });
3218
+ }
3219
+ else if (key === "\x7f" || key === "\b") {
3220
+ // Backspace - delete character before cursor
3221
+ if (cursorPos > 0) {
3222
+ editBuffer = editBuffer.slice(0, cursorPos - 1) + editBuffer.slice(cursorPos);
3223
+ cursorPos--;
3224
+ showPreview();
3225
+ }
3226
+ }
3227
+ else if (key === "\x1b[D") {
3228
+ // Left arrow
3229
+ if (cursorPos > 0) {
3230
+ cursorPos--;
3231
+ showPreview();
3232
+ }
3233
+ }
3234
+ else if (key === "\x1b[C") {
3235
+ // Right arrow
3236
+ if (cursorPos < editBuffer.length) {
3237
+ cursorPos++;
3238
+ showPreview();
3239
+ }
3240
+ }
3241
+ else if (key === "\x03") {
3242
+ // Ctrl+C - cancel
3243
+ cleanup();
3244
+ resolve({ action: "cancel" });
3245
+ }
3246
+ else if (keyCode >= 32 && keyCode < 127) {
3247
+ // Printable character - insert at cursor
3248
+ editBuffer = editBuffer.slice(0, cursorPos) + key + editBuffer.slice(cursorPos);
3249
+ cursorPos++;
3250
+ showPreview();
3251
+ }
3252
+ }
3253
+ else {
3254
+ // Preview mode key handling
3255
+ if (key === "\r" || key === "\n") {
3256
+ // Enter - send immediately
3257
+ cleanup();
3258
+ resolve({ action: "send", text: currentText });
3259
+ }
3260
+ else if (key === "\x1b") {
3261
+ // Escape - cancel
3262
+ cleanup();
3263
+ resolve({ action: "cancel" });
3264
+ }
3265
+ else if (key === "\x03") {
3266
+ // Ctrl+C - cancel
3267
+ cleanup();
3268
+ resolve({ action: "cancel" });
3269
+ }
3270
+ else {
3271
+ // Any other key - enter edit mode
3272
+ if (countdownInterval) {
3273
+ clearInterval(countdownInterval);
3274
+ countdownInterval = null;
3275
+ }
3276
+ inEditMode = true;
3277
+ editBuffer = currentText;
3278
+ // If printable character, start with it
3279
+ if (keyCode >= 32 && keyCode < 127) {
3280
+ editBuffer = currentText + key;
3281
+ cursorPos = editBuffer.length;
3282
+ }
3283
+ else {
3284
+ cursorPos = editBuffer.length;
3285
+ }
3286
+ showPreview();
3287
+ }
3288
+ }
3289
+ };
3290
+ stdin.on("data", onData);
3291
+ // Show initial preview
3292
+ console.log(); // New line before preview
3293
+ showPreview();
3294
+ });
3295
+ }
3296
+ /**
3297
+ * Voice Slash Command - /voice
3298
+ *
3299
+ * Records audio with VAD (voice activity detection), transcribes it,
3300
+ * and offers to send to Claude Code or copy to clipboard.
3301
+ */
3302
+ export async function voiceSlashCommand(options = {}) {
3303
+ const silenceThresholdDb = options.silenceThresholdDb ?? VAD_SILENCE_THRESHOLD_DB;
3304
+ const silenceDurationMs = options.silenceDurationMs ?? VAD_SILENCE_DURATION_MS;
3305
+ const maxDurationSecs = options.maxDurationSecs ?? 60;
3306
+ console.log(chalk.bold("\n🎤 Voice Recording\n"));
3307
+ console.log(chalk.gray(" Speak into your microphone. Recording will stop automatically"));
3308
+ console.log(chalk.gray(` after ${(silenceDurationMs / 1000).toFixed(1)}s of silence, or press Ctrl+C to stop.\n`));
3309
+ // Step 1: Check if whisper server is running
3310
+ const serverError = checkServerRunning();
3311
+ if (serverError) {
3312
+ handleVoiceError(serverError);
3313
+ return;
3314
+ }
3315
+ // Step 2: Check for auth token
3316
+ const authError = checkAuthToken();
3317
+ if (authError) {
3318
+ handleVoiceError(authError);
3319
+ return;
3320
+ }
3321
+ const authToken = readAuthToken();
3322
+ const socketPath = getVoiceSocketPath();
3323
+ // Step 3: Initialize audio recorder
3324
+ let recorder;
3325
+ try {
3326
+ recorder = new AudioRecorder({
3327
+ device: options.device,
3328
+ sampleRate: 16000,
3329
+ });
3330
+ }
3331
+ catch (error) {
3332
+ if (error instanceof VoiceError) {
3333
+ handleVoiceError(error);
3334
+ }
3335
+ else {
3336
+ handleVoiceError(createVoiceError(error instanceof Error ? error : new Error(String(error))));
3337
+ }
3338
+ return;
3339
+ }
3340
+ // Collect audio data
3341
+ const audioChunks = [];
3342
+ let peakLevel = 0;
3343
+ let recordingError = null;
3344
+ // VAD state
3345
+ let silenceStartTime = null;
3346
+ let hasVoiceActivity = false;
3347
+ const startTime = Date.now();
3348
+ // VS-012: Reset waveform buffer for new recording
3349
+ resetWaveformBuffer();
3350
+ const useWaveform = supportsWaveform();
3351
+ // Spinner for recording indicator
3352
+ const spinner = ora({
3353
+ text: chalk.cyan("Recording...") + chalk.gray(" (waiting for voice)"),
3354
+ prefixText: " ",
3355
+ spinner: "dots",
3356
+ });
3357
+ // Handle Ctrl+C gracefully
3358
+ let interrupted = false;
3359
+ const handleInterrupt = () => {
3360
+ interrupted = true;
3361
+ recorder.stop();
3362
+ };
3363
+ process.on("SIGINT", handleInterrupt);
3364
+ process.on("SIGTERM", handleInterrupt);
3365
+ // Set up recorder event handlers
3366
+ recorder.on("data", (chunk) => {
3367
+ audioChunks.push(chunk);
3368
+ // Calculate peak level
3369
+ const chunkPeak = calculatePeakAmplitude(chunk);
3370
+ if (chunkPeak > peakLevel) {
3371
+ peakLevel = chunkPeak;
3372
+ }
3373
+ // VAD: Check if audio level is above silence threshold
3374
+ const peakDb = amplitudeToDb(chunkPeak);
3375
+ const isSilent = peakDb < silenceThresholdDb;
3376
+ // VS-012: Add level to waveform buffer on every chunk
3377
+ addToWaveformBuffer(peakDb);
3378
+ // VS-012: Update spinner with waveform on every chunk for real-time feedback
3379
+ const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
3380
+ if (useWaveform) {
3381
+ const waveform = renderWaveform();
3382
+ spinner.text = chalk.cyan("Recording") + chalk.gray(` ${waveform} (${elapsed}s)`);
3383
+ }
3384
+ if (!isSilent) {
3385
+ // Voice activity detected
3386
+ hasVoiceActivity = true;
3387
+ silenceStartTime = null;
3388
+ }
3389
+ else if (hasVoiceActivity) {
3390
+ // Silence detected after voice activity
3391
+ if (silenceStartTime === null) {
3392
+ silenceStartTime = Date.now();
3393
+ }
3394
+ else {
3395
+ const silenceDuration = Date.now() - silenceStartTime;
3396
+ // Update spinner to show silence detection
3397
+ const remaining = Math.max(0, silenceDurationMs - silenceDuration);
3398
+ if (remaining > 0) {
3399
+ if (useWaveform) {
3400
+ const waveform = renderWaveform();
3401
+ spinner.text = chalk.cyan("Recording") + chalk.gray(` ${waveform}`) + chalk.yellow(` (silence: ${(remaining / 1000).toFixed(1)}s)`);
3402
+ }
3403
+ else {
3404
+ spinner.text = chalk.cyan("Recording") + chalk.yellow(` (silence: ${(remaining / 1000).toFixed(1)}s until stop)`);
3405
+ }
3406
+ }
3407
+ // Stop recording after silence duration
3408
+ if (silenceDuration >= silenceDurationMs) {
3409
+ recorder.stop();
3410
+ }
3411
+ }
3412
+ }
3413
+ // Check max duration
3414
+ const elapsedSecs = (Date.now() - startTime) / 1000;
3415
+ if (elapsedSecs >= maxDurationSecs) {
3416
+ recorder.stop();
3417
+ }
3418
+ });
3419
+ recorder.on("error", (error) => {
3420
+ recordingError = error;
3421
+ recorder.stop();
3422
+ });
3423
+ // Start recording
3424
+ try {
3425
+ await recorder.start();
3426
+ spinner.start();
3427
+ // VS-UX-2: Show system recording indicator (notification on macOS)
3428
+ showRecordingIndicator();
3429
+ }
3430
+ catch (error) {
3431
+ const voiceError = new VoiceError(VoiceErrorType.RECORDING_FAILED, {
3432
+ originalError: error instanceof Error ? error : new Error(String(error)),
3433
+ context: { device: options.device },
3434
+ });
3435
+ handleVoiceError(voiceError);
3436
+ process.removeListener("SIGINT", handleInterrupt);
3437
+ process.removeListener("SIGTERM", handleInterrupt);
3438
+ return;
3439
+ }
3440
+ // Wait for recording to stop
3441
+ await new Promise((resolve) => {
3442
+ const checkStopped = () => {
3443
+ if (recorder.getState() === "idle" || recorder.getState() === "error") {
3444
+ resolve();
3445
+ }
3446
+ else {
3447
+ setTimeout(checkStopped, 50);
3448
+ }
3449
+ };
3450
+ // Small delay to let the stop signal propagate
3451
+ setTimeout(checkStopped, 100);
3452
+ });
3453
+ // Clean up interrupt handler
3454
+ process.removeListener("SIGINT", handleInterrupt);
3455
+ process.removeListener("SIGTERM", handleInterrupt);
3456
+ // Stop spinner
3457
+ spinner.stop();
3458
+ // Handle interruption
3459
+ if (interrupted) {
3460
+ // VS-UX-2: Show cancelled indicator
3461
+ hideRecordingIndicator("cancelled");
3462
+ console.log(chalk.yellow("\n Recording stopped by user.\n"));
3463
+ }
3464
+ // Handle recording error
3465
+ if (recordingError !== null) {
3466
+ // VS-UX-2: Show stopped indicator on error
3467
+ hideRecordingIndicator("stopped");
3468
+ const voiceError = new VoiceError(VoiceErrorType.RECORDING_FAILED, {
3469
+ originalError: recordingError,
3470
+ context: { device: options.device },
3471
+ });
3472
+ handleVoiceError(voiceError);
3473
+ return;
3474
+ }
3475
+ // Check if we got any audio
3476
+ const totalBytes = audioChunks.reduce((sum, chunk) => sum + chunk.length, 0);
3477
+ if (totalBytes === 0) {
3478
+ // VS-UX-2: Show stopped indicator when no audio
3479
+ hideRecordingIndicator("stopped");
3480
+ const voiceError = new VoiceError(VoiceErrorType.MIC_UNAVAILABLE, {
3481
+ context: { totalBytes: 0, device: options.device },
3482
+ });
3483
+ handleVoiceError(voiceError);
3484
+ return;
3485
+ }
3486
+ // Check if there was any voice activity
3487
+ if (!hasVoiceActivity) {
3488
+ // VS-UX-2: Show stopped indicator when no voice detected
3489
+ hideRecordingIndicator("stopped");
3490
+ const voiceError = new VoiceError(VoiceErrorType.TRANSCRIPTION_EMPTY, {
3491
+ context: { reason: "No voice activity detected", peakLevel },
3492
+ });
3493
+ handleVoiceError(voiceError);
3494
+ return;
3495
+ }
3496
+ // Combine all audio chunks and normalize for better transcription
3497
+ const audioBuffer = normalizeAudio(Buffer.concat(audioChunks));
3498
+ const durationSecs = totalBytes / (16000 * 2); // 16kHz, 16-bit
3499
+ // VS-UX-2: Show completed indicator when moving to transcription
3500
+ hideRecordingIndicator("completed");
3501
+ console.log(chalk.gray(`\n Recorded ${durationSecs.toFixed(1)}s of audio.`));
3502
+ console.log(chalk.gray(" Transcribing...\n"));
3503
+ // Step 4: Connect to whisper server and send audio
3504
+ const transcribeSpinner = ora({
3505
+ text: "Transcribing...",
3506
+ prefixText: " ",
3507
+ }).start();
3508
+ const client = new VoiceClient({
3509
+ socketPath,
3510
+ authToken,
3511
+ maxReconnectAttempts: 1,
3512
+ });
3513
+ let transcription = "";
3514
+ let transcriptionReceived = false;
3515
+ let transcriptionError = null;
3516
+ client.onTranscript((text, isFinal) => {
3517
+ // Accept partial transcriptions too (in case final never arrives)
3518
+ if (text && text.trim() && !text.includes("[BLANK_AUDIO]")) {
3519
+ transcription = text;
3520
+ }
3521
+ if (isFinal) {
3522
+ transcriptionReceived = true;
3523
+ }
3524
+ });
3525
+ client.onError((error) => {
3526
+ transcriptionError = error;
3527
+ });
3528
+ try {
3529
+ await client.connect();
3530
+ // Send audio data and wait for it to be sent
3531
+ client.sendAudio(audioBuffer);
3532
+ // Wait for audio to be sent before signaling end
3533
+ await new Promise(resolve => setTimeout(resolve, 500));
3534
+ // Signal end of audio
3535
+ client.endAudio();
3536
+ // Wait for transcription with timeout
3537
+ const timeout = 30000; // 30 seconds
3538
+ const transcribeStartTime = Date.now();
3539
+ while (!transcriptionReceived && !transcriptionError) {
3540
+ if (Date.now() - transcribeStartTime > timeout) {
3541
+ transcriptionError = new VoiceError(VoiceErrorType.TIMEOUT, {
3542
+ context: { timeout, operation: "transcription" },
3543
+ recoverable: true,
3544
+ audioBuffer,
3545
+ });
3546
+ break;
3547
+ }
3548
+ await new Promise((resolve) => setTimeout(resolve, 100));
3549
+ }
3550
+ client.disconnect();
3551
+ }
3552
+ catch (error) {
3553
+ client.disconnect();
3554
+ const voiceError = error instanceof VoiceError
3555
+ ? error
3556
+ : createVoiceError(error instanceof Error ? error : new Error(String(error)), { operation: "connect" }, audioBuffer);
3557
+ handleVoiceError(voiceError, transcribeSpinner);
3558
+ return;
3559
+ }
3560
+ // Handle transcription error
3561
+ if (transcriptionError) {
3562
+ const voiceError = transcriptionError instanceof VoiceError
3563
+ ? transcriptionError
3564
+ : createVoiceError(transcriptionError, { operation: "transcription" }, audioBuffer);
3565
+ handleVoiceError(voiceError, transcribeSpinner);
3566
+ return;
3567
+ }
3568
+ // Handle empty transcription
3569
+ if (!transcription || transcription.trim() === "") {
3570
+ const voiceError = new VoiceError(VoiceErrorType.TRANSCRIPTION_EMPTY, {
3571
+ context: { audioLength: totalBytes, durationSecs, peakLevel },
3572
+ recoverable: true,
3573
+ audioBuffer,
3574
+ });
3575
+ handleVoiceError(voiceError, transcribeSpinner);
3576
+ return;
3577
+ }
3578
+ transcribeSpinner.succeed("Transcription complete!");
3579
+ // Step 5: Display transcription
3580
+ console.log();
3581
+ console.log(chalk.bold(" Transcription:"));
3582
+ console.log();
3583
+ console.log(chalk.cyan(` "${transcription.trim()}"`));
3584
+ console.log();
3585
+ // Step 6: Ask what to do with the transcription
3586
+ const { action } = await inquirer.prompt([
3587
+ {
3588
+ type: "list",
3589
+ name: "action",
3590
+ message: "What would you like to do?",
3591
+ choices: [
3592
+ { name: "Send to Claude Code (paste into conversation)", value: "send" },
3593
+ { name: "Copy to clipboard", value: "copy" },
3594
+ { name: "Cancel (do nothing)", value: "cancel" },
3595
+ ],
3596
+ default: "send",
3597
+ },
3598
+ ]);
3599
+ if (action === "send") {
3600
+ // For "send to Claude Code", we simply output the text so the user can paste it
3601
+ // In a full implementation, this could integrate with Claude Code's API
3602
+ console.log(chalk.green("\n ✓ Text ready to send to Claude Code:\n"));
3603
+ console.log(chalk.white(` ${transcription.trim()}`));
3604
+ console.log();
3605
+ console.log(chalk.gray(" Copy and paste this into your Claude Code conversation."));
3606
+ console.log();
3607
+ }
3608
+ else if (action === "copy") {
3609
+ const copied = copyToClipboard(transcription.trim());
3610
+ if (copied) {
3611
+ console.log(chalk.green("\n ✓ Copied to clipboard!"));
3612
+ }
3613
+ else {
3614
+ console.log(chalk.yellow("\n ⚠ Could not copy to clipboard."));
3615
+ console.log(chalk.gray(" On Linux, install xclip or xsel for clipboard support."));
3616
+ console.log(chalk.gray("\n Text:"));
3617
+ console.log(chalk.white(` ${transcription.trim()}`));
3618
+ }
3619
+ console.log();
3620
+ }
3621
+ else {
3622
+ console.log(chalk.gray("\n Cancelled.\n"));
3623
+ }
3624
+ }
3625
+ // =============================================================================
3626
+ // Hotkey Command - Global Keyboard Shortcut for Voice Recording
3627
+ // =============================================================================
3628
+ /**
3629
+ * Check if running on Wayland (Linux only)
3630
+ * VS-010: Wayland detection for Linux hotkey support
3631
+ * @returns true if running on Wayland, false otherwise
3632
+ */
3633
+ function isWayland() {
3634
+ if (platform() !== "linux") {
3635
+ return false;
3636
+ }
3637
+ // Check for Wayland indicators
3638
+ // WAYLAND_DISPLAY is set when running under a Wayland compositor
3639
+ // XDG_SESSION_TYPE is set to "wayland" on Wayland sessions
3640
+ return !!(process.env.WAYLAND_DISPLAY || process.env.XDG_SESSION_TYPE === "wayland");
3641
+ }
3642
+ /**
3643
+ * Check if Accessibility/keyboard permission is available
3644
+ * - macOS: Requires Accessibility permission for global hotkey capture
3645
+ * - Linux X11: Uses XGrabKey, no special permissions needed
3646
+ * - Linux Wayland: Not supported (returns false)
3647
+ * - Windows: Uses RegisterHotKey, no admin privileges needed
3648
+ */
3649
+ function checkAccessibilityPermission() {
3650
+ const currentPlatform = platform();
3651
+ if (currentPlatform === "darwin") {
3652
+ try {
3653
+ // Use osascript to check if Accessibility permission is granted
3654
+ // This method attempts to list processes, which requires accessibility
3655
+ const result = execSync(`osascript -e 'tell application "System Events" to get name of first process'`, { encoding: "utf-8", timeout: 5000, stdio: ["pipe", "pipe", "pipe"] });
3656
+ return true;
3657
+ }
3658
+ catch (error) {
3659
+ // If the script fails, accessibility permission is likely not granted
3660
+ return false;
3661
+ }
3662
+ }
3663
+ else if (currentPlatform === "linux") {
3664
+ // On Linux X11, we just need to verify X11 is available
3665
+ // The node-global-key-listener uses XGrabKey which doesn't need special permissions
3666
+ // Just check that we're not on Wayland (handled separately)
3667
+ return !isWayland();
3668
+ }
3669
+ else if (currentPlatform === "win32") {
3670
+ // On Windows, RegisterHotKey doesn't require special permissions
3671
+ // Works without admin privileges
3672
+ return true;
3673
+ }
3674
+ return false;
3675
+ }
3676
+ /**
3677
+ * Open system settings for keyboard/accessibility permissions
3678
+ * - macOS: Opens Accessibility pane in System Settings
3679
+ * - Linux: Prints instructions (no GUI settings for X11)
3680
+ * - Windows: Prints instructions (no special permissions needed)
3681
+ * VS-010/VS-011: Cross-platform settings guidance
3682
+ */
3683
+ function openAccessibilitySettings() {
3684
+ const currentPlatform = platform();
3685
+ if (currentPlatform === "darwin") {
3686
+ try {
3687
+ // macOS 13+ uses System Settings, older versions use System Preferences
3688
+ execSync(`open "x-apple.systempreferences:com.apple.preference.security?Privacy_Accessibility"`, { stdio: "ignore" });
3689
+ }
3690
+ catch {
3691
+ // Fallback for older macOS versions
3692
+ try {
3693
+ execSync(`open "/System/Library/PreferencePanes/Security.prefPane"`, { stdio: "ignore" });
3694
+ }
3695
+ catch {
3696
+ console.log(chalk.gray(" Could not open System Settings automatically."));
3697
+ }
3698
+ }
3699
+ }
3700
+ else if (currentPlatform === "linux") {
3701
+ // VS-010: Linux doesn't have a system settings pane for X11 keyboard access
3702
+ // Just print helpful information
3703
+ console.log(chalk.gray(" Linux X11 hotkey requirements:"));
3704
+ console.log(chalk.gray(" - Install xdotool: sudo apt-get install xdotool"));
3705
+ console.log(chalk.gray(" - Ensure you're running X11, not Wayland"));
3706
+ console.log(chalk.gray(" - Check session type: echo $XDG_SESSION_TYPE"));
3707
+ }
3708
+ else if (currentPlatform === "win32") {
3709
+ // VS-011: Windows doesn't need special permissions for RegisterHotKey
3710
+ console.log(chalk.gray(" Windows hotkey should work without special permissions."));
3711
+ console.log(chalk.gray(" If you have issues, try running as Administrator."));
3712
+ }
3713
+ }
3714
+ // =============================================================================
3715
+ // VS-013: Voice Daemon Commands
3716
+ // =============================================================================
3717
+ /**
3718
+ * Read the daemon PID from the PID file
3719
+ * @returns The PID number or null if not found/invalid
3720
+ */
3721
+ function readDaemonPid() {
3722
+ const pidPath = getVoiceDaemonPidPath();
3723
+ if (!existsSync(pidPath)) {
3724
+ return null;
3725
+ }
3726
+ try {
3727
+ const pidStr = readFileSync(pidPath, "utf-8").trim();
3728
+ const pid = parseInt(pidStr, 10);
3729
+ if (isNaN(pid) || pid <= 0) {
3730
+ return null;
3731
+ }
3732
+ return pid;
3733
+ }
3734
+ catch {
3735
+ return null;
3736
+ }
3737
+ }
3738
+ /**
3739
+ * Check if a process with the given PID is running
3740
+ * @param pid - Process ID to check
3741
+ * @returns true if process is running, false otherwise
3742
+ */
3743
+ function isProcessRunning(pid) {
3744
+ try {
3745
+ // Sending signal 0 doesn't kill the process, just checks if it exists
3746
+ process.kill(pid, 0);
3747
+ return true;
3748
+ }
3749
+ catch (error) {
3750
+ return false;
3751
+ }
3752
+ }
3753
+ /**
3754
+ * Get daemon uptime if running
3755
+ * @returns Uptime string or null if not running
3756
+ */
3757
+ function getDaemonUptime() {
3758
+ const pidPath = getVoiceDaemonPidPath();
3759
+ if (!existsSync(pidPath)) {
3760
+ return null;
3761
+ }
3762
+ try {
3763
+ const stats = statSync(pidPath);
3764
+ const startTime = stats.mtime.getTime();
3765
+ const uptime = Date.now() - startTime;
3766
+ // Format uptime
3767
+ const seconds = Math.floor(uptime / 1000) % 60;
3768
+ const minutes = Math.floor(uptime / (1000 * 60)) % 60;
3769
+ const hours = Math.floor(uptime / (1000 * 60 * 60)) % 24;
3770
+ const days = Math.floor(uptime / (1000 * 60 * 60 * 24));
3771
+ if (days > 0) {
3772
+ return `${days}d ${hours}h ${minutes}m`;
3773
+ }
3774
+ else if (hours > 0) {
3775
+ return `${hours}h ${minutes}m ${seconds}s`;
3776
+ }
3777
+ else if (minutes > 0) {
3778
+ return `${minutes}m ${seconds}s`;
3779
+ }
3780
+ else {
3781
+ return `${seconds}s`;
3782
+ }
3783
+ }
3784
+ catch {
3785
+ return null;
3786
+ }
3787
+ }
3788
+ /**
3789
+ * Daemon Start Command - Launch voice hotkey listener in background
3790
+ *
3791
+ * Spawns a detached child process running the hotkey listener that survives
3792
+ * terminal close. PID is stored in ~/.jfl/voice-daemon.pid
3793
+ */
3794
+ export async function daemonStartCommand(options = {}) {
3795
+ // Check platform - daemon only supported on macOS for now
3796
+ if (platform() !== "darwin") {
3797
+ const error = new VoiceError(VoiceErrorType.PLATFORM_NOT_SUPPORTED);
3798
+ handleVoiceError(error);
3799
+ return;
3800
+ }
3801
+ console.log(chalk.bold("\n🎤 Voice Daemon\n"));
3802
+ // Check if daemon is already running
3803
+ const existingPid = readDaemonPid();
3804
+ if (existingPid && isProcessRunning(existingPid)) {
3805
+ console.log(chalk.yellow(" Daemon is already running."));
3806
+ console.log(chalk.gray(` PID: ${existingPid}`));
3807
+ console.log(chalk.gray(` Uptime: ${getDaemonUptime()}`));
3808
+ console.log();
3809
+ console.log(chalk.gray(" Use 'jfl voice daemon stop' to stop it."));
3810
+ console.log(chalk.gray(" Use 'jfl voice daemon status' to check status."));
3811
+ console.log();
3812
+ return;
3813
+ }
3814
+ // Check Accessibility permission first
3815
+ console.log(chalk.gray(" Checking Accessibility permission..."));
3816
+ if (!checkAccessibilityPermission()) {
3817
+ console.log();
3818
+ const error = new VoiceError(VoiceErrorType.ACCESSIBILITY_NOT_GRANTED);
3819
+ handleVoiceError(error);
3820
+ console.log(chalk.cyan(" Opening System Settings..."));
3821
+ openAccessibilitySettings();
3822
+ console.log();
3823
+ console.log(chalk.yellow(" After granting permission:"));
3824
+ console.log(chalk.gray(" 1. Add your terminal app to Accessibility"));
3825
+ console.log(chalk.gray(" 2. Restart your terminal"));
3826
+ console.log(chalk.gray(" 3. Run 'jfl voice daemon start' again"));
3827
+ console.log();
3828
+ return;
3829
+ }
3830
+ console.log(chalk.green(" ✓ Accessibility permission granted"));
3831
+ // Check other prerequisites (server, auth)
3832
+ const serverError = checkServerRunning();
3833
+ if (serverError) {
3834
+ handleVoiceError(serverError);
3835
+ return;
3836
+ }
3837
+ const authError = checkAuthToken();
3838
+ if (authError) {
3839
+ handleVoiceError(authError);
3840
+ return;
3841
+ }
3842
+ console.log(chalk.gray(" Starting daemon..."));
3843
+ // Get the path to the current executable (jfl CLI)
3844
+ const jflPath = process.argv[1];
3845
+ const nodePath = process.argv[0];
3846
+ // Build the command arguments
3847
+ const args = ["voice", "hotkey"];
3848
+ if (options.mode) {
3849
+ args.push("--mode", options.mode);
3850
+ }
3851
+ // Spawn detached process
3852
+ const logPath = getVoiceDaemonLogPath();
3853
+ const pidPath = getVoiceDaemonPidPath();
3854
+ ensureDirectories();
3855
+ // Create log file for daemon output
3856
+ const logFd = openSync(logPath, "a");
3857
+ const child = spawn(nodePath, [jflPath, ...args], {
3858
+ detached: true,
3859
+ stdio: ["ignore", logFd, logFd],
3860
+ env: {
3861
+ ...process.env,
3862
+ JFL_VOICE_DAEMON: "1", // Mark this as daemon mode
3863
+ },
3864
+ });
3865
+ // Write PID to file
3866
+ if (child.pid) {
3867
+ writeFileSync(pidPath, child.pid.toString(), { mode: 0o644 });
3868
+ // Unref so parent can exit independently
3869
+ child.unref();
3870
+ // Close the log file descriptor in the parent
3871
+ closeSync(logFd);
3872
+ // Give it a moment to start and check if it's running
3873
+ await new Promise(resolve => setTimeout(resolve, 500));
3874
+ if (isProcessRunning(child.pid)) {
3875
+ console.log(chalk.green("\n ✓ Daemon started successfully!"));
3876
+ console.log(chalk.gray(` PID: ${child.pid}`));
3877
+ console.log(chalk.gray(` Log: ${logPath}`));
3878
+ console.log();
3879
+ console.log(chalk.cyan(" Hotkey: Ctrl+Shift+Space"));
3880
+ console.log(chalk.gray(" The daemon will continue running after you close this terminal."));
3881
+ console.log();
3882
+ console.log(chalk.gray(" Commands:"));
3883
+ console.log(chalk.gray(" jfl voice daemon status Check daemon status"));
3884
+ console.log(chalk.gray(" jfl voice daemon stop Stop the daemon"));
3885
+ console.log();
3886
+ }
3887
+ else {
3888
+ // Daemon may have exited immediately - check log for errors
3889
+ console.log(chalk.red("\n ✗ Daemon failed to start"));
3890
+ console.log(chalk.gray(` Check log for details: ${logPath}`));
3891
+ // Clean up PID file
3892
+ try {
3893
+ unlinkSync(pidPath);
3894
+ }
3895
+ catch { }
3896
+ console.log();
3897
+ }
3898
+ }
3899
+ else {
3900
+ closeSync(logFd);
3901
+ console.log(chalk.red("\n ✗ Failed to spawn daemon process"));
3902
+ console.log();
3903
+ }
3904
+ }
3905
+ /**
3906
+ * Daemon Stop Command - Stop the voice daemon gracefully
3907
+ *
3908
+ * Reads PID from ~/.jfl/voice-daemon.pid and sends SIGTERM
3909
+ */
3910
+ export async function daemonStopCommand() {
3911
+ console.log(chalk.bold("\n🎤 Voice Daemon\n"));
3912
+ const pid = readDaemonPid();
3913
+ const pidPath = getVoiceDaemonPidPath();
3914
+ if (!pid) {
3915
+ console.log(chalk.yellow(" Daemon is not running (no PID file)."));
3916
+ console.log();
3917
+ return;
3918
+ }
3919
+ if (!isProcessRunning(pid)) {
3920
+ console.log(chalk.yellow(" Daemon is not running (stale PID file)."));
3921
+ console.log(chalk.gray(" Cleaning up PID file..."));
3922
+ try {
3923
+ unlinkSync(pidPath);
3924
+ }
3925
+ catch { }
3926
+ console.log(chalk.green(" ✓ Cleaned up"));
3927
+ console.log();
3928
+ return;
3929
+ }
3930
+ console.log(chalk.gray(` Stopping daemon (PID: ${pid})...`));
3931
+ try {
3932
+ // Send SIGTERM for graceful shutdown
3933
+ process.kill(pid, "SIGTERM");
3934
+ // Wait for process to stop (up to 5 seconds)
3935
+ let stopped = false;
3936
+ for (let i = 0; i < 50; i++) {
3937
+ await new Promise(resolve => setTimeout(resolve, 100));
3938
+ if (!isProcessRunning(pid)) {
3939
+ stopped = true;
3940
+ break;
3941
+ }
3942
+ }
3943
+ if (stopped) {
3944
+ console.log(chalk.green(" ✓ Daemon stopped successfully"));
3945
+ // Clean up PID file
3946
+ try {
3947
+ unlinkSync(pidPath);
3948
+ }
3949
+ catch { }
3950
+ }
3951
+ else {
3952
+ console.log(chalk.yellow(" Daemon did not stop gracefully, sending SIGKILL..."));
3953
+ try {
3954
+ process.kill(pid, "SIGKILL");
3955
+ await new Promise(resolve => setTimeout(resolve, 500));
3956
+ console.log(chalk.green(" ✓ Daemon killed"));
3957
+ try {
3958
+ unlinkSync(pidPath);
3959
+ }
3960
+ catch { }
3961
+ }
3962
+ catch (error) {
3963
+ console.log(chalk.red(" ✗ Failed to kill daemon"));
3964
+ console.log(chalk.gray(` You may need to manually kill PID ${pid}`));
3965
+ }
3966
+ }
3967
+ }
3968
+ catch (error) {
3969
+ console.log(chalk.red(" ✗ Failed to stop daemon"));
3970
+ if (error instanceof Error) {
3971
+ console.log(chalk.gray(` ${error.message}`));
3972
+ }
3973
+ console.log(chalk.gray(` You may need to manually kill PID ${pid}`));
3974
+ }
3975
+ console.log();
3976
+ }
3977
+ /**
3978
+ * Daemon Status Command - Show daemon running status
3979
+ *
3980
+ * Checks if daemon is running based on PID file and process existence
3981
+ */
3982
+ export async function daemonStatusCommand() {
3983
+ console.log(chalk.bold("\n🎤 Voice Daemon Status\n"));
3984
+ const pid = readDaemonPid();
3985
+ const pidPath = getVoiceDaemonPidPath();
3986
+ const logPath = getVoiceDaemonLogPath();
3987
+ if (!pid) {
3988
+ console.log(chalk.yellow(" Status: stopped"));
3989
+ console.log(chalk.gray(" (no PID file found)"));
3990
+ console.log();
3991
+ console.log(chalk.gray(" Start with: jfl voice daemon start"));
3992
+ console.log();
3993
+ return;
3994
+ }
3995
+ if (!isProcessRunning(pid)) {
3996
+ console.log(chalk.yellow(" Status: stopped (stale)"));
3997
+ console.log(chalk.gray(` PID file exists but process ${pid} is not running`));
3998
+ console.log();
3999
+ console.log(chalk.gray(" Cleaning up stale PID file..."));
4000
+ try {
4001
+ unlinkSync(pidPath);
4002
+ console.log(chalk.green(" ✓ Cleaned up"));
4003
+ }
4004
+ catch { }
4005
+ console.log();
4006
+ console.log(chalk.gray(" Start with: jfl voice daemon start"));
4007
+ console.log();
4008
+ return;
4009
+ }
4010
+ // Daemon is running
4011
+ const uptime = getDaemonUptime();
4012
+ console.log(chalk.green(" Status: running"));
4013
+ console.log(chalk.gray(` PID: ${pid}`));
4014
+ if (uptime) {
4015
+ console.log(chalk.gray(` Uptime: ${uptime}`));
4016
+ }
4017
+ console.log(chalk.gray(` Log: ${logPath}`));
4018
+ console.log();
4019
+ console.log(chalk.cyan(" Hotkey: Ctrl+Shift+Space"));
4020
+ console.log();
4021
+ console.log(chalk.gray(" Commands:"));
4022
+ console.log(chalk.gray(" jfl voice daemon stop Stop the daemon"));
4023
+ console.log();
4024
+ // Show last few lines of log if it exists
4025
+ if (existsSync(logPath)) {
4026
+ try {
4027
+ const logContent = readFileSync(logPath, "utf-8");
4028
+ const lines = logContent.trim().split("\n");
4029
+ const lastLines = lines.slice(-5);
4030
+ if (lastLines.length > 0 && lastLines[0]) {
4031
+ console.log(chalk.gray(" Recent log:"));
4032
+ for (const line of lastLines) {
4033
+ console.log(chalk.gray(` ${line.substring(0, 80)}`));
4034
+ }
4035
+ console.log();
4036
+ }
4037
+ }
4038
+ catch { }
4039
+ }
4040
+ }
4041
+ /**
4042
+ * Hotkey Command - Start global hotkey listener
4043
+ *
4044
+ * Listens for Ctrl+Shift+Space globally (even when other apps have focus).
4045
+ * Supports multiple modes:
4046
+ * - auto: Smart detection - tap to toggle, or hold for hold-to-talk
4047
+ * - tap: Tap to start/stop recording
4048
+ * - hold: Hold to record, release to stop
4049
+ * VS-010/VS-011: Supported on macOS, Linux (X11), and Windows.
4050
+ * Requires Accessibility permission on macOS. On Linux Wayland, hotkeys are not supported.
4051
+ */
4052
+ export async function hotkeyCommand(options = {}) {
4053
+ const currentPlatform = platform();
4054
+ // VS-010: Check for Linux Wayland (not supported)
4055
+ if (currentPlatform === "linux" && isWayland()) {
4056
+ const error = new VoiceError(VoiceErrorType.WAYLAND_NOT_SUPPORTED);
4057
+ handleVoiceError(error);
4058
+ return;
4059
+ }
4060
+ // Check platform - hotkey supported on macOS, Linux (X11), and Windows
4061
+ if (currentPlatform !== "darwin" && currentPlatform !== "linux" && currentPlatform !== "win32") {
4062
+ const error = new VoiceError(VoiceErrorType.PLATFORM_NOT_SUPPORTED);
4063
+ handleVoiceError(error);
4064
+ return;
4065
+ }
4066
+ // Load hotkey config from voice.yaml, with command-line override
4067
+ const voiceConfig = readVoiceConfig();
4068
+ const hotkeyConfig = voiceConfig?.hotkey || DEFAULT_HOTKEY_CONFIG;
4069
+ const securityConfig = voiceConfig?.security || DEFAULT_SECURITY_CONFIG;
4070
+ const activeMode = options.mode || hotkeyConfig.mode;
4071
+ const holdThreshold = hotkeyConfig.holdThreshold;
4072
+ console.log(chalk.bold("\n🎤 Voice Hotkey Mode\n"));
4073
+ console.log(chalk.gray(" Global hotkey: Ctrl+Shift+Space"));
4074
+ // Show mode-specific instructions
4075
+ if (activeMode === "tap") {
4076
+ console.log(chalk.gray(" Mode: tap-to-toggle"));
4077
+ console.log(chalk.gray(" First tap starts recording, second tap stops.\n"));
4078
+ }
4079
+ else if (activeMode === "hold") {
4080
+ console.log(chalk.gray(" Mode: hold-to-talk"));
4081
+ console.log(chalk.gray(" Hold to record, release to stop.\n"));
4082
+ }
4083
+ else {
4084
+ console.log(chalk.gray(" Mode: auto (smart detection)"));
4085
+ console.log(chalk.gray(` Quick tap (<${holdThreshold}ms): toggle recording`));
4086
+ console.log(chalk.gray(` Hold (>${holdThreshold}ms): hold-to-talk\n`));
4087
+ }
4088
+ // Check Accessibility/keyboard permission (platform-specific)
4089
+ if (currentPlatform === "darwin") {
4090
+ console.log(chalk.gray(" Checking Accessibility permission..."));
4091
+ }
4092
+ else if (currentPlatform === "linux") {
4093
+ console.log(chalk.gray(" Checking X11 environment..."));
4094
+ }
4095
+ else if (currentPlatform === "win32") {
4096
+ console.log(chalk.gray(" Checking keyboard access..."));
4097
+ }
4098
+ if (!checkAccessibilityPermission()) {
4099
+ console.log();
4100
+ if (currentPlatform === "darwin") {
4101
+ const error = new VoiceError(VoiceErrorType.ACCESSIBILITY_NOT_GRANTED);
4102
+ handleVoiceError(error);
4103
+ console.log(chalk.cyan(" Opening System Settings..."));
4104
+ openAccessibilitySettings();
4105
+ console.log();
4106
+ console.log(chalk.yellow(" After granting permission:"));
4107
+ console.log(chalk.gray(" 1. Add your terminal app to Accessibility"));
4108
+ console.log(chalk.gray(" 2. Restart your terminal"));
4109
+ console.log(chalk.gray(" 3. Run 'jfl voice hotkey' again"));
4110
+ }
4111
+ else if (currentPlatform === "linux") {
4112
+ // VS-010: Linux X11 requirements
4113
+ console.log(chalk.red(" X11 environment not detected or xdotool not available."));
4114
+ console.log();
4115
+ console.log(chalk.yellow(" Requirements for Linux hotkey support:"));
4116
+ console.log(chalk.gray(" 1. Must be running an X11 session (not Wayland)"));
4117
+ console.log(chalk.gray(" 2. Install xdotool: sudo apt-get install xdotool"));
4118
+ console.log(chalk.gray(" 3. Run 'jfl voice hotkey' again"));
4119
+ console.log();
4120
+ console.log(chalk.gray(" To check your session type: echo $XDG_SESSION_TYPE"));
4121
+ }
4122
+ else if (currentPlatform === "win32") {
4123
+ // VS-011: Windows should work without special permissions
4124
+ console.log(chalk.red(" Keyboard access check failed."));
4125
+ console.log(chalk.gray(" This is unexpected on Windows. Please try restarting your terminal."));
4126
+ }
4127
+ console.log();
4128
+ return;
4129
+ }
4130
+ if (currentPlatform === "darwin") {
4131
+ console.log(chalk.green(" ✓ Accessibility permission granted\n"));
4132
+ }
4133
+ else if (currentPlatform === "linux") {
4134
+ console.log(chalk.green(" ✓ X11 environment detected\n"));
4135
+ }
4136
+ else if (currentPlatform === "win32") {
4137
+ console.log(chalk.green(" ✓ Keyboard access available\n"));
4138
+ }
4139
+ // Check other prerequisites (server, auth, model)
4140
+ const serverError = checkServerRunning();
4141
+ if (serverError) {
4142
+ handleVoiceError(serverError);
4143
+ return;
4144
+ }
4145
+ const authError = checkAuthToken();
4146
+ if (authError) {
4147
+ handleVoiceError(authError);
4148
+ return;
4149
+ }
4150
+ // Initialize keyboard listener
4151
+ let keyboardListener;
4152
+ try {
4153
+ keyboardListener = new GlobalKeyboardListener();
4154
+ }
4155
+ catch (error) {
4156
+ console.log(chalk.red("\n Failed to initialize keyboard listener."));
4157
+ if (currentPlatform === "darwin") {
4158
+ console.log(chalk.gray(" This may be due to missing Accessibility permission."));
4159
+ console.log();
4160
+ openAccessibilitySettings();
4161
+ }
4162
+ else if (currentPlatform === "linux") {
4163
+ // VS-010: Linux-specific error guidance
4164
+ console.log(chalk.gray(" On Linux X11, this requires the X11 display server."));
4165
+ console.log(chalk.gray(" Ensure you are running an X11 session and not Wayland."));
4166
+ console.log();
4167
+ console.log(chalk.yellow(" To check your session type:"));
4168
+ console.log(chalk.gray(" echo $XDG_SESSION_TYPE"));
4169
+ }
4170
+ else if (currentPlatform === "win32") {
4171
+ // VS-011: Windows-specific error guidance
4172
+ console.log(chalk.gray(" On Windows, this should work without special permissions."));
4173
+ console.log(chalk.gray(" Try running your terminal as Administrator if the issue persists."));
4174
+ }
4175
+ console.log();
4176
+ return;
4177
+ }
4178
+ // State management for hotkey
4179
+ let isRecording = false;
4180
+ let recordingPromise = null;
4181
+ let currentRecorder = null;
4182
+ let audioChunks = [];
4183
+ let hasVoiceActivity = false;
4184
+ let silenceStartTime = null;
4185
+ let recordingStartTime = null;
4186
+ let recordingSpinner = null;
4187
+ let focusedAppAtStart = null; // Track which app was focused when recording started
4188
+ // VAD settings
4189
+ const silenceThresholdDb = VAD_SILENCE_THRESHOLD_DB;
4190
+ const silenceDurationMs = VAD_SILENCE_DURATION_MS;
4191
+ // VS-SEC-3: Configurable max recording duration from security config
4192
+ const maxDurationSecs = securityConfig.maxRecordingDuration;
4193
+ const warningThresholdSecs = Math.max(10, maxDurationSecs - 10); // Warning 10 seconds before limit
4194
+ let warningShown = false; // Track if warning has been displayed
4195
+ // Helper function to start recording
4196
+ const startRecording = async () => {
4197
+ if (isRecording)
4198
+ return;
4199
+ // Capture the focused app before we start recording (VS-SEC-2)
4200
+ focusedAppAtStart = getFocusedApp();
4201
+ if (process.env.DEBUG && focusedAppAtStart) {
4202
+ console.log(chalk.gray(` [debug] Recording started in: ${focusedAppAtStart}`));
4203
+ }
4204
+ console.log(chalk.cyan("\n Recording started... (press Ctrl+Shift+Space to stop)\n"));
4205
+ // Play sound and show notification for daemon mode feedback
4206
+ if (!process.stdin.isTTY) {
4207
+ // Play start sound (Pop = friendly "go" sound)
4208
+ try {
4209
+ execSync('afplay /System/Library/Sounds/Pop.aiff &', { stdio: 'ignore' });
4210
+ }
4211
+ catch { }
4212
+ showNotification("🎤 Recording", "Speak now... Press Ctrl+Shift+Space to stop");
4213
+ }
4214
+ isRecording = true;
4215
+ audioChunks = [];
4216
+ hasVoiceActivity = false;
4217
+ silenceStartTime = null;
4218
+ recordingStartTime = Date.now();
4219
+ warningShown = false; // VS-SEC-3: Reset warning flag for new recording
4220
+ // VS-012: Reset waveform buffer for new recording
4221
+ resetWaveformBuffer();
4222
+ const useWaveform = supportsWaveform();
4223
+ try {
4224
+ currentRecorder = new AudioRecorder({
4225
+ device: options.device,
4226
+ sampleRate: 16000,
4227
+ });
4228
+ }
4229
+ catch (error) {
4230
+ console.log(chalk.red(" Failed to initialize recorder"));
4231
+ isRecording = false;
4232
+ return;
4233
+ }
4234
+ recordingSpinner = ora({
4235
+ text: chalk.cyan("Recording...") + chalk.gray(" (waiting for voice)"),
4236
+ prefixText: " ",
4237
+ spinner: "dots",
4238
+ });
4239
+ // Set up recorder event handlers
4240
+ currentRecorder.on("data", (chunk) => {
4241
+ audioChunks.push(chunk);
4242
+ // Calculate peak level
4243
+ const chunkPeak = calculatePeakAmplitude(chunk);
4244
+ const peakDb = amplitudeToDb(chunkPeak);
4245
+ const isSilent = peakDb < silenceThresholdDb;
4246
+ // VS-012: Add level to waveform buffer on every chunk
4247
+ addToWaveformBuffer(peakDb);
4248
+ // VS-012: Update spinner with waveform on every chunk
4249
+ if (recordingSpinner && recordingStartTime) {
4250
+ const elapsed = ((Date.now() - recordingStartTime) / 1000).toFixed(1);
4251
+ if (useWaveform) {
4252
+ const waveform = renderWaveform();
4253
+ recordingSpinner.text = chalk.cyan("Recording") + chalk.gray(` ${waveform} (${elapsed}s)`);
4254
+ }
4255
+ }
4256
+ if (!isSilent) {
4257
+ hasVoiceActivity = true;
4258
+ silenceStartTime = null;
4259
+ }
4260
+ else if (hasVoiceActivity) {
4261
+ if (silenceStartTime === null) {
4262
+ silenceStartTime = Date.now();
4263
+ }
4264
+ else {
4265
+ const silenceDuration = Date.now() - silenceStartTime;
4266
+ if (silenceDuration >= silenceDurationMs) {
4267
+ // Auto-stop on silence
4268
+ stopRecording();
4269
+ }
4270
+ }
4271
+ }
4272
+ // Check max duration (VS-SEC-3)
4273
+ if (recordingStartTime) {
4274
+ const elapsed = (Date.now() - recordingStartTime) / 1000;
4275
+ // Show warning 10 seconds before limit
4276
+ if (elapsed >= warningThresholdSecs && !warningShown) {
4277
+ warningShown = true;
4278
+ const remaining = Math.ceil(maxDurationSecs - elapsed);
4279
+ console.log(chalk.yellow(`\n ⚠ Recording will stop in ${remaining} seconds`));
4280
+ }
4281
+ // Auto-stop at max duration
4282
+ if (elapsed >= maxDurationSecs) {
4283
+ console.log(chalk.yellow(`\n ⚠ Maximum recording duration (${maxDurationSecs}s) reached`));
4284
+ stopRecording();
4285
+ }
4286
+ }
4287
+ });
4288
+ currentRecorder.on("error", (error) => {
4289
+ console.log(chalk.red(`\n Recording error: ${error.message}`));
4290
+ isRecording = false;
4291
+ });
4292
+ try {
4293
+ await currentRecorder.start();
4294
+ recordingSpinner.start();
4295
+ // VS-UX-2: Show system recording indicator (notification on macOS)
4296
+ showRecordingIndicator();
4297
+ }
4298
+ catch (error) {
4299
+ console.log(chalk.red(" Failed to start recording"));
4300
+ isRecording = false;
4301
+ }
4302
+ };
4303
+ // Helper function to stop recording and transcribe
4304
+ const stopRecording = async () => {
4305
+ if (!isRecording || !currentRecorder)
4306
+ return;
4307
+ currentRecorder.stop();
4308
+ isRecording = false;
4309
+ // Wait for recorder to fully stop
4310
+ await new Promise((resolve) => {
4311
+ const checkStopped = () => {
4312
+ if (!currentRecorder || currentRecorder.getState() === "idle" || currentRecorder.getState() === "error") {
4313
+ resolve();
4314
+ }
4315
+ else {
4316
+ setTimeout(checkStopped, 50);
4317
+ }
4318
+ };
4319
+ setTimeout(checkStopped, 100);
4320
+ });
4321
+ if (recordingSpinner) {
4322
+ recordingSpinner.stop();
4323
+ recordingSpinner = null;
4324
+ }
4325
+ // Check if we have audio
4326
+ const totalBytes = audioChunks.reduce((sum, chunk) => sum + chunk.length, 0);
4327
+ if (totalBytes === 0 || !hasVoiceActivity) {
4328
+ // VS-SEC-3: Zero audio buffers even on early return
4329
+ zeroBuffers(audioChunks);
4330
+ // VS-UX-2: Show stopped indicator when no audio
4331
+ hideRecordingIndicator("stopped");
4332
+ console.log(chalk.yellow(" No audio captured or no voice detected.\n"));
4333
+ console.log(chalk.gray(" Press Ctrl+Shift+Space to try again, or Ctrl+C to quit.\n"));
4334
+ return;
4335
+ }
4336
+ // VS-UX-2: Show completed indicator when moving to transcription
4337
+ hideRecordingIndicator("completed");
4338
+ // Combine audio and normalize for better transcription
4339
+ const audioBuffer = normalizeAudio(Buffer.concat(audioChunks));
4340
+ // VS-SEC-3: Zero the individual chunks immediately after combining
4341
+ zeroBuffers(audioChunks);
4342
+ const durationSecs = totalBytes / (16000 * 2);
4343
+ console.log(chalk.gray(`\n Recorded ${durationSecs.toFixed(1)}s of audio.`));
4344
+ console.log(chalk.gray(" Transcribing...\n"));
4345
+ // Transcribe
4346
+ const transcribeSpinner = ora({
4347
+ text: "Transcribing...",
4348
+ prefixText: " ",
4349
+ }).start();
4350
+ const authToken = readAuthToken();
4351
+ const socketPath = getVoiceSocketPath();
4352
+ const client = new VoiceClient({
4353
+ socketPath,
4354
+ authToken,
4355
+ maxReconnectAttempts: 1,
4356
+ });
4357
+ let transcription = "";
4358
+ let transcriptionReceived = false;
4359
+ let transcriptionError = null;
4360
+ client.onTranscript((text, isFinal) => {
4361
+ // Accept partial transcriptions too (in case final never arrives)
4362
+ if (text && text.trim() && !text.includes("[BLANK_AUDIO]")) {
4363
+ transcription = text;
4364
+ }
4365
+ if (isFinal) {
4366
+ transcriptionReceived = true;
4367
+ }
4368
+ });
4369
+ client.onError((error) => {
4370
+ transcriptionError = error;
4371
+ });
4372
+ try {
4373
+ await client.connect();
4374
+ client.sendAudio(audioBuffer);
4375
+ // Wait for audio to be sent before signaling end
4376
+ await new Promise(resolve => setTimeout(resolve, 300));
4377
+ client.endAudio();
4378
+ const timeout = 10000; // Reduced from 30s
4379
+ const startTime = Date.now();
4380
+ while (!transcriptionReceived && !transcriptionError) {
4381
+ // If we have a transcription, accept it even without final flag (wait 1s for final)
4382
+ if (transcription && Date.now() - startTime > 1000) {
4383
+ break;
4384
+ }
4385
+ if (Date.now() - startTime > timeout) {
4386
+ transcriptionError = new Error("Transcription timeout");
4387
+ break;
4388
+ }
4389
+ await new Promise((resolve) => setTimeout(resolve, 100));
4390
+ }
4391
+ client.disconnect();
4392
+ // VS-SEC-3: Zero combined audio buffer immediately after transcription
4393
+ zeroBuffer(audioBuffer);
4394
+ }
4395
+ catch (error) {
4396
+ client.disconnect();
4397
+ // VS-SEC-3: Zero combined audio buffer on error
4398
+ zeroBuffer(audioBuffer);
4399
+ transcribeSpinner.fail("Transcription failed");
4400
+ console.log(chalk.red(` ${error instanceof Error ? error.message : String(error)}\n`));
4401
+ console.log(chalk.gray(" Press Ctrl+Shift+Space to try again, or Ctrl+C to quit.\n"));
4402
+ return;
4403
+ }
4404
+ if (transcriptionError || !transcription || transcription.trim() === "") {
4405
+ transcribeSpinner.fail("No transcription result");
4406
+ console.log(chalk.gray(" Press Ctrl+Shift+Space to try again, or Ctrl+C to quit.\n"));
4407
+ return;
4408
+ }
4409
+ transcribeSpinner.succeed("Transcription complete!");
4410
+ const trimmedTranscription = transcription.trim();
4411
+ // VS-008: Preview transcript with configurable auto-send
4412
+ const previewConfig = voiceConfig?.preview || DEFAULT_PREVIEW_CONFIG;
4413
+ const previewResult = await previewTranscript(trimmedTranscription, previewConfig.timeout);
4414
+ if (previewResult.action === "cancel") {
4415
+ console.log(chalk.yellow(" Cancelled."));
4416
+ console.log(chalk.gray(" Press Ctrl+Shift+Space to record again, or Ctrl+C to quit.\n"));
4417
+ return;
4418
+ }
4419
+ // Use the final text (may have been edited by user)
4420
+ const finalText = previewResult.text;
4421
+ // VS-SEC-2: Focus verification before paste
4422
+ // Check if the same app is still focused
4423
+ const currentFocusedApp = getFocusedApp();
4424
+ const focusUnchanged = focusedAppAtStart && currentFocusedApp && focusedAppAtStart === currentFocusedApp;
4425
+ if (process.env.DEBUG) {
4426
+ console.log(chalk.gray(` [debug] Focus at start: ${focusedAppAtStart}`));
4427
+ console.log(chalk.gray(` [debug] Focus now: ${currentFocusedApp}`));
4428
+ console.log(chalk.gray(` [debug] Focus unchanged: ${focusUnchanged}`));
4429
+ }
4430
+ if (focusUnchanged) {
4431
+ // VS-007: Same app focused - copy to clipboard and simulate paste
4432
+ const copied = copyToClipboard(finalText);
4433
+ if (copied) {
4434
+ // Small delay to ensure clipboard is ready
4435
+ await new Promise(resolve => setTimeout(resolve, 50));
4436
+ const pasted = simulatePaste();
4437
+ if (pasted) {
4438
+ console.log(chalk.green(`\n ✓ Pasted to ${currentFocusedApp}!`));
4439
+ // Play success sound and show notification for daemon mode
4440
+ if (!process.stdin.isTTY) {
4441
+ try {
4442
+ execSync('afplay /System/Library/Sounds/Glass.aiff &', { stdio: 'ignore' });
4443
+ }
4444
+ catch { }
4445
+ const preview = finalText.length > 50 ? finalText.slice(0, 47) + "..." : finalText;
4446
+ showNotification("✓ Pasted", preview);
4447
+ }
4448
+ // VS-SEC-3: Schedule clipboard clear after successful paste
4449
+ scheduleClipboardClear(securityConfig.clipboardClearDelay);
4450
+ if (process.env.DEBUG) {
4451
+ console.log(chalk.gray(` [debug] Clipboard will be cleared in ${securityConfig.clipboardClearDelay}s`));
4452
+ }
4453
+ }
4454
+ else {
4455
+ console.log(chalk.green("\n ✓ Copied to clipboard!"));
4456
+ console.log(chalk.yellow(` ⚠ Could not auto-paste (${getPasteShortcut()}). Text is on clipboard.`));
4457
+ // Show notification for daemon mode
4458
+ if (!process.stdin.isTTY) {
4459
+ const preview = finalText.length > 50 ? finalText.slice(0, 47) + "..." : finalText;
4460
+ showNotification("📋 Copied", `${preview}\n\nPress ${getPasteShortcut()} to paste`);
4461
+ }
4462
+ // VS-SEC-3: Still schedule clipboard clear even if paste failed
4463
+ scheduleClipboardClear(securityConfig.clipboardClearDelay);
4464
+ }
4465
+ }
4466
+ else {
4467
+ console.log(chalk.yellow("\n ⚠ Could not copy to clipboard"));
4468
+ // Display result for manual copy
4469
+ console.log();
4470
+ console.log(chalk.bold(" Transcription:"));
4471
+ console.log();
4472
+ console.log(chalk.cyan(` "${finalText}"`));
4473
+ }
4474
+ }
4475
+ else {
4476
+ // Focus changed - show notification and don't auto-paste (security measure)
4477
+ console.log();
4478
+ console.log(chalk.yellow(" ⚠ Focus changed during recording"));
4479
+ if (focusedAppAtStart && currentFocusedApp) {
4480
+ console.log(chalk.gray(` Started in: ${focusedAppAtStart}`));
4481
+ console.log(chalk.gray(` Now in: ${currentFocusedApp}`));
4482
+ }
4483
+ // Copy to clipboard anyway for user convenience
4484
+ const copied = copyToClipboard(finalText);
4485
+ // Show notification with transcription
4486
+ const notified = showNotification("Voice Transcription", finalText.length > 100
4487
+ ? finalText.substring(0, 97) + "..."
4488
+ : finalText);
4489
+ if (notified) {
4490
+ console.log(chalk.cyan("\n 📋 Notification shown with transcription"));
4491
+ }
4492
+ if (copied) {
4493
+ console.log(chalk.green(` ✓ Copied to clipboard (${getPasteShortcut()} to paste manually)`));
4494
+ // VS-SEC-3: Schedule clipboard clear after copy
4495
+ scheduleClipboardClear(securityConfig.clipboardClearDelay);
4496
+ }
4497
+ // Display result
4498
+ console.log();
4499
+ console.log(chalk.bold(" Transcription:"));
4500
+ console.log();
4501
+ console.log(chalk.cyan(` "${finalText}"`));
4502
+ }
4503
+ console.log();
4504
+ console.log(chalk.gray(" Press Ctrl+Shift+Space to record again, or Ctrl+C to quit.\n"));
4505
+ };
4506
+ // Track modifier keys state
4507
+ let ctrlPressed = false;
4508
+ let shiftPressed = false;
4509
+ // Hold-to-talk state tracking
4510
+ let keyDownTime = null;
4511
+ let holdTimer = null;
4512
+ let isInHoldMode = false; // True when user has held key past threshold
4513
+ // Clear hold timer
4514
+ const clearHoldTimer = () => {
4515
+ if (holdTimer) {
4516
+ clearTimeout(holdTimer);
4517
+ holdTimer = null;
4518
+ }
4519
+ };
4520
+ // Add keyboard listener
4521
+ keyboardListener.addListener((event, isDown) => {
4522
+ // Update modifier key states
4523
+ if (event.name === "LEFT CTRL" || event.name === "RIGHT CTRL") {
4524
+ ctrlPressed = event.state === "DOWN";
4525
+ }
4526
+ if (event.name === "LEFT SHIFT" || event.name === "RIGHT SHIFT") {
4527
+ shiftPressed = event.state === "DOWN";
4528
+ }
4529
+ // Check for Ctrl+Shift+Space
4530
+ const isHotkeyCombo = event.name === "SPACE" && ctrlPressed && shiftPressed;
4531
+ if (!isHotkeyCombo)
4532
+ return;
4533
+ if (event.state === "DOWN") {
4534
+ // Key pressed down
4535
+ if (keyDownTime !== null) {
4536
+ // Already tracking a press, ignore (debounce)
4537
+ return;
4538
+ }
4539
+ keyDownTime = Date.now();
4540
+ if (activeMode === "tap") {
4541
+ // Pure tap mode: toggle on keydown
4542
+ if (isRecording) {
4543
+ stopRecording();
4544
+ }
4545
+ else {
4546
+ startRecording();
4547
+ }
4548
+ }
4549
+ else if (activeMode === "hold") {
4550
+ // Pure hold mode: start recording immediately on keydown
4551
+ if (!isRecording) {
4552
+ isInHoldMode = true;
4553
+ startRecording();
4554
+ }
4555
+ }
4556
+ else {
4557
+ // Auto mode: wait for threshold to determine behavior
4558
+ // Start a timer to enter hold mode
4559
+ holdTimer = setTimeout(() => {
4560
+ // Timer fired - we're in hold mode now
4561
+ isInHoldMode = true;
4562
+ if (!isRecording) {
4563
+ startRecording();
4564
+ }
4565
+ }, holdThreshold);
4566
+ }
4567
+ }
4568
+ else if (event.state === "UP") {
4569
+ // Key released
4570
+ const pressDuration = keyDownTime !== null ? Date.now() - keyDownTime : 0;
4571
+ keyDownTime = null;
4572
+ clearHoldTimer();
4573
+ if (activeMode === "tap") {
4574
+ // Pure tap mode: already handled on keydown, nothing to do on keyup
4575
+ // Reset state
4576
+ isInHoldMode = false;
4577
+ }
4578
+ else if (activeMode === "hold") {
4579
+ // Pure hold mode: stop recording on keyup
4580
+ if (isRecording) {
4581
+ stopRecording();
4582
+ }
4583
+ isInHoldMode = false;
4584
+ }
4585
+ else {
4586
+ // Auto mode: check if this was a tap or hold
4587
+ if (isInHoldMode) {
4588
+ // Was holding - stop recording on release
4589
+ if (isRecording) {
4590
+ stopRecording();
4591
+ }
4592
+ isInHoldMode = false;
4593
+ }
4594
+ else {
4595
+ // Was a quick tap (released before threshold)
4596
+ // Toggle recording
4597
+ if (isRecording) {
4598
+ stopRecording();
4599
+ }
4600
+ else {
4601
+ startRecording();
4602
+ }
4603
+ }
4604
+ }
4605
+ }
4606
+ });
4607
+ console.log(chalk.green(" ✓ Hotkey listener started"));
4608
+ if (activeMode === "tap") {
4609
+ console.log(chalk.gray(" Press Ctrl+Shift+Space to start/stop recording"));
4610
+ }
4611
+ else if (activeMode === "hold") {
4612
+ console.log(chalk.gray(" Hold Ctrl+Shift+Space to record, release to stop"));
4613
+ }
4614
+ else {
4615
+ console.log(chalk.gray(" Tap Ctrl+Shift+Space to toggle, or hold to talk"));
4616
+ }
4617
+ console.log(chalk.gray(" Press Ctrl+C to quit\n"));
4618
+ // Handle Ctrl+C to exit
4619
+ const cleanup = () => {
4620
+ console.log(chalk.yellow("\n Stopping hotkey listener...\n"));
4621
+ clearHoldTimer();
4622
+ keyboardListener.kill();
4623
+ if (currentRecorder) {
4624
+ currentRecorder.stop();
4625
+ }
4626
+ process.exit(0);
4627
+ };
4628
+ process.on("SIGINT", cleanup);
4629
+ process.on("SIGTERM", cleanup);
4630
+ // Keep the process running with an interval (needed for detached daemon mode)
4631
+ const keepAlive = setInterval(() => {
4632
+ // Keepalive - prevents Node from exiting when detached
4633
+ }, 60000);
4634
+ await new Promise(() => {
4635
+ // This promise never resolves - we run until Ctrl+C
4636
+ });
4637
+ }
4638
+ // Helper functions used by hotkeyCommand (reference existing functions)
4639
+ // calculatePeakAmplitude is already defined elsewhere in this file
4640
+ // amplitudeToDb is already defined elsewhere in this file
4641
+ // copyToClipboard is already defined elsewhere in this file
4642
+ // Main voice command handler
4643
+ export async function voiceCommand(action, subaction, arg, options) {
4644
+ // If no action, run the voice slash command (default behavior)
4645
+ if (!action) {
4646
+ await voiceSlashCommand({ device: options?.device });
4647
+ return;
4648
+ }
4649
+ // Handle help explicitly
4650
+ if (action === "help" || options?.help) {
4651
+ showVoiceHelp();
4652
+ return;
4653
+ }
4654
+ // Handle "record" as alias for the default voice slash command
4655
+ if (action === "record") {
4656
+ await voiceSlashCommand({ device: options?.device });
4657
+ return;
4658
+ }
4659
+ if (action === "model") {
4660
+ if (!subaction || subaction === "list") {
4661
+ await listModelsCommand();
4662
+ }
4663
+ else if (subaction === "download") {
4664
+ if (!arg) {
4665
+ console.log(chalk.red("Missing model name."));
4666
+ console.log(chalk.gray("Usage: jfl voice model download <name>"));
4667
+ console.log(chalk.gray("\nExample: jfl voice model download base"));
4668
+ return;
4669
+ }
4670
+ await downloadModelCommand(arg, options);
4671
+ }
4672
+ else if (subaction === "default") {
4673
+ if (!arg) {
4674
+ const current = getCurrentDefaultModel();
4675
+ console.log(chalk.gray(`Current default model: ${chalk.white(current)}`));
4676
+ console.log(chalk.gray("Usage: jfl voice model default <name>"));
4677
+ return;
4678
+ }
4679
+ await setDefaultModelCommand(arg);
4680
+ }
4681
+ else {
4682
+ console.log(chalk.red(`Unknown model command: ${subaction}`));
4683
+ console.log(chalk.gray("\nAvailable commands: list, download, default"));
4684
+ }
4685
+ return;
4686
+ }
4687
+ if (action === "devices") {
4688
+ await listDevicesCommand();
4689
+ return;
4690
+ }
4691
+ if (action === "test") {
4692
+ await voiceTestCommand({
4693
+ device: options?.device,
4694
+ });
4695
+ return;
4696
+ }
4697
+ if (action === "recording") {
4698
+ await testRecordingCommand({
4699
+ device: options?.device,
4700
+ duration: options?.duration,
4701
+ });
4702
+ return;
4703
+ }
4704
+ if (action === "setup") {
4705
+ await voiceSetupCommand();
4706
+ return;
4707
+ }
4708
+ if (action === "hotkey") {
4709
+ // Validate mode option if provided
4710
+ const validModes = ["auto", "tap", "hold"];
4711
+ let mode;
4712
+ if (options?.mode) {
4713
+ if (validModes.includes(options.mode)) {
4714
+ mode = options.mode;
4715
+ }
4716
+ else {
4717
+ console.log(chalk.red(`Invalid mode: ${options.mode}`));
4718
+ console.log(chalk.gray("Valid modes: auto, tap, hold"));
4719
+ return;
4720
+ }
4721
+ }
4722
+ await hotkeyCommand({ device: options?.device, mode });
4723
+ return;
4724
+ }
4725
+ // VS-013: Daemon commands for background hotkey listening
4726
+ if (action === "daemon") {
4727
+ // Validate mode option if provided
4728
+ const validModes = ["auto", "tap", "hold"];
4729
+ let mode;
4730
+ if (options?.mode) {
4731
+ if (validModes.includes(options.mode)) {
4732
+ mode = options.mode;
4733
+ }
4734
+ else {
4735
+ console.log(chalk.red(`Invalid mode: ${options.mode}`));
4736
+ console.log(chalk.gray("Valid modes: auto, tap, hold"));
4737
+ return;
4738
+ }
4739
+ }
4740
+ if (!subaction || subaction === "status") {
4741
+ await daemonStatusCommand();
4742
+ }
4743
+ else if (subaction === "start") {
4744
+ await daemonStartCommand({ mode });
4745
+ }
4746
+ else if (subaction === "stop") {
4747
+ await daemonStopCommand();
4748
+ }
4749
+ else {
4750
+ console.log(chalk.red(`Unknown daemon command: ${subaction}`));
4751
+ console.log(chalk.gray("\nAvailable commands: start, stop, status"));
4752
+ }
4753
+ return;
4754
+ }
4755
+ // Placeholder for future commands
4756
+ if (action === "server") {
4757
+ console.log(chalk.yellow(`\n⚠️ 'jfl voice ${action}' is coming soon.\n`));
4758
+ console.log(chalk.gray("For now, use 'jfl voice model' commands to manage whisper models."));
4759
+ console.log();
4760
+ return;
4761
+ }
4762
+ console.log(chalk.red(`Unknown voice command: ${action}`));
4763
+ console.log(chalk.gray("Run 'jfl voice' for help."));
4764
+ }
4765
+ //# sourceMappingURL=voice.js.map