@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,228 @@
1
+ import { Client, GatewayIntentBits, Partials, type Message } from 'discord.js';
2
+ import type { ChannelAdapter, ChannelHandler, ChannelMessage } from './telegram.ts';
3
+ import type { STTProvider } from '../voice.ts';
4
+
5
+ export class DiscordAdapter implements ChannelAdapter {
6
+ name = 'discord';
7
+ private token: string;
8
+ private handler: ChannelHandler | null = null;
9
+ private connected: boolean = false;
10
+ private client: Client | null = null;
11
+ private allowedUsers: string[];
12
+ private guildId: string | null;
13
+ private sttProvider: STTProvider | null;
14
+
15
+ constructor(token: string, opts?: {
16
+ allowedUsers?: string[];
17
+ guildId?: string;
18
+ sttProvider?: STTProvider;
19
+ }) {
20
+ this.token = token;
21
+ this.allowedUsers = opts?.allowedUsers ?? [];
22
+ this.guildId = opts?.guildId ?? null;
23
+ this.sttProvider = opts?.sttProvider ?? null;
24
+ }
25
+
26
+ setSTTProvider(provider: STTProvider): void {
27
+ this.sttProvider = provider;
28
+ }
29
+
30
+ async connect(): Promise<void> {
31
+ if (this.connected) {
32
+ console.warn('[DiscordAdapter] Already connected');
33
+ return;
34
+ }
35
+
36
+ this.client = new Client({
37
+ intents: [
38
+ GatewayIntentBits.Guilds,
39
+ GatewayIntentBits.GuildMessages,
40
+ GatewayIntentBits.DirectMessages,
41
+ GatewayIntentBits.MessageContent,
42
+ ],
43
+ partials: [Partials.Channel],
44
+ });
45
+
46
+ // Wait for ready event
47
+ let loginTimeout: ReturnType<typeof setTimeout> | null = null;
48
+ const readyPromise = new Promise<void>((resolve, reject) => {
49
+ loginTimeout = setTimeout(() => reject(new Error('Discord login timed out')), 30000);
50
+
51
+ this.client!.once('ready', () => {
52
+ clearTimeout(loginTimeout!);
53
+ this.connected = true;
54
+ console.log(`[DiscordAdapter] Connected as: ${this.client!.user?.tag}`);
55
+ resolve();
56
+ });
57
+
58
+ this.client!.once('error', (err) => {
59
+ clearTimeout(loginTimeout!);
60
+ reject(err);
61
+ });
62
+ });
63
+
64
+ // Set up message handler
65
+ this.client.on('messageCreate', async (message: Message) => {
66
+ try {
67
+ await this.processMessage(message);
68
+ } catch (err) {
69
+ console.error('[DiscordAdapter] Unhandled error in processMessage:', err);
70
+ }
71
+ });
72
+
73
+ try {
74
+ await this.client.login(this.token);
75
+ } catch (err) {
76
+ // Clear the ready timeout to prevent unhandled rejection
77
+ if (loginTimeout) clearTimeout(loginTimeout);
78
+ this.client.destroy();
79
+ this.client = null;
80
+ throw err;
81
+ }
82
+ await readyPromise;
83
+ }
84
+
85
+ async disconnect(): Promise<void> {
86
+ if (this.client) {
87
+ this.client.destroy();
88
+ this.client = null;
89
+ }
90
+ this.connected = false;
91
+ console.log('[DiscordAdapter] Disconnected');
92
+ }
93
+
94
+ async sendMessage(channelId: string, text: string): Promise<void> {
95
+ if (!this.client) throw new Error('Discord not connected');
96
+
97
+ const channel = await this.client.channels.fetch(channelId);
98
+ if (!channel || !channel.isTextBased()) {
99
+ throw new Error(`Invalid or non-text channel: ${channelId}`);
100
+ }
101
+
102
+ const chunks = splitMessage(text, 2000);
103
+ for (const chunk of chunks) {
104
+ await (channel as any).send(chunk);
105
+ }
106
+ }
107
+
108
+ onMessage(handler: ChannelHandler): void {
109
+ this.handler = handler;
110
+ }
111
+
112
+ isConnected(): boolean {
113
+ return this.connected;
114
+ }
115
+
116
+ private async processMessage(message: Message): Promise<void> {
117
+ // Ignore bot messages (including our own)
118
+ if (message.author.bot) return;
119
+ if (!this.handler) return;
120
+
121
+ // Security: check allowed users (empty = allow all)
122
+ if (this.allowedUsers.length > 0 && !this.allowedUsers.includes(message.author.id)) {
123
+ return;
124
+ }
125
+
126
+ // Security: check guild restriction
127
+ if (this.guildId && message.guildId && message.guildId !== this.guildId) {
128
+ return;
129
+ }
130
+
131
+ let text = message.content;
132
+
133
+ // Handle audio attachments via STT
134
+ const audioAttachment = message.attachments.find(a =>
135
+ a.contentType?.startsWith('audio/') ||
136
+ a.name?.endsWith('.ogg') ||
137
+ a.name?.endsWith('.mp3') ||
138
+ a.name?.endsWith('.wav') ||
139
+ a.name?.endsWith('.m4a')
140
+ );
141
+
142
+ if (audioAttachment && !text && this.sttProvider) {
143
+ try {
144
+ const resp = await fetch(audioAttachment.url);
145
+ if (!resp.ok) throw new Error(`Failed to download: ${resp.status}`);
146
+ const buffer = Buffer.from(await resp.arrayBuffer());
147
+ text = await this.sttProvider.transcribe(buffer);
148
+ console.log('[DiscordAdapter] Transcribed audio:', text.slice(0, 80));
149
+ } catch (err) {
150
+ console.error('[DiscordAdapter] STT error:', err);
151
+ await message.reply('Failed to transcribe audio. Please send text.');
152
+ return;
153
+ }
154
+ }
155
+
156
+ if (!text) return;
157
+
158
+ const channelMessage: ChannelMessage = {
159
+ id: message.id,
160
+ channel: 'discord',
161
+ from: message.author.username,
162
+ text,
163
+ timestamp: message.createdTimestamp,
164
+ metadata: {
165
+ userId: message.author.id,
166
+ channelId: message.channelId,
167
+ guildId: message.guildId,
168
+ isDM: !message.guildId,
169
+ isVoice: !!audioAttachment,
170
+ },
171
+ };
172
+
173
+ console.log('[DiscordAdapter] Message from', channelMessage.from, ':', text.slice(0, 80));
174
+
175
+ try {
176
+ // Show typing indicator
177
+ if (message.channel.isSendable()) {
178
+ await message.channel.sendTyping();
179
+ }
180
+
181
+ const response = await this.handler(channelMessage);
182
+
183
+ if (response) {
184
+ const chunks = splitMessage(response, 2000);
185
+ for (const chunk of chunks) {
186
+ await message.reply(chunk);
187
+ }
188
+ }
189
+ } catch (err) {
190
+ console.error('[DiscordAdapter] Error handling message:', err);
191
+ try {
192
+ await message.reply('Sorry, I encountered an error processing your message.');
193
+ } catch {
194
+ // Ignore send failure
195
+ }
196
+ }
197
+ }
198
+ }
199
+
200
+ export function splitMessage(text: string, maxLength: number): string[] {
201
+ if (text.length <= maxLength) return [text];
202
+
203
+ const chunks: string[] = [];
204
+ let remaining = text;
205
+
206
+ while (remaining.length > 0) {
207
+ if (remaining.length <= maxLength) {
208
+ chunks.push(remaining);
209
+ break;
210
+ }
211
+
212
+ // Try to split at newline
213
+ let splitIdx = remaining.lastIndexOf('\n', maxLength);
214
+ if (splitIdx < maxLength / 2) {
215
+ // Try space
216
+ splitIdx = remaining.lastIndexOf(' ', maxLength);
217
+ }
218
+ if (splitIdx < maxLength / 2) {
219
+ // Hard split
220
+ splitIdx = maxLength;
221
+ }
222
+
223
+ chunks.push(remaining.slice(0, splitIdx));
224
+ remaining = remaining.slice(splitIdx).trimStart();
225
+ }
226
+
227
+ return chunks;
228
+ }
@@ -0,0 +1,56 @@
1
+ import type { ChannelAdapter, ChannelHandler, ChannelMessage } from './telegram.ts';
2
+
3
+ export class SignalAdapter implements ChannelAdapter {
4
+ name = 'signal';
5
+ private phone: string;
6
+ private handler: ChannelHandler | null = null;
7
+ private connected: boolean = false;
8
+
9
+ constructor(config: { phone: string }) {
10
+ this.phone = config.phone;
11
+ }
12
+
13
+ async connect(): Promise<void> {
14
+ throw new Error(
15
+ 'Signal adapter not yet implemented. Requires signal-cli setup. ' +
16
+ 'Install signal-cli: https://github.com/AsamK/signal-cli'
17
+ );
18
+
19
+ // Future implementation would use signal-cli in daemon mode:
20
+ // 1. Ensure signal-cli is installed and registered with phone number
21
+ // 2. Start signal-cli in daemon mode with D-Bus interface
22
+ // 3. Subscribe to message events via D-Bus
23
+ // 4. Set this.connected = true
24
+ //
25
+ // Example using signal-cli REST API mode:
26
+ // Start signal-cli with: signal-cli -a <PHONE> daemon --http localhost:8080
27
+ // Then connect via HTTP API
28
+ }
29
+
30
+ async disconnect(): Promise<void> {
31
+ this.connected = false;
32
+ }
33
+
34
+ async sendMessage(recipient: string, text: string): Promise<void> {
35
+ throw new Error('Signal adapter not yet implemented.');
36
+
37
+ // Future implementation using signal-cli REST API:
38
+ // await fetch('http://localhost:8080/v2/send', {
39
+ // method: 'POST',
40
+ // headers: { 'Content-Type': 'application/json' },
41
+ // body: JSON.stringify({
42
+ // number: this.phone,
43
+ // recipients: [recipient],
44
+ // message: text,
45
+ // }),
46
+ // });
47
+ }
48
+
49
+ onMessage(handler: ChannelHandler): void {
50
+ this.handler = handler;
51
+ }
52
+
53
+ isConnected(): boolean {
54
+ return this.connected;
55
+ }
56
+ }
@@ -0,0 +1,316 @@
1
+ import type { STTProvider } from '../voice.ts';
2
+
3
+ export type ChannelMessage = {
4
+ id: string;
5
+ channel: string;
6
+ from: string;
7
+ text: string;
8
+ timestamp: number;
9
+ metadata: Record<string, unknown>;
10
+ };
11
+
12
+ export type ChannelHandler = (message: ChannelMessage) => Promise<string>;
13
+
14
+ export interface ChannelAdapter {
15
+ name: string;
16
+ connect(): Promise<void>;
17
+ disconnect(): Promise<void>;
18
+ sendMessage(to: string, text: string): Promise<void>;
19
+ onMessage(handler: ChannelHandler): void;
20
+ isConnected(): boolean;
21
+ }
22
+
23
+ type TelegramUpdate = {
24
+ update_id: number;
25
+ message?: {
26
+ message_id: number;
27
+ from: {
28
+ id: number;
29
+ first_name: string;
30
+ last_name?: string;
31
+ username?: string;
32
+ };
33
+ chat: {
34
+ id: number;
35
+ type: string;
36
+ };
37
+ date: number;
38
+ text?: string;
39
+ voice?: {
40
+ duration: number;
41
+ mime_type: string;
42
+ file_id: string;
43
+ file_unique_id: string;
44
+ file_size?: number;
45
+ };
46
+ audio?: {
47
+ duration: number;
48
+ mime_type: string;
49
+ file_id: string;
50
+ file_unique_id: string;
51
+ file_size?: number;
52
+ };
53
+ };
54
+ };
55
+
56
+ type TelegramGetUpdatesResponse = {
57
+ ok: boolean;
58
+ result: TelegramUpdate[];
59
+ };
60
+
61
+ export class TelegramAdapter implements ChannelAdapter {
62
+ name = 'telegram';
63
+ private token: string;
64
+ private handler: ChannelHandler | null = null;
65
+ private polling: boolean = false;
66
+ private offset: number = 0;
67
+ private baseUrl: string;
68
+ private pollingInterval: number = 1000;
69
+ private sttProvider: STTProvider | null = null;
70
+ private allowedUsers: number[];
71
+
72
+ constructor(token: string, opts?: { sttProvider?: STTProvider; allowedUsers?: number[] }) {
73
+ this.token = token;
74
+ this.baseUrl = `https://api.telegram.org/bot${token}`;
75
+ this.sttProvider = opts?.sttProvider ?? null;
76
+ this.allowedUsers = opts?.allowedUsers ?? [];
77
+ }
78
+
79
+ setSTTProvider(provider: STTProvider): void {
80
+ this.sttProvider = provider;
81
+ }
82
+
83
+ async connect(): Promise<void> {
84
+ if (this.polling) {
85
+ console.warn('[TelegramAdapter] Already connected');
86
+ return;
87
+ }
88
+
89
+ // Verify bot token by calling getMe
90
+ try {
91
+ const response = await fetch(`${this.baseUrl}/getMe`);
92
+ const data = await response.json() as any;
93
+
94
+ if (!data.ok) {
95
+ throw new Error(`Invalid bot token: ${data.description}`);
96
+ }
97
+
98
+ console.log('[TelegramAdapter] Connected as:', data.result.username);
99
+ } catch (error) {
100
+ throw new Error(
101
+ `Failed to connect to Telegram: ${error instanceof Error ? error.message : 'Unknown error'}`
102
+ );
103
+ }
104
+
105
+ this.polling = true;
106
+ this.startPolling();
107
+ }
108
+
109
+ async disconnect(): Promise<void> {
110
+ this.polling = false;
111
+ console.log('[TelegramAdapter] Disconnected');
112
+ }
113
+
114
+ async sendMessage(chatId: string, text: string): Promise<void> {
115
+ // Telegram has a 4096 char limit per message
116
+ const chunks = splitText(text, 4096);
117
+ for (const chunk of chunks) {
118
+ try {
119
+ const response = await fetch(`${this.baseUrl}/sendMessage`, {
120
+ method: 'POST',
121
+ headers: { 'Content-Type': 'application/json' },
122
+ body: JSON.stringify({
123
+ chat_id: chatId,
124
+ text: chunk,
125
+ parse_mode: 'Markdown',
126
+ }),
127
+ });
128
+
129
+ const data = await response.json() as any;
130
+
131
+ if (!data.ok) {
132
+ // Retry without Markdown if parsing failed
133
+ if (data.description?.includes('parse')) {
134
+ await fetch(`${this.baseUrl}/sendMessage`, {
135
+ method: 'POST',
136
+ headers: { 'Content-Type': 'application/json' },
137
+ body: JSON.stringify({ chat_id: chatId, text: chunk }),
138
+ });
139
+ } else {
140
+ throw new Error(`Telegram API error: ${data.description}`);
141
+ }
142
+ }
143
+ } catch (error) {
144
+ console.error('[TelegramAdapter] Error sending message:', error);
145
+ throw error;
146
+ }
147
+ }
148
+ }
149
+
150
+ onMessage(handler: ChannelHandler): void {
151
+ this.handler = handler;
152
+ }
153
+
154
+ isConnected(): boolean {
155
+ return this.polling;
156
+ }
157
+
158
+ private async startPolling(): Promise<void> {
159
+ console.log('[TelegramAdapter] Starting polling...');
160
+
161
+ while (this.polling) {
162
+ try {
163
+ const updates = await this.getUpdates();
164
+
165
+ for (const update of updates) {
166
+ await this.processUpdate(update);
167
+ }
168
+ } catch (error) {
169
+ console.error('[TelegramAdapter] Polling error:', error);
170
+ }
171
+
172
+ await new Promise(resolve => setTimeout(resolve, this.pollingInterval));
173
+ }
174
+
175
+ console.log('[TelegramAdapter] Polling stopped');
176
+ }
177
+
178
+ private async getUpdates(): Promise<TelegramUpdate[]> {
179
+ const response = await fetch(`${this.baseUrl}/getUpdates`, {
180
+ method: 'POST',
181
+ headers: { 'Content-Type': 'application/json' },
182
+ body: JSON.stringify({
183
+ offset: this.offset,
184
+ timeout: 30,
185
+ allowed_updates: ['message'],
186
+ }),
187
+ });
188
+
189
+ const data: TelegramGetUpdatesResponse = await response.json() as TelegramGetUpdatesResponse;
190
+
191
+ if (!data.ok) {
192
+ throw new Error('Failed to get updates');
193
+ }
194
+
195
+ if (data.result.length > 0) {
196
+ this.offset = data.result[data.result.length - 1]!.update_id + 1;
197
+ }
198
+
199
+ return data.result;
200
+ }
201
+
202
+ private async processUpdate(update: TelegramUpdate): Promise<void> {
203
+ if (!update.message || !this.handler) return;
204
+
205
+ const { message } = update;
206
+
207
+ // Security: check allowed users
208
+ if (this.allowedUsers.length > 0 && !this.allowedUsers.includes(message.from.id)) {
209
+ console.log(`[TelegramAdapter] Ignoring message from unauthorized user: ${message.from.id} (${message.from.username ?? message.from.first_name})`);
210
+ return;
211
+ }
212
+
213
+ let text = message.text ?? '';
214
+
215
+ // Handle voice/audio messages via STT
216
+ const voiceFile = message.voice ?? message.audio;
217
+ if (voiceFile && !text) {
218
+ if (!this.sttProvider) {
219
+ await this.sendMessage(
220
+ message.chat.id.toString(),
221
+ 'Voice messages require STT configuration. Set up an STT provider in the Dashboard Settings.'
222
+ );
223
+ return;
224
+ }
225
+ try {
226
+ const audioBuffer = await this.downloadFile(voiceFile.file_id);
227
+ text = await this.sttProvider.transcribe(audioBuffer);
228
+ console.log('[TelegramAdapter] Transcribed voice:', text.slice(0, 80));
229
+ } catch (err) {
230
+ console.error('[TelegramAdapter] STT error:', err);
231
+ await this.sendMessage(
232
+ message.chat.id.toString(),
233
+ 'Failed to transcribe voice message. Please try sending text.'
234
+ );
235
+ return;
236
+ }
237
+ }
238
+
239
+ if (!text) return;
240
+
241
+ const channelMessage: ChannelMessage = {
242
+ id: message.message_id.toString(),
243
+ channel: 'telegram',
244
+ from: message.from.username || message.from.first_name,
245
+ text,
246
+ timestamp: message.date * 1000,
247
+ metadata: {
248
+ chatId: message.chat.id,
249
+ userId: message.from.id,
250
+ chatType: message.chat.type,
251
+ firstName: message.from.first_name,
252
+ lastName: message.from.last_name,
253
+ isVoice: !!voiceFile,
254
+ },
255
+ };
256
+
257
+ console.log('[TelegramAdapter] Message from', channelMessage.from, ':', channelMessage.text.slice(0, 80));
258
+
259
+ try {
260
+ const response = await this.handler(channelMessage);
261
+
262
+ if (response) {
263
+ await this.sendMessage(message.chat.id.toString(), response);
264
+ }
265
+ } catch (error) {
266
+ console.error('[TelegramAdapter] Error handling message:', error);
267
+
268
+ await this.sendMessage(
269
+ message.chat.id.toString(),
270
+ 'Sorry, I encountered an error processing your message.'
271
+ );
272
+ }
273
+ }
274
+
275
+ private async downloadFile(fileId: string): Promise<Buffer> {
276
+ // Step 1: Get file path from Telegram
277
+ const fileResp = await fetch(`${this.baseUrl}/getFile`, {
278
+ method: 'POST',
279
+ headers: { 'Content-Type': 'application/json' },
280
+ body: JSON.stringify({ file_id: fileId }),
281
+ });
282
+ const fileData = await fileResp.json() as any;
283
+
284
+ if (!fileData.ok) {
285
+ throw new Error(`Failed to get file info: ${fileData.description}`);
286
+ }
287
+
288
+ // Step 2: Download the actual file
289
+ const filePath = fileData.result.file_path;
290
+ const downloadUrl = `https://api.telegram.org/file/bot${this.token}/${filePath}`;
291
+ const downloadResp = await fetch(downloadUrl);
292
+
293
+ if (!downloadResp.ok) {
294
+ throw new Error(`Failed to download file: ${downloadResp.status}`);
295
+ }
296
+
297
+ return Buffer.from(await downloadResp.arrayBuffer());
298
+ }
299
+ }
300
+
301
+ function splitText(text: string, maxLength: number): string[] {
302
+ if (text.length <= maxLength) return [text];
303
+ const chunks: string[] = [];
304
+ let remaining = text;
305
+ while (remaining.length > 0) {
306
+ if (remaining.length <= maxLength) {
307
+ chunks.push(remaining);
308
+ break;
309
+ }
310
+ let splitIdx = remaining.lastIndexOf('\n', maxLength);
311
+ if (splitIdx < maxLength / 2) splitIdx = maxLength;
312
+ chunks.push(remaining.slice(0, splitIdx));
313
+ remaining = remaining.slice(splitIdx);
314
+ }
315
+ return chunks;
316
+ }
@@ -0,0 +1,60 @@
1
+ import type { ChannelAdapter, ChannelHandler, ChannelMessage } from './telegram.ts';
2
+
3
+ export class WhatsAppAdapter implements ChannelAdapter {
4
+ name = 'whatsapp';
5
+ private phoneNumberId: string;
6
+ private accessToken: string;
7
+ private handler: ChannelHandler | null = null;
8
+ private connected: boolean = false;
9
+
10
+ constructor(config: { phoneNumberId: string; accessToken: string }) {
11
+ this.phoneNumberId = config.phoneNumberId;
12
+ this.accessToken = config.accessToken;
13
+ }
14
+
15
+ async connect(): Promise<void> {
16
+ throw new Error(
17
+ 'WhatsApp adapter not yet implemented. Requires WhatsApp Business API setup. ' +
18
+ 'Visit https://developers.facebook.com/docs/whatsapp/cloud-api/get-started'
19
+ );
20
+
21
+ // Future implementation would:
22
+ // 1. Set up webhook endpoint for incoming messages
23
+ // 2. Verify webhook with Facebook/Meta
24
+ // 3. Start listening for webhook events
25
+ // 4. Set this.connected = true
26
+ }
27
+
28
+ async disconnect(): Promise<void> {
29
+ this.connected = false;
30
+ }
31
+
32
+ async sendMessage(to: string, text: string): Promise<void> {
33
+ throw new Error('WhatsApp adapter not yet implemented.');
34
+
35
+ // Future implementation:
36
+ // const response = await fetch(
37
+ // `https://graph.facebook.com/v18.0/${this.phoneNumberId}/messages`,
38
+ // {
39
+ // method: 'POST',
40
+ // headers: {
41
+ // 'Authorization': `Bearer ${this.accessToken}`,
42
+ // 'Content-Type': 'application/json',
43
+ // },
44
+ // body: JSON.stringify({
45
+ // messaging_product: 'whatsapp',
46
+ // to,
47
+ // text: { body: text },
48
+ // }),
49
+ // }
50
+ // );
51
+ }
52
+
53
+ onMessage(handler: ChannelHandler): void {
54
+ this.handler = handler;
55
+ }
56
+
57
+ isConnected(): boolean {
58
+ return this.connected;
59
+ }
60
+ }