@usejarvis/brain 0.1.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 (266) hide show
  1. package/LICENSE +153 -0
  2. package/README.md +278 -0
  3. package/bin/jarvis.ts +413 -0
  4. package/package.json +74 -0
  5. package/scripts/ensure-bun.cjs +8 -0
  6. package/src/actions/README.md +421 -0
  7. package/src/actions/app-control/desktop-controller.test.ts +26 -0
  8. package/src/actions/app-control/desktop-controller.ts +438 -0
  9. package/src/actions/app-control/interface.ts +64 -0
  10. package/src/actions/app-control/linux.ts +273 -0
  11. package/src/actions/app-control/macos.ts +54 -0
  12. package/src/actions/app-control/sidecar-launcher.test.ts +23 -0
  13. package/src/actions/app-control/sidecar-launcher.ts +286 -0
  14. package/src/actions/app-control/windows.ts +44 -0
  15. package/src/actions/browser/cdp.ts +138 -0
  16. package/src/actions/browser/chrome-launcher.ts +252 -0
  17. package/src/actions/browser/session.ts +437 -0
  18. package/src/actions/browser/stealth.ts +49 -0
  19. package/src/actions/index.ts +20 -0
  20. package/src/actions/terminal/executor.ts +157 -0
  21. package/src/actions/terminal/wsl-bridge.ts +126 -0
  22. package/src/actions/test.ts +93 -0
  23. package/src/actions/tools/agents.ts +321 -0
  24. package/src/actions/tools/builtin.ts +846 -0
  25. package/src/actions/tools/commitments.ts +192 -0
  26. package/src/actions/tools/content.ts +217 -0
  27. package/src/actions/tools/delegate.ts +147 -0
  28. package/src/actions/tools/desktop.test.ts +55 -0
  29. package/src/actions/tools/desktop.ts +305 -0
  30. package/src/actions/tools/goals.ts +376 -0
  31. package/src/actions/tools/local-tools-guard.ts +20 -0
  32. package/src/actions/tools/registry.ts +171 -0
  33. package/src/actions/tools/research.ts +111 -0
  34. package/src/actions/tools/sidecar-list.ts +57 -0
  35. package/src/actions/tools/sidecar-route.ts +105 -0
  36. package/src/actions/tools/workflows.ts +216 -0
  37. package/src/agents/agent.ts +132 -0
  38. package/src/agents/delegation.ts +107 -0
  39. package/src/agents/hierarchy.ts +113 -0
  40. package/src/agents/index.ts +19 -0
  41. package/src/agents/messaging.ts +125 -0
  42. package/src/agents/orchestrator.ts +576 -0
  43. package/src/agents/role-discovery.ts +61 -0
  44. package/src/agents/sub-agent-runner.ts +307 -0
  45. package/src/agents/task-manager.ts +151 -0
  46. package/src/authority/approval-delivery.ts +59 -0
  47. package/src/authority/approval.ts +196 -0
  48. package/src/authority/audit.ts +158 -0
  49. package/src/authority/authority.test.ts +519 -0
  50. package/src/authority/deferred-executor.ts +103 -0
  51. package/src/authority/emergency.ts +66 -0
  52. package/src/authority/engine.ts +297 -0
  53. package/src/authority/index.ts +12 -0
  54. package/src/authority/learning.ts +111 -0
  55. package/src/authority/tool-action-map.ts +74 -0
  56. package/src/awareness/analytics.ts +466 -0
  57. package/src/awareness/awareness.test.ts +332 -0
  58. package/src/awareness/capture-engine.ts +305 -0
  59. package/src/awareness/context-graph.ts +130 -0
  60. package/src/awareness/context-tracker.ts +349 -0
  61. package/src/awareness/index.ts +25 -0
  62. package/src/awareness/intelligence.ts +321 -0
  63. package/src/awareness/ocr-engine.ts +88 -0
  64. package/src/awareness/service.ts +528 -0
  65. package/src/awareness/struggle-detector.ts +342 -0
  66. package/src/awareness/suggestion-engine.ts +476 -0
  67. package/src/awareness/types.ts +201 -0
  68. package/src/cli/autostart.ts +241 -0
  69. package/src/cli/deps.ts +449 -0
  70. package/src/cli/doctor.ts +230 -0
  71. package/src/cli/helpers.ts +401 -0
  72. package/src/cli/onboard.ts +580 -0
  73. package/src/comms/README.md +329 -0
  74. package/src/comms/auth-error.html +48 -0
  75. package/src/comms/channels/discord.ts +228 -0
  76. package/src/comms/channels/signal.ts +56 -0
  77. package/src/comms/channels/telegram.ts +316 -0
  78. package/src/comms/channels/whatsapp.ts +60 -0
  79. package/src/comms/channels.test.ts +173 -0
  80. package/src/comms/desktop-notify.ts +114 -0
  81. package/src/comms/example.ts +129 -0
  82. package/src/comms/index.ts +129 -0
  83. package/src/comms/streaming.ts +142 -0
  84. package/src/comms/voice.test.ts +152 -0
  85. package/src/comms/voice.ts +291 -0
  86. package/src/comms/websocket.test.ts +409 -0
  87. package/src/comms/websocket.ts +473 -0
  88. package/src/config/README.md +387 -0
  89. package/src/config/index.ts +6 -0
  90. package/src/config/loader.test.ts +137 -0
  91. package/src/config/loader.ts +142 -0
  92. package/src/config/types.ts +260 -0
  93. package/src/daemon/README.md +232 -0
  94. package/src/daemon/agent-service-interface.ts +9 -0
  95. package/src/daemon/agent-service.ts +600 -0
  96. package/src/daemon/api-routes.ts +2119 -0
  97. package/src/daemon/background-agent-service.ts +396 -0
  98. package/src/daemon/background-agent.test.ts +78 -0
  99. package/src/daemon/channel-service.ts +201 -0
  100. package/src/daemon/commitment-executor.ts +297 -0
  101. package/src/daemon/event-classifier.ts +239 -0
  102. package/src/daemon/event-coalescer.ts +123 -0
  103. package/src/daemon/event-reactor.ts +214 -0
  104. package/src/daemon/health.ts +220 -0
  105. package/src/daemon/index.ts +1004 -0
  106. package/src/daemon/llm-settings.ts +316 -0
  107. package/src/daemon/observer-service.ts +150 -0
  108. package/src/daemon/pid.ts +98 -0
  109. package/src/daemon/research-queue.ts +155 -0
  110. package/src/daemon/services.ts +175 -0
  111. package/src/daemon/ws-service.ts +788 -0
  112. package/src/goals/accountability.ts +240 -0
  113. package/src/goals/awareness-bridge.ts +185 -0
  114. package/src/goals/estimator.ts +185 -0
  115. package/src/goals/events.ts +28 -0
  116. package/src/goals/goals.test.ts +400 -0
  117. package/src/goals/integration.test.ts +329 -0
  118. package/src/goals/nl-builder.test.ts +220 -0
  119. package/src/goals/nl-builder.ts +256 -0
  120. package/src/goals/rhythm.test.ts +177 -0
  121. package/src/goals/rhythm.ts +275 -0
  122. package/src/goals/service.test.ts +135 -0
  123. package/src/goals/service.ts +348 -0
  124. package/src/goals/types.ts +106 -0
  125. package/src/goals/workflow-bridge.ts +96 -0
  126. package/src/integrations/google-api.ts +134 -0
  127. package/src/integrations/google-auth.ts +175 -0
  128. package/src/llm/README.md +291 -0
  129. package/src/llm/anthropic.ts +386 -0
  130. package/src/llm/gemini.ts +371 -0
  131. package/src/llm/index.ts +19 -0
  132. package/src/llm/manager.ts +153 -0
  133. package/src/llm/ollama.ts +307 -0
  134. package/src/llm/openai.ts +350 -0
  135. package/src/llm/provider.test.ts +231 -0
  136. package/src/llm/provider.ts +60 -0
  137. package/src/llm/test.ts +87 -0
  138. package/src/observers/README.md +278 -0
  139. package/src/observers/calendar.ts +113 -0
  140. package/src/observers/clipboard.ts +136 -0
  141. package/src/observers/email.ts +109 -0
  142. package/src/observers/example.ts +58 -0
  143. package/src/observers/file-watcher.ts +124 -0
  144. package/src/observers/index.ts +159 -0
  145. package/src/observers/notifications.ts +197 -0
  146. package/src/observers/observers.test.ts +203 -0
  147. package/src/observers/processes.ts +225 -0
  148. package/src/personality/README.md +61 -0
  149. package/src/personality/adapter.ts +196 -0
  150. package/src/personality/index.ts +20 -0
  151. package/src/personality/learner.ts +209 -0
  152. package/src/personality/model.ts +132 -0
  153. package/src/personality/personality.test.ts +236 -0
  154. package/src/roles/README.md +252 -0
  155. package/src/roles/authority.ts +119 -0
  156. package/src/roles/example-usage.ts +198 -0
  157. package/src/roles/index.ts +42 -0
  158. package/src/roles/loader.ts +143 -0
  159. package/src/roles/prompt-builder.ts +194 -0
  160. package/src/roles/test-multi.ts +102 -0
  161. package/src/roles/test-role.yaml +77 -0
  162. package/src/roles/test-utils.ts +93 -0
  163. package/src/roles/test.ts +106 -0
  164. package/src/roles/tool-guide.ts +190 -0
  165. package/src/roles/types.ts +36 -0
  166. package/src/roles/utils.ts +200 -0
  167. package/src/scripts/google-setup.ts +168 -0
  168. package/src/sidecar/connection.ts +179 -0
  169. package/src/sidecar/index.ts +6 -0
  170. package/src/sidecar/manager.ts +542 -0
  171. package/src/sidecar/protocol.ts +85 -0
  172. package/src/sidecar/rpc.ts +161 -0
  173. package/src/sidecar/scheduler.ts +136 -0
  174. package/src/sidecar/types.ts +112 -0
  175. package/src/sidecar/validator.ts +144 -0
  176. package/src/vault/README.md +110 -0
  177. package/src/vault/awareness.ts +341 -0
  178. package/src/vault/commitments.ts +299 -0
  179. package/src/vault/content-pipeline.ts +260 -0
  180. package/src/vault/conversations.ts +173 -0
  181. package/src/vault/entities.ts +180 -0
  182. package/src/vault/extractor.test.ts +356 -0
  183. package/src/vault/extractor.ts +345 -0
  184. package/src/vault/facts.ts +190 -0
  185. package/src/vault/goals.ts +477 -0
  186. package/src/vault/index.ts +87 -0
  187. package/src/vault/keychain.ts +99 -0
  188. package/src/vault/observations.ts +115 -0
  189. package/src/vault/relationships.ts +178 -0
  190. package/src/vault/retrieval.test.ts +126 -0
  191. package/src/vault/retrieval.ts +227 -0
  192. package/src/vault/schema.ts +658 -0
  193. package/src/vault/settings.ts +38 -0
  194. package/src/vault/vectors.ts +92 -0
  195. package/src/vault/workflows.ts +403 -0
  196. package/src/workflows/auto-suggest.ts +290 -0
  197. package/src/workflows/engine.ts +366 -0
  198. package/src/workflows/events.ts +24 -0
  199. package/src/workflows/executor.ts +207 -0
  200. package/src/workflows/nl-builder.ts +198 -0
  201. package/src/workflows/nodes/actions/agent-task.ts +73 -0
  202. package/src/workflows/nodes/actions/calendar-action.ts +85 -0
  203. package/src/workflows/nodes/actions/code-execution.ts +73 -0
  204. package/src/workflows/nodes/actions/discord.ts +77 -0
  205. package/src/workflows/nodes/actions/file-write.ts +73 -0
  206. package/src/workflows/nodes/actions/gmail.ts +69 -0
  207. package/src/workflows/nodes/actions/http-request.ts +117 -0
  208. package/src/workflows/nodes/actions/notification.ts +85 -0
  209. package/src/workflows/nodes/actions/run-tool.ts +55 -0
  210. package/src/workflows/nodes/actions/send-message.ts +82 -0
  211. package/src/workflows/nodes/actions/shell-command.ts +76 -0
  212. package/src/workflows/nodes/actions/telegram.ts +60 -0
  213. package/src/workflows/nodes/builtin.ts +119 -0
  214. package/src/workflows/nodes/error/error-handler.ts +37 -0
  215. package/src/workflows/nodes/error/fallback.ts +47 -0
  216. package/src/workflows/nodes/error/retry.ts +82 -0
  217. package/src/workflows/nodes/logic/delay.ts +42 -0
  218. package/src/workflows/nodes/logic/if-else.ts +41 -0
  219. package/src/workflows/nodes/logic/loop.ts +90 -0
  220. package/src/workflows/nodes/logic/merge.ts +38 -0
  221. package/src/workflows/nodes/logic/race.ts +40 -0
  222. package/src/workflows/nodes/logic/switch.ts +59 -0
  223. package/src/workflows/nodes/logic/template-render.ts +53 -0
  224. package/src/workflows/nodes/logic/variable-get.ts +37 -0
  225. package/src/workflows/nodes/logic/variable-set.ts +59 -0
  226. package/src/workflows/nodes/registry.ts +99 -0
  227. package/src/workflows/nodes/transform/aggregate.ts +99 -0
  228. package/src/workflows/nodes/transform/csv-parse.ts +70 -0
  229. package/src/workflows/nodes/transform/json-parse.ts +63 -0
  230. package/src/workflows/nodes/transform/map-filter.ts +84 -0
  231. package/src/workflows/nodes/transform/regex-match.ts +89 -0
  232. package/src/workflows/nodes/triggers/calendar.ts +33 -0
  233. package/src/workflows/nodes/triggers/clipboard.ts +32 -0
  234. package/src/workflows/nodes/triggers/cron.ts +40 -0
  235. package/src/workflows/nodes/triggers/email.ts +40 -0
  236. package/src/workflows/nodes/triggers/file-change.ts +45 -0
  237. package/src/workflows/nodes/triggers/git.ts +46 -0
  238. package/src/workflows/nodes/triggers/manual.ts +23 -0
  239. package/src/workflows/nodes/triggers/poll.ts +81 -0
  240. package/src/workflows/nodes/triggers/process.ts +44 -0
  241. package/src/workflows/nodes/triggers/screen-event.ts +37 -0
  242. package/src/workflows/nodes/triggers/webhook.ts +39 -0
  243. package/src/workflows/safe-eval.ts +139 -0
  244. package/src/workflows/template.ts +118 -0
  245. package/src/workflows/triggers/cron.ts +311 -0
  246. package/src/workflows/triggers/manager.ts +285 -0
  247. package/src/workflows/triggers/observer-bridge.ts +172 -0
  248. package/src/workflows/triggers/poller.ts +201 -0
  249. package/src/workflows/triggers/screen-condition.ts +218 -0
  250. package/src/workflows/triggers/triggers.test.ts +740 -0
  251. package/src/workflows/triggers/webhook.ts +191 -0
  252. package/src/workflows/types.ts +133 -0
  253. package/src/workflows/variables.ts +72 -0
  254. package/src/workflows/workflows.test.ts +383 -0
  255. package/src/workflows/yaml.ts +104 -0
  256. package/ui/dist/index-j75njzc1.css +1199 -0
  257. package/ui/dist/index-p2zh407q.js +80603 -0
  258. package/ui/dist/index.html +13 -0
  259. package/ui/public/openwakeword/models/embedding_model.onnx +0 -0
  260. package/ui/public/openwakeword/models/hey_jarvis_v0.1.onnx +0 -0
  261. package/ui/public/openwakeword/models/melspectrogram.onnx +0 -0
  262. package/ui/public/openwakeword/models/silero_vad.onnx +0 -0
  263. package/ui/public/ort/ort-wasm-simd-threaded.jsep.mjs +106 -0
  264. package/ui/public/ort/ort-wasm-simd-threaded.jsep.wasm +0 -0
  265. package/ui/public/ort/ort-wasm-simd-threaded.mjs +59 -0
  266. package/ui/public/ort/ort-wasm-simd-threaded.wasm +0 -0
@@ -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
+ }
@@ -0,0 +1,142 @@
1
+ import type { LLMStreamEvent } from '../llm/provider.ts';
2
+ import type { WebSocketServer, WSMessage } from './websocket.ts';
3
+
4
+ export type RelayOptions = {
5
+ /** Called each time a complete sentence is available during streaming. */
6
+ onSentence?: (sentence: string) => void;
7
+ /** Called when all text is done streaming. */
8
+ onTextDone?: () => void;
9
+ };
10
+
11
+ // Sentence boundary: period, exclamation, question mark, colon followed by whitespace or end
12
+ const SENTENCE_END_RE = /[.!?:]\s/;
13
+
14
+ export class StreamRelay {
15
+ private wsServer: WebSocketServer;
16
+
17
+ constructor(wsServer: WebSocketServer) {
18
+ this.wsServer = wsServer;
19
+ }
20
+
21
+ /**
22
+ * Relay LLM stream events to all connected WebSocket clients.
23
+ * Accumulates and returns the complete response text.
24
+ * Optionally fires onSentence callback as complete sentences arrive.
25
+ */
26
+ async relayStream(
27
+ stream: AsyncIterable<LLMStreamEvent>,
28
+ requestId: string,
29
+ options?: RelayOptions,
30
+ ): Promise<string> {
31
+ let fullText = '';
32
+ let sentenceBuffer = '';
33
+
34
+ try {
35
+ for await (const event of stream) {
36
+ if (event.type === 'text') {
37
+ fullText += event.text;
38
+
39
+ // Sentence-level TTS callback
40
+ if (options?.onSentence) {
41
+ sentenceBuffer += event.text;
42
+ // Flush complete sentences from the buffer
43
+ let match: RegExpExecArray | null;
44
+ while ((match = SENTENCE_END_RE.exec(sentenceBuffer)) !== null) {
45
+ const end = match.index + match[0].length;
46
+ const sentence = sentenceBuffer.slice(0, end).trim();
47
+ if (sentence) {
48
+ options.onSentence(sentence);
49
+ }
50
+ sentenceBuffer = sentenceBuffer.slice(end);
51
+ }
52
+ }
53
+
54
+ // Broadcast chunk to all connected clients
55
+ const message: WSMessage = {
56
+ type: 'stream',
57
+ payload: {
58
+ text: event.text,
59
+ requestId,
60
+ accumulated: fullText,
61
+ },
62
+ id: requestId,
63
+ timestamp: Date.now(),
64
+ };
65
+
66
+ this.wsServer.broadcast(message);
67
+ } else if (event.type === 'tool_call') {
68
+ // Broadcast tool call notification to clients
69
+ const toolMessage: WSMessage = {
70
+ type: 'stream',
71
+ payload: {
72
+ tool_call: {
73
+ name: event.tool_call.name,
74
+ arguments: event.tool_call.arguments,
75
+ },
76
+ requestId,
77
+ },
78
+ id: requestId,
79
+ timestamp: Date.now(),
80
+ };
81
+
82
+ this.wsServer.broadcast(toolMessage);
83
+ } else if (event.type === 'error') {
84
+ console.error('[StreamRelay] Stream error:', event.error);
85
+
86
+ const errorMessage: WSMessage = {
87
+ type: 'error',
88
+ payload: {
89
+ message: event.error,
90
+ requestId,
91
+ },
92
+ id: requestId,
93
+ timestamp: Date.now(),
94
+ };
95
+
96
+ this.wsServer.broadcast(errorMessage);
97
+ } else if (event.type === 'done') {
98
+ // Flush remaining sentence buffer
99
+ if (options?.onSentence && sentenceBuffer.trim()) {
100
+ options.onSentence(sentenceBuffer.trim());
101
+ sentenceBuffer = '';
102
+ }
103
+ options?.onTextDone?.();
104
+
105
+ console.log('[StreamRelay] Stream complete for request:', requestId);
106
+
107
+ const doneMessage: WSMessage = {
108
+ type: 'status',
109
+ payload: {
110
+ status: 'done',
111
+ requestId,
112
+ fullText,
113
+ usage: event.response.usage,
114
+ },
115
+ id: requestId,
116
+ timestamp: Date.now(),
117
+ };
118
+
119
+ this.wsServer.broadcast(doneMessage);
120
+ }
121
+ }
122
+ } catch (error) {
123
+ console.error('[StreamRelay] Error relaying stream:', error);
124
+
125
+ const errorMessage: WSMessage = {
126
+ type: 'error',
127
+ payload: {
128
+ message: error instanceof Error ? error.message : 'Stream relay error',
129
+ requestId,
130
+ },
131
+ id: requestId,
132
+ timestamp: Date.now(),
133
+ };
134
+
135
+ this.wsServer.broadcast(errorMessage);
136
+
137
+ throw error;
138
+ }
139
+
140
+ return fullText;
141
+ }
142
+ }
@@ -0,0 +1,152 @@
1
+ import { test, expect, describe } from 'bun:test';
2
+ import {
3
+ createSTTProvider,
4
+ createTTSProvider,
5
+ OpenAIWhisperSTT,
6
+ GroqWhisperSTT,
7
+ LocalWhisperSTT,
8
+ EdgeTTSProvider,
9
+ splitIntoSentences,
10
+ } from './voice.ts';
11
+ import type { STTConfig, TTSConfig } from '../config/types.ts';
12
+
13
+ describe('createSTTProvider factory', () => {
14
+ test('returns OpenAIWhisperSTT when provider=openai and key present', () => {
15
+ const config: STTConfig = {
16
+ provider: 'openai',
17
+ openai: { api_key: 'test-openai-key-not-real' },
18
+ };
19
+ const provider = createSTTProvider(config);
20
+ expect(provider).toBeInstanceOf(OpenAIWhisperSTT);
21
+ });
22
+
23
+ test('returns null when provider=openai and no key', () => {
24
+ const config: STTConfig = { provider: 'openai' };
25
+ const provider = createSTTProvider(config);
26
+ expect(provider).toBeNull();
27
+ });
28
+
29
+ test('returns GroqWhisperSTT when provider=groq and key present', () => {
30
+ const config: STTConfig = {
31
+ provider: 'groq',
32
+ groq: { api_key: 'gtest-openai-key-not-real' },
33
+ };
34
+ const provider = createSTTProvider(config);
35
+ expect(provider).toBeInstanceOf(GroqWhisperSTT);
36
+ });
37
+
38
+ test('returns null when provider=groq and no key', () => {
39
+ const config: STTConfig = { provider: 'groq' };
40
+ const provider = createSTTProvider(config);
41
+ expect(provider).toBeNull();
42
+ });
43
+
44
+ test('returns LocalWhisperSTT when provider=local (no key needed)', () => {
45
+ const config: STTConfig = { provider: 'local' };
46
+ const provider = createSTTProvider(config);
47
+ expect(provider).toBeInstanceOf(LocalWhisperSTT);
48
+ });
49
+
50
+ test('returns LocalWhisperSTT with custom endpoint', () => {
51
+ const config: STTConfig = {
52
+ provider: 'local',
53
+ local: { endpoint: 'http://my-server:9000' },
54
+ };
55
+ const provider = createSTTProvider(config);
56
+ expect(provider).toBeInstanceOf(LocalWhisperSTT);
57
+ });
58
+
59
+ test('returns null for unknown provider', () => {
60
+ const config = { provider: 'unknown' } as any;
61
+ const provider = createSTTProvider(config);
62
+ expect(provider).toBeNull();
63
+ });
64
+
65
+ test('returns OpenAI with custom model', () => {
66
+ const config: STTConfig = {
67
+ provider: 'openai',
68
+ openai: { api_key: 'test-key-not-real', model: 'whisper-large-v3' },
69
+ };
70
+ const provider = createSTTProvider(config);
71
+ expect(provider).toBeInstanceOf(OpenAIWhisperSTT);
72
+ });
73
+ });
74
+
75
+ describe('createTTSProvider factory', () => {
76
+ test('returns null when tts disabled', () => {
77
+ const config: TTSConfig = { enabled: false };
78
+ expect(createTTSProvider(config)).toBeNull();
79
+ });
80
+
81
+ test('returns EdgeTTSProvider when enabled', () => {
82
+ const config: TTSConfig = { enabled: true };
83
+ const provider = createTTSProvider(config);
84
+ expect(provider).toBeInstanceOf(EdgeTTSProvider);
85
+ });
86
+
87
+ test('passes voice config to provider', () => {
88
+ const config: TTSConfig = { enabled: true, voice: 'en-GB-SoniaNeural' };
89
+ const provider = createTTSProvider(config);
90
+ expect(provider).toBeInstanceOf(EdgeTTSProvider);
91
+ });
92
+
93
+ test('passes rate and volume config', () => {
94
+ const config: TTSConfig = { enabled: true, rate: '+20%', volume: '-10%' };
95
+ const provider = createTTSProvider(config);
96
+ expect(provider).not.toBeNull();
97
+ });
98
+ });
99
+
100
+ describe('EdgeTTSProvider', () => {
101
+ test('implements TTSProvider interface', () => {
102
+ const provider = new EdgeTTSProvider();
103
+ expect(typeof provider.synthesize).toBe('function');
104
+ expect(typeof provider.synthesizeStream).toBe('function');
105
+ });
106
+
107
+ test('constructor accepts custom voice/rate/volume', () => {
108
+ const provider = new EdgeTTSProvider('en-GB-SoniaNeural', '+10%', '-5%');
109
+ expect(provider).toBeInstanceOf(EdgeTTSProvider);
110
+ });
111
+ });
112
+
113
+ describe('splitIntoSentences', () => {
114
+ test('splits on period + capital letter', () => {
115
+ const result = splitIntoSentences('Hello there. World is great. This works.');
116
+ expect(result.length).toBe(3);
117
+ expect(result[0]).toBe('Hello there.');
118
+ expect(result[1]).toBe('World is great.');
119
+ expect(result[2]).toBe('This works.');
120
+ });
121
+
122
+ test('splits on exclamation and question marks', () => {
123
+ const result = splitIntoSentences('Wait! Are you sure? Yes I am.');
124
+ expect(result.length).toBe(3);
125
+ });
126
+
127
+ test('handles single sentence', () => {
128
+ const result = splitIntoSentences('Just one sentence.');
129
+ expect(result).toEqual(['Just one sentence.']);
130
+ });
131
+
132
+ test('handles empty string', () => {
133
+ const result = splitIntoSentences('');
134
+ expect(result).toEqual(['']);
135
+ });
136
+
137
+ test('collapses code blocks', () => {
138
+ const result = splitIntoSentences('Here is code:\n```\nconst x = 1;\n```\nDone.');
139
+ // Should not split inside code block
140
+ expect(result.length).toBeLessThanOrEqual(3);
141
+ });
142
+
143
+ test('splits on double newlines (paragraph breaks)', () => {
144
+ const result = splitIntoSentences('First paragraph\n\nSecond paragraph');
145
+ expect(result.length).toBe(2);
146
+ });
147
+
148
+ test('handles text with no sentence-ending punctuation', () => {
149
+ const result = splitIntoSentences('just some words without punctuation');
150
+ expect(result).toEqual(['just some words without punctuation']);
151
+ });
152
+ });