@swarmclawai/swarmclaw 0.6.7 → 0.7.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 (203) hide show
  1. package/README.md +82 -39
  2. package/next.config.ts +31 -6
  3. package/package.json +3 -2
  4. package/src/app/api/agents/[id]/thread/route.ts +1 -0
  5. package/src/app/api/agents/route.ts +19 -5
  6. package/src/app/api/approvals/route.ts +22 -0
  7. package/src/app/api/chatrooms/[id]/chat/route.ts +4 -0
  8. package/src/app/api/clawhub/install/route.ts +2 -2
  9. package/src/app/api/eval/run/route.ts +37 -0
  10. package/src/app/api/eval/scenarios/route.ts +24 -0
  11. package/src/app/api/eval/suite/route.ts +29 -0
  12. package/src/app/api/mcp-servers/[id]/conformance/route.ts +26 -0
  13. package/src/app/api/mcp-servers/[id]/invoke/route.ts +81 -0
  14. package/src/app/api/memory/graph/route.ts +46 -0
  15. package/src/app/api/memory/route.ts +36 -5
  16. package/src/app/api/notifications/route.ts +3 -0
  17. package/src/app/api/plugins/install/route.ts +57 -5
  18. package/src/app/api/plugins/marketplace/route.ts +73 -22
  19. package/src/app/api/plugins/route.ts +61 -1
  20. package/src/app/api/plugins/ui/route.ts +34 -0
  21. package/src/app/api/sessions/[id]/checkpoints/route.ts +31 -0
  22. package/src/app/api/sessions/[id]/restore/route.ts +36 -0
  23. package/src/app/api/settings/route.ts +62 -0
  24. package/src/app/api/setup/doctor/route.ts +22 -5
  25. package/src/app/api/souls/[id]/route.ts +65 -0
  26. package/src/app/api/souls/route.ts +70 -0
  27. package/src/app/api/tasks/[id]/approve/route.ts +4 -3
  28. package/src/app/api/tasks/[id]/route.ts +16 -3
  29. package/src/app/api/tasks/route.ts +10 -2
  30. package/src/app/api/usage/route.ts +9 -2
  31. package/src/app/globals.css +27 -0
  32. package/src/app/page.tsx +10 -5
  33. package/src/cli/index.js +37 -0
  34. package/src/components/activity/activity-feed.tsx +9 -2
  35. package/src/components/agents/agent-avatar.tsx +5 -1
  36. package/src/components/agents/agent-card.tsx +55 -9
  37. package/src/components/agents/agent-sheet.tsx +112 -34
  38. package/src/components/agents/inspector-panel.tsx +1 -1
  39. package/src/components/agents/soul-library-picker.tsx +84 -13
  40. package/src/components/auth/access-key-gate.tsx +63 -54
  41. package/src/components/auth/user-picker.tsx +37 -32
  42. package/src/components/chat/activity-moment.tsx +2 -0
  43. package/src/components/chat/chat-area.tsx +11 -0
  44. package/src/components/chat/chat-header.tsx +69 -25
  45. package/src/components/chat/chat-tool-toggles.tsx +2 -2
  46. package/src/components/chat/checkpoint-timeline.tsx +112 -0
  47. package/src/components/chat/code-block.tsx +3 -1
  48. package/src/components/chat/exec-approval-card.tsx +8 -1
  49. package/src/components/chat/message-bubble.tsx +164 -4
  50. package/src/components/chat/message-list.tsx +46 -4
  51. package/src/components/chat/session-approval-card.tsx +80 -0
  52. package/src/components/chat/session-debug-panel.tsx +106 -84
  53. package/src/components/chat/streaming-bubble.tsx +6 -5
  54. package/src/components/chat/task-approval-card.tsx +78 -0
  55. package/src/components/chat/thinking-indicator.tsx +48 -12
  56. package/src/components/chat/tool-call-bubble.tsx +3 -0
  57. package/src/components/chat/tool-request-banner.tsx +39 -20
  58. package/src/components/chatrooms/chatroom-list.tsx +11 -4
  59. package/src/components/chatrooms/chatroom-sheet.tsx +7 -2
  60. package/src/components/connectors/connector-list.tsx +33 -11
  61. package/src/components/connectors/connector-sheet.tsx +37 -7
  62. package/src/components/home/home-view.tsx +54 -24
  63. package/src/components/input/chat-input.tsx +22 -1
  64. package/src/components/knowledge/knowledge-list.tsx +17 -18
  65. package/src/components/knowledge/knowledge-sheet.tsx +9 -5
  66. package/src/components/layout/app-layout.tsx +87 -19
  67. package/src/components/mcp-servers/mcp-server-list.tsx +352 -50
  68. package/src/components/mcp-servers/mcp-server-sheet.tsx +25 -9
  69. package/src/components/memory/memory-browser.tsx +73 -45
  70. package/src/components/memory/memory-graph-view.tsx +203 -0
  71. package/src/components/memory/memory-list.tsx +20 -13
  72. package/src/components/plugins/plugin-list.tsx +214 -60
  73. package/src/components/plugins/plugin-sheet.tsx +119 -24
  74. package/src/components/projects/project-list.tsx +17 -9
  75. package/src/components/providers/provider-list.tsx +21 -6
  76. package/src/components/providers/provider-sheet.tsx +42 -25
  77. package/src/components/runs/run-list.tsx +17 -13
  78. package/src/components/schedules/schedule-card.tsx +10 -3
  79. package/src/components/schedules/schedule-list.tsx +2 -2
  80. package/src/components/schedules/schedule-sheet.tsx +28 -9
  81. package/src/components/secrets/secret-sheet.tsx +7 -2
  82. package/src/components/secrets/secrets-list.tsx +18 -5
  83. package/src/components/sessions/new-session-sheet.tsx +183 -376
  84. package/src/components/sessions/session-card.tsx +10 -2
  85. package/src/components/settings/gateway-connection-panel.tsx +9 -8
  86. package/src/components/shared/command-palette.tsx +13 -5
  87. package/src/components/shared/empty-state.tsx +20 -8
  88. package/src/components/shared/hint-tip.tsx +31 -0
  89. package/src/components/shared/notification-center.tsx +134 -86
  90. package/src/components/shared/profile-sheet.tsx +4 -0
  91. package/src/components/shared/settings/plugin-manager.tsx +360 -135
  92. package/src/components/shared/settings/section-capability-policy.tsx +3 -3
  93. package/src/components/shared/settings/section-runtime-loop.tsx +149 -4
  94. package/src/components/skills/clawhub-browser.tsx +1 -0
  95. package/src/components/skills/skill-list.tsx +31 -12
  96. package/src/components/skills/skill-sheet.tsx +20 -7
  97. package/src/components/tasks/approvals-panel.tsx +224 -0
  98. package/src/components/tasks/task-board.tsx +20 -12
  99. package/src/components/tasks/task-card.tsx +21 -7
  100. package/src/components/tasks/task-column.tsx +4 -3
  101. package/src/components/tasks/task-list.tsx +1 -1
  102. package/src/components/tasks/task-sheet.tsx +130 -1
  103. package/src/components/ui/dialog.tsx +1 -0
  104. package/src/components/ui/sheet.tsx +1 -0
  105. package/src/components/usage/metrics-dashboard.tsx +72 -48
  106. package/src/components/wallets/wallet-panel.tsx +65 -41
  107. package/src/components/wallets/wallet-section.tsx +9 -3
  108. package/src/components/webhooks/webhook-list.tsx +21 -12
  109. package/src/components/webhooks/webhook-sheet.tsx +13 -3
  110. package/src/lib/approval-display.test.ts +45 -0
  111. package/src/lib/approval-display.ts +62 -0
  112. package/src/lib/clipboard.ts +38 -0
  113. package/src/lib/memory.ts +8 -0
  114. package/src/lib/providers/claude-cli.ts +5 -3
  115. package/src/lib/providers/index.ts +67 -21
  116. package/src/lib/runtime-loop.ts +3 -2
  117. package/src/lib/server/approvals.ts +150 -0
  118. package/src/lib/server/chat-execution.ts +319 -74
  119. package/src/lib/server/chatroom-helpers.ts +63 -5
  120. package/src/lib/server/chatroom-orchestration.ts +74 -0
  121. package/src/lib/server/clawhub-client.ts +82 -6
  122. package/src/lib/server/connectors/manager.ts +27 -1
  123. package/src/lib/server/context-manager.ts +132 -50
  124. package/src/lib/server/cost.test.ts +73 -0
  125. package/src/lib/server/cost.ts +165 -34
  126. package/src/lib/server/daemon-state.ts +112 -1
  127. package/src/lib/server/data-dir.ts +18 -1
  128. package/src/lib/server/eval/runner.ts +126 -0
  129. package/src/lib/server/eval/scenarios.ts +218 -0
  130. package/src/lib/server/eval/scorer.ts +96 -0
  131. package/src/lib/server/eval/store.ts +37 -0
  132. package/src/lib/server/eval/types.ts +48 -0
  133. package/src/lib/server/execution-log.ts +12 -8
  134. package/src/lib/server/guardian.ts +34 -0
  135. package/src/lib/server/heartbeat-service.ts +53 -1
  136. package/src/lib/server/integrity-monitor.ts +208 -0
  137. package/src/lib/server/langgraph-checkpoint.ts +10 -0
  138. package/src/lib/server/link-understanding.ts +55 -0
  139. package/src/lib/server/llm-response-cache.test.ts +102 -0
  140. package/src/lib/server/llm-response-cache.ts +227 -0
  141. package/src/lib/server/main-agent-loop.ts +115 -16
  142. package/src/lib/server/main-session.ts +6 -3
  143. package/src/lib/server/mcp-conformance.test.ts +18 -0
  144. package/src/lib/server/mcp-conformance.ts +233 -0
  145. package/src/lib/server/memory-db.ts +193 -19
  146. package/src/lib/server/memory-retrieval.test.ts +56 -0
  147. package/src/lib/server/mmr.ts +73 -0
  148. package/src/lib/server/orchestrator-lg.ts +7 -1
  149. package/src/lib/server/orchestrator.ts +4 -3
  150. package/src/lib/server/plugins.ts +662 -132
  151. package/src/lib/server/process-manager.ts +18 -0
  152. package/src/lib/server/query-expansion.ts +57 -0
  153. package/src/lib/server/queue.ts +280 -11
  154. package/src/lib/server/runtime-settings.ts +9 -0
  155. package/src/lib/server/session-run-manager.test.ts +23 -0
  156. package/src/lib/server/session-run-manager.ts +32 -2
  157. package/src/lib/server/session-tools/canvas.ts +85 -50
  158. package/src/lib/server/session-tools/chatroom.ts +130 -127
  159. package/src/lib/server/session-tools/connector.ts +233 -454
  160. package/src/lib/server/session-tools/context-mgmt.ts +87 -105
  161. package/src/lib/server/session-tools/crud.ts +84 -7
  162. package/src/lib/server/session-tools/delegate.ts +351 -752
  163. package/src/lib/server/session-tools/discovery.ts +198 -0
  164. package/src/lib/server/session-tools/edit_file.ts +82 -0
  165. package/src/lib/server/session-tools/file-send.test.ts +39 -0
  166. package/src/lib/server/session-tools/file.ts +257 -425
  167. package/src/lib/server/session-tools/git.ts +87 -47
  168. package/src/lib/server/session-tools/http.ts +95 -33
  169. package/src/lib/server/session-tools/index.ts +217 -138
  170. package/src/lib/server/session-tools/memory.ts +154 -239
  171. package/src/lib/server/session-tools/monitor.ts +126 -0
  172. package/src/lib/server/session-tools/normalize-tool-args.test.ts +61 -0
  173. package/src/lib/server/session-tools/normalize-tool-args.ts +48 -0
  174. package/src/lib/server/session-tools/openclaw-nodes.ts +82 -99
  175. package/src/lib/server/session-tools/openclaw-workspace.ts +103 -93
  176. package/src/lib/server/session-tools/platform.ts +86 -0
  177. package/src/lib/server/session-tools/plugin-creator.ts +239 -0
  178. package/src/lib/server/session-tools/sample-ui.ts +97 -0
  179. package/src/lib/server/session-tools/sandbox.ts +175 -148
  180. package/src/lib/server/session-tools/schedule.ts +78 -0
  181. package/src/lib/server/session-tools/session-info.ts +104 -410
  182. package/src/lib/server/session-tools/shell-normalize.test.ts +43 -0
  183. package/src/lib/server/session-tools/shell.ts +171 -143
  184. package/src/lib/server/session-tools/subagent.ts +77 -77
  185. package/src/lib/server/session-tools/wallet.ts +182 -106
  186. package/src/lib/server/session-tools/web.ts +181 -327
  187. package/src/lib/server/storage.ts +36 -0
  188. package/src/lib/server/stream-agent-chat.ts +348 -242
  189. package/src/lib/server/task-quality-gate.test.ts +44 -0
  190. package/src/lib/server/task-quality-gate.ts +67 -0
  191. package/src/lib/server/task-validation.test.ts +78 -0
  192. package/src/lib/server/task-validation.ts +67 -2
  193. package/src/lib/server/tool-aliases.ts +68 -0
  194. package/src/lib/server/tool-capability-policy.ts +24 -5
  195. package/src/lib/server/tool-retry.ts +62 -0
  196. package/src/lib/server/transcript-repair.ts +72 -0
  197. package/src/lib/setup-defaults.ts +1 -0
  198. package/src/lib/tasks.ts +7 -1
  199. package/src/lib/tool-definitions.ts +24 -23
  200. package/src/lib/validation/schemas.ts +13 -0
  201. package/src/lib/view-routes.ts +2 -23
  202. package/src/stores/use-app-store.ts +23 -1
  203. package/src/types/index.ts +155 -10
@@ -6,6 +6,9 @@ import { loadConnectors, loadSettings, UPLOAD_DIR } from '../storage'
6
6
  import { genId } from '@/lib/id'
7
7
  import { synthesizeElevenLabsMp3 } from '../elevenlabs'
8
8
  import type { ToolBuildContext } from './context'
9
+ import type { Plugin, PluginHooks } from '@/types'
10
+ import { getPluginManager } from '../plugins'
11
+ import { normalizeToolInputArgs } from './normalize-tool-args'
9
12
 
10
13
  const CONNECTOR_ACTION_DEDUPE_TTL_MS = 30_000
11
14
  const CONNECTOR_TURN_SEND_TTL_MS = 180_000
@@ -62,7 +65,8 @@ function isAutonomousSystemTurn(userText: string): boolean {
62
65
  || text.includes('SWARM_HEARTBEAT_CHECK')
63
66
  }
64
67
 
65
- function isSignificantOutreachText(raw: string): boolean {
68
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
69
+ function _isSignificantOutreachText(raw: string): boolean {
66
70
  const text = (raw || '').trim().toLowerCase()
67
71
  if (text.length < 12) return false
68
72
  if (/\b(just checking in|checking in|touching base|quick check-in|hope you'?re well|any updates\??)\b/.test(text)) {
@@ -71,7 +75,8 @@ function isSignificantOutreachText(raw: string): boolean {
71
75
  return /\b(completed|complete|done|finished|failed|failure|error|blocked|urgent|important|deadline|overdue|incident|warning|reminder|birthday|anniversary|milestone|congrats|congratulations|celebrate|payment|invoice|appointment|meeting)\b/.test(text)
72
76
  }
73
77
 
74
- function isUrgentOutreachText(raw: string): boolean {
78
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
79
+ function _isUrgentOutreachText(raw: string): boolean {
75
80
  const text = (raw || '').toLowerCase()
76
81
  return /\b(urgent|immediately|asap|critical|incident|outage|failed|failure|blocked|overdue|deadline)\b/.test(text)
77
82
  }
@@ -111,7 +116,6 @@ function resolveUploadUrl(url: string | undefined): { mediaPath: string; mimeTyp
111
116
  if (!url) return null
112
117
  const match = url.match(/^\/api\/uploads\/([^?#]+)/)
113
118
  if (!match) return null
114
- // Decode URL-encoded filenames (e.g. from encodeURIComponent) before sanitizing
115
119
  let decoded: string
116
120
  try { decoded = decodeURIComponent(match[1]) } catch { decoded = match[1] }
117
121
  const safeName = decoded.replace(/[^a-zA-Z0-9._-]/g, '')
@@ -234,458 +238,233 @@ function resolveConnectorMediaInput(params: {
234
238
  }
235
239
  }
236
240
 
237
- export function buildConnectorTools(bctx: ToolBuildContext): StructuredToolInterface[] {
238
- const tools: StructuredToolInterface[] = []
239
- const { hasTool } = bctx
240
-
241
- if (hasTool('manage_connectors')) {
242
- const settings = loadSettings()
243
- const hasElevenLabsKey = !!String(settings.elevenLabsApiKey || process.env.ELEVENLABS_API_KEY || '').trim()
244
- const voiceNoteToolEnabled = settings.elevenLabsEnabled === true && hasElevenLabsKey
245
- const actionSchema = voiceNoteToolEnabled
246
- ? z.enum([
247
- 'list_running',
248
- 'list_targets',
249
- 'start',
250
- 'stop',
251
- 'send',
252
- 'send_voice_note',
253
- 'schedule_followup',
254
- 'message_react',
255
- 'message_edit',
256
- 'message_delete',
257
- 'message_pin',
258
- ] as const)
259
- : z.enum([
260
- 'list_running',
261
- 'list_targets',
262
- 'start',
263
- 'stop',
264
- 'send',
265
- 'schedule_followup',
266
- 'message_react',
267
- 'message_edit',
268
- 'message_delete',
269
- 'message_pin',
270
- ] as const)
271
- tools.push(
272
- tool(
273
- async ({
274
- action,
275
- connectorId,
276
- platform,
277
- to,
278
- message,
279
- voiceText,
280
- voiceId,
281
- imageUrl,
282
- fileUrl,
283
- mediaPath,
284
- mimeType,
285
- fileName,
286
- caption,
287
- delaySec,
288
- followUpMessage,
289
- followUpDelaySec,
290
- approved,
291
- ptt,
292
- }) => {
293
- try {
294
- const actionName = String(action)
295
- const { listRunningConnectors, sendConnectorMessage, getConnectorRecentChannelId, scheduleConnectorFollowUp } = await import('../connectors/manager')
296
- const running = listRunningConnectors(platform || undefined)
297
-
298
- if (actionName === 'list_running' || actionName === 'list_targets') {
299
- return JSON.stringify(running)
300
- }
301
-
302
- if (actionName === 'start') {
303
- if (!connectorId) {
304
- // If no ID given, list available connectors to start
305
- const allConnectors = loadConnectors()
306
- const stopped = Object.values(allConnectors)
307
- .filter((c) => !platform || c.platform === platform)
308
- .filter((c) => !running.find((r) => r.id === c.id))
309
- .map((c) => ({ id: c.id, name: c.name, platform: c.platform }))
310
- if (!stopped.length) return 'All connectors are already running.'
311
- return `Error: connectorId is required. Stopped connectors available to start: ${JSON.stringify(stopped)}`
312
- }
313
- const { startConnector: doStart } = await import('../connectors/manager')
314
- await doStart(connectorId)
315
- return JSON.stringify({ status: 'started', connectorId })
316
- }
317
-
318
- if (actionName === 'stop') {
319
- if (!connectorId) return 'Error: connectorId is required for stop action.'
320
- const { stopConnector: doStop } = await import('../connectors/manager')
321
- await doStop(connectorId)
322
- return JSON.stringify({ status: 'stopped', connectorId })
323
- }
324
-
325
- const resolveSelectedConnector = () => {
326
- if (!running.length) {
327
- const allConnectors = loadConnectors()
328
- const configured = Object.values(allConnectors)
329
- .filter((c) => !platform || c.platform === platform)
330
- .map((c) => ({ id: c.id, name: c.name, platform: c.platform, agentId: c.agentId || null }))
331
- if (configured.length) {
332
- return {
333
- error: `Error: no running connectors${platform ? ` for platform "${platform}"` : ''}, but ${configured.length} configured connector(s) found: ${JSON.stringify(configured)}. These connectors exist but are not currently started. Ask the user if they'd like you to start one (use action "start" with the connectorId), then retry the send.`,
334
- }
335
- }
336
- return {
337
- error: `Error: no running connectors${platform ? ` for platform "${platform}"` : ''}. No connectors are configured for this platform either — the user needs to set one up in the Connectors panel first.`,
338
- }
339
- }
340
- const selected = connectorId
341
- ? running.find((c) => c.id === connectorId)
342
- : running[0]
343
- if (!selected) return { error: `Error: running connector not found: ${connectorId}` }
344
- const connectors = loadConnectors()
345
- const connector = connectors[selected.id]
346
- if (!connector) return { error: `Error: connector not found: ${selected.id}` }
347
- return { selected, connector }
348
- }
349
-
350
- if (actionName === 'send' || actionName === 'send_voice_note' || actionName === 'schedule_followup') {
351
- const settings = loadSettings()
352
- if (settings.safetyRequireApprovalForOutbound === true && approved !== true) {
353
- return 'Error: outbound connector sends require explicit approval. Re-run with approved=true after user confirmation.'
354
- }
355
- const now = Date.now()
356
- pruneOldConnectorToolState(now)
357
- const resolved = resolveSelectedConnector()
358
- if ('error' in resolved) return resolved.error
359
- const { selected, connector } = resolved
360
-
361
- const target = pickChannelTarget({
362
- connector,
363
- to,
364
- recentChannelId: getConnectorRecentChannelId(selected.id),
365
- })
366
- if (target.error) return target.error
367
-
368
- let channelId = target.channelId
369
- if (connector.platform === 'whatsapp') channelId = normalizeWhatsAppTarget(channelId)
370
-
371
- const currentSession = bctx.resolveCurrentSession()
372
- const latestUserTurn = parseLatestUserTurn(currentSession)
373
- const sessionId = bctx.ctx?.sessionId || currentSession?.id || 'unknown-session'
374
- const turnKey = buildConnectorActionKey([sessionId, latestUserTurn.time || 'no-user-turn'])
375
- const multiOutboundAllowed = userExplicitlyWantsMultipleOutbound(latestUserTurn.text)
376
- const followupExplicitlyRequested = userExplicitlyRequestedFollowup(latestUserTurn.text)
377
- const autonomousTurn = isAutonomousSystemTurn(latestUserTurn.text)
378
- const existingBudget = connectorTurnSendBudget.get(turnKey)
379
- if (
380
- !multiOutboundAllowed
381
- && existingBudget
382
- && now - existingBudget.at <= CONNECTOR_TURN_SEND_TTL_MS
383
- && existingBudget.count >= 1
384
- ) {
385
- if (existingBudget.lastResult) {
386
- return normalizeDedupedReplayResult(existingBudget.lastResult, {
387
- connectorId: selected.id,
388
- platform: selected.platform,
389
- to: channelId,
390
- })
391
- }
392
- return JSON.stringify({
393
- status: 'sent',
394
- connectorId: selected.id,
395
- platform: selected.platform,
396
- to: channelId,
397
- deduped: true,
398
- })
399
- }
400
-
401
- if (actionName === 'send_voice_note') {
402
- if (!voiceNoteToolEnabled) {
403
- return 'Error: send_voice_note is unavailable. Enable ElevenLabs in Settings > Voice and set a valid API key.'
404
- }
405
- const ttsText = (voiceText || message || '').trim()
406
- if (!ttsText) return 'Error: voiceText or message is required for send_voice_note action.'
407
- const voiceActionKey = buildConnectorActionKey([
408
- sessionId,
409
- actionName,
410
- selected.id,
411
- channelId,
412
- ttsText,
413
- voiceId?.trim() || '',
414
- fileName?.trim() || '',
415
- caption?.trim() || '',
416
- ptt ?? true,
417
- ])
418
- const cachedVoice = recentConnectorActionCache.get(voiceActionKey)
419
- if (cachedVoice && now - cachedVoice.at <= CONNECTOR_ACTION_DEDUPE_TTL_MS) {
420
- return cachedVoice.result
421
- }
422
- const audioBuffer = await synthesizeElevenLabsMp3({ text: ttsText, voiceId: voiceId?.trim() || undefined })
423
- const voiceFileName = `${Date.now()}-${genId()}-voicenote.mp3`
424
- const voicePath = path.join(UPLOAD_DIR, voiceFileName)
425
- fs.writeFileSync(voicePath, audioBuffer)
426
-
427
- const sent = await sendConnectorMessage({
428
- connectorId: selected.id,
429
- channelId,
430
- text: '',
431
- mediaPath: voicePath,
432
- mimeType: 'audio/mpeg',
433
- fileName: fileName?.trim() || 'voicenote.mp3',
434
- caption: caption?.trim() || undefined,
435
- ptt: ptt ?? true,
436
- })
437
- const result = JSON.stringify({
438
- status: 'voice_sent',
439
- connectorId: sent.connectorId,
440
- platform: sent.platform,
441
- to: sent.channelId,
442
- messageId: sent.messageId || null,
443
- voiceFile: voicePath,
444
- })
445
- connectorTurnSendBudget.set(turnKey, {
446
- count: (existingBudget?.count || 0) + 1,
447
- at: now,
448
- lastResult: result,
449
- })
450
- recentConnectorActionCache.set(voiceActionKey, { at: now, result })
451
- return result
452
- }
453
-
454
- const media = resolveConnectorMediaInput({
455
- cwd: bctx.cwd,
456
- mediaPath,
457
- imageUrl,
458
- fileUrl,
459
- })
460
- if (media.error) return media.error
461
-
462
- const hasText = !!message?.trim()
463
- const hasMedia = !!media.mediaPath || !!media.imageUrl || !!media.fileUrl
464
- if (actionName === 'send' && !hasText && !hasMedia) {
465
- return 'Error: message, media URL, or mediaPath is required for send action.'
466
- }
467
-
468
- let followUpText = followUpMessage?.trim() || ''
469
- const followDelaySec = Number.isFinite(followUpDelaySec) ? Number(followUpDelaySec) : 300
470
-
471
- const proactivePayload = followUpText || message?.trim() || ''
472
- const significantAutonomousOutreach = autonomousTurn && isSignificantOutreachText(proactivePayload)
473
- const urgentAutonomousOutreach = autonomousTurn && isUrgentOutreachText(proactivePayload)
474
- const outreachBudgetKey = buildConnectorActionKey([selected.id, channelId])
475
- const priorAutonomousOutreach = autonomousOutreachBudget.get(outreachBudgetKey)
476
- if (
477
- autonomousTurn
478
- && significantAutonomousOutreach
479
- && priorAutonomousOutreach
480
- && !urgentAutonomousOutreach
481
- && now - priorAutonomousOutreach.at <= AUTONOMOUS_OUTREACH_COOLDOWN_MS
482
- ) {
483
- if (priorAutonomousOutreach.result) {
484
- return normalizeDedupedReplayResult(priorAutonomousOutreach.result, {
485
- connectorId: selected.id,
486
- platform: selected.platform,
487
- to: channelId,
488
- })
489
- }
490
- return JSON.stringify({
491
- status: 'sent',
492
- connectorId: selected.id,
493
- platform: selected.platform,
494
- to: channelId,
495
- deduped: true,
496
- })
497
- }
498
-
499
- if (followUpText && !followupExplicitlyRequested && !significantAutonomousOutreach) {
500
- followUpText = ''
501
- }
502
-
503
- if (actionName === 'schedule_followup') {
504
- if (!followupExplicitlyRequested && !significantAutonomousOutreach) {
505
- return 'Error: schedule_followup requires either an explicit user request or a significant autonomous event.'
506
- }
507
- const payload = followUpText || message?.trim() || ''
508
- if (!payload) return 'Error: followUpMessage or message is required for schedule_followup action.'
509
- const scheduleActionKey = buildConnectorActionKey([
510
- sessionId,
511
- actionName,
512
- selected.id,
513
- channelId,
514
- payload,
515
- Number.isFinite(delaySec) ? Number(delaySec) : followDelaySec,
516
- ])
517
- const cachedSchedule = recentConnectorActionCache.get(scheduleActionKey)
518
- if (cachedSchedule && now - cachedSchedule.at <= CONNECTOR_ACTION_DEDUPE_TTL_MS) {
519
- return cachedSchedule.result
520
- }
521
- const scheduled = scheduleConnectorFollowUp({
522
- connectorId: selected.id,
523
- channelId,
524
- text: payload,
525
- delaySec: Number.isFinite(delaySec) ? Number(delaySec) : followDelaySec,
526
- })
527
- const result = JSON.stringify({
528
- status: 'followup_scheduled',
529
- connectorId: selected.id,
530
- platform: selected.platform,
531
- to: channelId,
532
- followUpId: scheduled.followUpId,
533
- sendAt: scheduled.sendAt,
534
- })
535
- connectorTurnSendBudget.set(turnKey, {
536
- count: (existingBudget?.count || 0) + 1,
537
- at: now,
538
- lastResult: result,
539
- })
540
- if (autonomousTurn && significantAutonomousOutreach) {
541
- autonomousOutreachBudget.set(outreachBudgetKey, { at: now, result })
542
- }
543
- recentConnectorActionCache.set(scheduleActionKey, { at: now, result })
544
- return result
545
- }
546
-
547
- const sendActionKey = buildConnectorActionKey([
548
- sessionId,
549
- actionName,
550
- selected.id,
551
- channelId,
552
- message?.trim() || '',
553
- media.mediaPath || '',
554
- media.imageUrl || '',
555
- media.fileUrl || '',
556
- mimeType?.trim() || '',
557
- fileName?.trim() || '',
558
- caption?.trim() || '',
559
- ptt ?? '',
560
- followUpText,
561
- followDelaySec,
562
- ])
563
- const cachedSend = recentConnectorActionCache.get(sendActionKey)
564
- if (cachedSend && now - cachedSend.at <= CONNECTOR_ACTION_DEDUPE_TTL_MS) {
565
- return cachedSend.result
566
- }
567
-
568
- const sent = await sendConnectorMessage({
569
- connectorId: selected.id,
570
- channelId,
571
- text: message?.trim() || '',
572
- imageUrl: media.imageUrl,
573
- fileUrl: media.fileUrl,
574
- mediaPath: media.mediaPath,
575
- mimeType: mimeType?.trim() || undefined,
576
- fileName: fileName?.trim() || undefined,
577
- caption: caption?.trim() || undefined,
578
- ptt: ptt ?? undefined,
579
- })
580
-
581
- let followup: { followUpId: string; sendAt: number } | null = null
582
- if (followUpText) {
583
- followup = scheduleConnectorFollowUp({
584
- connectorId: selected.id,
585
- channelId,
586
- text: followUpText,
587
- delaySec: followDelaySec,
588
- })
589
- }
590
-
591
- const result = JSON.stringify({
592
- status: 'sent',
593
- connectorId: sent.connectorId,
594
- platform: sent.platform,
595
- to: sent.channelId,
596
- messageId: sent.messageId || null,
597
- ...(followup
598
- ? {
599
- followUpId: followup.followUpId,
600
- followUpSendAt: followup.sendAt,
601
- }
602
- : {}),
603
- })
604
- connectorTurnSendBudget.set(turnKey, {
605
- count: (existingBudget?.count || 0) + 1,
606
- at: now,
607
- lastResult: result,
608
- })
609
- if (autonomousTurn && significantAutonomousOutreach) {
610
- autonomousOutreachBudget.set(outreachBudgetKey, { at: now, result })
611
- }
612
- recentConnectorActionCache.set(sendActionKey, { at: now, result })
613
- return result
614
- }
615
-
616
- if (actionName === 'message_react' || actionName === 'message_edit' || actionName === 'message_pin' || actionName === 'message_delete') {
617
- if (!connectorId) return 'Error: connectorId is required for rich messaging actions.'
618
- const { getRunningInstance } = await import('../connectors/manager')
619
- const inst = getRunningInstance(connectorId)
620
- if (!inst) return `Error: connector "${connectorId}" is not running.`
621
-
622
- const targetChannel = to?.trim() || ''
623
- const targetMessageId = message?.trim() || ''
624
- if (!targetMessageId) return 'Error: message parameter (used as messageId) is required for rich messaging actions.'
625
-
626
- try {
627
- if (actionName === 'message_react') {
628
- if (!inst.sendReaction) return 'Error: this connector does not support reactions.'
629
- const emoji = caption?.trim() || '👍'
630
- await inst.sendReaction(targetChannel, targetMessageId, emoji)
631
- return JSON.stringify({ status: 'reacted', connectorId, messageId: targetMessageId, emoji })
632
- }
633
- if (actionName === 'message_edit') {
634
- if (!inst.editMessage) return 'Error: this connector does not support message editing.'
635
- const newText = caption?.trim() || ''
636
- if (!newText) return 'Error: caption (new text) is required for message_edit.'
637
- await inst.editMessage(targetChannel, targetMessageId, newText)
638
- return JSON.stringify({ status: 'edited', connectorId, messageId: targetMessageId })
639
- }
640
- if (actionName === 'message_delete') {
641
- if (!inst.deleteMessage) return 'Error: this connector does not support message deletion.'
642
- await inst.deleteMessage(targetChannel, targetMessageId)
643
- return JSON.stringify({ status: 'deleted', connectorId, messageId: targetMessageId })
644
- }
645
- if (actionName === 'message_pin') {
646
- if (!inst.pinMessage) return 'Error: this connector does not support message pinning.'
647
- await inst.pinMessage(targetChannel, targetMessageId)
648
- return JSON.stringify({ status: 'pinned', connectorId, messageId: targetMessageId })
649
- }
650
- } catch (err: unknown) {
651
- return `Error: ${err instanceof Error ? err.message : String(err)}`
652
- }
653
- }
654
-
655
- return 'Unknown action. Use list_running, list_targets, start, stop, send, send_voice_note, schedule_followup, or message_* actions.'
656
- } catch (err: unknown) {
657
- return `Error: ${err instanceof Error ? err.message : String(err)}`
241
+ /**
242
+ * Core Connector Execution Logic
243
+ */
244
+ interface ConnectorActionInput {
245
+ action?: string
246
+ connectorId?: string
247
+ platform?: string
248
+ to?: string
249
+ message?: string
250
+ voiceText?: string
251
+ voiceId?: string
252
+ imageUrl?: string
253
+ fileUrl?: string
254
+ mediaPath?: string
255
+ mimeType?: string
256
+ fileName?: string
257
+ caption?: string
258
+ delaySec?: number
259
+ followUpMessage?: string
260
+ followUpDelaySec?: number
261
+ approved?: boolean
262
+ ptt?: boolean
263
+ }
264
+
265
+ interface ConnectorActionContext {
266
+ cwd: string
267
+ resolveCurrentSession?: () => { messages?: Array<Record<string, unknown>>; id?: string } | null
268
+ ctx?: { sessionId?: string | null }
269
+ }
270
+
271
+ async function executeConnectorAction(input: ConnectorActionInput, bctx: ConnectorActionContext) {
272
+ const normalized = normalizeToolInputArgs((input ?? {}) as Record<string, unknown>)
273
+ const {
274
+ action,
275
+ connectorId,
276
+ platform,
277
+ to,
278
+ message,
279
+ voiceText,
280
+ voiceId,
281
+ imageUrl,
282
+ fileUrl,
283
+ mediaPath,
284
+ mimeType,
285
+ fileName,
286
+ caption,
287
+ approved,
288
+ ptt,
289
+ } = normalized as ConnectorActionInput
290
+
291
+ try {
292
+ const actionName = String(action)
293
+ const { listRunningConnectors, sendConnectorMessage, getConnectorRecentChannelId } = await import('../connectors/manager')
294
+ const running = listRunningConnectors(platform || undefined)
295
+
296
+ if (actionName === 'list_running' || actionName === 'list_targets') {
297
+ return JSON.stringify(running)
298
+ }
299
+
300
+ if (actionName === 'start') {
301
+ if (!connectorId) {
302
+ const allConnectors = loadConnectors()
303
+ const stopped = Object.values(allConnectors)
304
+ .filter((c) => !platform || c.platform === platform)
305
+ .filter((c) => !running.find((r) => r.id === c.id))
306
+ .map((c) => ({ id: c.id, name: c.name, platform: c.platform }))
307
+ if (!stopped.length) return 'All connectors are already running.'
308
+ return `Error: connectorId is required. Stopped connectors available to start: ${JSON.stringify(stopped)}`
309
+ }
310
+ const { startConnector: doStart } = await import('../connectors/manager')
311
+ await doStart(connectorId)
312
+ return JSON.stringify({ status: 'started', connectorId })
313
+ }
314
+
315
+ if (actionName === 'stop') {
316
+ if (!connectorId) return 'Error: connectorId is required for stop action.'
317
+ const { stopConnector: doStop } = await import('../connectors/manager')
318
+ await doStop(connectorId)
319
+ return JSON.stringify({ status: 'stopped', connectorId })
320
+ }
321
+
322
+ const resolveSelectedConnector = () => {
323
+ if (!running.length) {
324
+ const allConnectors = loadConnectors()
325
+ const configured = Object.values(allConnectors)
326
+ .filter((c) => !platform || c.platform === platform)
327
+ .map((c) => ({ id: c.id, name: c.name, platform: c.platform, agentId: c.agentId || null }))
328
+ if (configured.length) {
329
+ return {
330
+ error: `Error: no running connectors found. Ask user to start one. Configured: ${JSON.stringify(configured)}`,
658
331
  }
659
- },
660
- {
661
- name: 'connector_message_tool',
662
- description: voiceNoteToolEnabled
663
- ? 'Manage and send messages through chat platform connectors (WhatsApp, Telegram, Slack, Discord, etc.). Use "start"/"stop" to manage connector lifecycle, "list_running"/"list_targets" to discover available connectors and recipients, "send" to deliver text/media, "send_voice_note" to synthesize and send audio via ElevenLabs, "schedule_followup" for delayed check-ins, and rich actions (react, edit, delete, pin) for message management. When a send fails because no connector is running, check if one is configured and offer to start it. When no target is set, list available configured numbers and ask the user which to send to.'
664
- : 'Manage and send messages through chat platform connectors (WhatsApp, Telegram, Slack, Discord, etc.). Use "start"/"stop" to manage connector lifecycle, "list_running"/"list_targets" to discover available connectors and recipients, "send" to deliver text/media, "schedule_followup" for delayed check-ins, and rich actions (react, edit, delete, pin) for message management. Voice-note sending appears only when ElevenLabs is enabled with an API key in Settings > Voice. When a send fails because no connector is running, check if one is configured and offer to start it. When no target is set, list available configured numbers and ask the user which to send to.',
665
- schema: z.object({
666
- action: actionSchema.describe('connector messaging action'),
667
- connectorId: z.string().optional().describe('Optional connector id. Defaults to the first running connector (or first for selected platform).'),
668
- platform: z.string().optional().describe('Optional platform filter (whatsapp, telegram, slack, discord, bluebubbles, etc.).'),
669
- to: z.string().optional().describe('Target channel id / recipient. For WhatsApp, phone number or full JID.'),
670
- message: z.string().optional().describe('Message text to send. Required for send unless media is provided. Used as fallback for send_voice_note/schedule_followup when voiceText/followUpMessage are omitted.'),
671
- voiceText: z.string().optional().describe('Text to synthesize for send_voice_note. Uses message when omitted.'),
672
- voiceId: z.string().optional().describe('Optional ElevenLabs voice override for send_voice_note.'),
673
- imageUrl: z.string().optional().describe('Optional public image URL to attach/send where platform supports media.'),
674
- fileUrl: z.string().optional().describe('Optional public file URL to attach/send where platform supports documents.'),
675
- mediaPath: z.string().optional().describe('Absolute local file path to send (e.g. a screenshot). Auto-detects mime type from extension. Takes priority over imageUrl/fileUrl.'),
676
- mimeType: z.string().optional().describe('Optional MIME type for mediaPath or fileUrl.'),
677
- fileName: z.string().optional().describe('Optional display file name for mediaPath or fileUrl.'),
678
- caption: z.string().optional().describe('Optional caption used with image/file sends.'),
679
- delaySec: z.number().optional().describe('Delay in seconds for schedule_followup.'),
680
- followUpMessage: z.string().optional().describe('Optional delayed follow-up text (for send) or primary text for schedule_followup.'),
681
- followUpDelaySec: z.number().optional().describe('Delay in seconds for followUpMessage when action=send. Default 300 seconds.'),
682
- ptt: z.boolean().optional().describe('Send audio as a WhatsApp voice note (push-to-talk). Defaults to true for audio files.'),
683
- approved: z.boolean().optional().describe('Set true to explicitly confirm outbound send when safetyRequireApprovalForOutbound is enabled.'),
684
- }),
685
- },
686
- ),
687
- )
332
+ }
333
+ return {
334
+ error: `Error: no running connectors. User needs to set one up in the Connectors panel.`,
335
+ }
336
+ }
337
+ const selected = connectorId ? running.find((c) => c.id === connectorId) : running[0]
338
+ if (!selected) return { error: `Error: running connector not found: ${connectorId}` }
339
+ const connectors = loadConnectors()
340
+ const connector = connectors[selected.id]
341
+ if (!connector) return { error: `Error: connector not found: ${selected.id}` }
342
+ return { selected, connector }
343
+ }
344
+
345
+ if (actionName === 'send' || actionName === 'send_voice_note' || actionName === 'schedule_followup') {
346
+ const settings = loadSettings()
347
+ if (settings.safetyRequireApprovalForOutbound === true && approved !== true) {
348
+ return 'Error: outbound connector sends require explicit approval. Re-run with approved=true after user confirmation.'
349
+ }
350
+ const now = Date.now()
351
+ pruneOldConnectorToolState(now)
352
+ const resolved = resolveSelectedConnector()
353
+ if ('error' in resolved) return resolved.error
354
+ const { selected, connector } = resolved
355
+
356
+ const target = pickChannelTarget({
357
+ connector,
358
+ to,
359
+ recentChannelId: getConnectorRecentChannelId(selected.id),
360
+ })
361
+ if (target.error) return target.error
362
+
363
+ let channelId = target.channelId
364
+ if (connector.platform === 'whatsapp') channelId = normalizeWhatsAppTarget(channelId)
365
+
366
+ const currentSession = bctx.resolveCurrentSession?.()
367
+ const latestUserTurn = parseLatestUserTurn(currentSession)
368
+ const sessionId = bctx.ctx?.sessionId || currentSession?.id || 'unknown-session'
369
+ const turnKey = buildConnectorActionKey([sessionId, latestUserTurn.time || 'no-user-turn'])
370
+ const multiOutboundAllowed = userExplicitlyWantsMultipleOutbound(latestUserTurn.text)
371
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
372
+ const _followupExplicitlyRequested = userExplicitlyRequestedFollowup(latestUserTurn.text)
373
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
374
+ const _autonomousTurn = isAutonomousSystemTurn(latestUserTurn.text)
375
+ const existingBudget = connectorTurnSendBudget.get(turnKey)
376
+
377
+ if (!multiOutboundAllowed && existingBudget && now - existingBudget.at <= CONNECTOR_TURN_SEND_TTL_MS && existingBudget.count >= 1) {
378
+ if (existingBudget.lastResult) {
379
+ return normalizeDedupedReplayResult(existingBudget.lastResult, { connectorId: selected.id, platform: selected.platform, to: channelId })
380
+ }
381
+ return JSON.stringify({ status: 'sent', connectorId: selected.id, platform: selected.platform, to: channelId, deduped: true })
382
+ }
383
+
384
+ if (actionName === 'send_voice_note') {
385
+ const ttsText = (voiceText || message || '').trim()
386
+ if (!ttsText) return 'Error: voiceText or message is required.'
387
+ const audioBuffer = await synthesizeElevenLabsMp3({ text: ttsText, voiceId: voiceId?.trim() || undefined })
388
+ const voiceFileName = `${Date.now()}-${genId()}-voicenote.mp3`
389
+ const voicePath = path.join(UPLOAD_DIR, voiceFileName)
390
+ fs.writeFileSync(voicePath, audioBuffer)
391
+
392
+ const sent = await sendConnectorMessage({
393
+ connectorId: selected.id, channelId, text: '', mediaPath: voicePath, mimeType: 'audio/mpeg',
394
+ fileName: fileName?.trim() || 'voicenote.mp3', caption: caption?.trim() || undefined, ptt: ptt ?? true,
395
+ })
396
+ const result = JSON.stringify({ status: 'voice_sent', connectorId: sent.connectorId, platform: sent.platform, to: sent.channelId, voiceFile: voicePath })
397
+ connectorTurnSendBudget.set(turnKey, { count: (existingBudget?.count || 0) + 1, at: now, lastResult: result })
398
+ return result
399
+ }
400
+
401
+ const media = resolveConnectorMediaInput({ cwd: bctx.cwd, mediaPath, imageUrl, fileUrl })
402
+ if (media.error) return media.error
403
+
404
+ if (actionName === 'send' && !message?.trim() && !media.mediaPath && !media.imageUrl && !media.fileUrl) {
405
+ return 'Error: message or media required.'
406
+ }
407
+
408
+ const sent = await sendConnectorMessage({
409
+ connectorId: selected.id, channelId, text: message?.trim() || '',
410
+ imageUrl: media.imageUrl, fileUrl: media.fileUrl, mediaPath: media.mediaPath,
411
+ mimeType: mimeType?.trim() || undefined, fileName: fileName?.trim() || undefined,
412
+ caption: caption?.trim() || undefined, ptt: ptt ?? undefined,
413
+ })
414
+
415
+ const result = JSON.stringify({ status: 'sent', connectorId: sent.connectorId, platform: sent.platform, to: sent.channelId, messageId: sent.messageId || null })
416
+ connectorTurnSendBudget.set(turnKey, { count: (existingBudget?.count || 0) + 1, at: now, lastResult: result })
417
+ return result
418
+ }
419
+
420
+ return 'Unknown action.'
421
+ } catch (err: unknown) {
422
+ return `Error: ${err instanceof Error ? err.message : String(err)}`
688
423
  }
424
+ }
425
+
426
+ /**
427
+ * Register as a Built-in Plugin
428
+ */
429
+ const ConnectorPlugin: Plugin = {
430
+ name: 'Core Connectors',
431
+ description: 'Manage and send messages through chat platform connectors (WhatsApp, Telegram, Slack, etc.).',
432
+ hooks: {} as PluginHooks,
433
+ tools: [
434
+ {
435
+ name: 'connector_message_tool',
436
+ description: 'Send and manage outbound messages across chat platforms.',
437
+ parameters: {
438
+ type: 'object',
439
+ properties: {
440
+ action: { type: 'string', enum: ['list_running', 'start', 'stop', 'send', 'send_voice_note'] },
441
+ connectorId: { type: 'string' },
442
+ platform: { type: 'string' },
443
+ to: { type: 'string' },
444
+ message: { type: 'string' }
445
+ },
446
+ required: ['action']
447
+ },
448
+ execute: async (args, context) => executeConnectorAction(args as ConnectorActionInput, { ...context.session, cwd: context.session.cwd || process.cwd() })
449
+ }
450
+ ]
451
+ }
452
+
453
+ getPluginManager().registerBuiltin('connectors', ConnectorPlugin)
689
454
 
690
- return tools
455
+ /**
456
+ * Legacy Bridge
457
+ */
458
+ export function buildConnectorTools(bctx: ToolBuildContext): StructuredToolInterface[] {
459
+ if (!bctx.hasTool('manage_connectors')) return []
460
+ return [
461
+ tool(
462
+ async (args) => executeConnectorAction(args as ConnectorActionInput, bctx),
463
+ {
464
+ name: 'connector_message_tool',
465
+ description: ConnectorPlugin.tools![0].description,
466
+ schema: z.object({}).passthrough()
467
+ }
468
+ )
469
+ ]
691
470
  }