@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,173 @@
1
+ import { test, expect, describe } from 'bun:test';
2
+ import { ChannelManager } from './index.ts';
3
+ import type { ChannelAdapter, ChannelMessage } from './channels/telegram.ts';
4
+ import { splitMessage } from './channels/discord.ts';
5
+
6
+ // Mock channel adapter for testing
7
+ class MockChannel implements ChannelAdapter {
8
+ name = 'mock';
9
+ private _connected = false;
10
+ private _handler: ((msg: ChannelMessage) => Promise<string>) | null = null;
11
+
12
+ async connect(): Promise<void> {
13
+ this._connected = true;
14
+ }
15
+
16
+ async disconnect(): Promise<void> {
17
+ this._connected = false;
18
+ }
19
+
20
+ async sendMessage(to: string, text: string): Promise<void> {
21
+ // Mock implementation
22
+ }
23
+
24
+ onMessage(handler: (msg: ChannelMessage) => Promise<string>): void {
25
+ this._handler = handler;
26
+ }
27
+
28
+ isConnected(): boolean {
29
+ return this._connected;
30
+ }
31
+
32
+ // Test helper to simulate receiving a message
33
+ async simulateMessage(text: string): Promise<string | null> {
34
+ if (!this._handler) return null;
35
+
36
+ const msg: ChannelMessage = {
37
+ id: '1',
38
+ channel: 'mock',
39
+ from: 'testuser',
40
+ text,
41
+ timestamp: Date.now(),
42
+ metadata: {},
43
+ };
44
+
45
+ return this._handler(msg);
46
+ }
47
+ }
48
+
49
+ test('ChannelManager - register channel', () => {
50
+ const manager = new ChannelManager();
51
+ const channel = new MockChannel();
52
+
53
+ manager.register(channel);
54
+ expect(manager.listChannels()).toEqual(['mock']);
55
+ expect(manager.getChannel('mock')).toBe(channel);
56
+ });
57
+
58
+ test('ChannelManager - set handler', async () => {
59
+ const manager = new ChannelManager();
60
+ const channel = new MockChannel();
61
+
62
+ manager.register(channel);
63
+
64
+ let handlerCalled = false;
65
+ manager.setHandler(async (msg) => {
66
+ handlerCalled = true;
67
+ return `Echo: ${msg.text}`;
68
+ });
69
+
70
+ const response = await channel.simulateMessage('test');
71
+ expect(handlerCalled).toBe(true);
72
+ expect(response).toBe('Echo: test');
73
+ });
74
+
75
+ test('ChannelManager - connect all channels', async () => {
76
+ const manager = new ChannelManager();
77
+ const channel1 = new MockChannel();
78
+ const channel2 = new MockChannel();
79
+ channel2.name = 'mock2';
80
+
81
+ manager.register(channel1);
82
+ manager.register(channel2);
83
+
84
+ await manager.connectAll();
85
+
86
+ const status = manager.getStatus();
87
+ expect(status.mock).toBe(true);
88
+ expect(status.mock2).toBe(true);
89
+ });
90
+
91
+ test('ChannelManager - disconnect all channels', async () => {
92
+ const manager = new ChannelManager();
93
+ const channel = new MockChannel();
94
+
95
+ manager.register(channel);
96
+ await manager.connectAll();
97
+
98
+ expect(manager.getStatus().mock).toBe(true);
99
+
100
+ await manager.disconnectAll();
101
+ expect(manager.getStatus().mock).toBe(false);
102
+ });
103
+
104
+ test('ChannelManager - list channels', () => {
105
+ const manager = new ChannelManager();
106
+
107
+ expect(manager.listChannels()).toEqual([]);
108
+
109
+ manager.register(new MockChannel());
110
+ expect(manager.listChannels()).toEqual(['mock']);
111
+
112
+ const channel2 = new MockChannel();
113
+ channel2.name = 'mock2';
114
+ manager.register(channel2);
115
+
116
+ expect(manager.listChannels()).toContain('mock');
117
+ expect(manager.listChannels()).toContain('mock2');
118
+ });
119
+
120
+ // Discord splitMessage tests
121
+ describe('Discord splitMessage', () => {
122
+ test('returns single chunk for short message', () => {
123
+ const result = splitMessage('Hello world', 2000);
124
+ expect(result).toEqual(['Hello world']);
125
+ });
126
+
127
+ test('returns single chunk for message exactly at limit', () => {
128
+ const text = 'a'.repeat(2000);
129
+ const result = splitMessage(text, 2000);
130
+ expect(result).toEqual([text]);
131
+ });
132
+
133
+ test('splits long message at newline boundary', () => {
134
+ const line = 'This is a line of text\n';
135
+ // Create text that exceeds 100 chars, with newlines
136
+ const text = line.repeat(10); // 220 chars
137
+ const result = splitMessage(text, 100);
138
+ expect(result.length).toBeGreaterThan(1);
139
+ // Each chunk should be <= 100 chars
140
+ for (const chunk of result) {
141
+ expect(chunk.length).toBeLessThanOrEqual(100);
142
+ }
143
+ // Recombined should equal original (modulo trimmed whitespace)
144
+ const recombined = result.join('');
145
+ // The original text is preserved (whitespace trimming might remove leading spaces between chunks)
146
+ expect(recombined.replace(/\s+/g, '')).toBe(text.replace(/\s+/g, ''));
147
+ });
148
+
149
+ test('hard splits when no good break point', () => {
150
+ // No spaces or newlines — must hard split
151
+ const text = 'a'.repeat(5000);
152
+ const result = splitMessage(text, 2000);
153
+ expect(result.length).toBe(3); // 2000 + 2000 + 1000
154
+ expect(result[0]!.length).toBe(2000);
155
+ expect(result[1]!.length).toBe(2000);
156
+ expect(result[2]!.length).toBe(1000);
157
+ });
158
+
159
+ test('handles empty string', () => {
160
+ const result = splitMessage('', 2000);
161
+ expect(result).toEqual(['']);
162
+ });
163
+
164
+ test('splits at space when no newlines available', () => {
165
+ // Words separated by spaces, no newlines
166
+ const words = Array(200).fill('word').join(' '); // "word word word..." ~1000 chars
167
+ const result = splitMessage(words, 100);
168
+ expect(result.length).toBeGreaterThan(1);
169
+ for (const chunk of result) {
170
+ expect(chunk.length).toBeLessThanOrEqual(100);
171
+ }
172
+ });
173
+ });
@@ -0,0 +1,75 @@
1
+ import { timingSafeEqual } from 'node:crypto';
2
+ import type { JarvisConfig } from '../config/types.ts';
3
+ import {
4
+ createDashboardSession,
5
+ deleteDashboardSession,
6
+ validateDashboardSession,
7
+ type DashboardSession,
8
+ } from '../vault/dashboard-sessions.ts';
9
+
10
+ export const DASHBOARD_SESSION_COOKIE = 'jarvis_dashboard_session';
11
+ export const DASHBOARD_SESSION_MAX_AGE_MS = 30 * 24 * 60 * 60 * 1000;
12
+ export const DASHBOARD_SESSION_MAX_AGE_SECONDS = Math.floor(DASHBOARD_SESSION_MAX_AGE_MS / 1000);
13
+
14
+ export function getCookie(req: Request, name: string): string | null {
15
+ const cookies = req.headers.get('Cookie');
16
+ if (!cookies) return null;
17
+ const match = cookies.match(new RegExp(`(?:^|;\\s*)${name}=([^;]*)`));
18
+ return match ? decodeURIComponent(match[1]!) : null;
19
+ }
20
+
21
+ export function safeCompare(a: string, b: string): boolean {
22
+ if (a.length !== b.length) return false;
23
+ return timingSafeEqual(Buffer.from(a), Buffer.from(b));
24
+ }
25
+
26
+ export function isDashboardPasswordEnabled(config: Pick<JarvisConfig, 'dashboard'> | { dashboard?: { password_hash?: string } } | null | undefined): boolean {
27
+ return Boolean(config?.dashboard?.password_hash?.trim());
28
+ }
29
+
30
+ export function shouldUseSecureCookies(req: Request): boolean {
31
+ const forwardedProto = req.headers.get('x-forwarded-proto');
32
+ if (forwardedProto) {
33
+ return forwardedProto.split(',')[0]!.trim().toLowerCase() === 'https';
34
+ }
35
+ return new URL(req.url).protocol === 'https:';
36
+ }
37
+
38
+ export function buildCookieAttributes(req: Request, expiresAt?: number): string {
39
+ const parts = ['Path=/', 'HttpOnly', 'SameSite=Lax'];
40
+ if (shouldUseSecureCookies(req)) parts.push('Secure');
41
+ if (expiresAt) {
42
+ parts.push(`Max-Age=${DASHBOARD_SESSION_MAX_AGE_SECONDS}`);
43
+ parts.push(`Expires=${new Date(expiresAt).toUTCString()}`);
44
+ }
45
+ return parts.join('; ');
46
+ }
47
+
48
+ export function buildDashboardSessionCookie(req: Request, sessionId: string, expiresAt: number): string {
49
+ return `${DASHBOARD_SESSION_COOKIE}=${encodeURIComponent(sessionId)}; ${buildCookieAttributes(req, expiresAt)}`;
50
+ }
51
+
52
+ export function buildClearedDashboardSessionCookie(req: Request): string {
53
+ const epoch = new Date(0).toUTCString();
54
+ const parts = ['Path=/', 'HttpOnly', 'SameSite=Lax', 'Max-Age=0', `Expires=${epoch}`];
55
+ if (shouldUseSecureCookies(req)) parts.push('Secure');
56
+ return `${DASHBOARD_SESSION_COOKIE}=; ${parts.join('; ')}`;
57
+ }
58
+
59
+ export function createAuthenticatedDashboardSession(): DashboardSession {
60
+ const sessionId = crypto.randomUUID();
61
+ const expiresAt = Date.now() + DASHBOARD_SESSION_MAX_AGE_MS;
62
+ return createDashboardSession(sessionId, expiresAt);
63
+ }
64
+
65
+ export function getDashboardSessionFromRequest(req: Request): DashboardSession | null {
66
+ const sessionId = getCookie(req, DASHBOARD_SESSION_COOKIE);
67
+ if (!sessionId) return null;
68
+ return validateDashboardSession(sessionId);
69
+ }
70
+
71
+ export function revokeDashboardSessionFromRequest(req: Request): void {
72
+ const sessionId = getCookie(req, DASHBOARD_SESSION_COOKIE);
73
+ if (!sessionId) return;
74
+ deleteDashboardSession(sessionId);
75
+ }
@@ -0,0 +1,114 @@
1
+ /**
2
+ * Desktop Notification Sender
3
+ *
4
+ * Sends native desktop notifications.
5
+ * Tries in order:
6
+ * 1. notify-send (Linux/WSLg)
7
+ * 2. PowerShell toast (WSL2 → Windows)
8
+ * Gracefully degrades if neither is available.
9
+ */
10
+
11
+ type NotifyMethod = 'notify-send' | 'powershell' | null;
12
+
13
+ let method: NotifyMethod | undefined;
14
+
15
+ function detectMethod(): NotifyMethod {
16
+ if (method !== undefined) return method;
17
+
18
+ // Try notify-send first (native Linux/WSLg)
19
+ try {
20
+ const result = Bun.spawnSync(['which', 'notify-send']);
21
+ if (result.exitCode === 0) {
22
+ method = 'notify-send';
23
+ console.log('[DesktopNotify] Using notify-send');
24
+ return method;
25
+ }
26
+ } catch { /* continue */ }
27
+
28
+ // Try PowerShell (WSL2 → Windows toast)
29
+ try {
30
+ const result = Bun.spawnSync(['which', 'powershell.exe']);
31
+ if (result.exitCode === 0) {
32
+ method = 'powershell';
33
+ console.log('[DesktopNotify] Using PowerShell toasts');
34
+ return method;
35
+ }
36
+ } catch { /* continue */ }
37
+
38
+ method = null;
39
+ console.log('[DesktopNotify] No notification method available');
40
+ return method;
41
+ }
42
+
43
+ /**
44
+ * Send a native desktop notification.
45
+ * Returns true if sent, false if unavailable.
46
+ */
47
+ export function sendDesktopNotification(
48
+ title: string,
49
+ body: string,
50
+ options?: {
51
+ urgency?: 'low' | 'normal' | 'critical';
52
+ expireMs?: number;
53
+ }
54
+ ): boolean {
55
+ const m = detectMethod();
56
+ if (!m) return false;
57
+
58
+ try {
59
+ if (m === 'notify-send') {
60
+ return sendViaNotifySend(title, body, options);
61
+ } else {
62
+ return sendViaPowerShell(title, body);
63
+ }
64
+ } catch {
65
+ return false;
66
+ }
67
+ }
68
+
69
+ function sendViaNotifySend(
70
+ title: string,
71
+ body: string,
72
+ options?: { urgency?: string; expireMs?: number }
73
+ ): boolean {
74
+ const urgency = options?.urgency ?? 'normal';
75
+ const expireMs = options?.expireMs ?? (urgency === 'critical' ? 10000 : 5000);
76
+
77
+ Bun.spawn([
78
+ 'notify-send',
79
+ `--urgency=${urgency}`,
80
+ `--expire-time=${expireMs}`,
81
+ '--app-name=JARVIS',
82
+ title,
83
+ body,
84
+ ], { stdout: 'ignore', stderr: 'ignore' });
85
+ return true;
86
+ }
87
+
88
+ function sendViaPowerShell(title: string, body: string): boolean {
89
+ // Escape single quotes for PowerShell
90
+ const safeTitle = title.replace(/'/g, "''").slice(0, 100);
91
+ const safeBody = body.replace(/'/g, "''").slice(0, 200);
92
+
93
+ const script = `
94
+ [Windows.UI.Notifications.ToastNotificationManager, Windows.UI.Notifications, ContentType = WindowsRuntime] | Out-Null
95
+ [Windows.Data.Xml.Dom.XmlDocument, Windows.Data.Xml.Dom, ContentType = WindowsRuntime] | Out-Null
96
+ $xml = [Windows.Data.Xml.Dom.XmlDocument]::new()
97
+ $xml.LoadXml('<toast><visual><binding template="ToastText02"><text id="1">${safeTitle}</text><text id="2">${safeBody}</text></binding></visual></toast>')
98
+ $toast = [Windows.UI.Notifications.ToastNotification]::new($xml)
99
+ [Windows.UI.Notifications.ToastNotificationManager]::CreateToastNotifier('JARVIS').Show($toast)
100
+ `.trim();
101
+
102
+ Bun.spawn(['powershell.exe', '-NoProfile', '-NonInteractive', '-Command', script], {
103
+ stdout: 'ignore',
104
+ stderr: 'ignore',
105
+ });
106
+ return true;
107
+ }
108
+
109
+ /**
110
+ * Check if desktop notifications are available.
111
+ */
112
+ export function isDesktopNotifyAvailable(): boolean {
113
+ return detectMethod() !== null;
114
+ }
@@ -0,0 +1,129 @@
1
+ /**
2
+ * Example usage of J.A.R.V.I.S. Communication Layer
3
+ *
4
+ * This demonstrates how to:
5
+ * 1. Start the WebSocket server
6
+ * 2. Set up Telegram bot integration
7
+ * 3. Handle messages from multiple channels
8
+ * 4. Relay LLM streaming responses
9
+ */
10
+
11
+ import {
12
+ WebSocketServer,
13
+ ChannelManager,
14
+ TelegramAdapter,
15
+ StreamRelay,
16
+ type WSMessage,
17
+ type ChannelMessage,
18
+ } from './index.ts';
19
+
20
+ async function main() {
21
+ console.log('🤖 Starting J.A.R.V.I.S. Communication Layer...\n');
22
+
23
+ // 1. Initialize WebSocket server
24
+ const wsServer = new WebSocketServer(3142);
25
+
26
+ wsServer.setHandler({
27
+ async onMessage(msg: WSMessage) {
28
+ console.log('[WS] Received message:', msg.type);
29
+
30
+ if (msg.type === 'chat') {
31
+ // Echo back for demo
32
+ return {
33
+ type: 'chat' as const,
34
+ payload: {
35
+ reply: `You said: ${msg.payload}`,
36
+ },
37
+ id: msg.id,
38
+ timestamp: Date.now(),
39
+ };
40
+ }
41
+ },
42
+ onConnect() {
43
+ console.log('[WS] Client connected');
44
+ },
45
+ onDisconnect() {
46
+ console.log('[WS] Client disconnected');
47
+ },
48
+ });
49
+
50
+ wsServer.start();
51
+
52
+ // 2. Initialize Channel Manager
53
+ const channelManager = new ChannelManager();
54
+
55
+ // Set up unified message handler
56
+ channelManager.setHandler(async (message: ChannelMessage) => {
57
+ console.log(`\n[${message.channel.toUpperCase()}] Message from ${message.from}:`);
58
+ console.log(` "${message.text}"`);
59
+
60
+ // Broadcast to WebSocket clients
61
+ wsServer.broadcast({
62
+ type: 'chat',
63
+ payload: {
64
+ channel: message.channel,
65
+ from: message.from,
66
+ text: message.text,
67
+ },
68
+ id: message.id,
69
+ timestamp: message.timestamp,
70
+ });
71
+
72
+ // Simple echo response for demo
73
+ return `Received your message: "${message.text}"`;
74
+ });
75
+
76
+ // 3. Register channels (Telegram only for demo)
77
+ const telegramToken = process.env.TELEGRAM_BOT_TOKEN;
78
+
79
+ if (telegramToken) {
80
+ const telegram = new TelegramAdapter(telegramToken);
81
+ channelManager.register(telegram);
82
+ console.log('✓ Telegram adapter registered');
83
+ } else {
84
+ console.log('⚠️ TELEGRAM_BOT_TOKEN not set, skipping Telegram');
85
+ }
86
+
87
+ // Connect all channels
88
+ try {
89
+ await channelManager.connectAll();
90
+ console.log('\n✓ All channels connected');
91
+ console.log('Status:', channelManager.getStatus());
92
+ } catch (error) {
93
+ console.error('Error connecting channels:', error);
94
+ }
95
+
96
+ // 4. Demo: Stream relay (requires LLM provider setup)
97
+ const streamRelay = new StreamRelay(wsServer);
98
+
99
+ // Example stream simulation
100
+ console.log('\n📡 Streaming demo (simulated)...');
101
+ const simulatedStream = (async function* () {
102
+ yield { type: 'text' as const, text: 'Hello ' };
103
+ await new Promise(r => setTimeout(r, 100));
104
+ yield { type: 'text' as const, text: 'from ' };
105
+ await new Promise(r => setTimeout(r, 100));
106
+ yield { type: 'text' as const, text: 'J.A.R.V.I.S.!' };
107
+ await new Promise(r => setTimeout(r, 100));
108
+ yield { type: 'done' as const, response: { content: 'Hello from J.A.R.V.I.S.!', tool_calls: [], usage: { input_tokens: 0, output_tokens: 10 }, model: 'demo', finish_reason: 'stop' as const } };
109
+ })();
110
+
111
+ const fullResponse = await streamRelay.relayStream(simulatedStream, 'demo-123');
112
+ console.log('Full response:', fullResponse);
113
+
114
+ console.log('\n✓ Communication layer is running');
115
+ console.log(` WebSocket: ws://localhost:${wsServer.getPort()}/ws`);
116
+ console.log(` Health: http://localhost:${wsServer.getPort()}/health`);
117
+ console.log(` Channels: ${channelManager.listChannels().join(', ')}`);
118
+ console.log('\nPress Ctrl+C to stop...');
119
+
120
+ // Graceful shutdown
121
+ process.on('SIGINT', async () => {
122
+ console.log('\n\n🛑 Shutting down...');
123
+ await channelManager.disconnectAll();
124
+ wsServer.stop();
125
+ process.exit(0);
126
+ });
127
+ }
128
+
129
+ main().catch(console.error);
@@ -0,0 +1,129 @@
1
+ // Core WebSocket and Streaming
2
+ export { WebSocketServer, type WSMessage, type WSClientHandler } from './websocket.ts';
3
+ export { StreamRelay } from './streaming.ts';
4
+
5
+ // Voice I/O
6
+ export {
7
+ OpenAIWhisperSTT,
8
+ GroqWhisperSTT,
9
+ LocalWhisperSTT,
10
+ EdgeTTSProvider,
11
+ ElevenLabsTTSProvider,
12
+ createSTTProvider,
13
+ createTTSProvider,
14
+ listElevenLabsVoices,
15
+ splitIntoSentences,
16
+ type STTProvider,
17
+ type TTSProvider,
18
+ } from './voice.ts';
19
+
20
+ // Channel adapters
21
+ export {
22
+ TelegramAdapter,
23
+ type ChannelMessage,
24
+ type ChannelHandler,
25
+ type ChannelAdapter,
26
+ } from './channels/telegram.ts';
27
+ export { WhatsAppAdapter } from './channels/whatsapp.ts';
28
+ export { DiscordAdapter } from './channels/discord.ts';
29
+ export { SignalAdapter } from './channels/signal.ts';
30
+
31
+ // Channel Manager
32
+ export class ChannelManager {
33
+ private channels: Map<string, import('./channels/telegram.ts').ChannelAdapter> = new Map();
34
+ private handler: import('./channels/telegram.ts').ChannelHandler | null = null;
35
+
36
+ /**
37
+ * Register a channel adapter
38
+ */
39
+ register(adapter: import('./channels/telegram.ts').ChannelAdapter): void {
40
+ if (this.channels.has(adapter.name)) {
41
+ console.warn(`[ChannelManager] Channel "${adapter.name}" already registered, overwriting`);
42
+ }
43
+
44
+ this.channels.set(adapter.name, adapter);
45
+ console.log(`[ChannelManager] Registered channel: ${adapter.name}`);
46
+ }
47
+
48
+ /**
49
+ * Set the message handler for all channels
50
+ */
51
+ setHandler(handler: import('./channels/telegram.ts').ChannelHandler): void {
52
+ this.handler = handler;
53
+
54
+ // Apply handler to all registered channels
55
+ for (const adapter of this.channels.values()) {
56
+ adapter.onMessage(handler);
57
+ }
58
+
59
+ console.log('[ChannelManager] Handler set for all channels');
60
+ }
61
+
62
+ /**
63
+ * Connect all registered channels
64
+ */
65
+ async connectAll(): Promise<void> {
66
+ const results = await Promise.allSettled(
67
+ Array.from(this.channels.values()).map(async (adapter) => {
68
+ try {
69
+ await adapter.connect();
70
+ console.log(`[ChannelManager] Connected: ${adapter.name}`);
71
+ } catch (error) {
72
+ console.error(`[ChannelManager] Failed to connect ${adapter.name}:`, error);
73
+ throw error;
74
+ }
75
+ })
76
+ );
77
+
78
+ const failures = results.filter((r) => r.status === 'rejected');
79
+ if (failures.length > 0) {
80
+ console.warn(`[ChannelManager] ${failures.length} channel(s) failed to connect`);
81
+ }
82
+
83
+ const successes = results.filter((r) => r.status === 'fulfilled').length;
84
+ console.log(`[ChannelManager] ${successes}/${this.channels.size} channels connected`);
85
+ }
86
+
87
+ /**
88
+ * Disconnect all registered channels
89
+ */
90
+ async disconnectAll(): Promise<void> {
91
+ await Promise.allSettled(
92
+ Array.from(this.channels.values()).map(async (adapter) => {
93
+ try {
94
+ await adapter.disconnect();
95
+ console.log(`[ChannelManager] Disconnected: ${adapter.name}`);
96
+ } catch (error) {
97
+ console.error(`[ChannelManager] Error disconnecting ${adapter.name}:`, error);
98
+ }
99
+ })
100
+ );
101
+
102
+ console.log('[ChannelManager] All channels disconnected');
103
+ }
104
+
105
+ /**
106
+ * Get a specific channel adapter by name
107
+ */
108
+ getChannel(name: string): import('./channels/telegram.ts').ChannelAdapter | undefined {
109
+ return this.channels.get(name);
110
+ }
111
+
112
+ /**
113
+ * List all registered channel names
114
+ */
115
+ listChannels(): string[] {
116
+ return Array.from(this.channels.keys());
117
+ }
118
+
119
+ /**
120
+ * Get status of all channels
121
+ */
122
+ getStatus(): Record<string, boolean> {
123
+ const status: Record<string, boolean> = {};
124
+ for (const [name, adapter] of this.channels) {
125
+ status[name] = adapter.isConnected();
126
+ }
127
+ return status;
128
+ }
129
+ }