@swarmclawai/swarmclaw 0.4.0 → 0.5.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 (209) hide show
  1. package/README.md +21 -4
  2. package/bin/server-cmd.js +28 -19
  3. package/next.config.ts +13 -0
  4. package/package.json +3 -1
  5. package/src/app/api/agents/[id]/route.ts +39 -22
  6. package/src/app/api/agents/[id]/thread/route.ts +2 -2
  7. package/src/app/api/agents/route.ts +3 -2
  8. package/src/app/api/agents/trash/route.ts +44 -0
  9. package/src/app/api/clawhub/install/route.ts +2 -2
  10. package/src/app/api/connectors/[id]/route.ts +17 -7
  11. package/src/app/api/connectors/[id]/webhook/route.ts +103 -0
  12. package/src/app/api/connectors/route.ts +6 -3
  13. package/src/app/api/credentials/[id]/route.ts +2 -1
  14. package/src/app/api/credentials/route.ts +2 -2
  15. package/src/app/api/documents/route.ts +2 -2
  16. package/src/app/api/files/serve/route.ts +8 -0
  17. package/src/app/api/knowledge/[id]/route.ts +5 -4
  18. package/src/app/api/knowledge/upload/route.ts +2 -2
  19. package/src/app/api/mcp-servers/[id]/route.ts +11 -14
  20. package/src/app/api/mcp-servers/[id]/test/route.ts +2 -1
  21. package/src/app/api/mcp-servers/[id]/tools/route.ts +2 -1
  22. package/src/app/api/mcp-servers/route.ts +2 -2
  23. package/src/app/api/memory/[id]/route.ts +9 -8
  24. package/src/app/api/memory/route.ts +2 -2
  25. package/src/app/api/memory-images/[filename]/route.ts +2 -1
  26. package/src/app/api/openclaw/agent-files/route.ts +57 -0
  27. package/src/app/api/openclaw/approvals/route.ts +46 -0
  28. package/src/app/api/openclaw/config-sync/route.ts +33 -0
  29. package/src/app/api/openclaw/cron/route.ts +52 -0
  30. package/src/app/api/openclaw/directory/route.ts +27 -0
  31. package/src/app/api/openclaw/discover/route.ts +62 -0
  32. package/src/app/api/openclaw/dotenv-keys/route.ts +18 -0
  33. package/src/app/api/openclaw/exec-config/route.ts +41 -0
  34. package/src/app/api/openclaw/gateway/route.ts +72 -0
  35. package/src/app/api/openclaw/history/route.ts +109 -0
  36. package/src/app/api/openclaw/media/route.ts +53 -0
  37. package/src/app/api/openclaw/models/route.ts +12 -0
  38. package/src/app/api/openclaw/permissions/route.ts +39 -0
  39. package/src/app/api/openclaw/sandbox-env/route.ts +69 -0
  40. package/src/app/api/openclaw/skills/install/route.ts +32 -0
  41. package/src/app/api/openclaw/skills/remove/route.ts +24 -0
  42. package/src/app/api/openclaw/skills/route.ts +82 -0
  43. package/src/app/api/openclaw/sync/route.ts +31 -0
  44. package/src/app/api/orchestrator/run/route.ts +2 -2
  45. package/src/app/api/projects/[id]/route.ts +55 -0
  46. package/src/app/api/projects/route.ts +27 -0
  47. package/src/app/api/providers/[id]/models/route.ts +2 -1
  48. package/src/app/api/providers/[id]/route.ts +13 -15
  49. package/src/app/api/providers/route.ts +2 -2
  50. package/src/app/api/schedules/[id]/route.ts +16 -18
  51. package/src/app/api/schedules/[id]/run/route.ts +4 -3
  52. package/src/app/api/schedules/route.ts +2 -2
  53. package/src/app/api/secrets/[id]/route.ts +16 -17
  54. package/src/app/api/secrets/route.ts +2 -2
  55. package/src/app/api/sessions/[id]/clear/route.ts +2 -1
  56. package/src/app/api/sessions/[id]/deploy/route.ts +2 -1
  57. package/src/app/api/sessions/[id]/devserver/route.ts +2 -1
  58. package/src/app/api/sessions/[id]/edit-resend/route.ts +22 -0
  59. package/src/app/api/sessions/[id]/fork/route.ts +44 -0
  60. package/src/app/api/sessions/[id]/messages/route.ts +20 -2
  61. package/src/app/api/sessions/[id]/retry/route.ts +2 -1
  62. package/src/app/api/sessions/[id]/route.ts +14 -4
  63. package/src/app/api/sessions/route.ts +8 -4
  64. package/src/app/api/skills/[id]/route.ts +23 -21
  65. package/src/app/api/skills/import/route.ts +2 -2
  66. package/src/app/api/skills/route.ts +2 -2
  67. package/src/app/api/tasks/[id]/approve/route.ts +2 -1
  68. package/src/app/api/tasks/[id]/route.ts +6 -5
  69. package/src/app/api/tasks/route.ts +2 -2
  70. package/src/app/api/tts/stream/route.ts +48 -0
  71. package/src/app/api/upload/route.ts +2 -2
  72. package/src/app/api/uploads/[filename]/route.ts +4 -1
  73. package/src/app/api/webhooks/[id]/route.ts +29 -31
  74. package/src/app/api/webhooks/route.ts +2 -2
  75. package/src/app/globals.css +14 -0
  76. package/src/app/layout.tsx +5 -20
  77. package/src/app/page.tsx +3 -24
  78. package/src/cli/index.js +60 -0
  79. package/src/cli/index.ts +1 -1
  80. package/src/cli/spec.js +42 -0
  81. package/src/components/agents/agent-avatar.tsx +45 -0
  82. package/src/components/agents/agent-card.tsx +19 -5
  83. package/src/components/agents/agent-chat-list.tsx +31 -24
  84. package/src/components/agents/agent-files-editor.tsx +185 -0
  85. package/src/components/agents/agent-list.tsx +84 -3
  86. package/src/components/agents/agent-sheet.tsx +147 -14
  87. package/src/components/agents/cron-job-form.tsx +137 -0
  88. package/src/components/agents/exec-config-panel.tsx +147 -0
  89. package/src/components/agents/inspector-panel.tsx +310 -0
  90. package/src/components/agents/openclaw-skills-panel.tsx +230 -0
  91. package/src/components/agents/permission-preset-selector.tsx +79 -0
  92. package/src/components/agents/personality-builder.tsx +111 -0
  93. package/src/components/agents/sandbox-env-panel.tsx +72 -0
  94. package/src/components/agents/skill-install-dialog.tsx +102 -0
  95. package/src/components/agents/trash-list.tsx +109 -0
  96. package/src/components/chat/chat-area.tsx +41 -6
  97. package/src/components/chat/chat-header.tsx +305 -29
  98. package/src/components/chat/chat-preview-panel.tsx +113 -0
  99. package/src/components/chat/exec-approval-card.tsx +89 -0
  100. package/src/components/chat/message-bubble.tsx +218 -36
  101. package/src/components/chat/message-list.tsx +135 -31
  102. package/src/components/chat/streaming-bubble.tsx +59 -10
  103. package/src/components/chat/suggestions-bar.tsx +74 -0
  104. package/src/components/chat/thinking-indicator.tsx +20 -6
  105. package/src/components/chat/tool-call-bubble.tsx +98 -19
  106. package/src/components/chat/tool-request-banner.tsx +20 -2
  107. package/src/components/chat/trace-block.tsx +103 -0
  108. package/src/components/chat/voice-overlay.tsx +80 -0
  109. package/src/components/connectors/connector-list.tsx +6 -2
  110. package/src/components/connectors/connector-sheet.tsx +31 -7
  111. package/src/components/layout/app-layout.tsx +47 -25
  112. package/src/components/projects/project-list.tsx +123 -0
  113. package/src/components/projects/project-sheet.tsx +135 -0
  114. package/src/components/schedules/schedule-list.tsx +3 -1
  115. package/src/components/sessions/new-session-sheet.tsx +6 -6
  116. package/src/components/sessions/session-card.tsx +1 -1
  117. package/src/components/sessions/session-list.tsx +7 -7
  118. package/src/components/settings/gateway-connection-panel.tsx +278 -0
  119. package/src/components/shared/avatar.tsx +13 -2
  120. package/src/components/shared/connector-platform-icon.tsx +4 -0
  121. package/src/components/shared/settings/section-heartbeat.tsx +1 -1
  122. package/src/components/shared/settings/section-orchestrator.tsx +1 -2
  123. package/src/components/shared/settings/section-web-search.tsx +56 -0
  124. package/src/components/shared/settings/settings-page.tsx +74 -0
  125. package/src/components/skills/skill-list.tsx +2 -1
  126. package/src/components/tasks/task-board.tsx +1 -1
  127. package/src/components/tasks/task-list.tsx +5 -2
  128. package/src/components/tasks/task-sheet.tsx +12 -12
  129. package/src/hooks/use-continuous-speech.ts +181 -0
  130. package/src/hooks/use-openclaw-gateway.ts +63 -0
  131. package/src/hooks/use-view-router.ts +52 -0
  132. package/src/hooks/use-voice-conversation.ts +80 -0
  133. package/src/lib/id.ts +6 -0
  134. package/src/lib/notification-sounds.ts +58 -0
  135. package/src/lib/personality-parser.ts +97 -0
  136. package/src/lib/projects.ts +13 -0
  137. package/src/lib/provider-sets.ts +5 -0
  138. package/src/lib/providers/anthropic.ts +14 -1
  139. package/src/lib/providers/index.ts +6 -0
  140. package/src/lib/providers/ollama.ts +9 -1
  141. package/src/lib/providers/openai.ts +9 -1
  142. package/src/lib/providers/openclaw.ts +28 -2
  143. package/src/lib/runtime-loop.ts +2 -2
  144. package/src/lib/server/api-routes.test.ts +5 -6
  145. package/src/lib/server/build-llm.ts +17 -4
  146. package/src/lib/server/chat-execution.ts +82 -6
  147. package/src/lib/server/collection-helpers.ts +54 -0
  148. package/src/lib/server/connectors/bluebubbles.test.ts +217 -0
  149. package/src/lib/server/connectors/bluebubbles.ts +360 -0
  150. package/src/lib/server/connectors/connector-routing.test.ts +1 -1
  151. package/src/lib/server/connectors/googlechat.ts +51 -8
  152. package/src/lib/server/connectors/manager.ts +424 -13
  153. package/src/lib/server/connectors/media.ts +2 -2
  154. package/src/lib/server/connectors/openclaw.ts +65 -0
  155. package/src/lib/server/connectors/pairing.test.ts +99 -0
  156. package/src/lib/server/connectors/pairing.ts +256 -0
  157. package/src/lib/server/connectors/signal.ts +1 -0
  158. package/src/lib/server/connectors/teams.ts +5 -5
  159. package/src/lib/server/connectors/types.ts +10 -0
  160. package/src/lib/server/daemon-state.ts +11 -0
  161. package/src/lib/server/execution-log.ts +3 -3
  162. package/src/lib/server/heartbeat-service.ts +1 -1
  163. package/src/lib/server/knowledge-db.test.ts +2 -33
  164. package/src/lib/server/main-agent-loop.ts +8 -9
  165. package/src/lib/server/main-session.ts +21 -0
  166. package/src/lib/server/memory-db.ts +6 -6
  167. package/src/lib/server/openclaw-approvals.ts +105 -0
  168. package/src/lib/server/openclaw-config-sync.ts +107 -0
  169. package/src/lib/server/openclaw-exec-config.ts +52 -0
  170. package/src/lib/server/openclaw-gateway.ts +291 -0
  171. package/src/lib/server/openclaw-history-merge.ts +36 -0
  172. package/src/lib/server/openclaw-models.ts +56 -0
  173. package/src/lib/server/openclaw-permission-presets.ts +64 -0
  174. package/src/lib/server/openclaw-sync.ts +497 -0
  175. package/src/lib/server/orchestrator-lg.ts +30 -9
  176. package/src/lib/server/orchestrator.ts +4 -4
  177. package/src/lib/server/process-manager.ts +2 -2
  178. package/src/lib/server/queue.ts +24 -11
  179. package/src/lib/server/scheduler.ts +2 -2
  180. package/src/lib/server/session-mailbox.ts +2 -2
  181. package/src/lib/server/session-run-manager.ts +2 -2
  182. package/src/lib/server/session-tools/connector.ts +53 -6
  183. package/src/lib/server/session-tools/crud.ts +3 -3
  184. package/src/lib/server/session-tools/delegate.ts +22 -6
  185. package/src/lib/server/session-tools/file.ts +192 -19
  186. package/src/lib/server/session-tools/index.ts +4 -2
  187. package/src/lib/server/session-tools/memory.ts +2 -2
  188. package/src/lib/server/session-tools/openclaw-nodes.ts +112 -0
  189. package/src/lib/server/session-tools/sandbox.ts +33 -0
  190. package/src/lib/server/session-tools/search-providers.ts +277 -0
  191. package/src/lib/server/session-tools/session-info.ts +2 -2
  192. package/src/lib/server/session-tools/session-tools-wiring.test.ts +2 -2
  193. package/src/lib/server/session-tools/shell.ts +1 -1
  194. package/src/lib/server/session-tools/web.ts +53 -72
  195. package/src/lib/server/storage.ts +74 -11
  196. package/src/lib/server/stream-agent-chat.ts +53 -4
  197. package/src/lib/server/suggestions.ts +20 -0
  198. package/src/lib/server/task-result.test.ts +44 -0
  199. package/src/lib/server/task-result.ts +14 -0
  200. package/src/lib/server/ws-hub.ts +14 -0
  201. package/src/lib/tool-definitions.ts +5 -3
  202. package/src/lib/tts-stream.ts +130 -0
  203. package/src/lib/view-routes.ts +28 -0
  204. package/src/proxy.ts +3 -0
  205. package/src/stores/use-app-store.ts +80 -1
  206. package/src/stores/use-approval-store.ts +78 -0
  207. package/src/stores/use-chat-store.ts +162 -6
  208. package/src/types/index.ts +154 -3
  209. package/tsconfig.json +13 -4
@@ -1,4 +1,4 @@
1
- import crypto from 'crypto'
1
+ import { genId } from '@/lib/id'
2
2
  import {
3
3
  loadConnectors, saveConnectors, loadSessions, saveSessions,
4
4
  loadAgents, loadCredentials, decryptKey, loadSettings, loadSkills,
@@ -9,6 +9,17 @@ import { notify } from '../ws-hub'
9
9
  import { logExecution } from '../execution-log'
10
10
  import type { Connector } from '@/types'
11
11
  import type { ConnectorInstance, InboundMessage, InboundMedia } from './types'
12
+ import {
13
+ addAllowedSender,
14
+ approvePairingCode,
15
+ createOrTouchPairingRequest,
16
+ isSenderAllowed,
17
+ listPendingPairingRequests,
18
+ listStoredAllowedSenders,
19
+ parseAllowFromCsv,
20
+ parsePairingPolicy,
21
+ type PairingPolicy,
22
+ } from './pairing'
12
23
 
13
24
  /** Sentinel value agents return when no outbound reply should be sent */
14
25
  export const NO_MESSAGE_SENTINEL = 'NO_MESSAGE'
@@ -22,18 +33,25 @@ export function isNoMessage(text: string): boolean {
22
33
  * Stored on globalThis to survive HMR reloads in dev mode —
23
34
  * prevents duplicate sockets fighting for the same WhatsApp session. */
24
35
  const globalKey = '__swarmclaw_running_connectors__' as const
36
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
37
+ const g = globalThis as any
25
38
  const running: Map<string, ConnectorInstance> =
26
- (globalThis as any)[globalKey] ?? ((globalThis as any)[globalKey] = new Map<string, ConnectorInstance>())
39
+ g[globalKey] ?? (g[globalKey] = new Map<string, ConnectorInstance>())
27
40
 
28
41
  /** Most recent inbound channel per connector (used for proactive replies/default outbound target) */
29
42
  const lastInboundKey = '__swarmclaw_connector_last_inbound__' as const
30
43
  const lastInboundChannelByConnector: Map<string, string> =
31
- (globalThis as any)[lastInboundKey] ?? ((globalThis as any)[lastInboundKey] = new Map<string, string>())
44
+ g[lastInboundKey] ?? (g[lastInboundKey] = new Map<string, string>())
45
+
46
+ /** Last inbound message timestamp per connector (for presence indicators) */
47
+ const lastInboundTimeKey = '__swarmclaw_connector_last_inbound_time__' as const
48
+ const lastInboundTimeByConnector: Map<string, number> =
49
+ g[lastInboundTimeKey] ?? (g[lastInboundTimeKey] = new Map<string, number>())
32
50
 
33
51
  /** Per-connector lock to prevent concurrent start/stop operations */
34
52
  const lockKey = '__swarmclaw_connector_locks__' as const
35
53
  const locks: Map<string, Promise<void>> =
36
- (globalThis as any)[lockKey] ?? ((globalThis as any)[lockKey] = new Map<string, Promise<void>>())
54
+ g[lockKey] ?? (g[lockKey] = new Map<string, Promise<void>>())
37
55
 
38
56
  /** Get platform implementation lazily */
39
57
  export async function getPlatform(platform: string) {
@@ -43,6 +61,7 @@ export async function getPlatform(platform: string) {
43
61
  case 'slack': return (await import('./slack')).default
44
62
  case 'whatsapp': return (await import('./whatsapp')).default
45
63
  case 'openclaw': return (await import('./openclaw')).default
64
+ case 'bluebubbles': return (await import('./bluebubbles')).default
46
65
  case 'signal': return (await import('./signal')).default
47
66
  case 'teams': return (await import('./teams')).default
48
67
  case 'googlechat': return (await import('./googlechat')).default
@@ -78,19 +97,329 @@ export function formatInboundUserText(msg: InboundMessage): string {
78
97
  return lines.join('\n').trim()
79
98
  }
80
99
 
100
+ type ConnectorCommandName = 'help' | 'status' | 'new' | 'reset' | 'compact' | 'think' | 'pair'
101
+
102
+ interface ParsedConnectorCommand {
103
+ name: ConnectorCommandName
104
+ args: string
105
+ }
106
+
107
+ function parseConnectorCommand(text: string): ParsedConnectorCommand | null {
108
+ const trimmed = text.trim()
109
+ if (!trimmed.startsWith('/')) return null
110
+ const [head, ...rest] = trimmed.split(/\s+/)
111
+ const name = head.slice(1).toLowerCase()
112
+ const args = rest.join(' ').trim()
113
+ switch (name) {
114
+ case 'help':
115
+ case 'status':
116
+ case 'new':
117
+ case 'reset':
118
+ case 'compact':
119
+ case 'think':
120
+ case 'pair':
121
+ return { name, args } as ParsedConnectorCommand
122
+ default:
123
+ return null
124
+ }
125
+ }
126
+
127
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
128
+ function pushSessionMessage(session: Record<string, any>, role: 'user' | 'assistant', text: string): void {
129
+ if (!text.trim()) return
130
+ if (!Array.isArray(session.messages)) session.messages = []
131
+ session.messages.push({ role, text: text.trim(), time: Date.now() })
132
+ session.lastActiveAt = Date.now()
133
+ }
134
+
135
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
136
+ function persistSession(session: Record<string, any>): void {
137
+ const sessions = loadSessions()
138
+ sessions[session.id] = session
139
+ saveSessions(sessions)
140
+ notify(`messages:${session.id}`)
141
+ }
142
+
143
+ function summarizeForCompaction(messages: Array<{ role?: string; text?: string }>): string {
144
+ const preview = messages
145
+ .slice(-8)
146
+ .map((m, i) => {
147
+ const role = (m.role || 'unknown').toUpperCase()
148
+ const body = (m.text || '').replace(/\s+/g, ' ').trim()
149
+ const clipped = body.length > 180 ? `${body.slice(0, 177)}...` : body
150
+ return `${i + 1}. [${role}] ${clipped || '(no text)'}`
151
+ })
152
+ if (!preview.length) return 'No earlier messages to summarize.'
153
+ return preview.join('\n')
154
+ }
155
+
156
+ function resolvePairingAccess(connector: Connector, msg: InboundMessage): {
157
+ policy: PairingPolicy
158
+ configAllowFrom: string[]
159
+ isAllowed: boolean
160
+ hasAnyApprover: boolean
161
+ } {
162
+ const policy = parsePairingPolicy(connector.config?.dmPolicy, 'open')
163
+ const configAllowFrom = parseAllowFromCsv(connector.config?.allowFrom)
164
+ const stored = listStoredAllowedSenders(connector.id)
165
+ const isAllowed = isSenderAllowed({
166
+ connectorId: connector.id,
167
+ senderId: msg.senderId,
168
+ configAllowFrom,
169
+ })
170
+ return {
171
+ policy,
172
+ configAllowFrom,
173
+ isAllowed,
174
+ hasAnyApprover: (configAllowFrom.length + stored.length) > 0,
175
+ }
176
+ }
177
+
178
+ async function handlePairCommand(params: {
179
+ connector: Connector
180
+ msg: InboundMessage
181
+ args: string
182
+ }): Promise<string> {
183
+ const { connector, msg, args } = params
184
+ const access = resolvePairingAccess(connector, msg)
185
+ const parts = args.split(/\s+/).map((item) => item.trim()).filter(Boolean)
186
+ const subcommand = (parts[0] || 'status').toLowerCase()
187
+
188
+ if (subcommand === 'request') {
189
+ const request = createOrTouchPairingRequest({
190
+ connectorId: connector.id,
191
+ senderId: msg.senderId,
192
+ senderName: msg.senderName,
193
+ channelId: msg.channelId,
194
+ })
195
+ return request.created
196
+ ? `Pairing request created. Share this code with an approved user: ${request.code}`
197
+ : `Pairing request is already pending. Your code is: ${request.code}`
198
+ }
199
+
200
+ if (subcommand === 'list') {
201
+ if (access.hasAnyApprover && !access.isAllowed) {
202
+ return 'Pairing list is restricted to approved senders.'
203
+ }
204
+ const pending = listPendingPairingRequests(connector.id)
205
+ if (!pending.length) return 'No pending pairing requests.'
206
+ const lines = pending.slice(0, 20).map((entry) => {
207
+ const ageMin = Math.max(1, Math.round((Date.now() - entry.updatedAt) / 60_000))
208
+ const sender = entry.senderName ? `${entry.senderName} (${entry.senderId})` : entry.senderId
209
+ return `- ${entry.code} -> ${sender} (${ageMin}m ago)`
210
+ })
211
+ return `Pending pairing requests (${pending.length}):\n${lines.join('\n')}`
212
+ }
213
+
214
+ if (subcommand === 'approve') {
215
+ const code = (parts[1] || '').trim()
216
+ if (!code) return 'Usage: /pair approve <code>'
217
+ if (access.hasAnyApprover && !access.isAllowed) {
218
+ return 'Pairing approvals are restricted to approved senders.'
219
+ }
220
+ const approved = approvePairingCode(connector.id, code)
221
+ if (!approved.ok) return approved.reason || 'Pairing approval failed.'
222
+ const sender = approved.senderName ? `${approved.senderName} (${approved.senderId})` : approved.senderId
223
+ return `Pairing approved: ${sender}`
224
+ }
225
+
226
+ if (subcommand === 'allow') {
227
+ const senderId = (parts[1] || '').trim()
228
+ if (!senderId) return 'Usage: /pair allow <senderId>'
229
+ if (access.hasAnyApprover && !access.isAllowed) {
230
+ return 'Allowlist updates are restricted to approved senders.'
231
+ }
232
+ const result = addAllowedSender(connector.id, senderId)
233
+ if (!result.normalized) return 'Could not parse senderId.'
234
+ return result.added
235
+ ? `Allowed sender: ${result.normalized}`
236
+ : `Sender is already allowed: ${result.normalized}`
237
+ }
238
+
239
+ const pending = listPendingPairingRequests(connector.id)
240
+ const stored = listStoredAllowedSenders(connector.id)
241
+ const policyLine = `Policy: ${access.policy}`
242
+ const approvedLine = `You are ${access.isAllowed ? 'approved' : 'not approved'} as ${msg.senderId}`
243
+ return [
244
+ 'Pairing controls:',
245
+ policyLine,
246
+ approvedLine,
247
+ `- Stored approvals: ${stored.length}`,
248
+ `- Pending requests: ${pending.length}`,
249
+ '- Commands: /pair request, /pair list, /pair approve <code>, /pair allow <senderId>',
250
+ ].join('\n')
251
+ }
252
+
253
+ function enforceInboundAccessPolicy(connector: Connector, msg: InboundMessage): string | null {
254
+ if (msg.isGroup) return null
255
+ const { policy, configAllowFrom, isAllowed } = resolvePairingAccess(connector, msg)
256
+ const storedAllowFrom = listStoredAllowedSenders(connector.id)
257
+ if (policy === 'open') return null
258
+
259
+ if (policy === 'disabled') return NO_MESSAGE_SENTINEL
260
+ if (isAllowed) return null
261
+
262
+ if (policy === 'allowlist') {
263
+ if (!configAllowFrom.length && !storedAllowFrom.length) {
264
+ return 'This connector is set to allowlist mode, but no allowFrom entries are configured.'
265
+ }
266
+ return 'You are not authorized for this connector. Ask an approved user to add your sender ID via /pair allow <senderId>.'
267
+ }
268
+
269
+ if (policy === 'pairing') {
270
+ const request = createOrTouchPairingRequest({
271
+ connectorId: connector.id,
272
+ senderId: msg.senderId,
273
+ senderName: msg.senderName,
274
+ channelId: msg.channelId,
275
+ })
276
+ return [
277
+ 'Pairing is required before this connector will respond.',
278
+ `Your pairing code: ${request.code}`,
279
+ 'Ask an approved sender to run /pair approve <code>.',
280
+ 'Tip: if this is first-time setup with no approvals yet, run /pair approve <code> from this chat to bootstrap.',
281
+ ].join('\n')
282
+ }
283
+
284
+ return null
285
+ }
286
+
287
+ async function handleConnectorCommand(params: {
288
+ command: ParsedConnectorCommand
289
+ connector: Connector
290
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
291
+ session: Record<string, any>
292
+ msg: InboundMessage
293
+ agentName: string
294
+ }): Promise<string> {
295
+ const { command, connector, session, msg, agentName } = params
296
+ const inboundText = formatInboundUserText(msg)
297
+
298
+ if (command.name === 'help') {
299
+ const text = [
300
+ 'Connector commands:',
301
+ '/status — Show active session status',
302
+ '/new or /reset — Clear this connector conversation thread',
303
+ '/compact [keepLastN] — Summarize older history and keep recent messages (default 10)',
304
+ '/think <minimal|low|medium|high> — Set connector thread reasoning guidance',
305
+ '/pair — Pairing/access controls (status, request, list, approve, allow)',
306
+ '/help — Show this list',
307
+ ].join('\n')
308
+ pushSessionMessage(session, 'user', inboundText)
309
+ pushSessionMessage(session, 'assistant', text)
310
+ persistSession(session)
311
+ return text
312
+ }
313
+
314
+ if (command.name === 'status') {
315
+ const all = Array.isArray(session.messages) ? session.messages : []
316
+ const userCount = all.filter((m: { role?: string }) => m?.role === 'user').length
317
+ const assistantCount = all.filter((m: { role?: string }) => m?.role === 'assistant').length
318
+ const toolsCount = Array.isArray(session.tools) ? session.tools.length : 0
319
+ const statusText = [
320
+ `Status for ${connector.platform} / ${connector.name}:`,
321
+ `- Agent: ${agentName}`,
322
+ `- Session: ${session.id}`,
323
+ `- Model: ${session.provider}/${session.model}`,
324
+ `- Messages: ${all.length} (${userCount} user, ${assistantCount} assistant)`,
325
+ `- Tools enabled: ${toolsCount}`,
326
+ `- Channel: ${msg.channelName || msg.channelId}`,
327
+ `- Last active: ${new Date(session.lastActiveAt || session.createdAt || Date.now()).toLocaleString()}`,
328
+ ].join('\n')
329
+ pushSessionMessage(session, 'user', inboundText)
330
+ pushSessionMessage(session, 'assistant', statusText)
331
+ persistSession(session)
332
+ return statusText
333
+ }
334
+
335
+ if (command.name === 'new' || command.name === 'reset') {
336
+ const cleared = Array.isArray(session.messages) ? session.messages.length : 0
337
+ session.messages = []
338
+ session.claudeSessionId = null
339
+ session.codexThreadId = null
340
+ session.opencodeSessionId = null
341
+ session.delegateResumeIds = { claudeCode: null, codex: null, opencode: null }
342
+ session.lastActiveAt = Date.now()
343
+ persistSession(session)
344
+ return `Reset complete for ${connector.platform} channel thread. Cleared ${cleared} message(s).`
345
+ }
346
+
347
+ if (command.name === 'compact') {
348
+ const keepParsed = Number.parseInt(command.args, 10)
349
+ const keepLastN = Number.isFinite(keepParsed) ? Math.max(4, Math.min(50, keepParsed)) : 10
350
+ const history = Array.isArray(session.messages) ? session.messages : []
351
+ if (history.length <= keepLastN) {
352
+ const text = `Nothing to compact. Current history has ${history.length} message(s), keepLastN=${keepLastN}.`
353
+ pushSessionMessage(session, 'user', inboundText)
354
+ pushSessionMessage(session, 'assistant', text)
355
+ persistSession(session)
356
+ return text
357
+ }
358
+ const oldMessages = history.slice(0, -keepLastN)
359
+ const recentMessages = history.slice(-keepLastN)
360
+ const summary = summarizeForCompaction(oldMessages)
361
+ const summaryMessage = {
362
+ role: 'assistant' as const,
363
+ text: `[Context summary: compacted ${oldMessages.length} message(s)]\n${summary}`,
364
+ time: Date.now(),
365
+ kind: 'system' as const,
366
+ }
367
+ session.messages = [summaryMessage, ...recentMessages]
368
+ session.lastActiveAt = Date.now()
369
+ const text = `Compacted ${oldMessages.length} message(s). Kept ${recentMessages.length} recent message(s) plus a summary.`
370
+ pushSessionMessage(session, 'assistant', text)
371
+ persistSession(session)
372
+ return text
373
+ }
374
+
375
+ if (command.name === 'think') {
376
+ const requested = command.args.trim().toLowerCase()
377
+ const allowed = new Set(['minimal', 'low', 'medium', 'high'])
378
+ if (!requested) {
379
+ const current = typeof session.connectorThinkLevel === 'string' && allowed.has(session.connectorThinkLevel)
380
+ ? session.connectorThinkLevel
381
+ : 'medium'
382
+ const text = `Current /think level: ${current}. Usage: /think <minimal|low|medium|high>.`
383
+ pushSessionMessage(session, 'user', inboundText)
384
+ pushSessionMessage(session, 'assistant', text)
385
+ persistSession(session)
386
+ return text
387
+ }
388
+ if (!allowed.has(requested)) {
389
+ const text = 'Invalid /think level. Use one of: minimal, low, medium, high.'
390
+ pushSessionMessage(session, 'user', inboundText)
391
+ pushSessionMessage(session, 'assistant', text)
392
+ persistSession(session)
393
+ return text
394
+ }
395
+ session.connectorThinkLevel = requested
396
+ session.lastActiveAt = Date.now()
397
+ const text = `Set /think level to ${requested} for this connector thread.`
398
+ pushSessionMessage(session, 'user', inboundText)
399
+ pushSessionMessage(session, 'assistant', text)
400
+ persistSession(session)
401
+ return text
402
+ }
403
+
404
+ return 'Unknown command.'
405
+ }
406
+
81
407
  /** Route an inbound message through the assigned agent and return the response */
82
408
  async function routeMessage(connector: Connector, msg: InboundMessage): Promise<string> {
83
409
  if (msg?.channelId) {
84
410
  lastInboundChannelByConnector.set(connector.id, msg.channelId)
85
411
  }
412
+ lastInboundTimeByConnector.set(connector.id, Date.now())
86
413
 
87
414
  const agents = loadAgents()
88
- const agent = agents[connector.agentId]
415
+ const effectiveAgentId = msg.agentIdOverride || connector.agentId
416
+ const agent = agents[effectiveAgentId]
89
417
  if (!agent) return '[Error] Connector agent not found.'
90
418
 
91
419
  // Log connector trigger
92
420
  const triggerSessionKey = `connector:${connector.id}:${msg.channelId}`
93
421
  const allSessions = loadSessions()
422
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
94
423
  const existingSession = Object.values(allSessions).find((s: any) => s.name === triggerSessionKey)
95
424
  if (existingSession) {
96
425
  logExecution(existingSession.id, 'trigger', `${msg.platform} message from ${msg.senderName}`, {
@@ -120,9 +449,10 @@ async function routeMessage(connector: Connector, msg: InboundMessage): Promise<
120
449
  // Find or create a session keyed by platform + channel
121
450
  const sessionKey = `connector:${connector.id}:${msg.channelId}`
122
451
  const sessions = loadSessions()
452
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
123
453
  let session = Object.values(sessions).find((s: any) => s.name === sessionKey)
124
454
  if (!session) {
125
- const id = crypto.randomBytes(4).toString('hex')
455
+ const id = genId()
126
456
  session = {
127
457
  id,
128
458
  name: sessionKey,
@@ -151,6 +481,59 @@ async function routeMessage(connector: Connector, msg: InboundMessage): Promise<
151
481
  saveSessions(sessions)
152
482
  }
153
483
 
484
+ const parsedCommand = parseConnectorCommand(msg.text || '')
485
+ if (parsedCommand?.name === 'pair') {
486
+ const commandResult = await handlePairCommand({
487
+ connector,
488
+ msg,
489
+ args: parsedCommand.args,
490
+ })
491
+ logExecution(session.id, 'decision', 'Connector pair command handled', {
492
+ agentId: agent.id,
493
+ detail: {
494
+ platform: msg.platform,
495
+ channelId: msg.channelId,
496
+ command: 'pair',
497
+ args: parsedCommand.args || null,
498
+ },
499
+ })
500
+ return commandResult
501
+ }
502
+
503
+ const accessPolicyResult = enforceInboundAccessPolicy(connector, msg)
504
+ if (accessPolicyResult) {
505
+ logExecution(session.id, 'decision', 'Connector inbound blocked by access policy', {
506
+ agentId: agent.id,
507
+ detail: {
508
+ platform: msg.platform,
509
+ channelId: msg.channelId,
510
+ senderId: msg.senderId,
511
+ policy: parsePairingPolicy(connector.config?.dmPolicy, 'open'),
512
+ },
513
+ })
514
+ return accessPolicyResult
515
+ }
516
+
517
+ if (parsedCommand) {
518
+ const commandResult = await handleConnectorCommand({
519
+ command: parsedCommand,
520
+ connector,
521
+ session,
522
+ msg,
523
+ agentName: agent.name,
524
+ })
525
+ logExecution(session.id, 'decision', `Connector command handled: /${parsedCommand.name}`, {
526
+ agentId: agent.id,
527
+ detail: {
528
+ platform: msg.platform,
529
+ channelId: msg.channelId,
530
+ command: parsedCommand.name,
531
+ args: parsedCommand.args || null,
532
+ },
533
+ })
534
+ return commandResult
535
+ }
536
+
154
537
  // Build system prompt: [userPrompt] \n\n [soul] \n\n [systemPrompt]
155
538
  const settings = loadSettings()
156
539
  const promptParts: string[] = []
@@ -164,6 +547,12 @@ async function routeMessage(connector: Connector, msg: InboundMessage): Promise<
164
547
  if (skill?.content) promptParts.push(`## Skill: ${skill.name}\n${skill.content}`)
165
548
  }
166
549
  }
550
+ const thinkLevel = typeof session.connectorThinkLevel === 'string'
551
+ ? session.connectorThinkLevel.trim().toLowerCase()
552
+ : ''
553
+ if (thinkLevel) {
554
+ promptParts.push(`Connector thinking guidance: ${thinkLevel}. Keep responses concise and useful for chat.`)
555
+ }
167
556
  // Add connector context
168
557
  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.
169
558
 
@@ -210,9 +599,10 @@ The test: would a thoughtful friend feel compelled to type something back? If no
210
599
  // Use finalResponse for connectors — strips intermediate planning/tool-use text
211
600
  fullText = result.finalResponse
212
601
  console.log(`[connector] streamAgentChat returned ${result.fullText.length} chars total, ${fullText.length} chars final`)
213
- } catch (err: any) {
214
- console.error(`[connector] streamAgentChat error:`, err.message || err)
215
- return `[Error] ${err.message}`
602
+ } catch (err: unknown) {
603
+ const message = err instanceof Error ? err.message : String(err)
604
+ console.error(`[connector] streamAgentChat error:`, message)
605
+ return `[Error] ${message}`
216
606
  }
217
607
  } else {
218
608
  // Use the provider directly
@@ -326,6 +716,9 @@ async function _startConnectorImpl(connectorId: string): Promise<void> {
326
716
  if (!botToken && connector.config.botToken) {
327
717
  botToken = connector.config.botToken
328
718
  }
719
+ if (!botToken && connector.platform === 'bluebubbles' && connector.config.password) {
720
+ botToken = connector.config.password
721
+ }
329
722
 
330
723
  if (!botToken && connector.platform !== 'whatsapp' && connector.platform !== 'openclaw' && connector.platform !== 'signal') {
331
724
  throw new Error('No bot token configured')
@@ -347,10 +740,10 @@ async function _startConnectorImpl(connectorId: string): Promise<void> {
347
740
  notify('connectors')
348
741
 
349
742
  console.log(`[connector] Started ${connector.platform} connector: ${connector.name}`)
350
- } catch (err: any) {
743
+ } catch (err: unknown) {
351
744
  connector.status = 'error'
352
745
  connector.isEnabled = false
353
- connector.lastError = err.message
746
+ connector.lastError = err instanceof Error ? err.message : String(err)
354
747
  connector.updatedAt = Date.now()
355
748
  connectors[connectorId] = connector
356
749
  saveConnectors(connectors)
@@ -439,8 +832,8 @@ export async function autoStartConnectors(): Promise<void> {
439
832
  try {
440
833
  console.log(`[connector] Auto-starting ${connector.platform} connector: ${connector.name}`)
441
834
  await startConnector(connector.id)
442
- } catch (err: any) {
443
- console.error(`[connector] Failed to auto-start ${connector.name}:`, err.message)
835
+ } catch (err: unknown) {
836
+ console.error(`[connector] Failed to auto-start ${connector.name}:`, err instanceof Error ? err.message : err)
444
837
  }
445
838
  }
446
839
  }
@@ -475,6 +868,11 @@ export function listRunningConnectors(platform?: string): Array<{
475
868
  if (outboundJid) configuredTargets.push(outboundJid)
476
869
  const allowed = connector.config?.allowedJids?.split(',').map((s) => s.trim()).filter(Boolean) || []
477
870
  configuredTargets.push(...allowed)
871
+ } else if (connector.platform === 'bluebubbles') {
872
+ const outbound = connector.config?.outboundTarget?.trim()
873
+ if (outbound) configuredTargets.push(outbound)
874
+ const allowed = connector.config?.allowFrom?.split(',').map((s) => s.trim()).filter(Boolean) || []
875
+ configuredTargets.push(...allowed)
478
876
  }
479
877
  out.push({
480
878
  id,
@@ -494,6 +892,19 @@ export function getConnectorRecentChannelId(connectorId: string): string | null
494
892
  return lastInboundChannelByConnector.get(connectorId) || null
495
893
  }
496
894
 
895
+ /** Get presence info for a connector */
896
+ export function getConnectorPresence(connectorId: string): { lastMessageAt: number | null; channelId: string | null } {
897
+ return {
898
+ lastMessageAt: lastInboundTimeByConnector.get(connectorId) ?? null,
899
+ channelId: lastInboundChannelByConnector.get(connectorId) ?? null,
900
+ }
901
+ }
902
+
903
+ /** Get a running connector instance (internal use for rich messaging). */
904
+ export function getRunningInstance(connectorId: string): ConnectorInstance | undefined {
905
+ return running.get(connectorId)
906
+ }
907
+
497
908
  /**
498
909
  * Send an outbound message through a running connector.
499
910
  * Intended for proactive agent notifications (e.g. WhatsApp updates).
@@ -1,6 +1,6 @@
1
- import crypto from 'crypto'
2
1
  import fs from 'fs'
3
2
  import path from 'path'
3
+ import { genId } from '@/lib/id'
4
4
  import { UPLOAD_DIR } from '../storage'
5
5
  import type { InboundMedia, InboundMediaType } from './types'
6
6
 
@@ -94,7 +94,7 @@ export function saveInboundMediaBuffer(params: {
94
94
 
95
95
  const ext = extFromName(params.fileName) || extFromMime(params.mimeType) || '.bin'
96
96
  const base = safeBaseName(params.fileName)
97
- const unique = crypto.randomBytes(4).toString('hex')
97
+ const unique = genId()
98
98
  const filename = `${params.connectorId}-${Date.now()}-${base}-${unique}${ext}`
99
99
  const localPath = path.join(UPLOAD_DIR, filename)
100
100
  fs.writeFileSync(localPath, params.buffer)
@@ -827,6 +827,14 @@ const openclaw: PlatformConnector = {
827
827
  if (identity.deviceToken === normalized) return
828
828
  identity = { ...identity, deviceToken: normalized }
829
829
  persistIdentity(identityPath, identity)
830
+ // Cross-sync device token for provider identity resolution
831
+ if (normalized) {
832
+ try {
833
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
834
+ const { setSharedDeviceToken } = require('../openclaw-sync')
835
+ setSharedDeviceToken(normalized)
836
+ } catch { /* openclaw-sync not available */ }
837
+ }
830
838
  }
831
839
 
832
840
  function clearStaleTokenIfNeeded(reason?: string) {
@@ -928,6 +936,23 @@ const openclaw: PlatformConnector = {
928
936
  source: 'event' | 'history',
929
937
  ) {
930
938
  if (!matchesSessionKey(configuredSessionFilter, inbound.channelId)) return
939
+
940
+ // Multi-agent routing: match sessionKey against agentRouting config
941
+ const agentRouting = connector.config.agentRouting
942
+ if (agentRouting) {
943
+ try {
944
+ const routingMap: Record<string, string> = typeof agentRouting === 'string'
945
+ ? JSON.parse(agentRouting)
946
+ : agentRouting as unknown as Record<string, string>
947
+ for (const [pattern, agentId] of Object.entries(routingMap)) {
948
+ if (matchesSessionKey(pattern, inbound.channelId)) {
949
+ inbound.agentIdOverride = agentId
950
+ break
951
+ }
952
+ }
953
+ } catch { /* ignore malformed routing config */ }
954
+ }
955
+
931
956
  if (!rememberSeenEntry(seenInbound, dedupeKey, MAX_SEEN_CHAT_EVENTS)) return
932
957
 
933
958
  const now = Date.now()
@@ -1120,6 +1145,46 @@ const openclaw: PlatformConnector = {
1120
1145
  if (!connected) throw new Error('openclaw connector is not connected')
1121
1146
  await sendChat(channelId || defaultSessionKey, text, options)
1122
1147
  },
1148
+ async sendReaction(channelId, messageId, emoji) {
1149
+ if (!connected) throw new Error('openclaw connector is not connected')
1150
+ try {
1151
+ await rpcRequest('chat.react', { sessionKey: channelId || defaultSessionKey, messageId, emoji })
1152
+ } catch (err: unknown) {
1153
+ const msg = getErrorMessage(err)
1154
+ if (msg.toLowerCase().includes('unknown method')) return // graceful degrade
1155
+ throw err
1156
+ }
1157
+ },
1158
+ async editMessage(channelId, messageId, newText) {
1159
+ if (!connected) throw new Error('openclaw connector is not connected')
1160
+ try {
1161
+ await rpcRequest('chat.edit', { sessionKey: channelId || defaultSessionKey, messageId, text: newText })
1162
+ } catch (err: unknown) {
1163
+ const msg = getErrorMessage(err)
1164
+ if (msg.toLowerCase().includes('unknown method')) return
1165
+ throw err
1166
+ }
1167
+ },
1168
+ async deleteMessage(channelId, messageId) {
1169
+ if (!connected) throw new Error('openclaw connector is not connected')
1170
+ try {
1171
+ await rpcRequest('chat.delete', { sessionKey: channelId || defaultSessionKey, messageId })
1172
+ } catch (err: unknown) {
1173
+ const msg = getErrorMessage(err)
1174
+ if (msg.toLowerCase().includes('unknown method')) return
1175
+ throw err
1176
+ }
1177
+ },
1178
+ async pinMessage(channelId, messageId) {
1179
+ if (!connected) throw new Error('openclaw connector is not connected')
1180
+ try {
1181
+ await rpcRequest('chat.pin', { sessionKey: channelId || defaultSessionKey, messageId })
1182
+ } catch (err: unknown) {
1183
+ const msg = getErrorMessage(err)
1184
+ if (msg.toLowerCase().includes('unknown method')) return
1185
+ throw err
1186
+ }
1187
+ },
1123
1188
  async stop() {
1124
1189
  stopped = true
1125
1190
  cleanupSocket()