@shykaruu/jarvis-brain 0.4.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 (330) hide show
  1. package/LICENSE +153 -0
  2. package/README.md +428 -0
  3. package/bin/jarvis.ts +449 -0
  4. package/package.json +79 -0
  5. package/roles/activity-observer.yaml +60 -0
  6. package/roles/ceo-founder.yaml +144 -0
  7. package/roles/chief-of-staff.yaml +158 -0
  8. package/roles/dev-lead.yaml +182 -0
  9. package/roles/executive-assistant.yaml +77 -0
  10. package/roles/marketing-director.yaml +168 -0
  11. package/roles/personal-assistant.yaml +266 -0
  12. package/roles/research-specialist.yaml +60 -0
  13. package/roles/specialists/content-writer.yaml +53 -0
  14. package/roles/specialists/customer-support.yaml +57 -0
  15. package/roles/specialists/data-analyst.yaml +57 -0
  16. package/roles/specialists/financial-analyst.yaml +56 -0
  17. package/roles/specialists/hr-specialist.yaml +55 -0
  18. package/roles/specialists/legal-advisor.yaml +58 -0
  19. package/roles/specialists/marketing-strategist.yaml +56 -0
  20. package/roles/specialists/project-coordinator.yaml +55 -0
  21. package/roles/specialists/research-analyst.yaml +58 -0
  22. package/roles/specialists/software-engineer.yaml +57 -0
  23. package/roles/specialists/system-administrator.yaml +57 -0
  24. package/roles/system-admin.yaml +76 -0
  25. package/scripts/ensure-bun.cjs +16 -0
  26. package/src/actions/README.md +421 -0
  27. package/src/actions/app-control/desktop-controller.test.ts +26 -0
  28. package/src/actions/app-control/desktop-controller.ts +438 -0
  29. package/src/actions/app-control/interface.ts +64 -0
  30. package/src/actions/app-control/linux.ts +273 -0
  31. package/src/actions/app-control/macos.ts +54 -0
  32. package/src/actions/app-control/sidecar-launcher.test.ts +23 -0
  33. package/src/actions/app-control/sidecar-launcher.ts +286 -0
  34. package/src/actions/app-control/windows.ts +44 -0
  35. package/src/actions/browser/cdp.ts +138 -0
  36. package/src/actions/browser/chrome-launcher.ts +261 -0
  37. package/src/actions/browser/session.ts +506 -0
  38. package/src/actions/browser/stealth.ts +49 -0
  39. package/src/actions/index.ts +20 -0
  40. package/src/actions/terminal/executor.ts +157 -0
  41. package/src/actions/terminal/wsl-bridge.ts +126 -0
  42. package/src/actions/test.ts +93 -0
  43. package/src/actions/tools/agents.ts +363 -0
  44. package/src/actions/tools/builtin.ts +950 -0
  45. package/src/actions/tools/commitments.ts +192 -0
  46. package/src/actions/tools/content.ts +217 -0
  47. package/src/actions/tools/delegate.ts +147 -0
  48. package/src/actions/tools/desktop.test.ts +55 -0
  49. package/src/actions/tools/desktop.ts +305 -0
  50. package/src/actions/tools/documents.ts +169 -0
  51. package/src/actions/tools/goals.ts +376 -0
  52. package/src/actions/tools/local-tools-guard.ts +31 -0
  53. package/src/actions/tools/registry.ts +173 -0
  54. package/src/actions/tools/research.ts +111 -0
  55. package/src/actions/tools/sidecar-list.ts +57 -0
  56. package/src/actions/tools/sidecar-route.ts +105 -0
  57. package/src/actions/tools/workflows.ts +216 -0
  58. package/src/agents/agent.ts +132 -0
  59. package/src/agents/delegation.ts +107 -0
  60. package/src/agents/hierarchy.ts +113 -0
  61. package/src/agents/index.ts +19 -0
  62. package/src/agents/messaging.ts +125 -0
  63. package/src/agents/orchestrator.ts +592 -0
  64. package/src/agents/role-discovery.ts +61 -0
  65. package/src/agents/sub-agent-runner.ts +309 -0
  66. package/src/agents/task-manager.ts +151 -0
  67. package/src/authority/approval-delivery.ts +59 -0
  68. package/src/authority/approval.ts +196 -0
  69. package/src/authority/audit.ts +158 -0
  70. package/src/authority/authority.test.ts +519 -0
  71. package/src/authority/deferred-executor.ts +103 -0
  72. package/src/authority/emergency.ts +66 -0
  73. package/src/authority/engine.ts +301 -0
  74. package/src/authority/index.ts +12 -0
  75. package/src/authority/learning.ts +111 -0
  76. package/src/authority/tool-action-map.ts +74 -0
  77. package/src/awareness/analytics.ts +466 -0
  78. package/src/awareness/awareness.test.ts +332 -0
  79. package/src/awareness/capture-engine.ts +305 -0
  80. package/src/awareness/context-graph.ts +130 -0
  81. package/src/awareness/context-tracker.ts +349 -0
  82. package/src/awareness/index.ts +25 -0
  83. package/src/awareness/intelligence.ts +321 -0
  84. package/src/awareness/ocr-engine.ts +88 -0
  85. package/src/awareness/service.ts +528 -0
  86. package/src/awareness/struggle-detector.ts +342 -0
  87. package/src/awareness/suggestion-engine.ts +476 -0
  88. package/src/awareness/types.ts +201 -0
  89. package/src/cli/autostart.ts +417 -0
  90. package/src/cli/deps.ts +449 -0
  91. package/src/cli/doctor.ts +238 -0
  92. package/src/cli/helpers.ts +401 -0
  93. package/src/cli/onboard.ts +827 -0
  94. package/src/cli/uninstall.test.ts +37 -0
  95. package/src/cli/uninstall.ts +202 -0
  96. package/src/comms/README.md +329 -0
  97. package/src/comms/auth-error.html +48 -0
  98. package/src/comms/channels/discord.ts +228 -0
  99. package/src/comms/channels/signal.ts +56 -0
  100. package/src/comms/channels/telegram.ts +316 -0
  101. package/src/comms/channels/whatsapp.ts +60 -0
  102. package/src/comms/channels.test.ts +173 -0
  103. package/src/comms/dashboard-auth.ts +75 -0
  104. package/src/comms/desktop-notify.ts +114 -0
  105. package/src/comms/example.ts +129 -0
  106. package/src/comms/index.ts +129 -0
  107. package/src/comms/streaming.ts +149 -0
  108. package/src/comms/voice.test.ts +504 -0
  109. package/src/comms/voice.ts +341 -0
  110. package/src/comms/websocket.test.ts +409 -0
  111. package/src/comms/websocket.ts +669 -0
  112. package/src/config/README.md +389 -0
  113. package/src/config/index.ts +6 -0
  114. package/src/config/loader.test.ts +183 -0
  115. package/src/config/loader.ts +148 -0
  116. package/src/config/types.ts +293 -0
  117. package/src/daemon/README.md +232 -0
  118. package/src/daemon/agent-service-interface.ts +9 -0
  119. package/src/daemon/agent-service.ts +667 -0
  120. package/src/daemon/api-routes.ts +3067 -0
  121. package/src/daemon/background-agent-service.ts +396 -0
  122. package/src/daemon/background-agent.test.ts +78 -0
  123. package/src/daemon/channel-service.ts +201 -0
  124. package/src/daemon/commitment-executor.ts +297 -0
  125. package/src/daemon/dashboard-auth.test.ts +170 -0
  126. package/src/daemon/event-classifier.ts +239 -0
  127. package/src/daemon/event-coalescer.ts +123 -0
  128. package/src/daemon/event-reactor.ts +214 -0
  129. package/src/daemon/flock.c +7 -0
  130. package/src/daemon/health.ts +220 -0
  131. package/src/daemon/index.ts +1070 -0
  132. package/src/daemon/llm-settings.test.ts +78 -0
  133. package/src/daemon/llm-settings.ts +450 -0
  134. package/src/daemon/observer-service.ts +150 -0
  135. package/src/daemon/pid.test.ts +283 -0
  136. package/src/daemon/pid.ts +224 -0
  137. package/src/daemon/research-queue.ts +155 -0
  138. package/src/daemon/services.ts +175 -0
  139. package/src/daemon/ws-service.ts +926 -0
  140. package/src/global.d.ts +4 -0
  141. package/src/goals/accountability.ts +240 -0
  142. package/src/goals/awareness-bridge.ts +185 -0
  143. package/src/goals/estimator.ts +185 -0
  144. package/src/goals/events.ts +28 -0
  145. package/src/goals/goals.test.ts +400 -0
  146. package/src/goals/integration.test.ts +329 -0
  147. package/src/goals/nl-builder.test.ts +220 -0
  148. package/src/goals/nl-builder.ts +256 -0
  149. package/src/goals/rhythm.test.ts +177 -0
  150. package/src/goals/rhythm.ts +275 -0
  151. package/src/goals/service.test.ts +135 -0
  152. package/src/goals/service.ts +407 -0
  153. package/src/goals/types.ts +106 -0
  154. package/src/goals/workflow-bridge.ts +96 -0
  155. package/src/integrations/google-api.ts +134 -0
  156. package/src/integrations/google-auth.ts +175 -0
  157. package/src/llm/README.md +291 -0
  158. package/src/llm/anthropic.ts +400 -0
  159. package/src/llm/gemini.ts +380 -0
  160. package/src/llm/groq.ts +406 -0
  161. package/src/llm/history.ts +147 -0
  162. package/src/llm/index.ts +21 -0
  163. package/src/llm/manager.ts +226 -0
  164. package/src/llm/ollama.ts +316 -0
  165. package/src/llm/openai.ts +411 -0
  166. package/src/llm/openrouter.ts +390 -0
  167. package/src/llm/provider.test.ts +487 -0
  168. package/src/llm/provider.ts +61 -0
  169. package/src/llm/test.ts +88 -0
  170. package/src/observers/README.md +278 -0
  171. package/src/observers/calendar.ts +113 -0
  172. package/src/observers/clipboard.ts +136 -0
  173. package/src/observers/email.ts +109 -0
  174. package/src/observers/example.ts +58 -0
  175. package/src/observers/file-watcher.ts +124 -0
  176. package/src/observers/index.ts +159 -0
  177. package/src/observers/notifications.ts +197 -0
  178. package/src/observers/observers.test.ts +203 -0
  179. package/src/observers/processes.ts +225 -0
  180. package/src/personality/README.md +61 -0
  181. package/src/personality/adapter.ts +196 -0
  182. package/src/personality/index.ts +20 -0
  183. package/src/personality/learner.ts +209 -0
  184. package/src/personality/model.ts +132 -0
  185. package/src/personality/personality.test.ts +236 -0
  186. package/src/roles/README.md +252 -0
  187. package/src/roles/authority.ts +120 -0
  188. package/src/roles/example-usage.ts +198 -0
  189. package/src/roles/index.ts +42 -0
  190. package/src/roles/loader.ts +143 -0
  191. package/src/roles/prompt-builder.ts +218 -0
  192. package/src/roles/test-multi.ts +102 -0
  193. package/src/roles/test-role.yaml +77 -0
  194. package/src/roles/test-utils.ts +93 -0
  195. package/src/roles/test.ts +106 -0
  196. package/src/roles/tool-guide.ts +195 -0
  197. package/src/roles/types.ts +36 -0
  198. package/src/roles/utils.ts +200 -0
  199. package/src/scripts/google-setup.ts +168 -0
  200. package/src/sidecar/connection.ts +179 -0
  201. package/src/sidecar/index.ts +6 -0
  202. package/src/sidecar/manager.ts +542 -0
  203. package/src/sidecar/protocol.ts +85 -0
  204. package/src/sidecar/rpc.ts +161 -0
  205. package/src/sidecar/scheduler.ts +136 -0
  206. package/src/sidecar/types.ts +112 -0
  207. package/src/sidecar/validator.ts +144 -0
  208. package/src/sites/builder-tools.ts +215 -0
  209. package/src/sites/dev-server-manager.ts +286 -0
  210. package/src/sites/fixtures/security-test-site/.jarvis-project.json +6 -0
  211. package/src/sites/fixtures/security-test-site/Makefile +15 -0
  212. package/src/sites/fixtures/security-test-site/README.md +18 -0
  213. package/src/sites/fixtures/security-test-site/index.html +12 -0
  214. package/src/sites/fixtures/security-test-site/index.ts +16 -0
  215. package/src/sites/fixtures/security-test-site/package.json +13 -0
  216. package/src/sites/fixtures/security-test-site/src/app.tsx +780 -0
  217. package/src/sites/fixtures/security-test-site/tsconfig.json +10 -0
  218. package/src/sites/git-manager.ts +240 -0
  219. package/src/sites/github-manager.ts +355 -0
  220. package/src/sites/index.ts +25 -0
  221. package/src/sites/project-manager.ts +389 -0
  222. package/src/sites/proxy.ts +133 -0
  223. package/src/sites/service.ts +136 -0
  224. package/src/sites/templates.ts +169 -0
  225. package/src/sites/types.ts +89 -0
  226. package/src/user/profile-followup.test.ts +84 -0
  227. package/src/user/profile-followup.ts +185 -0
  228. package/src/user/profile.ts +224 -0
  229. package/src/vault/README.md +110 -0
  230. package/src/vault/awareness.ts +341 -0
  231. package/src/vault/commitments.ts +299 -0
  232. package/src/vault/content-pipeline.ts +270 -0
  233. package/src/vault/conversations.ts +173 -0
  234. package/src/vault/dashboard-sessions.ts +44 -0
  235. package/src/vault/documents.ts +130 -0
  236. package/src/vault/entities.ts +185 -0
  237. package/src/vault/extractor.test.ts +356 -0
  238. package/src/vault/extractor.ts +345 -0
  239. package/src/vault/facts.ts +190 -0
  240. package/src/vault/goals.ts +477 -0
  241. package/src/vault/index.ts +87 -0
  242. package/src/vault/keychain.ts +99 -0
  243. package/src/vault/observations.ts +115 -0
  244. package/src/vault/relationships.ts +178 -0
  245. package/src/vault/retrieval.test.ts +139 -0
  246. package/src/vault/retrieval.ts +258 -0
  247. package/src/vault/schema.ts +709 -0
  248. package/src/vault/settings.ts +38 -0
  249. package/src/vault/user-profile.test.ts +113 -0
  250. package/src/vault/user-profile.ts +176 -0
  251. package/src/vault/vectors.ts +92 -0
  252. package/src/vault/webapp-template-seeds.ts +116 -0
  253. package/src/vault/webapp-templates.ts +244 -0
  254. package/src/vault/workflows.ts +403 -0
  255. package/src/workflows/auto-suggest.ts +290 -0
  256. package/src/workflows/engine.ts +366 -0
  257. package/src/workflows/events.ts +24 -0
  258. package/src/workflows/executor.ts +207 -0
  259. package/src/workflows/nl-builder.ts +198 -0
  260. package/src/workflows/nodes/actions/agent-task.ts +73 -0
  261. package/src/workflows/nodes/actions/calendar-action.ts +85 -0
  262. package/src/workflows/nodes/actions/code-execution.ts +73 -0
  263. package/src/workflows/nodes/actions/discord.ts +77 -0
  264. package/src/workflows/nodes/actions/file-write.ts +73 -0
  265. package/src/workflows/nodes/actions/gmail.ts +69 -0
  266. package/src/workflows/nodes/actions/http-request.ts +117 -0
  267. package/src/workflows/nodes/actions/notification.ts +85 -0
  268. package/src/workflows/nodes/actions/run-tool.ts +55 -0
  269. package/src/workflows/nodes/actions/send-message.ts +82 -0
  270. package/src/workflows/nodes/actions/shell-command.ts +76 -0
  271. package/src/workflows/nodes/actions/telegram.ts +60 -0
  272. package/src/workflows/nodes/builtin.ts +119 -0
  273. package/src/workflows/nodes/error/error-handler.ts +37 -0
  274. package/src/workflows/nodes/error/fallback.ts +47 -0
  275. package/src/workflows/nodes/error/retry.ts +82 -0
  276. package/src/workflows/nodes/logic/delay.ts +42 -0
  277. package/src/workflows/nodes/logic/if-else.ts +41 -0
  278. package/src/workflows/nodes/logic/loop.ts +90 -0
  279. package/src/workflows/nodes/logic/merge.ts +38 -0
  280. package/src/workflows/nodes/logic/race.ts +40 -0
  281. package/src/workflows/nodes/logic/switch.ts +59 -0
  282. package/src/workflows/nodes/logic/template-render.ts +53 -0
  283. package/src/workflows/nodes/logic/variable-get.ts +37 -0
  284. package/src/workflows/nodes/logic/variable-set.ts +59 -0
  285. package/src/workflows/nodes/registry.ts +99 -0
  286. package/src/workflows/nodes/transform/aggregate.ts +99 -0
  287. package/src/workflows/nodes/transform/csv-parse.ts +70 -0
  288. package/src/workflows/nodes/transform/json-parse.ts +63 -0
  289. package/src/workflows/nodes/transform/map-filter.ts +84 -0
  290. package/src/workflows/nodes/transform/regex-match.ts +89 -0
  291. package/src/workflows/nodes/triggers/calendar.ts +33 -0
  292. package/src/workflows/nodes/triggers/clipboard.ts +32 -0
  293. package/src/workflows/nodes/triggers/cron.ts +40 -0
  294. package/src/workflows/nodes/triggers/email.ts +40 -0
  295. package/src/workflows/nodes/triggers/file-change.ts +45 -0
  296. package/src/workflows/nodes/triggers/git.ts +46 -0
  297. package/src/workflows/nodes/triggers/manual.ts +23 -0
  298. package/src/workflows/nodes/triggers/poll.ts +81 -0
  299. package/src/workflows/nodes/triggers/process.ts +44 -0
  300. package/src/workflows/nodes/triggers/screen-event.ts +37 -0
  301. package/src/workflows/nodes/triggers/webhook.ts +39 -0
  302. package/src/workflows/safe-eval.ts +139 -0
  303. package/src/workflows/template.ts +118 -0
  304. package/src/workflows/triggers/cron.ts +311 -0
  305. package/src/workflows/triggers/manager.ts +285 -0
  306. package/src/workflows/triggers/observer-bridge.ts +172 -0
  307. package/src/workflows/triggers/poller.ts +201 -0
  308. package/src/workflows/triggers/screen-condition.ts +218 -0
  309. package/src/workflows/triggers/triggers.test.ts +740 -0
  310. package/src/workflows/triggers/webhook.ts +191 -0
  311. package/src/workflows/types.ts +133 -0
  312. package/src/workflows/variables.ts +72 -0
  313. package/src/workflows/workflows.test.ts +383 -0
  314. package/src/workflows/yaml.ts +104 -0
  315. package/ui/dist/index-3gr23jt9.js +112614 -0
  316. package/ui/dist/index-9vmj8127.css +14239 -0
  317. package/ui/dist/index-hy9pc1gm.js +112873 -0
  318. package/ui/dist/index-j2ep5d1w.js +112374 -0
  319. package/ui/dist/index-jt00vjqs.js +112858 -0
  320. package/ui/dist/index-k9ymx5qb.js +112374 -0
  321. package/ui/dist/index.html +16 -0
  322. package/ui/public/audio/pcm-capture-processor.js +11 -0
  323. package/ui/public/openwakeword/models/embedding_model.onnx +0 -0
  324. package/ui/public/openwakeword/models/hey_jarvis_v0.1.onnx +0 -0
  325. package/ui/public/openwakeword/models/melspectrogram.onnx +0 -0
  326. package/ui/public/openwakeword/models/silero_vad.onnx +0 -0
  327. package/ui/public/ort/ort-wasm-simd-threaded.jsep.mjs +106 -0
  328. package/ui/public/ort/ort-wasm-simd-threaded.jsep.wasm +0 -0
  329. package/ui/public/ort/ort-wasm-simd-threaded.mjs +59 -0
  330. package/ui/public/ort/ort-wasm-simd-threaded.wasm +0 -0
@@ -0,0 +1,926 @@
1
+ /**
2
+ * WebSocket Service — The Mouth
3
+ *
4
+ * Wraps WebSocketServer and StreamRelay. Routes incoming messages
5
+ * to the AgentService and relays streamed responses back to clients.
6
+ */
7
+
8
+ import type { ServerWebSocket } from 'bun';
9
+ import type { Service, ServiceStatus } from './services.ts';
10
+ import type { AgentService } from './agent-service.ts';
11
+ import type { CommitmentExecutor } from './commitment-executor.ts';
12
+ import type { ChannelService } from './channel-service.ts';
13
+ import type { Commitment } from '../vault/commitments.ts';
14
+ import type { ContentItem } from '../vault/content-pipeline.ts';
15
+ import type { STTProvider, TTSProvider } from '../comms/voice.ts';
16
+ import { setDefaultCwd } from '../actions/tools/builtin.ts';
17
+ import type { ApprovalRequest } from '../authority/approval.ts';
18
+ import type { EmergencyState } from '../authority/emergency.ts';
19
+ import { createCommitment, updateCommitmentStatus, updateCommitmentAssignee } from '../vault/commitments.ts';
20
+ import { WebSocketServer, type WSMessage } from '../comms/websocket.ts';
21
+ import { StreamRelay } from '../comms/streaming.ts';
22
+ import { getOrCreateConversation, addMessage } from '../vault/conversations.ts';
23
+ import { maybeCreateUserProfileFollowupPrompt, recordUserProfileTurn } from '../user/profile-followup.ts';
24
+
25
+ type VoiceSession = {
26
+ requestId: string;
27
+ chunks: Buffer[];
28
+ startedAt: number;
29
+ };
30
+
31
+ export class WebSocketService implements Service {
32
+ name = 'websocket';
33
+ private _status: ServiceStatus = 'stopped';
34
+ private port: number;
35
+ private agentService: AgentService;
36
+ private wsServer: WebSocketServer;
37
+ private streamRelay: StreamRelay;
38
+ /** Tracks the commitment ID for the currently processing chat message */
39
+ private activeTaskId: string | null = null;
40
+ private commitmentExecutor: CommitmentExecutor | null = null;
41
+ private channelService: ChannelService | null = null;
42
+ private ttsProvider: TTSProvider | null = null;
43
+ private sttProvider: STTProvider | null = null;
44
+ private voiceSessions = new Map<ServerWebSocket<unknown>, VoiceSession>();
45
+ private siteBuilderService: import('../sites/service.ts').SiteBuilderService | null = null;
46
+
47
+ constructor(port: number, agentService: AgentService) {
48
+ this.port = port;
49
+ this.agentService = agentService;
50
+ this.wsServer = new WebSocketServer(port);
51
+ this.streamRelay = new StreamRelay(this.wsServer);
52
+
53
+ // Wire delegation callback: when PA delegates to a specialist,
54
+ // update the active task's assigned_to on the task board
55
+ this.agentService.setDelegationCallback((specialistName) => {
56
+ if (!this.activeTaskId) return;
57
+ try {
58
+ const updated = updateCommitmentAssignee(this.activeTaskId, specialistName);
59
+ if (updated) this.broadcastTaskUpdate(updated, 'updated');
60
+ } catch (err) {
61
+ console.error('[WSService] Failed to update task assignee:', err);
62
+ }
63
+ });
64
+ }
65
+
66
+ /**
67
+ * Set the site builder service for project-scoped chat.
68
+ */
69
+ setSiteBuilderService(svc: import('../sites/service.ts').SiteBuilderService): void {
70
+ this.siteBuilderService = svc;
71
+ }
72
+
73
+ /**
74
+ * Set the commitment executor for handling cancel commands.
75
+ */
76
+ setCommitmentExecutor(executor: CommitmentExecutor): void {
77
+ this.commitmentExecutor = executor;
78
+ }
79
+
80
+ /**
81
+ * Set the channel service for cross-channel broadcasts.
82
+ */
83
+ setChannelService(channelService: ChannelService): void {
84
+ this.channelService = channelService;
85
+ }
86
+
87
+ /**
88
+ * Set the TTS provider for voice responses.
89
+ */
90
+ setTTSProvider(provider: TTSProvider): void {
91
+ this.ttsProvider = provider;
92
+ console.log('[WSService] TTS provider set');
93
+ }
94
+
95
+ /**
96
+ * Set the STT provider for voice input transcription.
97
+ */
98
+ setSTTProvider(provider: STTProvider): void {
99
+ this.sttProvider = provider;
100
+ console.log('[WSService] STT provider set');
101
+ }
102
+
103
+ /**
104
+ * Get the underlying WebSocket server for direct broadcasting.
105
+ */
106
+ getServer(): WebSocketServer {
107
+ return this.wsServer;
108
+ }
109
+
110
+ /**
111
+ * Register API route handlers on the underlying WebSocket server.
112
+ * Must be called before start().
113
+ */
114
+ setApiRoutes(routes: Record<string, any>): void {
115
+ this.wsServer.setApiRoutes(routes);
116
+ }
117
+
118
+ /**
119
+ * Set directory for serving pre-built dashboard files.
120
+ * Must be called before start().
121
+ */
122
+ setStaticDir(dir: string): void {
123
+ this.wsServer.setStaticDir(dir);
124
+ }
125
+
126
+ setPublicDir(dir: string): void {
127
+ this.wsServer.setPublicDir(dir);
128
+ }
129
+
130
+ setAuthToken(token: string): void {
131
+ this.wsServer.setAuthToken(token);
132
+ }
133
+
134
+ setDashboardPasswordHash(passwordHash?: string | null): void {
135
+ this.wsServer.setDashboardPasswordHash(passwordHash);
136
+ }
137
+
138
+ async start(): Promise<void> {
139
+ this._status = 'starting';
140
+
141
+ try {
142
+ // Set up message handler
143
+ this.wsServer.setHandler({
144
+ onMessage: (msg, ws) => this.routeMessage(msg, ws),
145
+ onBinaryMessage: (data, ws) => this.handleVoiceAudio(data, ws),
146
+ onConnect: (_ws) => {
147
+ console.log('[WSService] Client connected');
148
+ },
149
+ onDisconnect: (ws) => {
150
+ // Clean up any pending voice session for this client
151
+ this.voiceSessions.delete(ws);
152
+ console.log('[WSService] Client disconnected');
153
+ },
154
+ });
155
+
156
+ // Start the server
157
+ this.wsServer.start();
158
+ this._status = 'running';
159
+ console.log(`[WSService] Started on port ${this.port}`);
160
+ } catch (error) {
161
+ this._status = 'error';
162
+ throw error;
163
+ }
164
+ }
165
+
166
+ async stop(): Promise<void> {
167
+ this._status = 'stopping';
168
+ this.wsServer.stop();
169
+ this._status = 'stopped';
170
+ console.log('[WSService] Stopped');
171
+ }
172
+
173
+ status(): ServiceStatus {
174
+ return this._status;
175
+ }
176
+
177
+ /**
178
+ * Broadcast a proactive heartbeat message to all connected clients
179
+ * and external channels.
180
+ */
181
+ broadcastHeartbeat(text: string): void {
182
+ const message: WSMessage = {
183
+ type: 'chat',
184
+ payload: {
185
+ text,
186
+ source: 'heartbeat',
187
+ },
188
+ priority: 'normal',
189
+ timestamp: Date.now(),
190
+ };
191
+ this.wsServer.broadcast(message);
192
+
193
+ // Also push to external channels
194
+ if (this.channelService) {
195
+ this.channelService.broadcastToAll(text).catch(err =>
196
+ console.error('[WSService] Channel heartbeat broadcast error:', err)
197
+ );
198
+ }
199
+ }
200
+
201
+ /**
202
+ * Broadcast a notification with priority level.
203
+ * Used by EventReactor for immediate event reactions.
204
+ * Urgent notifications are also pushed to all external channels.
205
+ */
206
+ broadcastNotification(text: string, priority: 'urgent' | 'normal' | 'low'): void {
207
+ const message: WSMessage = {
208
+ type: 'chat',
209
+ payload: {
210
+ text,
211
+ source: 'proactive',
212
+ },
213
+ priority,
214
+ timestamp: Date.now(),
215
+ };
216
+ this.wsServer.broadcast(message);
217
+
218
+ // Push urgent notifications to external channels (Telegram, Discord)
219
+ if (priority === 'urgent' && this.channelService) {
220
+ this.channelService.broadcastToAll(`[URGENT] ${text}`).catch(err =>
221
+ console.error('[WSService] Channel broadcast error:', err)
222
+ );
223
+ }
224
+ }
225
+
226
+ /**
227
+ * Broadcast task (commitment) changes to all connected clients.
228
+ * Used for real-time task board updates.
229
+ */
230
+ broadcastTaskUpdate(task: Commitment, action: 'created' | 'updated' | 'deleted'): void {
231
+ const message: WSMessage = {
232
+ type: 'notification',
233
+ payload: {
234
+ source: 'task_update',
235
+ action,
236
+ task,
237
+ },
238
+ timestamp: Date.now(),
239
+ };
240
+ this.wsServer.broadcast(message);
241
+ }
242
+
243
+ private broadcastAssistantMessage(text: string, requestId?: string): void {
244
+ const message: WSMessage = {
245
+ type: 'notification',
246
+ payload: {
247
+ source: 'assistant_message',
248
+ text,
249
+ },
250
+ id: requestId,
251
+ timestamp: Date.now(),
252
+ };
253
+ this.wsServer.broadcast(message);
254
+ }
255
+
256
+ /**
257
+ * Broadcast content pipeline changes to all connected clients.
258
+ * Used for real-time content pipeline updates.
259
+ */
260
+ broadcastContentUpdate(item: ContentItem, action: 'created' | 'updated' | 'deleted'): void {
261
+ const message: WSMessage = {
262
+ type: 'notification',
263
+ payload: {
264
+ source: 'content_update',
265
+ action,
266
+ item,
267
+ },
268
+ timestamp: Date.now(),
269
+ };
270
+ this.wsServer.broadcast(message);
271
+ }
272
+
273
+ /**
274
+ * Broadcast sub-agent progress events to all connected clients.
275
+ * Used by the delegation system for real-time visibility.
276
+ */
277
+ broadcastSubAgentProgress(event: {
278
+ type: 'text' | 'tool_call' | 'done';
279
+ agentName: string;
280
+ agentId: string;
281
+ data: unknown;
282
+ }): void {
283
+ const message: WSMessage = {
284
+ type: 'stream',
285
+ payload: {
286
+ ...event,
287
+ source: 'sub-agent',
288
+ },
289
+ timestamp: Date.now(),
290
+ };
291
+ this.wsServer.broadcast(message);
292
+ }
293
+
294
+ /**
295
+ * Broadcast an approval request to all connected dashboard clients.
296
+ * Always pushed via WS; urgent requests are also sent to external channels.
297
+ */
298
+ broadcastApprovalRequest(request: ApprovalRequest): void {
299
+ const shortId = request.id.slice(0, 8);
300
+ const message: WSMessage = {
301
+ type: 'notification',
302
+ payload: {
303
+ source: 'approval_request',
304
+ request,
305
+ shortId,
306
+ },
307
+ priority: request.urgency === 'urgent' ? 'urgent' : 'normal',
308
+ timestamp: Date.now(),
309
+ };
310
+ this.wsServer.broadcast(message);
311
+
312
+ // Push urgent approvals to external channels
313
+ if (request.urgency === 'urgent' && this.channelService) {
314
+ const text = `[APPROVAL NEEDED] ${request.agent_name} wants to run ${request.tool_name} (${request.action_category}).\nReason: ${request.reason}\nReply: approve ${shortId} / deny ${shortId}`;
315
+ this.channelService.broadcastToAll(text).catch(err =>
316
+ console.error('[WSService] Approval channel broadcast error:', err)
317
+ );
318
+ }
319
+ }
320
+
321
+ /**
322
+ * Broadcast emergency state changes to all connected clients.
323
+ */
324
+ broadcastEmergencyState(state: EmergencyState): void {
325
+ const message: WSMessage = {
326
+ type: 'notification',
327
+ payload: {
328
+ source: 'emergency_state',
329
+ state,
330
+ },
331
+ priority: 'urgent',
332
+ timestamp: Date.now(),
333
+ };
334
+ this.wsServer.broadcast(message);
335
+ }
336
+
337
+ /**
338
+ * Broadcast an approval resolution (approved/denied/executed) to all clients.
339
+ */
340
+ /**
341
+ * Broadcast an awareness event to all connected clients.
342
+ */
343
+ /**
344
+ * Broadcast a sidecar event to all connected clients.
345
+ */
346
+ broadcastSidecarEvent(sidecarId: string, event: { type: string; data: Record<string, unknown>; timestamp: number }): void {
347
+ const message: WSMessage = {
348
+ type: 'notification',
349
+ payload: {
350
+ source: 'sidecar_event',
351
+ sidecarId,
352
+ event,
353
+ },
354
+ timestamp: event.timestamp,
355
+ };
356
+ this.wsServer.broadcast(message);
357
+ }
358
+
359
+ broadcastAwarenessEvent(event: { type: string; data: Record<string, unknown>; timestamp: number }): void {
360
+ const message: WSMessage = {
361
+ type: 'notification',
362
+ payload: {
363
+ source: 'awareness_event',
364
+ event,
365
+ },
366
+ timestamp: event.timestamp,
367
+ };
368
+ this.wsServer.broadcast(message);
369
+ }
370
+
371
+ /**
372
+ * Synthesize TTS for a proactive message and broadcast audio to all clients.
373
+ * Used for awareness suggestions and other unsolicited voice notifications.
374
+ */
375
+ /**
376
+ * Synthesize TTS for a proactive message and broadcast audio to all clients.
377
+ * Used for awareness suggestions and other unsolicited voice notifications.
378
+ */
379
+ async broadcastProactiveVoice(text: string): Promise<void> {
380
+ if (!this.ttsProvider || !text) {
381
+ console.log(`[WSService] Proactive TTS skipped: ${!this.ttsProvider ? 'no TTS provider' : 'empty text'}`);
382
+ return;
383
+ }
384
+
385
+ if (this.wsServer.getClientCount() === 0) {
386
+ console.log('[WSService] Proactive TTS skipped: no connected clients');
387
+ return;
388
+ }
389
+
390
+ try {
391
+ const requestId = `proactive-${Date.now()}`;
392
+
393
+ // Signal TTS start to all clients
394
+ const startMsg: WSMessage = {
395
+ type: 'tts_start',
396
+ payload: { requestId },
397
+ timestamp: Date.now(),
398
+ };
399
+ this.wsServer.broadcast(startMsg);
400
+
401
+ let chunkCount = 0;
402
+ for await (const chunk of this.ttsProvider.synthesizeStream(text)) {
403
+ // Send binary audio to all connected clients
404
+ for (const ws of this.wsServer.getClients()) {
405
+ try {
406
+ ws.sendBinary(chunk);
407
+ } catch { /* client may have disconnected */ }
408
+ }
409
+ chunkCount++;
410
+ }
411
+
412
+ // Signal TTS end
413
+ const endMsg: WSMessage = {
414
+ type: 'tts_end',
415
+ payload: { requestId },
416
+ timestamp: Date.now(),
417
+ };
418
+ this.wsServer.broadcast(endMsg);
419
+ console.log(`[WSService] Proactive TTS complete: "${text.slice(0, 60)}..." (${chunkCount} chunks)`);
420
+ } catch (err) {
421
+ console.error('[WSService] Proactive TTS error:', err instanceof Error ? err.message : err);
422
+ // Still send tts_end so client doesn't get stuck
423
+ try {
424
+ this.wsServer.broadcast({ type: 'tts_end', payload: {}, timestamp: Date.now() });
425
+ } catch { /* ignore */ }
426
+ }
427
+ }
428
+
429
+ /**
430
+ * Broadcast a workflow execution event to all connected clients.
431
+ */
432
+ broadcastWorkflowEvent(event: { type: string; workflowId: string; executionId?: string; nodeId?: string; data: Record<string, unknown>; timestamp: number }): void {
433
+ const message: WSMessage = {
434
+ type: 'workflow_event',
435
+ payload: event,
436
+ timestamp: event.timestamp,
437
+ };
438
+ this.wsServer.broadcast(message);
439
+ }
440
+
441
+ /**
442
+ * Broadcast a goal event to all connected clients.
443
+ */
444
+ broadcastGoalEvent(event: { type: string; goalId?: string; data: Record<string, unknown>; timestamp: number }): void {
445
+ const message: WSMessage = {
446
+ type: 'goal_event',
447
+ payload: event,
448
+ timestamp: event.timestamp,
449
+ };
450
+ this.wsServer.broadcast(message);
451
+ }
452
+
453
+ broadcastSiteEvent(event: { type: string; projectId: string; data: Record<string, unknown>; timestamp: number }): void {
454
+ const message: WSMessage = {
455
+ type: 'site_event',
456
+ payload: event,
457
+ timestamp: event.timestamp,
458
+ };
459
+ this.wsServer.broadcast(message);
460
+ }
461
+
462
+ /**
463
+ * Format a FileEntry tree into a compact text listing.
464
+ */
465
+ private formatFileTree(entry: { name: string; path: string; type: 'file' | 'directory'; children?: { name: string; type: 'file' | 'directory' }[] }): string {
466
+ const lines: string[] = [];
467
+ if (entry.children) {
468
+ for (const child of entry.children) {
469
+ lines.push(child.type === 'directory' ? `${child.name}/` : child.name);
470
+ }
471
+ }
472
+ return lines.join('\n') + '\n';
473
+ }
474
+
475
+ broadcastApprovalUpdate(request: ApprovalRequest): void {
476
+ const message: WSMessage = {
477
+ type: 'notification',
478
+ payload: {
479
+ source: 'approval_update',
480
+ request,
481
+ },
482
+ timestamp: Date.now(),
483
+ };
484
+ this.wsServer.broadcast(message);
485
+ }
486
+
487
+ /**
488
+ * Route incoming WebSocket messages to the appropriate handler.
489
+ */
490
+ private async routeMessage(msg: WSMessage, ws: ServerWebSocket<unknown>): Promise<WSMessage | void> {
491
+ switch (msg.type) {
492
+ case 'chat':
493
+ return this.handleChat(msg, ws);
494
+
495
+ case 'command':
496
+ return this.handleCommand(msg);
497
+
498
+ case 'status':
499
+ return this.handleStatus();
500
+
501
+ case 'voice_start': {
502
+ const { requestId } = msg.payload as { requestId: string };
503
+ this.voiceSessions.set(ws, { requestId, chunks: [], startedAt: Date.now() });
504
+ return undefined;
505
+ }
506
+
507
+ case 'voice_end': {
508
+ const session = this.voiceSessions.get(ws);
509
+ if (!session) return undefined;
510
+ this.voiceSessions.delete(ws);
511
+ // Fire-and-forget: transcribe → process → TTS response
512
+ this.handleVoiceSession(session, ws).catch(err =>
513
+ console.error('[WSService] Voice session error:', err)
514
+ );
515
+ return undefined;
516
+ }
517
+
518
+ default:
519
+ return {
520
+ type: 'error',
521
+ payload: { message: `Unknown message type: ${msg.type}` },
522
+ timestamp: Date.now(),
523
+ };
524
+ }
525
+ }
526
+
527
+ /**
528
+ * Handle chat messages — stream response via StreamRelay.
529
+ * Auto-creates a task for non-trivial messages so the task board tracks agent work.
530
+ */
531
+ private async handleChat(msg: WSMessage, ws?: ServerWebSocket<unknown>): Promise<WSMessage | void> {
532
+ const payload = msg.payload as { text?: string; channel?: string; projectId?: string };
533
+ const text = payload?.text;
534
+ const projectId = payload?.projectId ?? null;
535
+
536
+ if (!text) {
537
+ return {
538
+ type: 'error',
539
+ payload: { message: 'Missing text in chat payload' },
540
+ id: msg.id,
541
+ timestamp: Date.now(),
542
+ };
543
+ }
544
+
545
+ const channel = payload.channel ?? 'websocket';
546
+ const requestId = msg.id ?? crypto.randomUUID();
547
+
548
+ // Build site builder system prompt context (injected into system prompt, not user message)
549
+ let siteContext: string | undefined;
550
+ if (projectId && this.siteBuilderService) {
551
+ // Project-scoped chat (from the Site Builder page)
552
+ const project = await this.siteBuilderService.getProjectWithStatus(projectId);
553
+ if (project) {
554
+ let fileTreeText = '';
555
+ try {
556
+ const tree = this.siteBuilderService.projectManager.getFileTree(projectId, 1);
557
+ fileTreeText = this.formatFileTree(tree);
558
+ } catch { /* ignore */ }
559
+
560
+ siteContext = `# Site Builder Context
561
+
562
+ You are working on project "${project.name}" (${project.framework}).
563
+ - Path: ${project.path}
564
+ - Branch: ${project.gitBranch ?? 'main'}
565
+ - Dev server: ${project.status}
566
+ ${project.githubUrl ? `- GitHub: ${project.githubUrl}` : ''}
567
+ ${fileTreeText ? `\n## Project Structure\n\`\`\`\n${fileTreeText}\`\`\`` : ''}
568
+
569
+ ## Rules
570
+ - Use site_read_file, site_write_file, site_list_files, site_run_command, site_git_commit, site_github_push tools with project_id="${projectId}".
571
+ - Do NOT use regular read_file, write_file, or run_command — always use the site_* variants.
572
+ - Do NOT start dev servers via site_run_command. The dev server is managed by the dashboard (make dev runs automatically).
573
+ - Changes are auto-committed after this conversation turn completes.
574
+ - For the "bun-react" framework: the server uses Bun.serve() with HTML imports (import from "./index.html"). Run with "bun --hot index.ts", NOT vite or webpack.`;
575
+ }
576
+ } else if (this.siteBuilderService) {
577
+ // General chat (main dashboard) — give the LLM awareness of site builder projects
578
+ try {
579
+ const projects = await this.siteBuilderService.listProjectsWithStatus();
580
+ if (projects.length > 0) {
581
+ const projectList = projects.map(p =>
582
+ ` - "${p.name}" (id: ${p.id}, framework: ${p.framework}, branch: ${p.gitBranch ?? 'main'}${p.githubUrl ? `, github: ${p.githubUrl}` : ''})`
583
+ ).join('\n');
584
+
585
+ siteContext = `# Site Builder
586
+
587
+ You have access to the Site Builder feature with ${projects.length} project(s):
588
+ ${projectList}
589
+
590
+ You can work on any of these projects using site builder tools (site_read_file, site_write_file, site_list_files, site_run_command, site_git_commit, site_github_push) by passing the project's id as project_id.
591
+ When the user asks you to build, edit, or work on a website/app, use these tools to make changes directly in the project files.
592
+ If the user wants to create a new project, tell them to use the Site Builder page (Sites tab in the sidebar) to create one first.`;
593
+ }
594
+ } catch { /* ignore — site builder may not be fully started */ }
595
+ }
596
+
597
+ // Auto-create a task for non-trivial messages
598
+ const isTrivial = text.trim().length < 10;
599
+ let taskCommitment: Commitment | null = null;
600
+
601
+ if (!isTrivial) {
602
+ try {
603
+ const taskLabel = text.length > 80 ? text.slice(0, 77) + '...' : text;
604
+ taskCommitment = createCommitment(taskLabel, {
605
+ assigned_to: 'jarvis',
606
+ created_from: 'user',
607
+ });
608
+ updateCommitmentStatus(taskCommitment.id, 'active');
609
+ taskCommitment.status = 'active';
610
+ this.activeTaskId = taskCommitment.id;
611
+ this.broadcastTaskUpdate(taskCommitment, 'created');
612
+ } catch (err) {
613
+ console.error('[WSService] Failed to auto-create task:', err);
614
+ }
615
+ }
616
+
617
+ // Persist user message
618
+ try {
619
+ const conversation = getOrCreateConversation(channel);
620
+ recordUserProfileTurn(text);
621
+ addMessage(conversation.id, { role: 'user', content: text });
622
+
623
+ // Set default cwd for general tools (run_command, read_file, etc.)
624
+ // so they operate in the project directory during site builder conversations
625
+ if (projectId && this.siteBuilderService) {
626
+ const projectPath = this.siteBuilderService.projectManager.getProjectPath(projectId);
627
+ setDefaultCwd(projectPath);
628
+ }
629
+
630
+ const { stream, onComplete } = this.agentService.streamMessage(text, channel, siteContext);
631
+
632
+ // Set up streaming TTS: speak sentences as they arrive
633
+ const ttsActive = !!(this.ttsProvider && ws);
634
+ let ttsSentenceQueue: string[] = [];
635
+ let ttsSpeaking = false;
636
+ let ttsStartSent = false;
637
+ let ttsStreamFullyDone = false; // set AFTER relayStream returns, not per-turn 'done'
638
+ let ttsSentenceCount = 0;
639
+ let ttsChunkCount = 0;
640
+
641
+ const speakNextSentence = async () => {
642
+ if (ttsSpeaking || !ttsActive || !ws) return;
643
+ const sentence = ttsSentenceQueue.shift();
644
+ if (!sentence) {
645
+ // Queue empty — send tts_end only if stream is fully done
646
+ if (ttsStreamFullyDone && ttsStartSent) {
647
+ console.log(`[WSService] TTS complete: ${ttsSentenceCount} sentences, ${ttsChunkCount} audio chunks`);
648
+ this.wsServer.sendToClient(ws, {
649
+ type: 'tts_end',
650
+ payload: { requestId },
651
+ id: requestId,
652
+ timestamp: Date.now(),
653
+ });
654
+ ttsStartSent = false; // prevent duplicate tts_end
655
+ }
656
+ return;
657
+ }
658
+
659
+ // Send tts_start exactly once before the first audio chunk
660
+ if (!ttsStartSent) {
661
+ ttsStartSent = true;
662
+ this.wsServer.sendToClient(ws, {
663
+ type: 'tts_start',
664
+ payload: { requestId },
665
+ id: requestId,
666
+ timestamp: Date.now(),
667
+ });
668
+ }
669
+
670
+ ttsSpeaking = true;
671
+ ttsSentenceCount++;
672
+ try {
673
+ if (this.ttsProvider) {
674
+ for await (const chunk of this.ttsProvider.synthesizeStream(sentence)) {
675
+ ttsChunkCount++;
676
+ this.wsServer.sendBinary(ws, chunk);
677
+ }
678
+ }
679
+ } catch (err) {
680
+ console.error('[WSService] TTS sentence error:', err);
681
+ }
682
+ ttsSpeaking = false;
683
+ speakNextSentence();
684
+ };
685
+
686
+ // Relay stream to all WebSocket clients, collect full text.
687
+ // onSentence fires for each complete sentence during streaming.
688
+ // NOTE: onTextDone fires per LLM turn (tool loop), NOT once at the end.
689
+ // We ignore onTextDone and use the relayStream return to mark stream completion.
690
+ const fullText = await this.streamRelay.relayStream(stream, requestId, ttsActive ? {
691
+ onSentence: (sentence) => {
692
+ ttsSentenceQueue.push(sentence);
693
+ speakNextSentence();
694
+ },
695
+ } : undefined);
696
+
697
+ // Stream is now fully done (all tool loop turns complete)
698
+ ttsStreamFullyDone = true;
699
+ if (ttsActive) {
700
+ if (!ttsSpeaking && ttsSentenceQueue.length === 0 && ttsStartSent) {
701
+ // Everything already played, send tts_end now
702
+ this.wsServer.sendToClient(ws!, {
703
+ type: 'tts_end',
704
+ payload: { requestId },
705
+ id: requestId,
706
+ timestamp: Date.now(),
707
+ });
708
+ ttsStartSent = false;
709
+ }
710
+ // Otherwise speakNextSentence will send tts_end when queue drains
711
+ }
712
+
713
+ // Persist assistant response
714
+ addMessage(conversation.id, { role: 'assistant', content: fullText });
715
+
716
+ const followupPrompt = maybeCreateUserProfileFollowupPrompt();
717
+ if (followupPrompt) {
718
+ this.broadcastAssistantMessage(followupPrompt);
719
+ addMessage(conversation.id, { role: 'assistant', content: followupPrompt });
720
+ }
721
+
722
+ // Mark task as completed
723
+ if (taskCommitment) {
724
+ try {
725
+ const resultSummary = fullText.length > 200 ? fullText.slice(0, 197) + '...' : fullText;
726
+ const updated = updateCommitmentStatus(taskCommitment.id, 'completed', resultSummary);
727
+ if (updated) this.broadcastTaskUpdate(updated, 'updated');
728
+ } catch (err) {
729
+ console.error('[WSService] Failed to complete task:', err);
730
+ } finally {
731
+ this.activeTaskId = null;
732
+ }
733
+ }
734
+
735
+ // Clear site builder default cwd now that the turn is done
736
+ setDefaultCwd(null);
737
+
738
+ // Fire-and-forget: run post-processing (extraction, personality)
739
+ onComplete(fullText).catch((err) =>
740
+ console.error('[WSService] onComplete error:', err)
741
+ );
742
+
743
+ // Auto-commit site builder changes after chat turn
744
+ if (projectId && this.siteBuilderService) {
745
+ try {
746
+ const projectPath = this.siteBuilderService.projectManager.getProjectPath(projectId);
747
+ if (projectPath) {
748
+ const commitMsg = text.length > 60 ? text.slice(0, 57) + '...' : text;
749
+ const commit = await this.siteBuilderService.gitManager.autoCommit(projectPath, commitMsg);
750
+ if (commit) {
751
+ this.broadcastSiteEvent({
752
+ type: 'git_commit',
753
+ projectId,
754
+ data: { commit },
755
+ timestamp: Date.now(),
756
+ });
757
+ }
758
+ }
759
+ } catch (err) {
760
+ console.error('[WSService] Site builder auto-commit error:', err);
761
+ }
762
+ }
763
+
764
+ // Don't return a direct response — StreamRelay already broadcast everything
765
+ return undefined;
766
+ } catch (error) {
767
+ console.error('[WSService] Chat error:', error);
768
+
769
+ // Mark task as failed
770
+ if (taskCommitment) {
771
+ try {
772
+ const reason = error instanceof Error ? error.message : 'Processing failed';
773
+ const updated = updateCommitmentStatus(taskCommitment.id, 'failed', reason);
774
+ if (updated) this.broadcastTaskUpdate(updated, 'updated');
775
+ } catch (err) {
776
+ console.error('[WSService] Failed to fail task:', err);
777
+ } finally {
778
+ this.activeTaskId = null;
779
+ }
780
+ }
781
+
782
+ return {
783
+ type: 'error',
784
+ payload: {
785
+ message: error instanceof Error ? error.message : 'Chat processing failed',
786
+ },
787
+ id: requestId,
788
+ timestamp: Date.now(),
789
+ };
790
+ }
791
+ }
792
+
793
+ /**
794
+ * Handle binary audio data from voice recording.
795
+ * Accumulates chunks into the active voice session for this client.
796
+ */
797
+ private async handleVoiceAudio(data: Buffer, ws: ServerWebSocket<unknown>): Promise<void> {
798
+ const session = this.voiceSessions.get(ws);
799
+ if (!session) {
800
+ console.warn('[WSService] Binary audio received with no active voice session');
801
+ return;
802
+ }
803
+ session.chunks.push(data);
804
+ }
805
+
806
+ /**
807
+ * Process a completed voice session: STT → chat → TTS response.
808
+ */
809
+ private async handleVoiceSession(session: VoiceSession, ws: ServerWebSocket<unknown>): Promise<void> {
810
+ if (!this.sttProvider) {
811
+ this.wsServer.sendToClient(ws, {
812
+ type: 'error',
813
+ payload: { message: 'STT not configured. Enable it in Settings > Channels.' },
814
+ timestamp: Date.now(),
815
+ });
816
+ return;
817
+ }
818
+
819
+ const audioBuffer = Buffer.concat(session.chunks);
820
+ if (audioBuffer.length === 0) return;
821
+
822
+ try {
823
+ const transcript = await this.sttProvider.transcribe(audioBuffer);
824
+ if (!transcript.trim()) return;
825
+
826
+ console.log('[WSService] Voice transcript:', transcript);
827
+
828
+ // Echo transcript back so the UI shows it as a user message
829
+ this.wsServer.sendToClient(ws, {
830
+ type: 'chat',
831
+ payload: { text: transcript, source: 'voice_transcript' },
832
+ id: session.requestId,
833
+ timestamp: Date.now(),
834
+ });
835
+
836
+ // Reuse existing chat flow
837
+ await this.handleChat({
838
+ type: 'chat',
839
+ payload: { text: transcript },
840
+ id: session.requestId,
841
+ timestamp: Date.now(),
842
+ }, ws);
843
+ } catch (err) {
844
+ console.error('[WSService] STT error:', err);
845
+ const message = err instanceof Error ? err.message : 'Voice transcription failed';
846
+ this.wsServer.sendToClient(ws, {
847
+ type: 'error',
848
+ payload: { message },
849
+ timestamp: Date.now(),
850
+ });
851
+ }
852
+ }
853
+
854
+ /**
855
+ * Handle system commands.
856
+ */
857
+ private async handleCommand(msg: WSMessage): Promise<WSMessage> {
858
+ const payload = msg.payload as { command?: string };
859
+ const command = payload?.command;
860
+
861
+ switch (command) {
862
+ case 'health':
863
+ return {
864
+ type: 'status',
865
+ payload: {
866
+ status: 'ok',
867
+ service: this.name,
868
+ clients: this.wsServer.getClientCount(),
869
+ },
870
+ id: msg.id,
871
+ timestamp: Date.now(),
872
+ };
873
+
874
+ case 'ping':
875
+ return {
876
+ type: 'status',
877
+ payload: { pong: true },
878
+ id: msg.id,
879
+ timestamp: Date.now(),
880
+ };
881
+
882
+ case 'cancel_execution': {
883
+ const commitmentId = (msg.payload as any)?.commitmentId;
884
+ if (this.commitmentExecutor && commitmentId) {
885
+ const cancelled = this.commitmentExecutor.cancelExecution(commitmentId);
886
+ return {
887
+ type: 'status',
888
+ payload: { cancelled, commitmentId },
889
+ id: msg.id,
890
+ timestamp: Date.now(),
891
+ };
892
+ }
893
+ return {
894
+ type: 'error',
895
+ payload: { message: 'No executor available or missing commitmentId' },
896
+ id: msg.id,
897
+ timestamp: Date.now(),
898
+ };
899
+ }
900
+
901
+ default:
902
+ return {
903
+ type: 'error',
904
+ payload: { message: `Unknown command: ${command}` },
905
+ id: msg.id,
906
+ timestamp: Date.now(),
907
+ };
908
+ }
909
+ }
910
+
911
+ /**
912
+ * Handle status requests.
913
+ */
914
+ private handleStatus(): WSMessage {
915
+ return {
916
+ type: 'status',
917
+ payload: {
918
+ service: this.name,
919
+ status: this._status,
920
+ clients: this.wsServer.getClientCount(),
921
+ port: this.port,
922
+ },
923
+ timestamp: Date.now(),
924
+ };
925
+ }
926
+ }