@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,559 @@
1
+ import crypto from 'crypto'
2
+ import {
3
+ loadConnectors, saveConnectors, loadSessions, saveSessions,
4
+ loadAgents, loadCredentials, decryptKey, loadSettings, loadSkills,
5
+ } from '../storage'
6
+ import { streamAgentChat } from '../stream-agent-chat'
7
+ import { logExecution } from '../execution-log'
8
+ import type { Connector } from '@/types'
9
+ import type { ConnectorInstance, InboundMessage, InboundMedia } from './types'
10
+
11
+ /** Sentinel value agents return when no outbound reply should be sent */
12
+ export const NO_MESSAGE_SENTINEL = 'NO_MESSAGE'
13
+
14
+ /** Check if an agent response is the NO_MESSAGE sentinel (case-insensitive, trimmed) */
15
+ export function isNoMessage(text: string): boolean {
16
+ return text.trim().toUpperCase() === NO_MESSAGE_SENTINEL
17
+ }
18
+
19
+ /** Map of running connector instances by connector ID.
20
+ * Stored on globalThis to survive HMR reloads in dev mode —
21
+ * prevents duplicate sockets fighting for the same WhatsApp session. */
22
+ const globalKey = '__swarmclaw_running_connectors__' as const
23
+ const running: Map<string, ConnectorInstance> =
24
+ (globalThis as any)[globalKey] ?? ((globalThis as any)[globalKey] = new Map<string, ConnectorInstance>())
25
+
26
+ /** Most recent inbound channel per connector (used for proactive replies/default outbound target) */
27
+ const lastInboundKey = '__swarmclaw_connector_last_inbound__' as const
28
+ const lastInboundChannelByConnector: Map<string, string> =
29
+ (globalThis as any)[lastInboundKey] ?? ((globalThis as any)[lastInboundKey] = new Map<string, string>())
30
+
31
+ /** Per-connector lock to prevent concurrent start/stop operations */
32
+ const lockKey = '__swarmclaw_connector_locks__' as const
33
+ const locks: Map<string, Promise<void>> =
34
+ (globalThis as any)[lockKey] ?? ((globalThis as any)[lockKey] = new Map<string, Promise<void>>())
35
+
36
+ /** Get platform implementation lazily */
37
+ export async function getPlatform(platform: string) {
38
+ switch (platform) {
39
+ case 'discord': return (await import('./discord')).default
40
+ case 'telegram': return (await import('./telegram')).default
41
+ case 'slack': return (await import('./slack')).default
42
+ case 'whatsapp': return (await import('./whatsapp')).default
43
+ case 'openclaw': return (await import('./openclaw')).default
44
+ case 'signal': return (await import('./signal')).default
45
+ case 'teams': return (await import('./teams')).default
46
+ case 'googlechat': return (await import('./googlechat')).default
47
+ case 'matrix': return (await import('./matrix')).default
48
+ default: throw new Error(`Unknown platform: ${platform}`)
49
+ }
50
+ }
51
+
52
+ export function formatMediaLine(media: InboundMedia): string {
53
+ const typeLabel = media.type.toUpperCase()
54
+ const name = media.fileName || media.mimeType || 'attachment'
55
+ const size = media.sizeBytes ? ` (${Math.max(1, Math.round(media.sizeBytes / 1024))} KB)` : ''
56
+ if (media.url) return `- ${typeLabel}: ${name}${size} -> ${media.url}`
57
+ return `- ${typeLabel}: ${name}${size}`
58
+ }
59
+
60
+ export function formatInboundUserText(msg: InboundMessage): string {
61
+ const baseText = (msg.text || '').trim()
62
+ const lines: string[] = []
63
+ if (baseText) lines.push(`[${msg.senderName}] ${baseText}`)
64
+ else lines.push(`[${msg.senderName}]`)
65
+
66
+ if (Array.isArray(msg.media) && msg.media.length > 0) {
67
+ lines.push('')
68
+ lines.push('Media received:')
69
+ const preview = msg.media.slice(0, 6)
70
+ for (const media of preview) lines.push(formatMediaLine(media))
71
+ if (msg.media.length > preview.length) {
72
+ lines.push(`- ...and ${msg.media.length - preview.length} more attachment(s)`)
73
+ }
74
+ }
75
+
76
+ return lines.join('\n').trim()
77
+ }
78
+
79
+ /** Route an inbound message through the assigned agent and return the response */
80
+ async function routeMessage(connector: Connector, msg: InboundMessage): Promise<string> {
81
+ if (msg?.channelId) {
82
+ lastInboundChannelByConnector.set(connector.id, msg.channelId)
83
+ }
84
+
85
+ const agents = loadAgents()
86
+ const agent = agents[connector.agentId]
87
+ if (!agent) return '[Error] Connector agent not found.'
88
+
89
+ // Log connector trigger
90
+ const triggerSessionKey = `connector:${connector.id}:${msg.channelId}`
91
+ const allSessions = loadSessions()
92
+ const existingSession = Object.values(allSessions).find((s: any) => s.name === triggerSessionKey)
93
+ if (existingSession) {
94
+ logExecution(existingSession.id, 'trigger', `${msg.platform} message from ${msg.senderName}`, {
95
+ agentId: agent.id,
96
+ detail: {
97
+ source: 'connector',
98
+ platform: msg.platform,
99
+ connectorId: connector.id,
100
+ channelId: msg.channelId,
101
+ senderName: msg.senderName,
102
+ messagePreview: (msg.text || '').slice(0, 200),
103
+ hasMedia: !!(msg.media?.length || msg.imageUrl),
104
+ },
105
+ })
106
+ }
107
+
108
+ // Resolve API key for the agent's provider
109
+ let apiKey: string | null = null
110
+ if (agent.credentialId) {
111
+ const creds = loadCredentials()
112
+ const cred = creds[agent.credentialId]
113
+ if (cred?.encryptedKey) {
114
+ try { apiKey = decryptKey(cred.encryptedKey) } catch { /* ignore */ }
115
+ }
116
+ }
117
+
118
+ // Find or create a session keyed by platform + channel
119
+ const sessionKey = `connector:${connector.id}:${msg.channelId}`
120
+ const sessions = loadSessions()
121
+ let session = Object.values(sessions).find((s: any) => s.name === sessionKey)
122
+ if (!session) {
123
+ const id = crypto.randomBytes(4).toString('hex')
124
+ session = {
125
+ id,
126
+ name: sessionKey,
127
+ cwd: process.cwd(),
128
+ user: 'connector',
129
+ provider: agent.provider === 'claude-cli' ? 'anthropic' : agent.provider,
130
+ model: agent.model,
131
+ credentialId: agent.credentialId || null,
132
+ apiEndpoint: agent.apiEndpoint || null,
133
+ claudeSessionId: null,
134
+ codexThreadId: null,
135
+ opencodeSessionId: null,
136
+ delegateResumeIds: {
137
+ claudeCode: null,
138
+ codex: null,
139
+ opencode: null,
140
+ },
141
+ messages: [],
142
+ createdAt: Date.now(),
143
+ lastActiveAt: Date.now(),
144
+ sessionType: 'human' as const,
145
+ agentId: agent.id,
146
+ tools: agent.tools || [],
147
+ }
148
+ sessions[id] = session
149
+ saveSessions(sessions)
150
+ }
151
+
152
+ // Build system prompt: [userPrompt] \n\n [soul] \n\n [systemPrompt]
153
+ const settings = loadSettings()
154
+ const promptParts: string[] = []
155
+ if (settings.userPrompt) promptParts.push(settings.userPrompt)
156
+ if (agent.soul) promptParts.push(agent.soul)
157
+ if (agent.systemPrompt) promptParts.push(agent.systemPrompt)
158
+ if (agent.skillIds?.length) {
159
+ const allSkills = loadSkills()
160
+ for (const skillId of agent.skillIds) {
161
+ const skill = allSkills[skillId]
162
+ if (skill?.content) promptParts.push(`## Skill: ${skill.name}\n${skill.content}`)
163
+ }
164
+ }
165
+ // Add connector context
166
+ promptParts.push(`\nYou are receiving messages via ${msg.platform}. The user "${msg.senderName}" is messaging from channel "${msg.channelName || msg.channelId}". Respond naturally and conversationally.
167
+
168
+ ## Knowing When Not to Reply
169
+ Real conversations have natural pauses — not every message needs a response. Reply with exactly "NO_MESSAGE" (nothing else) to stay silent when replying would feel unnatural or forced.
170
+ Stay silent for simple acknowledgments ("okay", "alright", "cool", "got it", "sounds good"), conversation closers ("thanks", "bye", "night", "ttyl"), reactions (emoji, "haha", "lol"), and forwarded content with no question attached.
171
+ Always reply when there's a question, task, instruction, emotional sharing, or something genuinely useful to add.
172
+ The test: would a thoughtful friend feel compelled to type something back? If not, NO_MESSAGE.`)
173
+ const systemPrompt = promptParts.join('\n\n')
174
+
175
+ // Add message to session
176
+ const firstImage = msg.media?.find((m) => m.type === 'image')
177
+ const firstImageUrl = msg.imageUrl || (firstImage?.url) || undefined
178
+ const firstImagePath = firstImage?.localPath || undefined
179
+ const inboundText = formatInboundUserText(msg)
180
+ session.messages.push({
181
+ role: 'user',
182
+ text: inboundText,
183
+ time: Date.now(),
184
+ imageUrl: firstImageUrl,
185
+ imagePath: firstImagePath,
186
+ })
187
+ session.lastActiveAt = Date.now()
188
+ const s1 = loadSessions()
189
+ s1[session.id] = session
190
+ saveSessions(s1)
191
+
192
+ // Stream the response
193
+ let fullText = ''
194
+ const hasTools = session.tools?.length && session.provider !== 'claude-cli'
195
+ console.log(`[connector] Routing message to agent "${agent.name}" (${agent.provider}/${agent.model}), hasTools=${!!hasTools}`)
196
+
197
+ if (hasTools) {
198
+ try {
199
+ const result = await streamAgentChat({
200
+ session,
201
+ message: msg.text,
202
+ imagePath: firstImagePath,
203
+ apiKey,
204
+ systemPrompt,
205
+ write: () => {}, // no SSE needed for connectors
206
+ history: session.messages,
207
+ })
208
+ // Use finalResponse for connectors — strips intermediate planning/tool-use text
209
+ fullText = result.finalResponse
210
+ console.log(`[connector] streamAgentChat returned ${result.fullText.length} chars total, ${fullText.length} chars final`)
211
+ } catch (err: any) {
212
+ console.error(`[connector] streamAgentChat error:`, err.message || err)
213
+ return `[Error] ${err.message}`
214
+ }
215
+ } else {
216
+ // Use the provider directly
217
+ const { getProvider } = await import('../../providers')
218
+ const provider = getProvider(session.provider)
219
+ if (!provider) return '[Error] Provider not found.'
220
+
221
+ await provider.handler.streamChat({
222
+ session,
223
+ message: msg.text,
224
+ imagePath: firstImagePath,
225
+ apiKey,
226
+ systemPrompt,
227
+ write: (data: string) => {
228
+ if (data.startsWith('data: ')) {
229
+ try {
230
+ const event = JSON.parse(data.slice(6))
231
+ if (event.t === 'd') fullText += event.text || ''
232
+ else if (event.t === 'r') fullText = event.text || ''
233
+ } catch { /* ignore */ }
234
+ }
235
+ },
236
+ active: new Map(),
237
+ loadHistory: () => session.messages,
238
+ })
239
+ }
240
+
241
+ // If the agent chose NO_MESSAGE, skip saving it to history — the user's message
242
+ // is already recorded, and saving the sentinel would pollute the LLM's context
243
+ if (isNoMessage(fullText)) {
244
+ console.log(`[connector] Agent returned NO_MESSAGE — suppressing outbound reply`)
245
+ logExecution(session.id, 'decision', 'Agent suppressed outbound (NO_MESSAGE)', {
246
+ agentId: agent.id,
247
+ detail: { platform: msg.platform, channelId: msg.channelId },
248
+ })
249
+ return NO_MESSAGE_SENTINEL
250
+ }
251
+
252
+ // Log outbound message
253
+ logExecution(session.id, 'outbound', `Reply sent via ${msg.platform}`, {
254
+ agentId: agent.id,
255
+ detail: {
256
+ platform: msg.platform,
257
+ channelId: msg.channelId,
258
+ recipientName: msg.senderName,
259
+ responsePreview: fullText.slice(0, 500),
260
+ responseLength: fullText.length,
261
+ },
262
+ })
263
+
264
+ // Save assistant response to session
265
+ if (fullText.trim()) {
266
+ session.messages.push({ role: 'assistant', text: fullText.trim(), time: Date.now() })
267
+ session.lastActiveAt = Date.now()
268
+ const s2 = loadSessions()
269
+ s2[session.id] = session
270
+ saveSessions(s2)
271
+ }
272
+
273
+ return fullText || '(no response)'
274
+ }
275
+
276
+ /** Start a connector (serialized per ID to prevent concurrent start/stop races) */
277
+ export async function startConnector(connectorId: string): Promise<void> {
278
+ // Wait for any pending operation on this connector to finish (with timeout)
279
+ const pending = locks.get(connectorId)
280
+ if (pending) {
281
+ await Promise.race([pending, new Promise(r => setTimeout(r, 15_000))]).catch(() => {})
282
+ locks.delete(connectorId)
283
+ }
284
+
285
+ const op = withTimeout(_startConnectorImpl(connectorId), 30_000, 'Connector start timed out')
286
+ locks.set(connectorId, op)
287
+ try { await op } finally {
288
+ if (locks.get(connectorId) === op) locks.delete(connectorId)
289
+ }
290
+ }
291
+
292
+ function withTimeout<T>(promise: Promise<T>, ms: number, msg: string): Promise<T> {
293
+ return new Promise((resolve, reject) => {
294
+ const timer = setTimeout(() => reject(new Error(msg)), ms)
295
+ promise.then(resolve, reject).finally(() => clearTimeout(timer))
296
+ })
297
+ }
298
+
299
+ async function _startConnectorImpl(connectorId: string): Promise<void> {
300
+ // If already running, stop it first (handles stale entries)
301
+ if (running.has(connectorId)) {
302
+ try {
303
+ const existing = running.get(connectorId)
304
+ await existing?.stop()
305
+ } catch { /* ignore cleanup errors */ }
306
+ running.delete(connectorId)
307
+ }
308
+
309
+ const connectors = loadConnectors()
310
+ const connector = connectors[connectorId] as Connector | undefined
311
+ if (!connector) throw new Error('Connector not found')
312
+
313
+ // Resolve bot token from credential
314
+ let botToken = ''
315
+ if (connector.credentialId) {
316
+ const creds = loadCredentials()
317
+ const cred = creds[connector.credentialId]
318
+ if (cred?.encryptedKey) {
319
+ try { botToken = decryptKey(cred.encryptedKey) } catch { /* ignore */ }
320
+ }
321
+ }
322
+ // Also check config for inline token (some platforms)
323
+ if (!botToken && connector.config.botToken) {
324
+ botToken = connector.config.botToken
325
+ }
326
+
327
+ if (!botToken && connector.platform !== 'whatsapp' && connector.platform !== 'openclaw' && connector.platform !== 'signal') {
328
+ throw new Error('No bot token configured')
329
+ }
330
+
331
+ const platform = await getPlatform(connector.platform)
332
+
333
+ try {
334
+ const instance = await platform.start(connector, botToken, (msg) => routeMessage(connector, msg))
335
+ running.set(connectorId, instance)
336
+
337
+ // Update status in storage
338
+ connector.status = 'running'
339
+ connector.isEnabled = true
340
+ connector.lastError = null
341
+ connector.updatedAt = Date.now()
342
+ connectors[connectorId] = connector
343
+ saveConnectors(connectors)
344
+
345
+ console.log(`[connector] Started ${connector.platform} connector: ${connector.name}`)
346
+ } catch (err: any) {
347
+ connector.status = 'error'
348
+ connector.isEnabled = false
349
+ connector.lastError = err.message
350
+ connector.updatedAt = Date.now()
351
+ connectors[connectorId] = connector
352
+ saveConnectors(connectors)
353
+ throw err
354
+ }
355
+ }
356
+
357
+ /** Stop a connector */
358
+ export async function stopConnector(connectorId: string): Promise<void> {
359
+ const instance = running.get(connectorId)
360
+ if (instance) {
361
+ await instance.stop()
362
+ running.delete(connectorId)
363
+ }
364
+
365
+ const connectors = loadConnectors()
366
+ const connector = connectors[connectorId]
367
+ if (connector) {
368
+ connector.status = 'stopped'
369
+ connector.isEnabled = false
370
+ connector.lastError = null
371
+ connector.updatedAt = Date.now()
372
+ connectors[connectorId] = connector
373
+ saveConnectors(connectors)
374
+ }
375
+
376
+ console.log(`[connector] Stopped connector: ${connectorId}`)
377
+ }
378
+
379
+ /** Get the runtime status of a connector */
380
+ export function getConnectorStatus(connectorId: string): 'running' | 'stopped' {
381
+ return running.has(connectorId) ? 'running' : 'stopped'
382
+ }
383
+
384
+ /** Get the QR code data URL for a WhatsApp connector (null if not available) */
385
+ export function getConnectorQR(connectorId: string): string | null {
386
+ const instance = running.get(connectorId)
387
+ return instance?.qrDataUrl ?? null
388
+ }
389
+
390
+ /** Check if a WhatsApp connector has authenticated (paired) */
391
+ export function isConnectorAuthenticated(connectorId: string): boolean {
392
+ const instance = running.get(connectorId)
393
+ if (!instance) return false
394
+ return instance.authenticated === true
395
+ }
396
+
397
+ /** Check if a WhatsApp connector has stored credentials */
398
+ export function hasConnectorCredentials(connectorId: string): boolean {
399
+ const instance = running.get(connectorId)
400
+ if (!instance) return false
401
+ return instance.hasCredentials === true
402
+ }
403
+
404
+ /** Clear WhatsApp auth state and restart connector for fresh QR pairing */
405
+ export async function repairConnector(connectorId: string): Promise<void> {
406
+ // Stop existing instance
407
+ const instance = running.get(connectorId)
408
+ if (instance) {
409
+ await instance.stop()
410
+ running.delete(connectorId)
411
+ }
412
+
413
+ // Clear auth directory
414
+ const { clearAuthDir } = await import('./whatsapp')
415
+ clearAuthDir(connectorId)
416
+
417
+ // Restart the connector — will get fresh QR
418
+ await startConnector(connectorId)
419
+ }
420
+
421
+ /** Stop all running connectors (for cleanup) */
422
+ export async function stopAllConnectors(): Promise<void> {
423
+ for (const [id] of running) {
424
+ await stopConnector(id)
425
+ }
426
+ }
427
+
428
+ /** Auto-start connectors that are marked as enabled (skips already-running ones) */
429
+ export async function autoStartConnectors(): Promise<void> {
430
+ const connectors = loadConnectors()
431
+ for (const connector of Object.values(connectors) as Connector[]) {
432
+ if (connector.isEnabled && !running.has(connector.id)) {
433
+ try {
434
+ console.log(`[connector] Auto-starting ${connector.platform} connector: ${connector.name}`)
435
+ await startConnector(connector.id)
436
+ } catch (err: any) {
437
+ console.error(`[connector] Failed to auto-start ${connector.name}:`, err.message)
438
+ }
439
+ }
440
+ }
441
+ }
442
+
443
+ /** List connector IDs that are currently running (optionally by platform) */
444
+ export function listRunningConnectors(platform?: string): Array<{
445
+ id: string
446
+ name: string
447
+ platform: string
448
+ supportsSend: boolean
449
+ configuredTargets: string[]
450
+ recentChannelId: string | null
451
+ }> {
452
+ const connectors = loadConnectors()
453
+ const out: Array<{
454
+ id: string
455
+ name: string
456
+ platform: string
457
+ supportsSend: boolean
458
+ configuredTargets: string[]
459
+ recentChannelId: string | null
460
+ }> = []
461
+
462
+ for (const [id, instance] of running.entries()) {
463
+ const connector = connectors[id] as Connector | undefined
464
+ if (!connector) continue
465
+ if (platform && connector.platform !== platform) continue
466
+ const configuredTargets: string[] = []
467
+ if (connector.platform === 'whatsapp') {
468
+ const outboundJid = connector.config?.outboundJid?.trim()
469
+ if (outboundJid) configuredTargets.push(outboundJid)
470
+ const allowed = connector.config?.allowedJids?.split(',').map((s) => s.trim()).filter(Boolean) || []
471
+ configuredTargets.push(...allowed)
472
+ }
473
+ out.push({
474
+ id,
475
+ name: connector.name,
476
+ platform: connector.platform,
477
+ supportsSend: typeof instance.sendMessage === 'function',
478
+ configuredTargets: Array.from(new Set(configuredTargets)),
479
+ recentChannelId: lastInboundChannelByConnector.get(id) || null,
480
+ })
481
+ }
482
+
483
+ return out
484
+ }
485
+
486
+ /** Get the most recent inbound channel id seen for a connector */
487
+ export function getConnectorRecentChannelId(connectorId: string): string | null {
488
+ return lastInboundChannelByConnector.get(connectorId) || null
489
+ }
490
+
491
+ /**
492
+ * Send an outbound message through a running connector.
493
+ * Intended for proactive agent notifications (e.g. WhatsApp updates).
494
+ */
495
+ export async function sendConnectorMessage(params: {
496
+ connectorId?: string
497
+ platform?: string
498
+ channelId: string
499
+ text: string
500
+ imageUrl?: string
501
+ fileUrl?: string
502
+ mediaPath?: string
503
+ mimeType?: string
504
+ fileName?: string
505
+ caption?: string
506
+ }): Promise<{ connectorId: string; platform: string; channelId: string; messageId?: string }> {
507
+ const connectors = loadConnectors()
508
+ const requestedId = params.connectorId?.trim()
509
+ let connector: Connector | undefined
510
+ let connectorId: string | undefined
511
+
512
+ if (requestedId) {
513
+ connector = connectors[requestedId] as Connector | undefined
514
+ connectorId = requestedId
515
+ if (!connector) throw new Error(`Connector not found: ${requestedId}`)
516
+ } else {
517
+ const candidates = Object.values(connectors) as Connector[]
518
+ const filtered = candidates.filter((c) => {
519
+ if (params.platform && c.platform !== params.platform) return false
520
+ return running.has(c.id)
521
+ })
522
+ if (!filtered.length) {
523
+ throw new Error(`No running connector found${params.platform ? ` for platform "${params.platform}"` : ''}.`)
524
+ }
525
+ connector = filtered[0]
526
+ connectorId = connector.id
527
+ }
528
+
529
+ if (!connector || !connectorId) throw new Error('Connector resolution failed.')
530
+
531
+ const instance = running.get(connectorId)
532
+ if (!instance) {
533
+ throw new Error(`Connector "${connectorId}" is not running.`)
534
+ }
535
+ if (typeof instance.sendMessage !== 'function') {
536
+ throw new Error(`Connector "${connector.name}" (${connector.platform}) does not support outbound sends.`)
537
+ }
538
+
539
+ // Apply NO_MESSAGE filter at the delivery layer so all outbound paths respect it
540
+ if (isNoMessage(params.text) && !params.imageUrl && !params.fileUrl && !params.mediaPath) {
541
+ console.log(`[connector] sendConnectorMessage: NO_MESSAGE — suppressing outbound send`)
542
+ return { connectorId, platform: connector.platform, channelId: params.channelId }
543
+ }
544
+
545
+ const result = await instance.sendMessage(params.channelId, params.text, {
546
+ imageUrl: params.imageUrl,
547
+ fileUrl: params.fileUrl,
548
+ mediaPath: params.mediaPath,
549
+ mimeType: params.mimeType,
550
+ fileName: params.fileName,
551
+ caption: params.caption,
552
+ })
553
+ return {
554
+ connectorId,
555
+ platform: connector.platform,
556
+ channelId: params.channelId,
557
+ messageId: result?.messageId,
558
+ }
559
+ }
@@ -0,0 +1,78 @@
1
+ import fs from 'fs'
2
+ import path from 'path'
3
+ import { DATA_DIR } from '../data-dir'
4
+ import type { PlatformConnector, ConnectorInstance, InboundMessage } from './types'
5
+ import { isNoMessage } from './manager'
6
+
7
+ const matrix: PlatformConnector = {
8
+ async start(connector, botToken, onMessage): Promise<ConnectorInstance> {
9
+ const pkg = 'matrix-bot-sdk'
10
+ const { MatrixClient, SimpleFsStorageProvider, AutojoinRoomsMixin } = await import(/* webpackIgnore: true */ pkg)
11
+
12
+ const homeserverUrl = connector.config.homeserverUrl
13
+ if (!homeserverUrl) throw new Error('Missing homeserverUrl in connector config')
14
+
15
+ // Ensure storage directory exists
16
+ const storageDir = path.join(DATA_DIR, 'matrix-storage', connector.id)
17
+ fs.mkdirSync(storageDir, { recursive: true })
18
+
19
+ const storage = new SimpleFsStorageProvider(path.join(storageDir, 'bot.json'))
20
+ const client = new MatrixClient(homeserverUrl, botToken, storage)
21
+
22
+ AutojoinRoomsMixin.setupOnClient(client)
23
+
24
+ // Optional: restrict to specific rooms
25
+ const allowedRooms = connector.config.roomIds
26
+ ? connector.config.roomIds.split(',').map((s: string) => s.trim()).filter(Boolean)
27
+ : null
28
+
29
+ client.on('room.message', async (roomId: string, event: any) => {
30
+ // Ignore own messages
31
+ const userId = await client.getUserId()
32
+ if (event.sender === userId) return
33
+
34
+ // Ignore non-text messages and edits
35
+ if (!event.content?.body) return
36
+ if (event.content['m.relates_to']?.rel_type === 'm.replace') return
37
+
38
+ // Filter by allowed rooms if configured
39
+ if (allowedRooms && !allowedRooms.includes(roomId)) return
40
+
41
+ const inbound: InboundMessage = {
42
+ platform: 'matrix',
43
+ channelId: roomId,
44
+ channelName: roomId,
45
+ senderId: event.sender,
46
+ senderName: event.sender.split(':')[0].replace('@', '') || event.sender,
47
+ text: event.content.body || '',
48
+ }
49
+
50
+ try {
51
+ const response = await onMessage(inbound)
52
+ if (isNoMessage(response)) return
53
+ await client.sendText(roomId, response)
54
+ } catch (err: any) {
55
+ console.error(`[matrix] Error handling message:`, err.message)
56
+ try {
57
+ await client.sendText(roomId, 'Sorry, I encountered an error processing your message.')
58
+ } catch { /* ignore */ }
59
+ }
60
+ })
61
+
62
+ await client.start()
63
+ console.log(`[matrix] Bot connected to ${homeserverUrl}`)
64
+
65
+ return {
66
+ connector,
67
+ async sendMessage(channelId, text) {
68
+ await client.sendText(channelId, text)
69
+ },
70
+ async stop() {
71
+ client.stop()
72
+ console.log(`[matrix] Bot disconnected`)
73
+ },
74
+ }
75
+ },
76
+ }
77
+
78
+ export default matrix