@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,183 @@
1
+ import { spawn, execSync } from 'child_process'
2
+ import type { ChildProcess } from 'child_process'
3
+ import type { Connector } from '@/types'
4
+ import type { PlatformConnector, ConnectorInstance, InboundMessage } from './types'
5
+ import { isNoMessage } from './manager'
6
+
7
+ const signal: PlatformConnector = {
8
+ async start(connector, _botToken, onMessage): Promise<ConnectorInstance> {
9
+ const phoneNumber = connector.config.phoneNumber
10
+ if (!phoneNumber) throw new Error('Missing phoneNumber in connector config')
11
+
12
+ const cliPath = connector.config.signalCliPath || 'signal-cli'
13
+ const mode = connector.config.signalCliMode || 'stdio'
14
+ const httpUrl = connector.config.signalCliHttpUrl || 'http://localhost:8080'
15
+
16
+ let stopped = false
17
+ let daemonProc: ChildProcess | null = null
18
+ let pollTimer: ReturnType<typeof setInterval> | null = null
19
+
20
+ if (mode === 'stdio') {
21
+ // Spawn signal-cli in daemon mode with JSON output
22
+ daemonProc = spawn(cliPath, ['-u', phoneNumber, 'daemon', '--json'], {
23
+ stdio: ['ignore', 'pipe', 'pipe'],
24
+ })
25
+
26
+ let buffer = ''
27
+
28
+ daemonProc.stdout?.on('data', (chunk: Buffer) => {
29
+ buffer += chunk.toString()
30
+ const lines = buffer.split('\n')
31
+ buffer = lines.pop() || ''
32
+
33
+ for (const line of lines) {
34
+ if (!line.trim()) continue
35
+ try {
36
+ const event = JSON.parse(line)
37
+ handleSignalEvent(event, connector, onMessage)
38
+ } catch {
39
+ // Not valid JSON, skip
40
+ }
41
+ }
42
+ })
43
+
44
+ daemonProc.stderr?.on('data', (chunk: Buffer) => {
45
+ const msg = chunk.toString().trim()
46
+ if (msg) console.error(`[signal] stderr: ${msg}`)
47
+ })
48
+
49
+ daemonProc.on('exit', (code) => {
50
+ if (!stopped) {
51
+ console.error(`[signal] daemon exited unexpectedly with code ${code}`)
52
+ }
53
+ })
54
+
55
+ console.log(`[signal] Daemon started in stdio mode for ${phoneNumber}`)
56
+ } else if (mode === 'http') {
57
+ // Poll the signal-cli REST API for incoming messages
58
+ const pollInterval = 2000
59
+
60
+ const poll = async () => {
61
+ if (stopped) return
62
+ try {
63
+ const res = await fetch(`${httpUrl}/v1/receive/${phoneNumber}`)
64
+ if (!res.ok) return
65
+ const messages = await res.json()
66
+ if (Array.isArray(messages)) {
67
+ for (const event of messages) {
68
+ handleSignalEvent(event, connector, onMessage)
69
+ }
70
+ }
71
+ } catch {
72
+ // Silently retry on connection errors
73
+ }
74
+ }
75
+
76
+ pollTimer = setInterval(poll, pollInterval)
77
+ console.log(`[signal] Polling ${httpUrl} for ${phoneNumber} every ${pollInterval}ms`)
78
+ } else {
79
+ throw new Error(`Unknown signalCliMode: ${mode}. Use 'stdio' or 'http'.`)
80
+ }
81
+
82
+ return {
83
+ connector,
84
+ async sendMessage(channelId, text) {
85
+ if (stopped) throw new Error('Connector is stopped')
86
+
87
+ if (mode === 'http') {
88
+ const res = await fetch(`${httpUrl}/v2/send`, {
89
+ method: 'POST',
90
+ headers: { 'Content-Type': 'application/json' },
91
+ body: JSON.stringify({
92
+ message: text,
93
+ number: phoneNumber,
94
+ recipients: [channelId],
95
+ }),
96
+ })
97
+ if (!res.ok) {
98
+ throw new Error(`Signal HTTP send failed: ${res.status} ${res.statusText}`)
99
+ }
100
+ } else {
101
+ // Use signal-cli send command
102
+ try {
103
+ execSync(
104
+ `${cliPath} -u ${phoneNumber} send -m ${JSON.stringify(text)} ${channelId}`,
105
+ { timeout: 15_000 },
106
+ )
107
+ } catch (err: any) {
108
+ throw new Error(`Signal send failed: ${err.message}`)
109
+ }
110
+ }
111
+ },
112
+ async stop() {
113
+ stopped = true
114
+ if (pollTimer) {
115
+ clearInterval(pollTimer)
116
+ pollTimer = null
117
+ }
118
+ if (daemonProc) {
119
+ daemonProc.kill('SIGTERM')
120
+ daemonProc = null
121
+ }
122
+ console.log(`[signal] Connector stopped for ${phoneNumber}`)
123
+ },
124
+ }
125
+ },
126
+ }
127
+
128
+ /** Parse a signal-cli JSON event and route it as an inbound message */
129
+ export async function handleSignalEvent(
130
+ event: any,
131
+ connector: Connector,
132
+ onMessage: (msg: InboundMessage) => Promise<string>,
133
+ ) {
134
+ // signal-cli JSON output structure varies; handle the common envelope format
135
+ const envelope = event.envelope || event
136
+ const dataMessage = envelope.dataMessage
137
+ if (!dataMessage?.message && !dataMessage?.body) return
138
+
139
+ const sender = envelope.source || envelope.sourceNumber || ''
140
+ const text = dataMessage.message || dataMessage.body || ''
141
+ const groupId = dataMessage.groupInfo?.groupId || null
142
+
143
+ const inbound: InboundMessage = {
144
+ platform: 'signal',
145
+ channelId: groupId || sender,
146
+ channelName: groupId ? `group:${groupId}` : sender,
147
+ senderId: sender,
148
+ senderName: envelope.sourceName || sender,
149
+ text,
150
+ }
151
+
152
+ try {
153
+ const response = await onMessage(inbound)
154
+ if (isNoMessage(response)) return
155
+
156
+ // Send reply back
157
+ const cliPath = connector.config.signalCliPath || 'signal-cli'
158
+ const phoneNumber = connector.config.phoneNumber
159
+ const mode = connector.config.signalCliMode || 'stdio'
160
+ const httpUrl = connector.config.signalCliHttpUrl || 'http://localhost:8080'
161
+
162
+ if (mode === 'http') {
163
+ await fetch(`${httpUrl}/v2/send`, {
164
+ method: 'POST',
165
+ headers: { 'Content-Type': 'application/json' },
166
+ body: JSON.stringify({
167
+ message: response,
168
+ number: phoneNumber,
169
+ recipients: [inbound.channelId],
170
+ }),
171
+ })
172
+ } else {
173
+ execSync(
174
+ `${cliPath} -u ${phoneNumber} send -m ${JSON.stringify(response)} ${inbound.channelId}`,
175
+ { timeout: 15_000 },
176
+ )
177
+ }
178
+ } catch (err: any) {
179
+ console.error(`[signal] Error handling message:`, err.message)
180
+ }
181
+ }
182
+
183
+ export default signal
@@ -0,0 +1,258 @@
1
+ import { App, LogLevel } from '@slack/bolt'
2
+ import fs from 'fs'
3
+ import path from 'path'
4
+ import type { Connector } from '@/types'
5
+ import type { PlatformConnector, ConnectorInstance, InboundMessage } from './types'
6
+ import { downloadInboundMediaToUpload, inferInboundMediaType, mimeFromPath, isImageMime } from './media'
7
+ import { isNoMessage } from './manager'
8
+
9
+ const slack: PlatformConnector = {
10
+ async start(connector, botToken, onMessage): Promise<ConnectorInstance> {
11
+ const appToken = connector.config.appToken || ''
12
+ const signingSecret = connector.config.signingSecret || 'not-used-in-socket-mode'
13
+
14
+ // Socket Mode requires an app-level token (xapp-...) — without it, Bolt starts an HTTP server
15
+ if (!appToken) {
16
+ throw new Error(
17
+ 'App-Level Token (xapp-...) is required. Enable Socket Mode in your Slack app settings ' +
18
+ 'and generate an App-Level Token under Basic Information > App-Level Tokens.'
19
+ )
20
+ }
21
+
22
+ if (!appToken.startsWith('xapp-')) {
23
+ throw new Error(
24
+ `Invalid App-Level Token — must start with "xapp-" (got "${appToken.slice(0, 5)}..."). ` +
25
+ 'The App-Level Token is different from the Bot Token (xoxb-). ' +
26
+ 'Find it under Basic Information > App-Level Tokens in your Slack app settings.'
27
+ )
28
+ }
29
+
30
+ // Validate the bot token format and auth
31
+ if (!botToken.startsWith('xoxb-')) {
32
+ throw new Error(
33
+ `Invalid Bot Token — must start with "xoxb-" (got "${botToken.slice(0, 5)}..."). ` +
34
+ 'Find it under OAuth & Permissions > Bot User OAuth Token.'
35
+ )
36
+ }
37
+
38
+ const { WebClient } = await import('@slack/web-api')
39
+ const testClient = new WebClient(botToken)
40
+ let botUserId: string | undefined
41
+ try {
42
+ const auth = await testClient.auth.test()
43
+ if (!auth.user_id || !auth.team) {
44
+ throw new Error('Auth test returned empty — the bot token may be revoked or the app uninstalled')
45
+ }
46
+ botUserId = auth.user_id as string
47
+ console.log(`[slack] Authenticated as @${auth.user} in workspace "${auth.team}"`)
48
+ } catch (err: any) {
49
+ const hint = err.code === 'slack_webapi_platform_error'
50
+ ? '. Check that your Bot Token (xoxb-...) is correct and the app is installed to the workspace.'
51
+ : ''
52
+ throw new Error(`Slack auth failed: ${err.message}${hint}`)
53
+ }
54
+
55
+ const app = new App({
56
+ token: botToken,
57
+ appToken,
58
+ signingSecret,
59
+ socketMode: true,
60
+ logLevel: LogLevel.WARN,
61
+ })
62
+
63
+ // Catch global errors so they don't become unhandled rejections
64
+ app.error(async (error) => {
65
+ console.error(`[slack] App error:`, error)
66
+ })
67
+
68
+ // Optional: restrict to specific channels
69
+ const allowedChannels = connector.config.channelIds
70
+ ? connector.config.channelIds.split(',').map((s) => s.trim()).filter(Boolean)
71
+ : null
72
+
73
+ // Handle messages
74
+ app.message(async ({ message, say, client }) => {
75
+ // Only handle user messages (not bot messages or own messages)
76
+ if (!('text' in message) || ('bot_id' in message)) return
77
+ const msg = message as any
78
+ if (botUserId && msg.user === botUserId) return
79
+
80
+ const channelId = msg.channel
81
+ if (allowedChannels && !allowedChannels.includes(channelId)) return
82
+
83
+ console.log(`[slack] Message in ${channelId} from ${msg.user}: ${(msg.text || '').slice(0, 80)}`)
84
+
85
+ // Get user info for display name
86
+ let senderName = msg.user || 'unknown'
87
+ try {
88
+ const userInfo = await client.users.info({ user: msg.user })
89
+ senderName = userInfo.user?.real_name || userInfo.user?.name || senderName
90
+ } catch { /* use ID as fallback */ }
91
+
92
+ // Get channel name
93
+ let channelName = channelId
94
+ try {
95
+ const channelInfo = await client.conversations.info({ channel: channelId })
96
+ channelName = (channelInfo.channel as any)?.name || channelId
97
+ } catch { /* use ID as fallback */ }
98
+
99
+ const media: NonNullable<InboundMessage['media']> = []
100
+ if (Array.isArray(msg.files)) {
101
+ for (const f of msg.files as any[]) {
102
+ const mediaType = inferInboundMediaType(f?.mimetype, f?.name, 'document')
103
+ const sourceUrl = f?.url_private_download || f?.url_private || f?.permalink_public || f?.permalink
104
+ if (typeof sourceUrl === 'string' && /^https?:\/\//i.test(sourceUrl)) {
105
+ try {
106
+ const stored = await downloadInboundMediaToUpload({
107
+ connectorId: connector.id,
108
+ mediaType,
109
+ url: sourceUrl,
110
+ headers: { Authorization: `Bearer ${botToken}` },
111
+ fileName: f?.name || undefined,
112
+ mimeType: f?.mimetype || undefined,
113
+ })
114
+ if (stored) {
115
+ media.push(stored)
116
+ continue
117
+ }
118
+ } catch (err: any) {
119
+ console.warn(`[slack] Media download failed (${f?.name || 'file'}):`, err?.message || String(err))
120
+ }
121
+ }
122
+ media.push({
123
+ type: mediaType,
124
+ fileName: f?.name || undefined,
125
+ mimeType: f?.mimetype || undefined,
126
+ sizeBytes: typeof f?.size === 'number' ? f.size : undefined,
127
+ url: typeof sourceUrl === 'string' ? sourceUrl : undefined,
128
+ })
129
+ }
130
+ }
131
+
132
+ const inbound: InboundMessage = {
133
+ platform: 'slack',
134
+ channelId,
135
+ channelName,
136
+ senderId: msg.user,
137
+ senderName,
138
+ text: msg.text || (media.length > 0 ? '(media message)' : ''),
139
+ imageUrl: media.find((m) => m.type === 'image')?.url,
140
+ media,
141
+ }
142
+
143
+ try {
144
+ const response = await onMessage(inbound)
145
+
146
+ if (isNoMessage(response)) return
147
+
148
+ // Slack has a 4000 char limit for messages
149
+ if (response.length <= 4000) {
150
+ await say(response)
151
+ } else {
152
+ const chunks = response.match(/[\s\S]{1,3990}/g) || [response]
153
+ for (const chunk of chunks) {
154
+ await say(chunk)
155
+ }
156
+ }
157
+ } catch (err: any) {
158
+ console.error(`[slack] Error handling message:`, err.message)
159
+ try {
160
+ await say('Sorry, I encountered an error processing your message.')
161
+ } catch { /* ignore */ }
162
+ }
163
+ })
164
+
165
+ // Handle @mentions
166
+ app.event('app_mention', async ({ event, say, client }) => {
167
+ if (allowedChannels && !allowedChannels.includes(event.channel)) return
168
+
169
+ let senderName = event.user || 'unknown'
170
+ try {
171
+ const userInfo = await client.users.info({ user: event.user! })
172
+ senderName = userInfo.user?.real_name || userInfo.user?.name || senderName
173
+ } catch { /* use ID */ }
174
+
175
+ const inbound: InboundMessage = {
176
+ platform: 'slack',
177
+ channelId: event.channel,
178
+ channelName: event.channel,
179
+ senderId: event.user || 'unknown',
180
+ senderName,
181
+ text: event.text.replace(/<@[^>]+>/g, '').trim(), // Strip @mentions
182
+ }
183
+
184
+ try {
185
+ const response = await onMessage(inbound)
186
+ if (isNoMessage(response)) return
187
+ await say(response)
188
+ } catch (err: any) {
189
+ console.error(`[slack] Error handling mention:`, err.message)
190
+ }
191
+ })
192
+
193
+ await app.start()
194
+ console.log(`[slack] Bot connected (socket mode)`)
195
+
196
+ return {
197
+ connector,
198
+ async sendMessage(channelId, text, options) {
199
+ const webClient = app.client
200
+
201
+ // File upload (local path or URL)
202
+ const hasMedia = options?.mediaPath || options?.imageUrl || options?.fileUrl
203
+ if (hasMedia) {
204
+ let fileContent: Buffer | undefined
205
+ let fileUrl: string | undefined
206
+ let fileName = options?.fileName || 'attachment'
207
+
208
+ if (options?.mediaPath) {
209
+ if (!fs.existsSync(options.mediaPath)) throw new Error(`File not found: ${options.mediaPath}`)
210
+ fileContent = fs.readFileSync(options.mediaPath)
211
+ fileName = options.fileName || path.basename(options.mediaPath)
212
+ } else {
213
+ fileUrl = options?.imageUrl || options?.fileUrl
214
+ }
215
+
216
+ if (fileContent) {
217
+ const result = await webClient.filesUploadV2({
218
+ channel_id: channelId,
219
+ file: fileContent,
220
+ filename: fileName,
221
+ initial_comment: options?.caption || text || undefined,
222
+ })
223
+ return { messageId: (result as any)?.files?.[0]?.id }
224
+ } else if (fileUrl) {
225
+ // Send URL as message with unfurl
226
+ const msg = await webClient.chat.postMessage({
227
+ channel: channelId,
228
+ text: `${options?.caption || text || ''}\n${fileUrl}`.trim(),
229
+ unfurl_links: true,
230
+ unfurl_media: true,
231
+ })
232
+ return { messageId: msg.ts || undefined }
233
+ }
234
+ }
235
+
236
+ // Text only
237
+ const payload = text || options?.caption || ''
238
+ if (payload.length <= 4000) {
239
+ const msg = await webClient.chat.postMessage({ channel: channelId, text: payload })
240
+ return { messageId: msg.ts || undefined }
241
+ }
242
+ const chunks = payload.match(/[\s\S]{1,3990}/g) || [payload]
243
+ let lastTs: string | undefined
244
+ for (const chunk of chunks) {
245
+ const msg = await webClient.chat.postMessage({ channel: channelId, text: chunk })
246
+ lastTs = msg.ts || undefined
247
+ }
248
+ return { messageId: lastTs }
249
+ },
250
+ async stop() {
251
+ await app.stop()
252
+ console.log(`[slack] Bot disconnected`)
253
+ },
254
+ }
255
+ },
256
+ }
257
+
258
+ export default slack
@@ -0,0 +1,94 @@
1
+ import type { PlatformConnector, ConnectorInstance, InboundMessage } from './types'
2
+ import { isNoMessage } from './manager'
3
+
4
+ const teams: PlatformConnector = {
5
+ async start(connector, botToken, onMessage): Promise<ConnectorInstance> {
6
+ const pkg = 'botbuilder'
7
+ const { BotFrameworkAdapter, TurnContext } = await import(/* webpackIgnore: true */ pkg)
8
+
9
+ const appId = connector.config.appId
10
+ if (!appId) throw new Error('Missing appId in connector config')
11
+
12
+ const adapter = new BotFrameworkAdapter({
13
+ appId,
14
+ appPassword: botToken,
15
+ })
16
+
17
+ adapter.onTurnError = async (_context: unknown, error: Error) => {
18
+ console.error(`[teams] Turn error:`, error.message)
19
+ }
20
+
21
+ // Store conversation references for proactive messaging
22
+ const conversationReferences = new Map<string, any>()
23
+ let stopped = false
24
+
25
+ // Process incoming activities — called from the webhook endpoint
26
+ // POST /api/connectors/[id]/webhook should pipe req/res through this
27
+ const processActivity = async (req: any, res: any) => {
28
+ if (stopped) return
29
+ await adapter.processActivity(req, res, async (context: any) => {
30
+ if (context.activity.type !== 'message') return
31
+ if (!context.activity.text) return
32
+
33
+ // Save conversation reference for proactive messaging
34
+ const ref = TurnContext.getConversationReference(context.activity)
35
+ const convId = context.activity.conversation?.id || ''
36
+ conversationReferences.set(convId, ref)
37
+
38
+ const inbound: InboundMessage = {
39
+ platform: 'teams',
40
+ channelId: convId,
41
+ channelName: context.activity.conversation?.name || convId,
42
+ senderId: context.activity.from?.id || '',
43
+ senderName: context.activity.from?.name || 'Unknown',
44
+ text: context.activity.text || '',
45
+ }
46
+
47
+ try {
48
+ const response = await onMessage(inbound)
49
+ if (isNoMessage(response)) return
50
+ await context.sendActivity(response)
51
+ } catch (err: any) {
52
+ console.error(`[teams] Error handling message:`, err.message)
53
+ try {
54
+ await context.sendActivity('Sorry, I encountered an error processing your message.')
55
+ } catch { /* ignore */ }
56
+ }
57
+ })
58
+ }
59
+
60
+ // Store processActivity on globalThis so the webhook route can access it
61
+ const handlerKey = `__swarmclaw_teams_handler_${connector.id}__`
62
+ ;(globalThis as any)[handlerKey] = processActivity
63
+
64
+ console.log(`[teams] Bot registered (appId: ${appId})`)
65
+ console.log(`[teams] Configure your bot's messaging endpoint to POST to /api/connectors/${connector.id}/webhook`)
66
+
67
+ return {
68
+ connector,
69
+ async sendMessage(channelId, text) {
70
+ if (stopped) throw new Error('Connector is stopped')
71
+
72
+ const ref = conversationReferences.get(channelId)
73
+ if (!ref) {
74
+ throw new Error(`No conversation reference found for ${channelId}. The bot must receive a message first.`)
75
+ }
76
+
77
+ let messageId: string | undefined
78
+ await adapter.continueConversation(ref, async (context: any) => {
79
+ const sent = await context.sendActivity(text)
80
+ messageId = sent?.id
81
+ })
82
+ return { messageId }
83
+ },
84
+ async stop() {
85
+ stopped = true
86
+ delete (globalThis as any)[handlerKey]
87
+ conversationReferences.clear()
88
+ console.log(`[teams] Bot disconnected`)
89
+ },
90
+ }
91
+ },
92
+ }
93
+
94
+ export default teams