@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,669 @@
1
+ import type { Server, ServerWebSocket } from 'bun';
2
+ import path from 'node:path';
3
+ import type { SidecarManager } from '../sidecar/manager.ts';
4
+ import {
5
+ getCookie,
6
+ getDashboardSessionFromRequest,
7
+ safeCompare,
8
+ } from './dashboard-auth.ts';
9
+
10
+ export type WSMessage = {
11
+ type: 'chat' | 'command' | 'status' | 'stream' | 'error' | 'notification'
12
+ | 'tts_start' | 'tts_end' | 'voice_start' | 'voice_end'
13
+ | 'workflow_event'
14
+ | 'goal_event'
15
+ | 'site_event';
16
+ payload: unknown;
17
+ id?: string;
18
+ priority?: 'urgent' | 'normal' | 'low';
19
+ timestamp: number;
20
+ };
21
+
22
+ export type WSClientHandler = {
23
+ onMessage: (msg: WSMessage, ws: ServerWebSocket<unknown>) => Promise<WSMessage | void>;
24
+ onBinaryMessage?: (data: Buffer, ws: ServerWebSocket<unknown>) => Promise<void>;
25
+ onConnect: (ws: ServerWebSocket<unknown>) => void;
26
+ onDisconnect: (ws: ServerWebSocket<unknown>) => void;
27
+ };
28
+
29
+ type RouteHandler = (req: Request) => Response | Promise<Response>;
30
+ type MethodRoutes = { [method: string]: RouteHandler };
31
+
32
+ /** 401 HTML page loaded from auth-error.html */
33
+ const AUTH_ERROR_HTML = await Bun.file(path.join(import.meta.dir, 'auth-error.html')).text();
34
+
35
+ /** Inline script injected into authed HTML pages — strips ?token= from the hash. */
36
+ const TOKEN_STRIP_SCRIPT = `<script>(function(){var h=location.hash,i=h.indexOf('?');if(i===-1)return;var p=new URLSearchParams(h.slice(i));if(!p.has('token'))return;p.delete('token');var c=h.slice(0,i),r=p.toString();if(r)c+='?'+r;location.replace(location.pathname+location.search+c)})()</script>`;
37
+
38
+ function isPublicRoute(pathname: string, method: string): boolean {
39
+ return (
40
+ pathname === '/health' ||
41
+ pathname === '/sidecar/connect' ||
42
+ pathname === '/api/sidecars/.well-known/jwks.json' ||
43
+ pathname === '/api/auth/login' ||
44
+ pathname === '/api/auth/logout' ||
45
+ pathname === '/api/auth/session' ||
46
+ pathname === '/api/auth/google/callback' ||
47
+ pathname.startsWith('/api/webhooks/') ||
48
+ method === 'OPTIONS'
49
+ );
50
+ }
51
+
52
+ /** Simple sliding-window rate limiter for proxy requests */
53
+ class ProxyRateLimiter {
54
+ private windowMs: number;
55
+ private maxRequests: number;
56
+ private requests: number[] = [];
57
+
58
+ constructor(windowMs = 10_000, maxRequests = 200) {
59
+ this.windowMs = windowMs;
60
+ this.maxRequests = maxRequests;
61
+ }
62
+
63
+ allow(): boolean {
64
+ const now = Date.now();
65
+ // Evict stale entries
66
+ while (this.requests.length > 0 && this.requests[0]! < now - this.windowMs) {
67
+ this.requests.shift();
68
+ }
69
+ if (this.requests.length >= this.maxRequests) return false;
70
+ this.requests.push(now);
71
+ return true;
72
+ }
73
+ }
74
+
75
+ export class WebSocketServer {
76
+ private server: Server<any> | null = null;
77
+ private clients: Set<ServerWebSocket<unknown>> = new Set();
78
+ private handler: WSClientHandler | null = null;
79
+ private port: number;
80
+ private startTime: number = 0;
81
+ private apiRoutes: Map<string, MethodRoutes> = new Map();
82
+ private staticDir: string | null = null;
83
+ private publicDir: string | null = null;
84
+ private sidecarManager: SidecarManager | null = null;
85
+ private authToken: string | null = null;
86
+ private dashboardPasswordHash: string | null = null;
87
+ private corsOrigin: string | null = null;
88
+ private proxyLimiter = new ProxyRateLimiter();
89
+
90
+ constructor(port: number = 3142) {
91
+ this.port = port;
92
+ this.corsOrigin = `http://localhost:${port}`;
93
+ }
94
+
95
+ setAuthToken(token: string): void {
96
+ this.authToken = token;
97
+ }
98
+
99
+ setDashboardPasswordHash(passwordHash?: string | null): void {
100
+ this.dashboardPasswordHash = passwordHash?.trim() || null;
101
+ }
102
+
103
+ setHandler(handler: WSClientHandler): void {
104
+ this.handler = handler;
105
+ }
106
+
107
+ setSidecarManager(manager: SidecarManager): void {
108
+ this.sidecarManager = manager;
109
+ }
110
+
111
+ private siteProxy: import('../sites/proxy.ts').SiteProxy | null = null;
112
+
113
+ setSiteProxy(proxy: import('../sites/proxy.ts').SiteProxy): void {
114
+ this.siteProxy = proxy;
115
+ }
116
+
117
+ /**
118
+ * Register API route handlers (method-based).
119
+ * Example: setApiRoutes({ '/api/health': { GET: handler } })
120
+ */
121
+ setApiRoutes(routes: Record<string, MethodRoutes>): void {
122
+ for (const [path, methods] of Object.entries(routes)) {
123
+ this.apiRoutes.set(path, methods);
124
+ }
125
+ }
126
+
127
+ /**
128
+ * Set directory for serving static files (pre-built dashboard).
129
+ */
130
+ setStaticDir(dir: string): void {
131
+ this.staticDir = dir;
132
+ }
133
+
134
+ /**
135
+ * Set directory for serving public assets (models, WASM, etc.).
136
+ * Falls through to this if file not found in staticDir.
137
+ */
138
+ setPublicDir(dir: string): void {
139
+ this.publicDir = dir;
140
+ }
141
+
142
+ start(): void {
143
+ if (this.server) {
144
+ console.warn('[WebSocketServer] Server already running');
145
+ return;
146
+ }
147
+
148
+ this.startTime = Date.now();
149
+ const self = this;
150
+
151
+ this.server = Bun.serve<{ sidecar_id?: string; proxy_target?: string; _proxyUpstream?: WebSocket }>({
152
+ port: this.port,
153
+ idleTimeout: 30, // seconds — prevent timeout during heavy processing (OCR, PowerShell)
154
+
155
+ async fetch(req, server) {
156
+ const url = new URL(req.url);
157
+ const pathname = url.pathname;
158
+
159
+ // 0. Sidecar WebSocket upgrade (has its own JWT auth)
160
+ if (pathname === '/sidecar/connect' && self.sidecarManager) {
161
+ const authHeader = req.headers.get('Authorization');
162
+ const token = authHeader?.startsWith('Bearer ')
163
+ ? authHeader.slice(7)
164
+ : null;
165
+ if (!token) {
166
+ return new Response('Missing token', { status: 401 });
167
+ }
168
+
169
+ const claims = await self.sidecarManager.validateToken(token);
170
+ if (!claims) {
171
+ return new Response('Invalid or revoked token', { status: 403 });
172
+ }
173
+
174
+ const success = server.upgrade(req, { data: { sidecar_id: claims.sid } });
175
+ if (success) return undefined;
176
+ return new Response('WebSocket upgrade failed', { status: 500 });
177
+ }
178
+
179
+ // 1. Auth check (if configured)
180
+ const tokenAuthEnabled = Boolean(self.authToken);
181
+ const passwordAuthEnabled = Boolean(self.dashboardPasswordHash);
182
+ const dashboardAuthEnabled = tokenAuthEnabled || passwordAuthEnabled;
183
+ const cookieToken = tokenAuthEnabled ? getCookie(req, 'token') : null;
184
+ const hasValidToken = Boolean(
185
+ tokenAuthEnabled &&
186
+ self.authToken &&
187
+ cookieToken &&
188
+ safeCompare(cookieToken, self.authToken),
189
+ );
190
+ const queryToken = tokenAuthEnabled ? url.searchParams.get('token') : null;
191
+ const hasValidQueryToken = Boolean(
192
+ tokenAuthEnabled &&
193
+ self.authToken &&
194
+ queryToken &&
195
+ safeCompare(queryToken, self.authToken),
196
+ );
197
+ const session = passwordAuthEnabled ? getDashboardSessionFromRequest(req) : null;
198
+ const isAuthenticated = hasValidToken || Boolean(session);
199
+ const wantsJsonUnauthorized = pathname.startsWith('/api/') || pathname === '/ws';
200
+
201
+ if (dashboardAuthEnabled && !isPublicRoute(pathname, req.method) && hasValidQueryToken) {
202
+ const cleanParams = new URLSearchParams(url.searchParams);
203
+ cleanParams.delete('token');
204
+ const qs = cleanParams.toString();
205
+ const redirectTo = pathname + (qs ? '?' + qs : '');
206
+ return new Response(null, {
207
+ status: 302,
208
+ headers: {
209
+ 'Location': redirectTo || '/',
210
+ 'Set-Cookie': `token=${queryToken}; Path=/; SameSite=Lax; HttpOnly`,
211
+ },
212
+ });
213
+ }
214
+
215
+ if (dashboardAuthEnabled && !isPublicRoute(pathname, req.method)) {
216
+ if (wantsJsonUnauthorized && !isAuthenticated) {
217
+ return Response.json({ error: 'Unauthorized' }, { status: 401 });
218
+ }
219
+ if (!passwordAuthEnabled && !isAuthenticated) {
220
+ return new Response(AUTH_ERROR_HTML, {
221
+ status: 401,
222
+ headers: { 'Content-Type': 'text/html' },
223
+ });
224
+ }
225
+ }
226
+
227
+
228
+ // Check ?token= query param — set cookie via Set-Cookie and redirect
229
+ /*
230
+ const queryToken = url.searchParams.get('token');
231
+ if (queryToken && safeCompare(queryToken, self.authToken)) {
232
+ const cleanParams = new URLSearchParams(url.searchParams);
233
+ cleanParams.delete('token');
234
+ const qs = cleanParams.toString();
235
+ const redirectTo = pathname + (qs ? '?' + qs : '');
236
+ return new Response(null, {
237
+ status: 302,
238
+ headers: {
239
+ 'Location': redirectTo || '/',
240
+ 'Set-Cookie': `token=${queryToken}; Path=/; SameSite=Lax; HttpOnly`,
241
+ },
242
+ });
243
+ }
244
+ // No valid auth — API & WebSocket get JSON 401; browsers get the auth error page
245
+ if (pathname.startsWith('/api/') || pathname === '/ws') {
246
+ return Response.json({ error: 'Unauthorized' }, { status: 401 });
247
+ }
248
+ return new Response(AUTH_ERROR_HTML, {
249
+ status: 401,
250
+ headers: { 'Content-Type': 'text/html' },
251
+ });
252
+ }
253
+ */
254
+
255
+
256
+ // 2. WebSocket upgrade — validate Origin to block cross-origin connections
257
+ // (e.g., dev server iframes on different ports attempting ws://localhost:3142/ws)
258
+ if (pathname === '/ws') {
259
+ const origin = req.headers.get('origin');
260
+ const expectedOrigin = self.corsOrigin || `http://localhost:${self.port}`;
261
+ if (origin && origin !== expectedOrigin) {
262
+ return new Response('Forbidden: origin mismatch', { status: 403 });
263
+ }
264
+ const success = server.upgrade(req, { data: {} });
265
+ if (success) return undefined;
266
+ return new Response('WebSocket upgrade failed', { status: 500 });
267
+ }
268
+
269
+ // 3. Health check (always public)
270
+ if (pathname === '/health') {
271
+ return Response.json({
272
+ status: 'ok',
273
+ uptime: Date.now() - self.startTime,
274
+ clients: self.clients.size,
275
+ timestamp: Date.now(),
276
+ });
277
+ }
278
+
279
+ // 3b. Site builder proxy — intercept before API route matching
280
+ if (self.siteProxy && pathname.startsWith('/api/sites/') && pathname.includes('/proxy')) {
281
+ const match = self.siteProxy.matchProxy(pathname);
282
+ if (match) {
283
+ // Rate limit proxy requests
284
+ if (!self.proxyLimiter.allow()) {
285
+ return new Response(JSON.stringify({ error: 'Too many proxy requests' }), {
286
+ status: 429,
287
+ headers: { 'Content-Type': 'application/json', 'Retry-After': '10' },
288
+ });
289
+ }
290
+ // WebSocket upgrade for HMR — bridge to dev server
291
+ if (req.headers.get('upgrade')?.toLowerCase() === 'websocket') {
292
+ const targetUrl = self.siteProxy.getWebSocketTarget(match.projectId, match.subPath);
293
+ if (!targetUrl) {
294
+ return new Response('Dev server not running', { status: 502 });
295
+ }
296
+ const success = server.upgrade(req, {
297
+ data: { proxy_target: targetUrl },
298
+ });
299
+ if (success) return undefined;
300
+ return new Response('WebSocket upgrade failed', { status: 500 });
301
+ }
302
+ // HTTP proxy
303
+ return self.siteProxy.proxyHttp(req, match.projectId, match.subPath);
304
+ }
305
+ }
306
+
307
+ // 4. API routes
308
+ if (pathname.startsWith('/api/')) {
309
+ // Handle CORS preflight
310
+ if (req.method === 'OPTIONS') {
311
+ const allowedOrigin = self.corsOrigin || `http://localhost:${self.port}`;
312
+ return new Response(null, {
313
+ status: 204,
314
+ headers: {
315
+ 'Access-Control-Allow-Origin': allowedOrigin,
316
+ 'Access-Control-Allow-Methods': 'GET, POST, PATCH, DELETE, OPTIONS',
317
+ 'Access-Control-Allow-Headers': 'Content-Type',
318
+ },
319
+ });
320
+ }
321
+
322
+ // Try exact match first
323
+ const exactRoute = self.apiRoutes.get(pathname);
324
+ if (exactRoute) {
325
+ const handler = exactRoute[req.method];
326
+ if (handler) return handler(req);
327
+ return new Response('Method Not Allowed', { status: 405 });
328
+ }
329
+
330
+ // Try parameterized routes (e.g., /api/vault/entities/:id)
331
+ for (const [pattern, methods] of self.apiRoutes) {
332
+ const params = matchRoute(pattern, pathname);
333
+ if (params) {
334
+ const handler = methods[req.method];
335
+ if (handler) {
336
+ // Attach params to request
337
+ (req as any).params = params;
338
+ return handler(req);
339
+ }
340
+ return new Response('Method Not Allowed', { status: 405 });
341
+ }
342
+ }
343
+
344
+ return Response.json({ error: 'Not found' }, { status: 404 });
345
+ }
346
+
347
+ // 5a. Overlay widget (served from ui/ source, not dist/)
348
+ if (pathname === '/overlay' && self.staticDir) {
349
+ if (dashboardAuthEnabled && !isAuthenticated) {
350
+ return new Response(AUTH_ERROR_HTML, {
351
+ status: 401,
352
+ headers: { 'Content-Type': 'text/html' },
353
+ });
354
+ }
355
+ // overlay.html lives in the ui/ source directory (parent of dist/)
356
+ const overlayPath = path.join(self.staticDir, '..', 'overlay.html');
357
+ const overlayFile = Bun.file(overlayPath);
358
+ if (await overlayFile.exists()) {
359
+ if (self.authToken) {
360
+ const html = await overlayFile.text();
361
+ return new Response(injectTokenStrip(html), { headers: { 'Content-Type': 'text/html' } });
362
+ }
363
+ return new Response(overlayFile, { headers: { 'Content-Type': 'text/html' } });
364
+ }
365
+ }
366
+
367
+ // 5b. Static files (dashboard)
368
+ if (self.staticDir) {
369
+ let filePath: string;
370
+
371
+ if (pathname === '/' || pathname === '/index.html') {
372
+ filePath = path.join(self.staticDir, 'index.html');
373
+ } else {
374
+ // Serve JS/CSS/assets — resolve and validate within staticDir
375
+ filePath = path.resolve(self.staticDir, '.' + pathname);
376
+ }
377
+
378
+ // Prevent path traversal outside staticDir
379
+ if (!filePath.startsWith(path.resolve(self.staticDir) + path.sep) && filePath !== path.resolve(self.staticDir, 'index.html')) {
380
+ return new Response('Forbidden', { status: 403 });
381
+ }
382
+
383
+ const file = Bun.file(filePath);
384
+ if (await file.exists()) {
385
+ if (self.authToken && filePath.endsWith('.html')) {
386
+ const html = await file.text();
387
+ return new Response(injectTokenStrip(html), { headers: { 'Content-Type': 'text/html' } });
388
+ }
389
+ return new Response(file);
390
+ }
391
+ }
392
+
393
+ // 6. Public assets fallback (models, WASM, etc.)
394
+ if (self.publicDir) {
395
+ const publicPath = path.resolve(self.publicDir, '.' + pathname);
396
+ // Prevent path traversal outside publicDir
397
+ if (!publicPath.startsWith(path.resolve(self.publicDir) + path.sep)) {
398
+ return new Response('Forbidden', { status: 403 });
399
+ }
400
+ const publicFile = Bun.file(publicPath);
401
+ if (await publicFile.exists()) {
402
+ return new Response(publicFile);
403
+ }
404
+ }
405
+
406
+ // 7. Site builder catch-all — proxy unmatched paths to the active
407
+ // dev server using the __proj cookie set by the explicit proxy route.
408
+ // This handles absolute paths (/src/main.tsx, /node_modules/...) that
409
+ // frameworks emit and that don't match any JARVIS route.
410
+ if (self.siteProxy) {
411
+ if (dashboardAuthEnabled && !isAuthenticated) {
412
+ return new Response('Unauthorized', { status: 401 });
413
+ }
414
+ // WebSocket upgrade (e.g. Vite HMR)
415
+ if (req.headers.get('upgrade')?.toLowerCase() === 'websocket') {
416
+ const targetUrl = self.siteProxy.getWebSocketTargetFromCookie(req, pathname);
417
+ if (targetUrl) {
418
+ const success = server.upgrade(req, { data: { proxy_target: targetUrl } });
419
+ if (success) return undefined;
420
+ }
421
+ }
422
+ // HTTP
423
+ const proxyResp = await self.siteProxy.proxyCatchAll(req, pathname + url.search);
424
+ if (proxyResp) return proxyResp;
425
+ }
426
+
427
+ return new Response('Not Found', { status: 404 });
428
+ },
429
+
430
+ websocket: {
431
+ // Limit individual WS messages to 16 MB (defense against abusive HMR payloads)
432
+ maxPayloadLength: 16 * 1024 * 1024,
433
+
434
+ open(ws) {
435
+ // HMR proxy WebSocket — bridge to dev server
436
+ const proxyTarget = (ws.data as any)?.proxy_target as string | undefined;
437
+ if (proxyTarget) {
438
+ const upstream = new WebSocket(proxyTarget);
439
+ (ws.data as any)._proxyUpstream = upstream;
440
+ upstream.onmessage = (e) => {
441
+ try {
442
+ // Enforce size limit on upstream messages too
443
+ const data = e.data;
444
+ const size = typeof data === 'string' ? data.length : (data as ArrayBuffer).byteLength ?? 0;
445
+ if (size > 16 * 1024 * 1024) return; // drop oversized frames
446
+ ws.send(data);
447
+ } catch { /* client gone */ }
448
+ };
449
+ upstream.onerror = () => { try { ws.close(); } catch { /* ignore */ } };
450
+ upstream.onclose = () => { try { ws.close(); } catch { /* ignore */ } };
451
+ return;
452
+ }
453
+
454
+ const sidecarId = (ws.data as any)?.sidecar_id as string | undefined;
455
+ if (sidecarId && self.sidecarManager) {
456
+ self.sidecarManager.handleSidecarConnect(ws, sidecarId);
457
+ return;
458
+ }
459
+
460
+ self.clients.add(ws);
461
+ console.log('[WebSocketServer] Client connected. Total clients:', self.clients.size);
462
+ self.handler?.onConnect(ws);
463
+ },
464
+
465
+ async message(ws, message) {
466
+ // HMR proxy — forward to upstream dev server
467
+ const proxyUpstream = (ws.data as any)?._proxyUpstream as WebSocket | undefined;
468
+ if (proxyUpstream) {
469
+ if (proxyUpstream.readyState === WebSocket.OPEN) {
470
+ proxyUpstream.send(message);
471
+ }
472
+ return;
473
+ }
474
+
475
+ const sidecarId = (ws.data as any)?.sidecar_id as string | undefined;
476
+ if (sidecarId && self.sidecarManager) {
477
+ self.sidecarManager.handleSidecarMessage(ws, message);
478
+ return;
479
+ }
480
+
481
+ // Binary frame = audio data (mic audio from client)
482
+ if (message instanceof Buffer) {
483
+ if (self.handler?.onBinaryMessage) {
484
+ try {
485
+ await self.handler.onBinaryMessage(message, ws);
486
+ } catch (error) {
487
+ console.error('[WebSocketServer] Error processing binary message:', error);
488
+ }
489
+ }
490
+ return;
491
+ }
492
+
493
+ // Text frame = JSON message (existing protocol)
494
+ try {
495
+ const msg: WSMessage = JSON.parse(message.toString());
496
+ console.log('[WebSocketServer] Received:', msg.type, msg.id);
497
+
498
+ if (self.handler) {
499
+ const response = await self.handler.onMessage(msg, ws);
500
+ if (response) {
501
+ ws.send(JSON.stringify(response));
502
+ }
503
+ }
504
+ } catch (error) {
505
+ console.error('[WebSocketServer] Error processing message:', error);
506
+ const errorMsg: WSMessage = {
507
+ type: 'error',
508
+ payload: {
509
+ message: error instanceof Error ? error.message : 'Unknown error',
510
+ },
511
+ timestamp: Date.now(),
512
+ };
513
+ ws.send(JSON.stringify(errorMsg));
514
+ }
515
+ },
516
+
517
+ pong(ws) {
518
+ const sidecarId = (ws.data as any)?.sidecar_id as string | undefined;
519
+ if (sidecarId && self.sidecarManager) {
520
+ self.sidecarManager.handleSidecarPong(sidecarId);
521
+ }
522
+ },
523
+
524
+ close(ws) {
525
+ // HMR proxy cleanup
526
+ const proxyUpstream = (ws.data as any)?._proxyUpstream as WebSocket | undefined;
527
+ if (proxyUpstream) {
528
+ try { proxyUpstream.close(); } catch { /* ignore */ }
529
+ return;
530
+ }
531
+
532
+ const sidecarId = (ws.data as any)?.sidecar_id as string | undefined;
533
+ if (sidecarId && self.sidecarManager) {
534
+ self.sidecarManager.handleSidecarDisconnect(sidecarId);
535
+ return;
536
+ }
537
+
538
+ self.clients.delete(ws);
539
+ console.log('[WebSocketServer] Client disconnected. Total clients:', self.clients.size);
540
+ self.handler?.onDisconnect(ws);
541
+ },
542
+ },
543
+ });
544
+
545
+ console.log(`[WebSocketServer] Started on ws://localhost:${this.port}/ws`);
546
+ console.log(`[WebSocketServer] Health endpoint: http://localhost:${this.port}/health`);
547
+ if (this.staticDir) {
548
+ console.log(`[WebSocketServer] Dashboard: http://localhost:${this.port}/`);
549
+ }
550
+ }
551
+
552
+ stop(): void {
553
+ if (this.server) {
554
+ this.server.stop();
555
+ this.server = null;
556
+ this.clients.clear();
557
+ console.log('[WebSocketServer] Stopped');
558
+ }
559
+ }
560
+
561
+ broadcast(message: WSMessage): void {
562
+ const payload = JSON.stringify(message);
563
+ let sent = 0;
564
+
565
+ for (const client of this.clients) {
566
+ try {
567
+ client.send(payload);
568
+ sent++;
569
+ } catch (error) {
570
+ console.error('[WebSocketServer] Error broadcasting to client:', error);
571
+ }
572
+ }
573
+
574
+ // Only log errors or when no clients received the message
575
+ if (sent === 0 && this.clients.size > 0) {
576
+ console.warn(`[WebSocketServer] Broadcast failed: 0/${this.clients.size} clients received message`);
577
+ }
578
+ }
579
+
580
+ send(client: ServerWebSocket<unknown>, message: WSMessage): void {
581
+ try {
582
+ client.send(JSON.stringify(message));
583
+ } catch (error) {
584
+ console.error('[WebSocketServer] Error sending to client:', error);
585
+ }
586
+ }
587
+
588
+ /**
589
+ * Unicast a JSON message to a specific client (e.g. tts_start/tts_end signals).
590
+ */
591
+ sendToClient(ws: ServerWebSocket<unknown>, message: WSMessage): void {
592
+ try {
593
+ ws.send(JSON.stringify(message));
594
+ } catch (error) {
595
+ console.error('[WebSocketServer] Error unicasting to client:', error);
596
+ }
597
+ }
598
+
599
+ /**
600
+ * Unicast binary data to a specific client (e.g. TTS audio chunks).
601
+ */
602
+ sendBinary(ws: ServerWebSocket<unknown>, data: Buffer): void {
603
+ try {
604
+ ws.sendBinary(data);
605
+ } catch (error) {
606
+ console.error('[WebSocketServer] Error sending binary to client:', error);
607
+ }
608
+ }
609
+
610
+ isRunning(): boolean {
611
+ return this.server !== null;
612
+ }
613
+
614
+ getPort(): number {
615
+ return this.port;
616
+ }
617
+
618
+ getClientCount(): number {
619
+ return this.clients.size;
620
+ }
621
+
622
+ getClients(): Set<ServerWebSocket<unknown>> {
623
+ return this.clients;
624
+ }
625
+ }
626
+
627
+ /**
628
+ * Inject the token-stripping script into an HTML page (right after <head>).
629
+ */
630
+ function injectTokenStrip(html: string): string {
631
+ const headIdx = html.indexOf('<head>');
632
+ if (headIdx !== -1) {
633
+ return html.slice(0, headIdx + 6) + TOKEN_STRIP_SCRIPT + html.slice(headIdx + 6);
634
+ }
635
+ const htmlIdx = html.indexOf('<html');
636
+ if (htmlIdx !== -1) {
637
+ const closeTag = html.indexOf('>', htmlIdx);
638
+ if (closeTag !== -1) {
639
+ return html.slice(0, closeTag + 1) + TOKEN_STRIP_SCRIPT + html.slice(closeTag + 1);
640
+ }
641
+ }
642
+ return TOKEN_STRIP_SCRIPT + html;
643
+ }
644
+
645
+ /**
646
+ * Match a route pattern like '/api/vault/entities/:id/facts' against a pathname.
647
+ * Returns params object if matched, null otherwise.
648
+ */
649
+ function matchRoute(pattern: string, pathname: string): Record<string, string> | null {
650
+ // Skip wildcard patterns
651
+ if (pattern.includes('*')) return null;
652
+
653
+ const patternParts = pattern.split('/');
654
+ const pathParts = pathname.split('/');
655
+
656
+ if (patternParts.length !== pathParts.length) return null;
657
+
658
+ const params: Record<string, string> = {};
659
+
660
+ for (let i = 0; i < patternParts.length; i++) {
661
+ if (patternParts[i]!.startsWith(':')) {
662
+ params[patternParts[i]!.slice(1)] = pathParts[i]!;
663
+ } else if (patternParts[i] !== pathParts[i]) {
664
+ return null;
665
+ }
666
+ }
667
+
668
+ return Object.keys(params).length > 0 ? params : null;
669
+ }