@swarmclawai/swarmclaw 0.2.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 (319) hide show
  1. package/README.md +577 -0
  2. package/bin/server-cmd.js +359 -0
  3. package/bin/swarmclaw.js +29 -0
  4. package/bin/swarmclaw.mjs +1504 -0
  5. package/next.config.ts +33 -0
  6. package/package.json +112 -0
  7. package/postcss.config.mjs +7 -0
  8. package/public/branding/swarmclaw-org-avatar.png +0 -0
  9. package/public/branding/swarmclaw-org-avatar.svg +58 -0
  10. package/public/file.svg +1 -0
  11. package/public/globe.svg +1 -0
  12. package/public/next.svg +1 -0
  13. package/public/screenshots/agents.png +0 -0
  14. package/public/screenshots/connectors.png +0 -0
  15. package/public/screenshots/dashboard.png +0 -0
  16. package/public/screenshots/new-session-openclaw.png +0 -0
  17. package/public/screenshots/providers.png +0 -0
  18. package/public/screenshots/schedules.png +0 -0
  19. package/public/screenshots/tasks.png +0 -0
  20. package/public/vercel.svg +1 -0
  21. package/public/window.svg +1 -0
  22. package/src/app/api/agents/[id]/route.ts +30 -0
  23. package/src/app/api/agents/[id]/thread/route.ts +66 -0
  24. package/src/app/api/agents/generate/route.ts +42 -0
  25. package/src/app/api/agents/route.ts +33 -0
  26. package/src/app/api/auth/route.ts +25 -0
  27. package/src/app/api/claude-skills/route.ts +42 -0
  28. package/src/app/api/clawhub/install/route.ts +39 -0
  29. package/src/app/api/clawhub/search/route.ts +11 -0
  30. package/src/app/api/connectors/[id]/route.ts +79 -0
  31. package/src/app/api/connectors/route.ts +60 -0
  32. package/src/app/api/credentials/[id]/route.ts +14 -0
  33. package/src/app/api/credentials/route.ts +31 -0
  34. package/src/app/api/daemon/health-check/route.ts +11 -0
  35. package/src/app/api/daemon/route.ts +22 -0
  36. package/src/app/api/dirs/pick/route.ts +60 -0
  37. package/src/app/api/dirs/route.ts +29 -0
  38. package/src/app/api/documents/[id]/route.ts +47 -0
  39. package/src/app/api/documents/route.ts +93 -0
  40. package/src/app/api/files/serve/route.ts +69 -0
  41. package/src/app/api/generate/info/route.ts +12 -0
  42. package/src/app/api/generate/route.ts +106 -0
  43. package/src/app/api/ip/route.ts +6 -0
  44. package/src/app/api/knowledge/[id]/route.ts +61 -0
  45. package/src/app/api/knowledge/route.ts +48 -0
  46. package/src/app/api/knowledge/upload/route.ts +86 -0
  47. package/src/app/api/logs/route.ts +65 -0
  48. package/src/app/api/mcp-servers/[id]/route.ts +32 -0
  49. package/src/app/api/mcp-servers/[id]/test/route.ts +23 -0
  50. package/src/app/api/mcp-servers/[id]/tools/route.ts +32 -0
  51. package/src/app/api/mcp-servers/route.ts +27 -0
  52. package/src/app/api/memory/[id]/route.ts +126 -0
  53. package/src/app/api/memory/maintenance/route.ts +63 -0
  54. package/src/app/api/memory/route.ts +111 -0
  55. package/src/app/api/memory-images/[filename]/route.ts +36 -0
  56. package/src/app/api/orchestrator/run/route.ts +43 -0
  57. package/src/app/api/plugins/install/route.ts +58 -0
  58. package/src/app/api/plugins/marketplace/route.ts +33 -0
  59. package/src/app/api/plugins/route.ts +21 -0
  60. package/src/app/api/preview-server/route.ts +339 -0
  61. package/src/app/api/providers/[id]/models/route.ts +29 -0
  62. package/src/app/api/providers/[id]/route.ts +34 -0
  63. package/src/app/api/providers/configs/route.ts +7 -0
  64. package/src/app/api/providers/ollama/route.ts +30 -0
  65. package/src/app/api/providers/openclaw/health/route.ts +23 -0
  66. package/src/app/api/providers/route.ts +28 -0
  67. package/src/app/api/runs/[id]/route.ts +9 -0
  68. package/src/app/api/runs/route.ts +13 -0
  69. package/src/app/api/schedules/[id]/route.ts +28 -0
  70. package/src/app/api/schedules/[id]/run/route.ts +104 -0
  71. package/src/app/api/schedules/route.ts +78 -0
  72. package/src/app/api/secrets/[id]/route.ts +29 -0
  73. package/src/app/api/secrets/route.ts +42 -0
  74. package/src/app/api/sessions/[id]/browser/route.ts +13 -0
  75. package/src/app/api/sessions/[id]/chat/route.ts +96 -0
  76. package/src/app/api/sessions/[id]/clear/route.ts +19 -0
  77. package/src/app/api/sessions/[id]/deploy/route.ts +34 -0
  78. package/src/app/api/sessions/[id]/devserver/route.ts +69 -0
  79. package/src/app/api/sessions/[id]/mailbox/route.ts +70 -0
  80. package/src/app/api/sessions/[id]/main-loop/route.ts +94 -0
  81. package/src/app/api/sessions/[id]/messages/route.ts +9 -0
  82. package/src/app/api/sessions/[id]/retry/route.ts +28 -0
  83. package/src/app/api/sessions/[id]/route.ts +103 -0
  84. package/src/app/api/sessions/[id]/stop/route.ts +13 -0
  85. package/src/app/api/sessions/heartbeat/route.ts +26 -0
  86. package/src/app/api/sessions/route.ts +85 -0
  87. package/src/app/api/settings/route.ts +58 -0
  88. package/src/app/api/setup/check-provider/route.ts +326 -0
  89. package/src/app/api/setup/doctor/route.ts +250 -0
  90. package/src/app/api/skills/[id]/route.ts +40 -0
  91. package/src/app/api/skills/import/route.ts +69 -0
  92. package/src/app/api/skills/route.ts +28 -0
  93. package/src/app/api/tasks/[id]/route.ts +102 -0
  94. package/src/app/api/tasks/route.ts +115 -0
  95. package/src/app/api/tts/route.ts +40 -0
  96. package/src/app/api/upload/route.ts +18 -0
  97. package/src/app/api/uploads/[filename]/route.ts +59 -0
  98. package/src/app/api/usage/route.ts +35 -0
  99. package/src/app/api/version/route.ts +81 -0
  100. package/src/app/api/version/update/route.ts +95 -0
  101. package/src/app/api/webhooks/[id]/history/route.ts +13 -0
  102. package/src/app/api/webhooks/[id]/route.ts +204 -0
  103. package/src/app/api/webhooks/route.ts +37 -0
  104. package/src/app/favicon.ico +0 -0
  105. package/src/app/globals.css +370 -0
  106. package/src/app/layout.tsx +52 -0
  107. package/src/app/page.tsx +172 -0
  108. package/src/cli/index.js +1232 -0
  109. package/src/cli/index.test.js +281 -0
  110. package/src/cli/index.ts +1158 -0
  111. package/src/cli/spec.js +284 -0
  112. package/src/components/agents/agent-card.tsx +219 -0
  113. package/src/components/agents/agent-chat-list.tsx +165 -0
  114. package/src/components/agents/agent-list.tsx +110 -0
  115. package/src/components/agents/agent-sheet.tsx +1220 -0
  116. package/src/components/auth/access-key-gate.tsx +248 -0
  117. package/src/components/auth/setup-wizard.tsx +940 -0
  118. package/src/components/auth/user-picker.tsx +88 -0
  119. package/src/components/chat/chat-area.tsx +406 -0
  120. package/src/components/chat/chat-header.tsx +491 -0
  121. package/src/components/chat/chat-tool-toggles.tsx +161 -0
  122. package/src/components/chat/code-block.tsx +146 -0
  123. package/src/components/chat/dev-server-bar.tsx +39 -0
  124. package/src/components/chat/message-bubble.tsx +486 -0
  125. package/src/components/chat/message-list.tsx +299 -0
  126. package/src/components/chat/session-debug-panel.tsx +196 -0
  127. package/src/components/chat/streaming-bubble.tsx +85 -0
  128. package/src/components/chat/thinking-indicator.tsx +26 -0
  129. package/src/components/chat/tool-call-bubble.tsx +438 -0
  130. package/src/components/chat/tool-request-banner.tsx +103 -0
  131. package/src/components/connectors/connector-list.tsx +196 -0
  132. package/src/components/connectors/connector-sheet.tsx +804 -0
  133. package/src/components/input/chat-input.tsx +235 -0
  134. package/src/components/knowledge/knowledge-list.tsx +206 -0
  135. package/src/components/knowledge/knowledge-sheet.tsx +316 -0
  136. package/src/components/layout/app-layout.tsx +1016 -0
  137. package/src/components/layout/daemon-indicator.tsx +56 -0
  138. package/src/components/layout/mobile-header.tsx +31 -0
  139. package/src/components/layout/network-banner.tsx +17 -0
  140. package/src/components/layout/update-banner.tsx +130 -0
  141. package/src/components/logs/log-list.tsx +358 -0
  142. package/src/components/mcp-servers/mcp-server-list.tsx +122 -0
  143. package/src/components/mcp-servers/mcp-server-sheet.tsx +243 -0
  144. package/src/components/memory/memory-card.tsx +63 -0
  145. package/src/components/memory/memory-detail.tsx +339 -0
  146. package/src/components/memory/memory-list.tsx +198 -0
  147. package/src/components/memory/memory-sheet.tsx +70 -0
  148. package/src/components/plugins/plugin-list.tsx +60 -0
  149. package/src/components/plugins/plugin-sheet.tsx +311 -0
  150. package/src/components/providers/provider-list.tsx +96 -0
  151. package/src/components/providers/provider-sheet.tsx +542 -0
  152. package/src/components/runs/run-list.tsx +231 -0
  153. package/src/components/schedules/schedule-card.tsx +63 -0
  154. package/src/components/schedules/schedule-list.tsx +76 -0
  155. package/src/components/schedules/schedule-sheet.tsx +336 -0
  156. package/src/components/secrets/secret-sheet.tsx +180 -0
  157. package/src/components/secrets/secrets-list.tsx +91 -0
  158. package/src/components/sessions/new-session-sheet.tsx +478 -0
  159. package/src/components/sessions/session-card.tsx +144 -0
  160. package/src/components/sessions/session-list.tsx +202 -0
  161. package/src/components/shared/ai-gen-block.tsx +77 -0
  162. package/src/components/shared/avatar.tsx +48 -0
  163. package/src/components/shared/bottom-sheet.tsx +30 -0
  164. package/src/components/shared/confirm-dialog.tsx +47 -0
  165. package/src/components/shared/connector-platform-icon.tsx +113 -0
  166. package/src/components/shared/dir-browser.tsx +285 -0
  167. package/src/components/shared/dropdown.tsx +55 -0
  168. package/src/components/shared/icon-button.tsx +25 -0
  169. package/src/components/shared/settings/plugin-manager.tsx +207 -0
  170. package/src/components/shared/settings/section-capability-policy.tsx +93 -0
  171. package/src/components/shared/settings/section-embedding.tsx +99 -0
  172. package/src/components/shared/settings/section-heartbeat.tsx +168 -0
  173. package/src/components/shared/settings/section-memory.tsx +77 -0
  174. package/src/components/shared/settings/section-orchestrator.tsx +108 -0
  175. package/src/components/shared/settings/section-providers.tsx +181 -0
  176. package/src/components/shared/settings/section-runtime-loop.tsx +183 -0
  177. package/src/components/shared/settings/section-secrets.tsx +132 -0
  178. package/src/components/shared/settings/section-user-preferences.tsx +24 -0
  179. package/src/components/shared/settings/section-voice.tsx +53 -0
  180. package/src/components/shared/settings/settings-sheet.tsx +88 -0
  181. package/src/components/shared/settings/types.ts +7 -0
  182. package/src/components/shared/settings/utils.ts +13 -0
  183. package/src/components/shared/settings-sheet.tsx +1 -0
  184. package/src/components/shared/skeleton.tsx +19 -0
  185. package/src/components/shared/usage-badge.tsx +28 -0
  186. package/src/components/skills/clawhub-browser.tsx +225 -0
  187. package/src/components/skills/skill-list.tsx +70 -0
  188. package/src/components/skills/skill-sheet.tsx +254 -0
  189. package/src/components/tasks/task-board.tsx +96 -0
  190. package/src/components/tasks/task-card.tsx +179 -0
  191. package/src/components/tasks/task-column.tsx +73 -0
  192. package/src/components/tasks/task-list.tsx +118 -0
  193. package/src/components/tasks/task-sheet.tsx +415 -0
  194. package/src/components/ui/avatar.tsx +109 -0
  195. package/src/components/ui/badge.tsx +48 -0
  196. package/src/components/ui/button.tsx +64 -0
  197. package/src/components/ui/card.tsx +92 -0
  198. package/src/components/ui/dialog.tsx +158 -0
  199. package/src/components/ui/dropdown-menu.tsx +257 -0
  200. package/src/components/ui/input.tsx +21 -0
  201. package/src/components/ui/scroll-area.tsx +58 -0
  202. package/src/components/ui/select.tsx +190 -0
  203. package/src/components/ui/separator.tsx +28 -0
  204. package/src/components/ui/sheet.tsx +143 -0
  205. package/src/components/ui/sonner.tsx +22 -0
  206. package/src/components/ui/textarea.tsx +18 -0
  207. package/src/components/ui/tooltip.tsx +56 -0
  208. package/src/components/usage/usage-list.tsx +105 -0
  209. package/src/components/webhooks/webhook-list.tsx +166 -0
  210. package/src/components/webhooks/webhook-sheet.tsx +402 -0
  211. package/src/hooks/use-auto-resize.ts +20 -0
  212. package/src/hooks/use-media-query.ts +21 -0
  213. package/src/hooks/use-speech-recognition.ts +83 -0
  214. package/src/instrumentation.ts +8 -0
  215. package/src/lib/agents.ts +13 -0
  216. package/src/lib/api-client.ts +100 -0
  217. package/src/lib/chat.ts +60 -0
  218. package/src/lib/memory.ts +42 -0
  219. package/src/lib/openclaw-endpoint.test.ts +48 -0
  220. package/src/lib/openclaw-endpoint.ts +67 -0
  221. package/src/lib/provider-config.ts +13 -0
  222. package/src/lib/providers/anthropic.ts +135 -0
  223. package/src/lib/providers/claude-cli.ts +202 -0
  224. package/src/lib/providers/codex-cli.ts +260 -0
  225. package/src/lib/providers/index.ts +351 -0
  226. package/src/lib/providers/ollama.ts +131 -0
  227. package/src/lib/providers/openai.ts +164 -0
  228. package/src/lib/providers/openclaw.ts +330 -0
  229. package/src/lib/providers/opencode-cli.ts +164 -0
  230. package/src/lib/runtime-loop.ts +15 -0
  231. package/src/lib/schedule-dedupe.test.ts +84 -0
  232. package/src/lib/schedule-dedupe.ts +174 -0
  233. package/src/lib/schedule-name.ts +62 -0
  234. package/src/lib/schedules.ts +16 -0
  235. package/src/lib/server/agent-registry.ts +70 -0
  236. package/src/lib/server/api-routes.test.ts +362 -0
  237. package/src/lib/server/autonomy-contract.ts +200 -0
  238. package/src/lib/server/build-llm.ts +155 -0
  239. package/src/lib/server/capability-router.test.ts +21 -0
  240. package/src/lib/server/capability-router.ts +172 -0
  241. package/src/lib/server/chat-execution.ts +894 -0
  242. package/src/lib/server/clawhub-client.test.ts +161 -0
  243. package/src/lib/server/clawhub-client.ts +26 -0
  244. package/src/lib/server/connectors/connector-routing.test.ts +243 -0
  245. package/src/lib/server/connectors/discord.ts +116 -0
  246. package/src/lib/server/connectors/googlechat.ts +66 -0
  247. package/src/lib/server/connectors/manager.ts +559 -0
  248. package/src/lib/server/connectors/matrix.ts +78 -0
  249. package/src/lib/server/connectors/media.ts +149 -0
  250. package/src/lib/server/connectors/openclaw.test.ts +375 -0
  251. package/src/lib/server/connectors/openclaw.ts +1132 -0
  252. package/src/lib/server/connectors/signal.ts +183 -0
  253. package/src/lib/server/connectors/slack.ts +258 -0
  254. package/src/lib/server/connectors/teams.ts +94 -0
  255. package/src/lib/server/connectors/telegram.ts +221 -0
  256. package/src/lib/server/connectors/types.ts +62 -0
  257. package/src/lib/server/connectors/whatsapp.ts +349 -0
  258. package/src/lib/server/context-manager.ts +232 -0
  259. package/src/lib/server/cost.ts +31 -0
  260. package/src/lib/server/daemon-state.ts +354 -0
  261. package/src/lib/server/data-dir.ts +3 -0
  262. package/src/lib/server/embeddings.ts +111 -0
  263. package/src/lib/server/execution-log.ts +257 -0
  264. package/src/lib/server/gateway/protocol.test.ts +54 -0
  265. package/src/lib/server/gateway/protocol.ts +114 -0
  266. package/src/lib/server/heartbeat-service.ts +366 -0
  267. package/src/lib/server/knowledge-db.test.ts +441 -0
  268. package/src/lib/server/logger.ts +47 -0
  269. package/src/lib/server/main-agent-loop.ts +1017 -0
  270. package/src/lib/server/mcp-client.test.ts +342 -0
  271. package/src/lib/server/mcp-client.ts +130 -0
  272. package/src/lib/server/memory-db.ts +1078 -0
  273. package/src/lib/server/memory-graph.test.ts +153 -0
  274. package/src/lib/server/memory-graph.ts +138 -0
  275. package/src/lib/server/openclaw-health.ts +245 -0
  276. package/src/lib/server/orchestrator-lg.ts +431 -0
  277. package/src/lib/server/orchestrator.ts +364 -0
  278. package/src/lib/server/playwright-proxy.mjs +70 -0
  279. package/src/lib/server/plugins.ts +229 -0
  280. package/src/lib/server/process-manager.ts +327 -0
  281. package/src/lib/server/provider-health.ts +113 -0
  282. package/src/lib/server/queue.ts +859 -0
  283. package/src/lib/server/runtime-settings.ts +119 -0
  284. package/src/lib/server/scheduler.ts +196 -0
  285. package/src/lib/server/session-mailbox.ts +129 -0
  286. package/src/lib/server/session-run-manager.ts +512 -0
  287. package/src/lib/server/session-tools/connector.ts +124 -0
  288. package/src/lib/server/session-tools/context-mgmt.ts +103 -0
  289. package/src/lib/server/session-tools/context.ts +114 -0
  290. package/src/lib/server/session-tools/crud.ts +673 -0
  291. package/src/lib/server/session-tools/delegate.ts +708 -0
  292. package/src/lib/server/session-tools/file.ts +264 -0
  293. package/src/lib/server/session-tools/index.ts +164 -0
  294. package/src/lib/server/session-tools/memory.ts +230 -0
  295. package/src/lib/server/session-tools/session-info.ts +422 -0
  296. package/src/lib/server/session-tools/session-tools-wiring.test.ts +166 -0
  297. package/src/lib/server/session-tools/shell.ts +171 -0
  298. package/src/lib/server/session-tools/web.ts +408 -0
  299. package/src/lib/server/session-tools.ts +9 -0
  300. package/src/lib/server/skills-normalize.ts +130 -0
  301. package/src/lib/server/storage-mcp.test.ts +161 -0
  302. package/src/lib/server/storage.ts +670 -0
  303. package/src/lib/server/stream-agent-chat.ts +571 -0
  304. package/src/lib/server/task-reports.ts +122 -0
  305. package/src/lib/server/task-result.ts +161 -0
  306. package/src/lib/server/task-validation.test.ts +27 -0
  307. package/src/lib/server/task-validation.ts +90 -0
  308. package/src/lib/server/tool-capability-policy.test.ts +58 -0
  309. package/src/lib/server/tool-capability-policy.ts +262 -0
  310. package/src/lib/sessions.ts +68 -0
  311. package/src/lib/tasks.ts +20 -0
  312. package/src/lib/tts.ts +42 -0
  313. package/src/lib/upload.ts +10 -0
  314. package/src/lib/utils.ts +6 -0
  315. package/src/proxy.ts +43 -0
  316. package/src/stores/use-app-store.ts +468 -0
  317. package/src/stores/use-chat-store.ts +323 -0
  318. package/src/types/index.ts +621 -0
  319. package/tsconfig.json +34 -0
@@ -0,0 +1,221 @@
1
+ import { Bot, InputFile } from 'grammy'
2
+ import fs from 'fs'
3
+ import path from 'path'
4
+ import type { Connector } from '@/types'
5
+ import type { PlatformConnector, ConnectorInstance, InboundMessage, InboundMediaType } from './types'
6
+ import { downloadInboundMediaToUpload, inferInboundMediaType, mimeFromPath, isImageMime } from './media'
7
+ import { isNoMessage } from './manager'
8
+
9
+ const telegram: PlatformConnector = {
10
+ async start(connector, botToken, onMessage): Promise<ConnectorInstance> {
11
+ const bot = new Bot(botToken)
12
+
13
+ // Optional: restrict to specific chat IDs
14
+ const allowedChats = connector.config.chatIds
15
+ ? connector.config.chatIds.split(',').map((s) => s.trim()).filter(Boolean)
16
+ : null
17
+
18
+ // Log all errors
19
+ bot.catch((err) => {
20
+ console.error(`[telegram] Bot error:`, err.message || err)
21
+ })
22
+
23
+ // Delete any existing webhook so long polling works
24
+ await bot.api.deleteWebhook().catch((err) => {
25
+ console.error('[telegram] Failed to delete webhook:', err.message)
26
+ })
27
+
28
+ // Log all incoming updates for debugging
29
+ bot.use(async (ctx, next) => {
30
+ console.log(`[telegram] Update received: chat=${ctx.chat?.id}, from=${ctx.from?.first_name}, hasText=${!!ctx.message?.text}`)
31
+ await next()
32
+ })
33
+
34
+ // Handle /start command (required for new conversations)
35
+ bot.command('start', async (ctx) => {
36
+ console.log(`[telegram] /start from ${ctx.from?.first_name} (chat=${ctx.chat.id})`)
37
+ await ctx.reply('Hello! I\'m ready to chat. Send me a message.')
38
+ })
39
+
40
+ bot.on('message', async (ctx) => {
41
+ if (!ctx.message || !ctx.from || !ctx.chat) return
42
+ const chatId = String(ctx.chat.id)
43
+ const raw = ctx.message as any
44
+ const text = raw.text || raw.caption || ''
45
+ console.log(`[telegram] Message from ${ctx.from.first_name} (chat=${chatId}): ${String(text).slice(0, 80)}`)
46
+
47
+ // Filter by allowed chats if configured
48
+ if (allowedChats && !allowedChats.includes(chatId)) {
49
+ console.log(`[telegram] Skipping — chat ${chatId} not in allowed list: ${allowedChats.join(',')}`)
50
+ return
51
+ }
52
+
53
+ const media: NonNullable<InboundMessage['media']> = []
54
+ const mediaCandidates: Array<{ fileId: string; mimeType?: string; fileName?: string; type: InboundMediaType }> = []
55
+
56
+ if (Array.isArray(raw.photo) && raw.photo.length > 0) {
57
+ const largest = raw.photo[raw.photo.length - 1]
58
+ if (largest?.file_id) mediaCandidates.push({ fileId: largest.file_id, type: 'image' })
59
+ }
60
+ if (raw.video?.file_id) {
61
+ mediaCandidates.push({
62
+ fileId: raw.video.file_id,
63
+ type: 'video',
64
+ mimeType: raw.video.mime_type || undefined,
65
+ fileName: raw.video.file_name || undefined,
66
+ })
67
+ }
68
+ if (raw.audio?.file_id) {
69
+ mediaCandidates.push({
70
+ fileId: raw.audio.file_id,
71
+ type: 'audio',
72
+ mimeType: raw.audio.mime_type || undefined,
73
+ fileName: raw.audio.file_name || undefined,
74
+ })
75
+ }
76
+ if (raw.voice?.file_id) {
77
+ mediaCandidates.push({
78
+ fileId: raw.voice.file_id,
79
+ type: 'audio',
80
+ mimeType: raw.voice.mime_type || 'audio/ogg',
81
+ fileName: 'voice.ogg',
82
+ })
83
+ }
84
+ if (raw.document?.file_id) {
85
+ mediaCandidates.push({
86
+ fileId: raw.document.file_id,
87
+ type: inferInboundMediaType(raw.document.mime_type || undefined, raw.document.file_name || undefined, 'document'),
88
+ mimeType: raw.document.mime_type || undefined,
89
+ fileName: raw.document.file_name || undefined,
90
+ })
91
+ }
92
+ if (raw.animation?.file_id) {
93
+ mediaCandidates.push({
94
+ fileId: raw.animation.file_id,
95
+ type: 'video',
96
+ mimeType: raw.animation.mime_type || undefined,
97
+ fileName: raw.animation.file_name || undefined,
98
+ })
99
+ }
100
+
101
+ for (const m of mediaCandidates) {
102
+ try {
103
+ const file = await bot.api.getFile(m.fileId)
104
+ if (!file?.file_path) throw new Error('Missing Telegram file_path')
105
+ const sourceUrl = `https://api.telegram.org/file/bot${botToken}/${file.file_path}`
106
+ const stored = await downloadInboundMediaToUpload({
107
+ connectorId: connector.id,
108
+ mediaType: m.type,
109
+ url: sourceUrl,
110
+ fileName: m.fileName,
111
+ mimeType: m.mimeType,
112
+ })
113
+ if (stored) media.push(stored)
114
+ } catch (err: any) {
115
+ console.warn(`[telegram] Failed to fetch media ${m.fileId}:`, err?.message || String(err))
116
+ media.push({
117
+ type: m.type,
118
+ fileName: m.fileName,
119
+ mimeType: m.mimeType,
120
+ })
121
+ }
122
+ }
123
+
124
+ const inbound: InboundMessage = {
125
+ platform: 'telegram',
126
+ channelId: chatId,
127
+ channelName: ctx.chat.type === 'private'
128
+ ? `DM:${ctx.from.first_name}`
129
+ : ('title' in ctx.chat ? ctx.chat.title : chatId),
130
+ senderId: String(ctx.from.id),
131
+ senderName: ctx.from.first_name + (ctx.from.last_name ? ` ${ctx.from.last_name}` : ''),
132
+ text: text || (media.length > 0 ? '(media message)' : ''),
133
+ imageUrl: media.find((m) => m.type === 'image')?.url,
134
+ media,
135
+ }
136
+
137
+ try {
138
+ await ctx.api.sendChatAction(ctx.chat.id, 'typing')
139
+ const response = await onMessage(inbound)
140
+
141
+ if (isNoMessage(response)) return
142
+
143
+ // Telegram has a 4096 char limit
144
+ if (response.length <= 4096) {
145
+ await ctx.reply(response)
146
+ } else {
147
+ const chunks = response.match(/[\s\S]{1,4090}/g) || [response]
148
+ for (const chunk of chunks) {
149
+ await ctx.api.sendMessage(ctx.chat.id, chunk)
150
+ }
151
+ }
152
+ } catch (err: any) {
153
+ console.error(`[telegram] Error handling message:`, err.message)
154
+ try {
155
+ await ctx.reply('Sorry, I encountered an error processing your message.')
156
+ } catch { /* ignore */ }
157
+ }
158
+ })
159
+
160
+ // Start polling — not awaited (runs in background)
161
+ bot.start({
162
+ allowed_updates: ['message', 'edited_message'],
163
+ onStart: (botInfo) => {
164
+ console.log(`[telegram] Bot started as @${botInfo.username} — polling for updates`)
165
+ },
166
+ }).catch((err) => {
167
+ console.error(`[telegram] Polling stopped with error:`, err.message || err)
168
+ })
169
+
170
+ return {
171
+ connector,
172
+ async sendMessage(channelId, text, options) {
173
+ const chatId = channelId
174
+ const caption = options?.caption || text || undefined
175
+
176
+ // Local file
177
+ if (options?.mediaPath) {
178
+ if (!fs.existsSync(options.mediaPath)) throw new Error(`File not found: ${options.mediaPath}`)
179
+ const mime = options.mimeType || mimeFromPath(options.mediaPath)
180
+ const inputFile = new InputFile(options.mediaPath, options.fileName || path.basename(options.mediaPath))
181
+ if (isImageMime(mime)) {
182
+ const msg = await bot.api.sendPhoto(chatId, inputFile, { caption })
183
+ return { messageId: String(msg.message_id) }
184
+ } else {
185
+ const msg = await bot.api.sendDocument(chatId, inputFile, { caption })
186
+ return { messageId: String(msg.message_id) }
187
+ }
188
+ }
189
+ // URL-based image
190
+ if (options?.imageUrl) {
191
+ const msg = await bot.api.sendPhoto(chatId, options.imageUrl, { caption })
192
+ return { messageId: String(msg.message_id) }
193
+ }
194
+ // URL-based file
195
+ if (options?.fileUrl) {
196
+ const msg = await bot.api.sendDocument(chatId, options.fileUrl, { caption })
197
+ return { messageId: String(msg.message_id) }
198
+ }
199
+ // Text only
200
+ const payload = text || caption || ''
201
+ if (payload.length <= 4096) {
202
+ const msg = await bot.api.sendMessage(chatId, payload)
203
+ return { messageId: String(msg.message_id) }
204
+ }
205
+ const chunks = payload.match(/[\s\S]{1,4090}/g) || [payload]
206
+ let lastId: string | undefined
207
+ for (const chunk of chunks) {
208
+ const msg = await bot.api.sendMessage(chatId, chunk)
209
+ lastId = String(msg.message_id)
210
+ }
211
+ return { messageId: lastId }
212
+ },
213
+ async stop() {
214
+ await bot.stop()
215
+ console.log(`[telegram] Bot stopped`)
216
+ },
217
+ }
218
+ },
219
+ }
220
+
221
+ export default telegram
@@ -0,0 +1,62 @@
1
+ import type { Connector } from '@/types'
2
+
3
+ export type InboundMediaType = 'image' | 'video' | 'audio' | 'document' | 'file'
4
+
5
+ export interface InboundMedia {
6
+ type: InboundMediaType
7
+ fileName?: string
8
+ mimeType?: string
9
+ sizeBytes?: number
10
+ /** Public URL when available (typically /api/uploads/...) */
11
+ url?: string
12
+ /** Absolute local path where media was persisted, if stored */
13
+ localPath?: string
14
+ }
15
+
16
+ /** Inbound message from a chat platform */
17
+ export interface InboundMessage {
18
+ platform: string
19
+ channelId: string // platform-specific channel/chat ID
20
+ channelName?: string // human-readable name
21
+ senderId: string // platform-specific user ID
22
+ senderName: string // display name
23
+ text: string
24
+ imageUrl?: string
25
+ media?: InboundMedia[]
26
+ replyToMessageId?: string
27
+ }
28
+
29
+ /** A running connector instance */
30
+ export interface ConnectorInstance {
31
+ connector: Connector
32
+ stop: () => Promise<void>
33
+ /** Optional outbound send support for proactive agent notifications */
34
+ sendMessage?: (
35
+ channelId: string,
36
+ text: string,
37
+ options?: {
38
+ imageUrl?: string
39
+ fileUrl?: string
40
+ /** Absolute local file path (e.g. screenshot saved to disk) */
41
+ mediaPath?: string
42
+ mimeType?: string
43
+ fileName?: string
44
+ caption?: string
45
+ },
46
+ ) => Promise<{ messageId?: string } | void>
47
+ /** Current QR code data URL (WhatsApp only, null when paired) */
48
+ qrDataUrl?: string | null
49
+ /** Whether the connector has successfully authenticated (WhatsApp only) */
50
+ authenticated?: boolean
51
+ /** Whether the connector has existing saved credentials (WhatsApp only) */
52
+ hasCredentials?: boolean
53
+ }
54
+
55
+ /** Platform-specific connector implementation */
56
+ export interface PlatformConnector {
57
+ start(
58
+ connector: Connector,
59
+ botToken: string,
60
+ onMessage: (msg: InboundMessage) => Promise<string>,
61
+ ): Promise<ConnectorInstance>
62
+ }
@@ -0,0 +1,349 @@
1
+ import makeWASocket, {
2
+ useMultiFileAuthState,
3
+ DisconnectReason,
4
+ fetchLatestBaileysVersion,
5
+ normalizeMessageContent,
6
+ downloadMediaMessage,
7
+ } from '@whiskeysockets/baileys'
8
+ import QRCode from 'qrcode'
9
+ import path from 'path'
10
+ import fs from 'fs'
11
+ import type { Connector } from '@/types'
12
+ import type { PlatformConnector, ConnectorInstance, InboundMessage } from './types'
13
+ import { saveInboundMediaBuffer, mimeFromPath, isImageMime } from './media'
14
+ import { isNoMessage } from './manager'
15
+
16
+ import { DATA_DIR } from '../data-dir'
17
+
18
+ const AUTH_DIR = path.join(DATA_DIR, 'whatsapp-auth')
19
+
20
+ /** Normalize a phone number for JID matching — strip leading 0 or + */
21
+ function normalizeNumber(num: string): string {
22
+ let n = num.replace(/[\s\-()]/g, '')
23
+ // UK local: 07xxx → 447xxx
24
+ if (n.startsWith('0') && n.length >= 10) {
25
+ n = '44' + n.slice(1)
26
+ }
27
+ // Strip leading +
28
+ if (n.startsWith('+')) n = n.slice(1)
29
+ return n
30
+ }
31
+
32
+ /** Check if auth directory has saved credentials */
33
+ function hasStoredCreds(authDir: string): boolean {
34
+ try {
35
+ return fs.existsSync(path.join(authDir, 'creds.json'))
36
+ } catch { return false }
37
+ }
38
+
39
+ /** Clear auth directory to force fresh QR pairing */
40
+ export function clearAuthDir(connectorId: string): void {
41
+ const authDir = path.join(AUTH_DIR, connectorId)
42
+ if (fs.existsSync(authDir)) {
43
+ fs.rmSync(authDir, { recursive: true, force: true })
44
+ console.log(`[whatsapp] Cleared auth state for connector ${connectorId}`)
45
+ }
46
+ }
47
+
48
+ const whatsapp: PlatformConnector = {
49
+ async start(connector, _botToken, onMessage): Promise<ConnectorInstance> {
50
+ // Each connector gets its own auth directory
51
+ const authDir = path.join(AUTH_DIR, connector.id)
52
+ if (!fs.existsSync(authDir)) fs.mkdirSync(authDir, { recursive: true })
53
+
54
+ const { state, saveCreds } = await useMultiFileAuthState(authDir)
55
+ const { version } = await fetchLatestBaileysVersion()
56
+
57
+ let sock: ReturnType<typeof makeWASocket> | null = null
58
+ let stopped = false
59
+ let socketGen = 0 // Track socket generation to ignore stale events
60
+
61
+ const instance: ConnectorInstance = {
62
+ connector,
63
+ qrDataUrl: null,
64
+ authenticated: false,
65
+ hasCredentials: hasStoredCreds(authDir),
66
+ async sendMessage(channelId, text, options) {
67
+ if (!sock) throw new Error('WhatsApp connector is not connected')
68
+ // Local file path takes priority
69
+ if (options?.mediaPath) {
70
+ if (!fs.existsSync(options.mediaPath)) throw new Error(`File not found: ${options.mediaPath}`)
71
+ const buf = fs.readFileSync(options.mediaPath)
72
+ const mime = options.mimeType || mimeFromPath(options.mediaPath)
73
+ const caption = options.caption || text || undefined
74
+ const fName = options.fileName || path.basename(options.mediaPath)
75
+ let sent
76
+ if (isImageMime(mime)) {
77
+ sent = await sock.sendMessage(channelId, { image: buf, caption, mimetype: mime })
78
+ } else {
79
+ sent = await sock.sendMessage(channelId, { document: buf, fileName: fName, mimetype: mime, caption })
80
+ }
81
+ if (sent?.key?.id) sentMessageIds.add(sent.key.id)
82
+ return { messageId: sent?.key?.id || undefined }
83
+ }
84
+ if (options?.imageUrl) {
85
+ const sent = await sock.sendMessage(channelId, {
86
+ image: { url: options.imageUrl },
87
+ caption: options.caption || text || undefined,
88
+ })
89
+ if (sent?.key?.id) sentMessageIds.add(sent.key.id)
90
+ return { messageId: sent?.key?.id || undefined }
91
+ }
92
+ if (options?.fileUrl) {
93
+ const sent = await sock.sendMessage(channelId, {
94
+ document: { url: options.fileUrl },
95
+ fileName: options.fileName || 'attachment',
96
+ mimetype: options.mimeType || 'application/octet-stream',
97
+ caption: options.caption || text || undefined,
98
+ })
99
+ if (sent?.key?.id) sentMessageIds.add(sent.key.id)
100
+ return { messageId: sent?.key?.id || undefined }
101
+ }
102
+
103
+ const payload = text || options?.caption || ''
104
+ const chunks = payload.length <= 4096 ? [payload] : (payload.match(/[\s\S]{1,4000}/g) || [payload])
105
+ let lastMessageId: string | undefined
106
+ for (const chunk of chunks) {
107
+ const sent = await sock.sendMessage(channelId, { text: chunk })
108
+ if (sent?.key?.id) {
109
+ lastMessageId = sent.key.id
110
+ sentMessageIds.add(sent.key.id)
111
+ }
112
+ }
113
+ return { messageId: lastMessageId }
114
+ },
115
+ async stop() {
116
+ stopped = true
117
+ try { sock?.end(undefined) } catch { /* ignore */ }
118
+ sock = null
119
+ console.log(`[whatsapp] Stopped connector: ${connector.name}`)
120
+ },
121
+ }
122
+
123
+ // Normalize allowed JIDs for matching
124
+ const allowedJids = connector.config.allowedJids
125
+ ? connector.config.allowedJids.split(',').map((s) => normalizeNumber(s.trim())).filter(Boolean)
126
+ : null
127
+
128
+ // Track message IDs sent by the bot to avoid infinite loops in self-chat
129
+ const sentMessageIds = new Set<string>()
130
+
131
+ if (allowedJids) {
132
+ console.log(`[whatsapp] Allowed JIDs (normalized): ${allowedJids.join(', ')}`)
133
+ }
134
+
135
+ const startSocket = () => {
136
+ // Close previous socket to prevent stale event handlers
137
+ if (sock) {
138
+ try { sock.ev.removeAllListeners('connection.update') } catch { /* ignore */ }
139
+ try { sock.ev.removeAllListeners('messages.upsert') } catch { /* ignore */ }
140
+ try { sock.ev.removeAllListeners('creds.update') } catch { /* ignore */ }
141
+ try { sock.end(undefined) } catch { /* ignore */ }
142
+ sock = null
143
+ }
144
+
145
+ const gen = ++socketGen // Capture generation for stale detection
146
+ console.log(`[whatsapp] Starting socket gen=${gen} for ${connector.name} (hasCreds=${instance.hasCredentials})`)
147
+
148
+ sock = makeWASocket({
149
+ version,
150
+ auth: state,
151
+ browser: ['SwarmClaw', 'Chrome', '120.0'],
152
+ })
153
+
154
+ sock.ev.on('creds.update', () => {
155
+ saveCreds()
156
+ // Update hasCredentials after first cred save
157
+ instance.hasCredentials = true
158
+ })
159
+
160
+ sock.ev.on('connection.update', async (update) => {
161
+ if (gen !== socketGen) return // Ignore events from stale sockets
162
+
163
+ const { connection, lastDisconnect, qr } = update
164
+ console.log(`[whatsapp] Connection update gen=${gen}: connection=${connection}, hasQR=${!!qr}`)
165
+
166
+ if (qr) {
167
+ console.log(`[whatsapp] QR code generated for ${connector.name}`)
168
+ try {
169
+ instance.qrDataUrl = await QRCode.toDataURL(qr, {
170
+ width: 280,
171
+ margin: 2,
172
+ color: { dark: '#000000', light: '#ffffff' },
173
+ })
174
+ } catch (err) {
175
+ console.error('[whatsapp] Failed to generate QR data URL:', err)
176
+ }
177
+ }
178
+ if (connection === 'close') {
179
+ instance.qrDataUrl = null
180
+ const reason = (lastDisconnect?.error as any)?.output?.statusCode
181
+ console.log(`[whatsapp] Connection closed: reason=${reason} stopped=${stopped}`)
182
+
183
+ if (reason === DisconnectReason.loggedOut) {
184
+ // Session invalidated — clear auth and restart to get fresh QR
185
+ console.log(`[whatsapp] Logged out — clearing auth and restarting for fresh QR`)
186
+ instance.authenticated = false
187
+ instance.hasCredentials = false
188
+ clearAuthDir(connector.id)
189
+ if (!stopped) {
190
+ // Recreate auth dir and state for fresh start
191
+ fs.mkdirSync(authDir, { recursive: true })
192
+ setTimeout(startSocket, 1000)
193
+ }
194
+ } else if (reason === 440) {
195
+ // Conflict — another session replaced this one. Do NOT reconnect
196
+ // (reconnecting would create a ping-pong loop with the other session)
197
+ console.log(`[whatsapp] Session conflict (replaced by another connection) — stopping`)
198
+ instance.authenticated = false
199
+ } else if (!stopped) {
200
+ console.log(`[whatsapp] Reconnecting in 3s...`)
201
+ setTimeout(startSocket, 3000)
202
+ } else {
203
+ console.log(`[whatsapp] Disconnected permanently`)
204
+ }
205
+ } else if (connection === 'open') {
206
+ instance.authenticated = true
207
+ instance.hasCredentials = true
208
+ instance.qrDataUrl = null
209
+ console.log(`[whatsapp] Connected as ${sock?.user?.id}`)
210
+ }
211
+ })
212
+
213
+ sock.ev.on('messages.upsert', async (upsert) => {
214
+ const { messages, type } = upsert
215
+ console.log(`[whatsapp] messages.upsert gen=${gen}: type=${type}, count=${messages.length}`)
216
+
217
+ if (gen !== socketGen) {
218
+ console.log(`[whatsapp] Ignoring stale socket event (gen=${gen}, current=${socketGen})`)
219
+ return
220
+ }
221
+ if (type !== 'notify') {
222
+ console.log(`[whatsapp] Ignoring non-notify upsert type: ${type}`)
223
+ return
224
+ }
225
+
226
+ for (const msg of messages) {
227
+ console.log(`[whatsapp] Processing message: fromMe=${msg.key.fromMe}, jid=${msg.key.remoteJid}, hasConversation=${!!msg.message?.conversation}, hasExtended=${!!msg.message?.extendedTextMessage}`)
228
+
229
+ if (msg.key.remoteJid === 'status@broadcast') continue
230
+
231
+ // Skip messages sent by the bot itself (tracked by ID to prevent infinite loops)
232
+ if (msg.key.id && sentMessageIds.has(msg.key.id)) {
233
+ console.log(`[whatsapp] Skipping own bot reply: ${msg.key.id}`)
234
+ sentMessageIds.delete(msg.key.id) // Clean up
235
+ continue
236
+ }
237
+
238
+ // Handle self-chat (same number messaging itself for testing)
239
+ // Self-chat JID can be phone format (447xxx@s.whatsapp.net) or LID format (185xxx@lid)
240
+ const remoteNum = msg.key.remoteJid?.split('@')[0] || ''
241
+ const remoteHost = msg.key.remoteJid?.split('@')[1] || ''
242
+ const myPhoneNum = sock?.user?.id?.split(':')[0] || ''
243
+ const myLid = sock?.user?.lid?.split(':')[0] || ''
244
+ const isSelfChat = (remoteNum === myPhoneNum) || (remoteHost === 'lid' && (myLid ? remoteNum === myLid : true))
245
+ console.log(`[whatsapp] Self-chat check: remote=${remoteNum}@${remoteHost}, myPhone=${myPhoneNum}, myLid=${myLid}, isSelf=${isSelfChat}`)
246
+ if (msg.key.fromMe && !isSelfChat) continue
247
+
248
+ const jid = msg.key.remoteJid || ''
249
+
250
+ // Match allowed JIDs using normalized numbers
251
+ // Self-chat always passes the filter (it's the bot's own account)
252
+ if (allowedJids && !isSelfChat) {
253
+ const jidNumber = jid.split('@')[0]
254
+ const matched = allowedJids.some((n) => jidNumber.includes(n) || n.includes(jidNumber))
255
+ console.log(`[whatsapp] JID filter: jidNumber=${jidNumber}, allowedJids=${allowedJids.join(',')}, matched=${matched}`)
256
+ if (!matched) {
257
+ console.log(`[whatsapp] Skipping message from non-allowed JID: ${jid}`)
258
+ continue
259
+ }
260
+ }
261
+
262
+ const content: any = normalizeMessageContent(msg.message as any) || msg.message || {}
263
+ const text = content?.conversation
264
+ || content?.extendedTextMessage?.text
265
+ || content?.imageMessage?.caption
266
+ || content?.videoMessage?.caption
267
+ || content?.documentMessage?.caption
268
+ || ''
269
+
270
+ const media: NonNullable<InboundMessage['media']> = []
271
+ const mediaCandidate:
272
+ | { kind: 'image' | 'video' | 'audio' | 'document' | 'file'; payload: any }
273
+ | null =
274
+ content?.imageMessage
275
+ ? { kind: 'image', payload: content.imageMessage }
276
+ : content?.videoMessage
277
+ ? { kind: 'video', payload: content.videoMessage }
278
+ : content?.audioMessage
279
+ ? { kind: 'audio', payload: content.audioMessage }
280
+ : content?.documentMessage
281
+ ? { kind: 'document', payload: content.documentMessage }
282
+ : content?.stickerMessage
283
+ ? { kind: 'image', payload: content.stickerMessage }
284
+ : null
285
+
286
+ if (mediaCandidate) {
287
+ try {
288
+ const buffer = await downloadMediaMessage(msg as any, 'buffer', {})
289
+ const saved = saveInboundMediaBuffer({
290
+ connectorId: connector.id,
291
+ buffer: buffer as Buffer,
292
+ mediaType: mediaCandidate.kind,
293
+ mimeType: mediaCandidate.payload?.mimetype || undefined,
294
+ fileName: mediaCandidate.payload?.fileName || undefined,
295
+ })
296
+ media.push(saved)
297
+ } catch (err: any) {
298
+ console.error(`[whatsapp] Failed to decode media: ${err?.message || String(err)}`)
299
+ media.push({
300
+ type: mediaCandidate.kind,
301
+ fileName: mediaCandidate.payload?.fileName || undefined,
302
+ mimeType: mediaCandidate.payload?.mimetype || undefined,
303
+ })
304
+ }
305
+ }
306
+
307
+ if (!text && media.length === 0) continue
308
+
309
+ const senderName = msg.pushName || jid.split('@')[0]
310
+ const isGroup = jid.endsWith('@g.us')
311
+
312
+ console.log(`[whatsapp] Message from ${senderName} (${jid}): ${text.slice(0, 80)}`)
313
+
314
+ const inbound: InboundMessage = {
315
+ platform: 'whatsapp',
316
+ channelId: jid,
317
+ channelName: isGroup ? jid : `DM:${senderName}`,
318
+ senderId: msg.key.participant || jid,
319
+ senderName,
320
+ text: text || '(media message)',
321
+ imageUrl: media.find((m) => m.type === 'image')?.url,
322
+ media,
323
+ }
324
+
325
+ try {
326
+ await sock!.sendPresenceUpdate('composing', jid)
327
+ const response = await onMessage(inbound)
328
+ await sock!.sendPresenceUpdate('paused', jid)
329
+
330
+ if (!isNoMessage(response)) {
331
+ await instance.sendMessage?.(jid, response)
332
+ }
333
+ } catch (err: any) {
334
+ console.error(`[whatsapp] Error handling message:`, err.message)
335
+ try {
336
+ await sock!.sendMessage(jid, { text: 'Sorry, I encountered an error processing your message.' })
337
+ } catch { /* ignore */ }
338
+ }
339
+ }
340
+ })
341
+ }
342
+
343
+ startSocket()
344
+
345
+ return instance
346
+ },
347
+ }
348
+
349
+ export default whatsapp