@swarmclawai/swarmclaw 0.7.8 → 0.8.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 (251) hide show
  1. package/README.md +12 -15
  2. package/next.config.ts +13 -2
  3. package/package.json +4 -2
  4. package/src/app/api/agents/[id]/thread/route.ts +9 -0
  5. package/src/app/api/agents/route.ts +4 -0
  6. package/src/app/api/agents/thread-route.test.ts +133 -0
  7. package/src/app/api/approvals/route.test.ts +148 -0
  8. package/src/app/api/canvas/[sessionId]/route.ts +3 -1
  9. package/src/app/api/chatrooms/[id]/chat/route.ts +4 -2
  10. package/src/app/api/chats/[id]/devserver/route.ts +48 -7
  11. package/src/app/api/chats/[id]/messages/route.ts +42 -18
  12. package/src/app/api/chats/[id]/route.ts +1 -1
  13. package/src/app/api/chats/[id]/stop/route.ts +5 -4
  14. package/src/app/api/chats/route.ts +22 -2
  15. package/src/app/api/clawhub/install/route.ts +28 -8
  16. package/src/app/api/connectors/[id]/route.ts +26 -1
  17. package/src/app/api/external-agents/route.test.ts +165 -0
  18. package/src/app/api/gateways/[id]/health/route.ts +27 -12
  19. package/src/app/api/gateways/[id]/route.ts +2 -0
  20. package/src/app/api/gateways/health-route.test.ts +135 -0
  21. package/src/app/api/gateways/route.ts +2 -0
  22. package/src/app/api/mcp-servers/route.test.ts +130 -0
  23. package/src/app/api/openclaw/deploy/route.ts +38 -5
  24. package/src/app/api/plugins/install/route.ts +46 -6
  25. package/src/app/api/plugins/marketplace/route.ts +48 -15
  26. package/src/app/api/preview-server/route.ts +26 -11
  27. package/src/app/api/schedules/[id]/run/route.ts +4 -0
  28. package/src/app/api/schedules/route.test.ts +86 -0
  29. package/src/app/api/schedules/route.ts +6 -1
  30. package/src/app/api/setup/check-provider/route.test.ts +19 -0
  31. package/src/app/api/setup/check-provider/route.ts +40 -10
  32. package/src/app/api/skills/[id]/route.ts +12 -0
  33. package/src/app/api/skills/import/route.ts +14 -12
  34. package/src/app/api/skills/route.ts +13 -1
  35. package/src/app/api/tasks/[id]/route.ts +10 -1
  36. package/src/app/api/tasks/import/github/route.test.ts +65 -0
  37. package/src/app/api/tasks/import/github/route.ts +337 -0
  38. package/src/app/api/wallets/[id]/approve/route.ts +17 -3
  39. package/src/app/api/wallets/[id]/route.ts +79 -33
  40. package/src/app/api/wallets/[id]/send/route.ts +19 -33
  41. package/src/app/api/wallets/route.ts +78 -61
  42. package/src/app/api/webhooks/[id]/route.ts +33 -6
  43. package/src/app/api/webhooks/route.test.ts +272 -0
  44. package/src/cli/index.js +1 -0
  45. package/src/cli/spec.js +1 -0
  46. package/src/components/agents/agent-card.tsx +9 -2
  47. package/src/components/agents/agent-chat-list.tsx +18 -2
  48. package/src/components/agents/agent-list.tsx +1 -0
  49. package/src/components/agents/agent-sheet.tsx +73 -24
  50. package/src/components/agents/inspector-panel.tsx +41 -0
  51. package/src/components/canvas/canvas-panel.tsx +236 -65
  52. package/src/components/chat/chat-card.tsx +36 -13
  53. package/src/components/chat/chat-header.tsx +44 -16
  54. package/src/components/chat/chat-list.tsx +28 -4
  55. package/src/components/chat/checkpoint-timeline.tsx +50 -34
  56. package/src/components/chat/message-bubble.tsx +208 -145
  57. package/src/components/chat/message-list.tsx +48 -19
  58. package/src/components/chatrooms/chatroom-message.tsx +2 -2
  59. package/src/components/chatrooms/chatroom-sheet.tsx +16 -2
  60. package/src/components/connectors/connector-health.tsx +1 -1
  61. package/src/components/connectors/connector-list.tsx +7 -2
  62. package/src/components/connectors/connector-sheet.tsx +337 -148
  63. package/src/components/gateways/gateway-sheet.tsx +2 -2
  64. package/src/components/mcp-servers/mcp-server-list.tsx +26 -5
  65. package/src/components/mcp-servers/mcp-server-sheet.tsx +19 -2
  66. package/src/components/openclaw/openclaw-deploy-panel.tsx +269 -21
  67. package/src/components/plugins/plugin-list.tsx +45 -9
  68. package/src/components/plugins/plugin-sheet.tsx +55 -7
  69. package/src/components/providers/provider-list.tsx +2 -1
  70. package/src/components/providers/provider-sheet.tsx +21 -2
  71. package/src/components/schedules/schedule-card.tsx +25 -1
  72. package/src/components/schedules/schedule-sheet.tsx +44 -2
  73. package/src/components/secrets/secret-sheet.tsx +21 -2
  74. package/src/components/shared/agent-switch-dialog.tsx +12 -1
  75. package/src/components/shared/bottom-sheet.tsx +13 -3
  76. package/src/components/shared/command-palette.tsx +8 -1
  77. package/src/components/shared/confirm-dialog.tsx +19 -4
  78. package/src/components/shared/connector-platform-icon.test.ts +28 -0
  79. package/src/components/shared/connector-platform-icon.tsx +39 -6
  80. package/src/components/shared/settings/plugin-manager.tsx +29 -6
  81. package/src/components/shared/settings/section-capability-policy.tsx +7 -3
  82. package/src/components/skills/skill-list.tsx +25 -0
  83. package/src/components/skills/skill-sheet.tsx +84 -12
  84. package/src/components/tasks/approvals-panel.tsx +191 -95
  85. package/src/components/tasks/task-board.tsx +273 -2
  86. package/src/components/tasks/task-card.tsx +38 -9
  87. package/src/components/ui/dialog.tsx +2 -2
  88. package/src/components/wallets/wallet-approval-dialog.tsx +4 -2
  89. package/src/components/wallets/wallet-panel.tsx +435 -90
  90. package/src/components/wallets/wallet-section.tsx +198 -48
  91. package/src/components/webhooks/webhook-sheet.tsx +22 -2
  92. package/src/lib/approval-display.ts +20 -0
  93. package/src/lib/canvas-content.ts +198 -0
  94. package/src/lib/chat-artifact-summary.ts +165 -0
  95. package/src/lib/chat-display.test.ts +91 -0
  96. package/src/lib/chat-display.ts +58 -0
  97. package/src/lib/chat-streaming-state.test.ts +47 -1
  98. package/src/lib/chat-streaming-state.ts +42 -0
  99. package/src/lib/ollama-model.ts +10 -0
  100. package/src/lib/openclaw-endpoint.test.ts +8 -0
  101. package/src/lib/openclaw-endpoint.ts +6 -1
  102. package/src/lib/plugin-install-cors.ts +46 -0
  103. package/src/lib/plugin-sources.test.ts +43 -0
  104. package/src/lib/plugin-sources.ts +77 -0
  105. package/src/lib/providers/ollama.ts +16 -6
  106. package/src/lib/providers/openclaw.test.ts +54 -0
  107. package/src/lib/providers/openclaw.ts +127 -11
  108. package/src/lib/schedule-dedupe-advanced.test.ts +1335 -0
  109. package/src/lib/schedule-dedupe.test.ts +66 -1
  110. package/src/lib/schedule-dedupe.ts +169 -12
  111. package/src/lib/schedule-origin.test.ts +20 -0
  112. package/src/lib/schedule-origin.ts +15 -0
  113. package/src/lib/server/__fixtures__/fake-mcp-stdio-server.mjs +27 -0
  114. package/src/lib/server/agent-availability.ts +16 -0
  115. package/src/lib/server/agent-runtime-config.ts +12 -4
  116. package/src/lib/server/agent-thread-session.test.ts +51 -0
  117. package/src/lib/server/agent-thread-session.ts +7 -0
  118. package/src/lib/server/approval-match.ts +205 -0
  119. package/src/lib/server/approvals-auto-approve.test.ts +538 -1
  120. package/src/lib/server/approvals.ts +214 -1
  121. package/src/lib/server/assistant-control.test.ts +29 -0
  122. package/src/lib/server/assistant-control.ts +23 -0
  123. package/src/lib/server/build-llm.test.ts +79 -0
  124. package/src/lib/server/build-llm.ts +14 -4
  125. package/src/lib/server/canvas-content.test.ts +32 -0
  126. package/src/lib/server/canvas-content.ts +6 -0
  127. package/src/lib/server/capability-router.test.ts +11 -0
  128. package/src/lib/server/capability-router.ts +26 -1
  129. package/src/lib/server/chat-execution-advanced.test.ts +651 -0
  130. package/src/lib/server/chat-execution-disabled.test.ts +94 -0
  131. package/src/lib/server/chat-execution-tool-events.test.ts +157 -0
  132. package/src/lib/server/chat-execution.ts +353 -72
  133. package/src/lib/server/clawhub-client.test.ts +14 -8
  134. package/src/lib/server/connectors/manager.test.ts +1147 -0
  135. package/src/lib/server/connectors/manager.ts +362 -63
  136. package/src/lib/server/connectors/pairing.ts +26 -5
  137. package/src/lib/server/connectors/types.ts +2 -0
  138. package/src/lib/server/connectors/whatsapp.test.ts +134 -0
  139. package/src/lib/server/connectors/whatsapp.ts +271 -47
  140. package/src/lib/server/context-manager.ts +6 -1
  141. package/src/lib/server/daemon-state.ts +1 -1
  142. package/src/lib/server/data-dir.test.ts +37 -0
  143. package/src/lib/server/data-dir.ts +20 -1
  144. package/src/lib/server/delegation-jobs-advanced.test.ts +513 -0
  145. package/src/lib/server/devserver-launch.test.ts +60 -0
  146. package/src/lib/server/devserver-launch.ts +85 -0
  147. package/src/lib/server/elevenlabs.test.ts +189 -1
  148. package/src/lib/server/elevenlabs.ts +147 -43
  149. package/src/lib/server/ethereum.ts +590 -0
  150. package/src/lib/server/eval/agent-regression-advanced.test.ts +302 -0
  151. package/src/lib/server/eval/agent-regression.test.ts +18 -1
  152. package/src/lib/server/eval/agent-regression.ts +383 -11
  153. package/src/lib/server/evm-swap.ts +475 -0
  154. package/src/lib/server/execution-log.ts +1 -0
  155. package/src/lib/server/heartbeat-service-timer.test.ts +173 -0
  156. package/src/lib/server/heartbeat-service.ts +15 -10
  157. package/src/lib/server/heartbeat-wake.test.ts +112 -0
  158. package/src/lib/server/heartbeat-wake.ts +338 -57
  159. package/src/lib/server/main-agent-loop-advanced.test.ts +538 -0
  160. package/src/lib/server/mcp-client.test.ts +16 -0
  161. package/src/lib/server/mcp-client.ts +25 -0
  162. package/src/lib/server/memory-integration.test.ts +719 -0
  163. package/src/lib/server/memory-policy.test.ts +43 -0
  164. package/src/lib/server/memory-policy.ts +132 -0
  165. package/src/lib/server/memory-tiers.test.ts +60 -0
  166. package/src/lib/server/memory-tiers.ts +16 -0
  167. package/src/lib/server/ollama-runtime.ts +58 -0
  168. package/src/lib/server/openclaw-deploy.test.ts +109 -1
  169. package/src/lib/server/openclaw-deploy.ts +557 -81
  170. package/src/lib/server/openclaw-gateway.test.ts +131 -0
  171. package/src/lib/server/openclaw-gateway.ts +10 -4
  172. package/src/lib/server/openclaw-health.test.ts +35 -0
  173. package/src/lib/server/openclaw-health.ts +215 -47
  174. package/src/lib/server/orchestrator-lg.ts +2 -2
  175. package/src/lib/server/plugins-advanced.test.ts +351 -0
  176. package/src/lib/server/plugins.ts +205 -5
  177. package/src/lib/server/queue-advanced.test.ts +528 -0
  178. package/src/lib/server/queue-followups.test.ts +262 -0
  179. package/src/lib/server/queue-reconcile.test.ts +128 -0
  180. package/src/lib/server/queue.ts +293 -61
  181. package/src/lib/server/scheduler.ts +29 -1
  182. package/src/lib/server/session-note.test.ts +36 -0
  183. package/src/lib/server/session-note.ts +42 -0
  184. package/src/lib/server/session-run-manager.ts +52 -4
  185. package/src/lib/server/session-tools/canvas.ts +14 -12
  186. package/src/lib/server/session-tools/connector.test.ts +138 -0
  187. package/src/lib/server/session-tools/connector.ts +348 -61
  188. package/src/lib/server/session-tools/context.ts +12 -3
  189. package/src/lib/server/session-tools/crud.ts +221 -10
  190. package/src/lib/server/session-tools/delegate-fallback.test.ts +103 -0
  191. package/src/lib/server/session-tools/delegate.ts +64 -8
  192. package/src/lib/server/session-tools/discovery-approvals.test.ts +142 -0
  193. package/src/lib/server/session-tools/discovery.ts +80 -12
  194. package/src/lib/server/session-tools/file-normalize.test.ts +36 -0
  195. package/src/lib/server/session-tools/file.ts +43 -4
  196. package/src/lib/server/session-tools/human-loop.ts +35 -5
  197. package/src/lib/server/session-tools/index.ts +44 -9
  198. package/src/lib/server/session-tools/manage-connectors.test.ts +139 -0
  199. package/src/lib/server/session-tools/manage-schedules-advanced.test.ts +564 -0
  200. package/src/lib/server/session-tools/manage-schedules.test.ts +283 -0
  201. package/src/lib/server/session-tools/manage-tasks-advanced.test.ts +852 -0
  202. package/src/lib/server/session-tools/memory.test.ts +93 -0
  203. package/src/lib/server/session-tools/memory.ts +546 -79
  204. package/src/lib/server/session-tools/normalize-tool-args.ts +1 -1
  205. package/src/lib/server/session-tools/plugin-creator.ts +57 -1
  206. package/src/lib/server/session-tools/primitive-tools.test.ts +6 -0
  207. package/src/lib/server/session-tools/schedule.ts +6 -1
  208. package/src/lib/server/session-tools/shell-normalize.test.ts +25 -1
  209. package/src/lib/server/session-tools/shell.ts +22 -3
  210. package/src/lib/server/session-tools/wallet-tool.test.ts +254 -0
  211. package/src/lib/server/session-tools/wallet.ts +1374 -139
  212. package/src/lib/server/session-tools/web-inputs.test.ts +162 -1
  213. package/src/lib/server/session-tools/web.ts +468 -64
  214. package/src/lib/server/skill-discovery.ts +128 -0
  215. package/src/lib/server/skill-eligibility.test.ts +84 -0
  216. package/src/lib/server/skill-eligibility.ts +95 -0
  217. package/src/lib/server/skill-prompt-budget.test.ts +102 -0
  218. package/src/lib/server/skill-prompt-budget.ts +125 -0
  219. package/src/lib/server/skills-normalize.test.ts +54 -0
  220. package/src/lib/server/skills-normalize.ts +372 -26
  221. package/src/lib/server/solana.ts +214 -29
  222. package/src/lib/server/storage.ts +65 -36
  223. package/src/lib/server/stream-agent-chat.test.ts +419 -9
  224. package/src/lib/server/stream-agent-chat.ts +887 -83
  225. package/src/lib/server/system-events.ts +1 -1
  226. package/src/lib/server/tool-capability-policy-advanced.test.ts +502 -0
  227. package/src/lib/server/tool-loop-detection.test.ts +105 -0
  228. package/src/lib/server/tool-loop-detection.ts +260 -0
  229. package/src/lib/server/tool-planning.ts +4 -2
  230. package/src/lib/server/wallet-execution.test.ts +198 -0
  231. package/src/lib/server/wallet-portfolio.test.ts +98 -0
  232. package/src/lib/server/wallet-portfolio.ts +724 -0
  233. package/src/lib/server/wallet-service.test.ts +57 -0
  234. package/src/lib/server/wallet-service.ts +213 -0
  235. package/src/lib/server/watch-jobs-advanced.test.ts +594 -0
  236. package/src/lib/server/watch-jobs.ts +17 -2
  237. package/src/lib/server/workspace-context.ts +111 -0
  238. package/src/lib/skill-save-payload.test.ts +39 -0
  239. package/src/lib/skill-save-payload.ts +37 -0
  240. package/src/lib/tasks.ts +28 -0
  241. package/src/lib/tool-event-summary.test.ts +30 -0
  242. package/src/lib/tool-event-summary.ts +37 -0
  243. package/src/lib/validation/schemas.ts +1 -0
  244. package/src/lib/wallet-transactions.test.ts +75 -0
  245. package/src/lib/wallet-transactions.ts +43 -0
  246. package/src/lib/wallet.test.ts +17 -0
  247. package/src/lib/wallet.ts +183 -0
  248. package/src/proxy.test.ts +31 -0
  249. package/src/proxy.ts +34 -2
  250. package/src/stores/use-chat-store.ts +15 -1
  251. package/src/types/index.ts +210 -14
@@ -5,6 +5,7 @@ import fs from 'fs'
5
5
  import { loadConnectors, loadSettings, UPLOAD_DIR } from '../storage'
6
6
  import { genId } from '@/lib/id'
7
7
  import { synthesizeElevenLabsMp3 } from '../elevenlabs'
8
+ import { isAudioMime, mimeFromPath } from '../connectors/media'
8
9
  import type { ToolBuildContext } from './context'
9
10
  import type { Plugin, PluginHooks } from '@/types'
10
11
  import { getPluginManager } from '../plugins'
@@ -17,6 +18,78 @@ const recentConnectorActionCache = new Map<string, { at: number; result: string
17
18
  const connectorTurnSendBudget = new Map<string, { count: number; at: number; lastResult?: string }>()
18
19
  const autonomousOutreachBudget = new Map<string, { at: number; result?: string }>()
19
20
 
21
+ export const CONNECTOR_MESSAGE_TOOL_ACTIONS = [
22
+ 'list_running',
23
+ 'list_targets',
24
+ 'start',
25
+ 'stop',
26
+ 'send',
27
+ 'send_voice_note',
28
+ 'schedule_followup',
29
+ 'react',
30
+ 'edit',
31
+ 'delete',
32
+ 'pin',
33
+ 'message_react',
34
+ 'message_edit',
35
+ 'message_delete',
36
+ 'message_pin',
37
+ ] as const
38
+
39
+ export const CONNECTOR_MESSAGE_TOOL_PARAMETERS = {
40
+ type: 'object',
41
+ properties: {
42
+ action: { type: 'string', enum: [...CONNECTOR_MESSAGE_TOOL_ACTIONS] },
43
+ connectorId: { type: 'string' },
44
+ connector: { type: 'string' },
45
+ connector_id: { type: 'string' },
46
+ runningConnectorId: { type: 'string' },
47
+ id: { type: 'string' },
48
+ platform: { type: 'string' },
49
+ to: { type: 'string' },
50
+ channel: { type: 'string' },
51
+ channelId: { type: 'string' },
52
+ recipientId: { type: 'string' },
53
+ phoneNumber: { type: 'string' },
54
+ configuredTarget: { type: 'string' },
55
+ target: { type: 'string' },
56
+ recipient: { type: 'string' },
57
+ path: { type: 'string' },
58
+ targets: { type: 'string' },
59
+ message: { type: 'string' },
60
+ text: { type: 'string' },
61
+ content: { type: 'string' },
62
+ body: { type: 'string' },
63
+ messageId: { type: 'string' },
64
+ targetMessage: { type: 'string', enum: ['last_inbound', 'last_outbound'] },
65
+ emoji: { type: 'string' },
66
+ voiceText: { type: 'string' },
67
+ voiceId: { type: 'string' },
68
+ imageUrl: { type: 'string' },
69
+ fileUrl: { type: 'string' },
70
+ mediaPath: { type: 'string' },
71
+ mimeType: { type: 'string' },
72
+ fileName: { type: 'string' },
73
+ caption: { type: 'string' },
74
+ replyToMessageId: { type: 'string' },
75
+ threadId: { type: 'string' },
76
+ delaySec: { type: 'number' },
77
+ followUpMessage: { type: 'string' },
78
+ followupMessage: { type: 'string' },
79
+ followUpDelaySec: { type: 'number' },
80
+ dedupeKey: { type: 'string' },
81
+ approved: { type: 'boolean' },
82
+ ptt: { type: 'boolean' },
83
+ },
84
+ } as const
85
+
86
+ const LEGACY_CONNECTOR_ACTION_ALIASES: Record<string, string> = {
87
+ message_react: 'react',
88
+ message_edit: 'edit',
89
+ message_delete: 'delete',
90
+ message_pin: 'pin',
91
+ }
92
+
20
93
  function pruneOldConnectorToolState(now: number): void {
21
94
  for (const [key, entry] of recentConnectorActionCache.entries()) {
22
95
  if (now - entry.at > CONNECTOR_ACTION_DEDUPE_TTL_MS) recentConnectorActionCache.delete(key)
@@ -109,6 +182,130 @@ function normalizeDedupedReplayResult(raw: string, fallback: { connectorId: stri
109
182
  }
110
183
  }
111
184
 
185
+ export function normalizeConnectorActionName(action: string): string {
186
+ const normalized = String(action || '').trim()
187
+ return LEGACY_CONNECTOR_ACTION_ALIASES[normalized] || normalized
188
+ }
189
+
190
+ export function inferConnectorActionName(input: Record<string, unknown>): string | null {
191
+ const explicit = typeof input.action === 'string' ? input.action.trim() : ''
192
+ if (explicit) return explicit
193
+ if (typeof input.voiceText === 'string' && input.voiceText.trim()) return 'send_voice_note'
194
+ if (
195
+ typeof input.followUpMessage === 'string'
196
+ || typeof input.followupMessage === 'string'
197
+ || typeof input.followUpDelaySec === 'number'
198
+ || typeof input.delaySec === 'number'
199
+ ) return 'schedule_followup'
200
+ if (
201
+ typeof input.message === 'string'
202
+ || typeof input.text === 'string'
203
+ || typeof input.content === 'string'
204
+ || typeof input.body === 'string'
205
+ || typeof input.mediaPath === 'string'
206
+ || typeof input.imageUrl === 'string'
207
+ || typeof input.fileUrl === 'string'
208
+ ) return 'send'
209
+ return null
210
+ }
211
+
212
+ function pickConnectorString(value: unknown): string | null {
213
+ if (typeof value === 'string') {
214
+ const trimmed = value.trim()
215
+ return trimmed || null
216
+ }
217
+ if (Array.isArray(value)) {
218
+ for (const entry of value) {
219
+ const picked = pickConnectorString(entry)
220
+ if (picked) return picked
221
+ }
222
+ }
223
+ return null
224
+ }
225
+
226
+ function resolveRunningConnectorId(
227
+ running: Array<{ id?: string; name?: string }>,
228
+ value: unknown,
229
+ ): string | null {
230
+ const candidate = pickConnectorString(value)
231
+ if (!candidate) return null
232
+ const matched = running.find((connector) => (
233
+ String(connector.id || '').trim() === candidate
234
+ || String(connector.name || '').trim() === candidate
235
+ ))
236
+ return matched ? String(matched.id || '').trim() || null : null
237
+ }
238
+
239
+ export function normalizeConnectorActionInputAliases(
240
+ input: Record<string, unknown>,
241
+ running: Array<{ id?: string; name?: string }> = [],
242
+ ): Record<string, unknown> {
243
+ const normalized = { ...input }
244
+ const actionName = normalizeConnectorActionName(inferConnectorActionName(normalized) || String(normalized.action || ''))
245
+ const messageActionUsesRawId = actionName === 'react'
246
+ || actionName === 'edit'
247
+ || actionName === 'delete'
248
+ || actionName === 'pin'
249
+ const messageAlias = pickConnectorString(
250
+ normalized.message
251
+ ?? normalized.text
252
+ ?? normalized.content
253
+ ?? normalized.body,
254
+ )
255
+ if (!pickConnectorString(normalized.message) && messageAlias) {
256
+ normalized.message = messageAlias
257
+ }
258
+
259
+ const followUpAlias = pickConnectorString(
260
+ normalized.followUpMessage
261
+ ?? normalized.followupMessage,
262
+ )
263
+ if (!pickConnectorString(normalized.followUpMessage) && followUpAlias) {
264
+ normalized.followUpMessage = followUpAlias
265
+ }
266
+
267
+ const rawId = pickConnectorString(normalized.id)
268
+ const explicitConnectorId = pickConnectorString(
269
+ normalized.connectorId
270
+ ?? normalized.runningConnectorId
271
+ ?? normalized.connector
272
+ ?? normalized.connector_id,
273
+ )
274
+ const aliasConnectorId = explicitConnectorId
275
+ ? resolveRunningConnectorId(running, explicitConnectorId) || explicitConnectorId
276
+ : resolveRunningConnectorId(running, normalized.channel) || resolveRunningConnectorId(running, rawId)
277
+
278
+ if (!pickConnectorString(normalized.connectorId) && aliasConnectorId) {
279
+ normalized.connectorId = aliasConnectorId
280
+ }
281
+
282
+ const rawIdIsConnector = !!(rawId && resolveRunningConnectorId(running, rawId))
283
+ if (!pickConnectorString(normalized.messageId) && rawId && !rawIdIsConnector && messageActionUsesRawId) {
284
+ normalized.messageId = rawId
285
+ }
286
+ const targetAlias = pickConnectorString(
287
+ normalized.to
288
+ ?? normalized.channelId
289
+ ?? normalized.recipientId
290
+ ?? normalized.phoneNumber
291
+ ?? normalized.configuredTarget
292
+ ?? normalized.target
293
+ ?? normalized.recipient
294
+ ?? normalized.path
295
+ ?? normalized.targets,
296
+ )
297
+
298
+ if (!pickConnectorString(normalized.to)) {
299
+ if (targetAlias) {
300
+ normalized.to = targetAlias
301
+ } else if (rawId && !rawIdIsConnector && !messageActionUsesRawId) {
302
+ normalized.to = rawId
303
+ }
304
+ }
305
+
306
+ return normalized
307
+ }
308
+
112
309
  /** Resolve /api/uploads/filename URLs to actual disk paths */
113
310
  function resolveUploadUrl(url: string | undefined): { mediaPath: string; mimeType?: string } | null {
114
311
  if (!url) return null
@@ -140,13 +337,83 @@ function parseCsv(raw: string | undefined): string[] {
140
337
  return raw.split(',').map((s) => s.trim()).filter(Boolean)
141
338
  }
142
339
 
340
+ function trimToString(value: unknown): string {
341
+ return typeof value === 'string' ? value.trim() : ''
342
+ }
343
+
344
+ function resolveSessionConnectorTargets(
345
+ session: {
346
+ connectorContext?: Record<string, unknown>
347
+ messages?: Array<Record<string, unknown>>
348
+ } | null | undefined,
349
+ connectorId: string,
350
+ ): Array<{ channelId: string; senderId?: string; senderName?: string }> {
351
+ const targets: Array<{ channelId: string; senderId?: string; senderName?: string }> = []
352
+ const seen = new Set<string>()
353
+ const pushTarget = (target: { channelId: string; senderId?: string; senderName?: string } | null) => {
354
+ if (!target?.channelId || seen.has(target.channelId)) return
355
+ seen.add(target.channelId)
356
+ targets.push(target)
357
+ }
358
+
359
+ const context = session?.connectorContext
360
+ if (trimToString(context?.connectorId) === connectorId) {
361
+ const channelId = trimToString(context?.channelId)
362
+ pushTarget(channelId
363
+ ? {
364
+ channelId,
365
+ senderId: trimToString(context?.senderId) || undefined,
366
+ senderName: trimToString(context?.senderName) || undefined,
367
+ }
368
+ : null)
369
+ }
370
+
371
+ const messages = Array.isArray(session?.messages) ? session.messages : []
372
+ for (let i = messages.length - 1; i >= 0; i -= 1) {
373
+ const source = messages[i]?.source as Record<string, unknown> | undefined
374
+ if (!source || trimToString(source.connectorId) !== connectorId) continue
375
+ const channelId = trimToString(source.channelId)
376
+ if (!channelId) continue
377
+ pushTarget({
378
+ channelId,
379
+ senderId: trimToString(source.senderId) || undefined,
380
+ senderName: trimToString(source.senderName) || undefined,
381
+ })
382
+ }
383
+
384
+ return targets
385
+ }
386
+
143
387
  function pickChannelTarget(params: {
144
388
  connector: { config?: Record<string, string> }
389
+ connectorId: string
145
390
  to?: string
146
391
  recentChannelId: string | null
392
+ currentSession?: {
393
+ connectorContext?: Record<string, unknown>
394
+ messages?: Array<Record<string, unknown>>
395
+ } | null
147
396
  }): { channelId: string; error?: string } {
148
397
  let channelId = params.to?.trim() || ''
149
398
  const connector = params.connector
399
+ const sessionTargets = resolveSessionConnectorTargets(params.currentSession, params.connectorId)
400
+
401
+ if (!channelId && sessionTargets.length === 1) {
402
+ channelId = sessionTargets[0].channelId
403
+ }
404
+ if (!channelId && sessionTargets.length > 1) {
405
+ const choices = sessionTargets.map((target) => (
406
+ target.senderName
407
+ ? `${target.senderName} (${target.channelId})`
408
+ : target.senderId
409
+ ? `${target.senderId} (${target.channelId})`
410
+ : target.channelId
411
+ ))
412
+ return {
413
+ channelId: '',
414
+ error: `Error: this chat currently references multiple connector recipients for this connector: ${JSON.stringify(choices)}. Re-call with the "to" parameter so the message goes to the right person.`,
415
+ }
416
+ }
150
417
 
151
418
  if (!channelId) {
152
419
  const outbound = connector.config?.outboundJid?.trim()
@@ -290,33 +557,10 @@ interface ConnectorActionContext {
290
557
  }
291
558
 
292
559
  async function executeConnectorAction(input: ConnectorActionInput, bctx: ConnectorActionContext) {
293
- const normalized = normalizeToolInputArgs((input ?? {}) as Record<string, unknown>)
294
- const {
295
- action,
296
- connectorId,
297
- platform,
298
- to,
299
- message,
300
- voiceText,
301
- voiceId,
302
- imageUrl,
303
- fileUrl,
304
- mediaPath,
305
- mimeType,
306
- fileName,
307
- caption,
308
- messageId,
309
- targetMessage,
310
- emoji,
311
- replyToMessageId,
312
- threadId,
313
- dedupeKey,
314
- approved,
315
- ptt,
316
- } = normalized as ConnectorActionInput
560
+ const baseNormalized = normalizeToolInputArgs((input ?? {}) as Record<string, unknown>)
317
561
 
318
562
  try {
319
- const actionName = String(action)
563
+ const tentativePlatform = pickConnectorString(baseNormalized.platform)
320
564
  const {
321
565
  listRunningConnectors,
322
566
  sendConnectorMessage,
@@ -324,7 +568,34 @@ async function executeConnectorAction(input: ConnectorActionInput, bctx: Connect
324
568
  scheduleConnectorFollowUp,
325
569
  performConnectorMessageAction,
326
570
  } = await import('../connectors/manager')
327
- const running = listRunningConnectors(platform || undefined)
571
+ const running = listRunningConnectors(tentativePlatform || undefined)
572
+ const normalized = normalizeConnectorActionInputAliases(baseNormalized, running)
573
+ const inferredAction = inferConnectorActionName(normalized)
574
+ const {
575
+ action,
576
+ connectorId,
577
+ platform,
578
+ to,
579
+ message,
580
+ voiceText,
581
+ voiceId,
582
+ imageUrl,
583
+ fileUrl,
584
+ mediaPath,
585
+ mimeType,
586
+ fileName,
587
+ caption,
588
+ messageId,
589
+ targetMessage,
590
+ emoji,
591
+ replyToMessageId,
592
+ threadId,
593
+ dedupeKey,
594
+ approved,
595
+ ptt,
596
+ } = normalized as ConnectorActionInput
597
+ const actionName = normalizeConnectorActionName(String(inferredAction || action || ''))
598
+ if (!actionName) return 'Error: action is required.'
328
599
 
329
600
  if (actionName === 'list_running' || actionName === 'list_targets') {
330
601
  return JSON.stringify(running)
@@ -391,8 +662,10 @@ async function executeConnectorAction(input: ConnectorActionInput, bctx: Connect
391
662
 
392
663
  const target = pickChannelTarget({
393
664
  connector,
665
+ connectorId: selected.id,
394
666
  to,
395
667
  recentChannelId: getConnectorRecentChannelId(selected.id),
668
+ currentSession,
396
669
  })
397
670
  if (target.error) return target.error
398
671
 
@@ -416,22 +689,61 @@ async function executeConnectorAction(input: ConnectorActionInput, bctx: Connect
416
689
  }
417
690
 
418
691
  if (actionName === 'send_voice_note') {
692
+ const media = resolveConnectorMediaInput({ cwd: bctx.cwd, mediaPath, imageUrl, fileUrl })
693
+ if (media.error) return media.error
694
+ if (media.imageUrl || media.fileUrl) {
695
+ return 'Error: send_voice_note requires an audio mediaPath or voiceText. Remote image/file URLs are not valid voice-note inputs.'
696
+ }
419
697
  const ttsText = (voiceText || message || '').trim()
420
- if (!ttsText) return 'Error: voiceText or message is required.'
421
- const audioBuffer = await synthesizeElevenLabsMp3({ text: ttsText, voiceId: voiceId?.trim() || undefined })
422
- const voiceFileName = `${Date.now()}-${genId()}-voicenote.mp3`
423
- const voicePath = path.join(UPLOAD_DIR, voiceFileName)
424
- fs.writeFileSync(voicePath, audioBuffer)
698
+ if (!media.mediaPath && !ttsText) return 'Error: voiceText, message, or an audio mediaPath is required.'
699
+ const voiceActionKey = buildConnectorActionKey([
700
+ sessionId,
701
+ actionName,
702
+ selected.id,
703
+ channelId,
704
+ media.mediaPath || '',
705
+ ttsText,
706
+ voiceId?.trim() || '',
707
+ fileName?.trim() || '',
708
+ caption?.trim() || '',
709
+ ptt ?? true,
710
+ ])
711
+ const cachedVoice = recentConnectorActionCache.get(voiceActionKey)
712
+ if (cachedVoice && now - cachedVoice.at <= CONNECTOR_ACTION_DEDUPE_TTL_MS) {
713
+ return cachedVoice.result
714
+ }
715
+ let voicePath = media.mediaPath
716
+ let outboundMimeType = mimeType?.trim() || undefined
717
+ if (voicePath) {
718
+ outboundMimeType = outboundMimeType || mimeFromPath(voicePath)
719
+ if (!isAudioMime(outboundMimeType)) {
720
+ return `Error: send_voice_note mediaPath must point to an audio file. Resolved MIME type was "${outboundMimeType}".`
721
+ }
722
+ } else {
723
+ const audioBuffer = await synthesizeElevenLabsMp3({ text: ttsText, voiceId: voiceId?.trim() || undefined })
724
+ const voiceFileName = `${Date.now()}-${genId()}-voicenote.mp3`
725
+ voicePath = path.join(UPLOAD_DIR, voiceFileName)
726
+ fs.writeFileSync(voicePath, audioBuffer)
727
+ outboundMimeType = 'audio/mpeg'
728
+ }
425
729
 
426
730
  const sent = await sendConnectorMessage({
427
- connectorId: selected.id, channelId, text: '', mediaPath: voicePath, mimeType: 'audio/mpeg',
731
+ connectorId: selected.id, channelId, text: '', mediaPath: voicePath, mimeType: outboundMimeType,
428
732
  fileName: fileName?.trim() || 'voicenote.mp3', caption: caption?.trim() || undefined, ptt: ptt ?? true,
429
733
  sessionId,
430
734
  replyToMessageId: replyToMessageId?.trim() || undefined,
431
735
  threadId: threadId?.trim() || undefined,
432
736
  })
433
- const result = JSON.stringify({ status: 'voice_sent', connectorId: sent.connectorId, platform: sent.platform, to: sent.channelId, voiceFile: voicePath })
737
+ const result = JSON.stringify({
738
+ status: 'voice_sent',
739
+ connectorId: sent.connectorId,
740
+ platform: sent.platform,
741
+ to: sent.channelId,
742
+ messageId: sent.messageId || null,
743
+ voiceFile: voicePath,
744
+ })
434
745
  connectorTurnSendBudget.set(turnKey, { count: (existingBudget?.count || 0) + 1, at: now, lastResult: result })
746
+ recentConnectorActionCache.set(voiceActionKey, { at: now, result })
435
747
  return result
436
748
  }
437
749
 
@@ -503,8 +815,10 @@ async function executeConnectorAction(input: ConnectorActionInput, bctx: Connect
503
815
  const { selected } = resolved
504
816
  const target = pickChannelTarget({
505
817
  connector: resolved.connector,
818
+ connectorId: selected.id,
506
819
  to,
507
820
  recentChannelId: getConnectorRecentChannelId(selected.id),
821
+ currentSession,
508
822
  })
509
823
  if (target.error) return target.error
510
824
  const result = await performConnectorMessageAction({
@@ -546,34 +860,7 @@ const ConnectorPlugin: Plugin = {
546
860
  {
547
861
  name: 'connector_message_tool',
548
862
  description: 'Send and manage outbound messages across chat platforms.',
549
- parameters: {
550
- type: 'object',
551
- properties: {
552
- action: { type: 'string', enum: ['list_running', 'start', 'stop', 'send', 'send_voice_note', 'schedule_followup', 'react', 'edit', 'delete', 'pin'] },
553
- connectorId: { type: 'string' },
554
- platform: { type: 'string' },
555
- to: { type: 'string' },
556
- message: { type: 'string' },
557
- messageId: { type: 'string' },
558
- targetMessage: { type: 'string', enum: ['last_inbound', 'last_outbound'] },
559
- emoji: { type: 'string' },
560
- voiceText: { type: 'string' },
561
- voiceId: { type: 'string' },
562
- imageUrl: { type: 'string' },
563
- fileUrl: { type: 'string' },
564
- mediaPath: { type: 'string' },
565
- mimeType: { type: 'string' },
566
- fileName: { type: 'string' },
567
- caption: { type: 'string' },
568
- replyToMessageId: { type: 'string' },
569
- threadId: { type: 'string' },
570
- delaySec: { type: 'number' },
571
- followUpMessage: { type: 'string' },
572
- followUpDelaySec: { type: 'number' },
573
- dedupeKey: { type: 'string' },
574
- },
575
- required: ['action']
576
- },
863
+ parameters: CONNECTOR_MESSAGE_TOOL_PARAMETERS,
577
864
  execute: async (args, context) => executeConnectorAction(args as ConnectorActionInput, { ...context.session, cwd: context.session.cwd || process.cwd() })
578
865
  }
579
866
  ]
@@ -48,15 +48,24 @@ function normalizeWorkspaceAlias(cwd: string, filePath: string): string {
48
48
  return trimmed
49
49
  }
50
50
 
51
+ /**
52
+ * Safe absolute paths that agents are allowed to write to outside the workspace.
53
+ * Kept minimal to prevent accidental writes to sensitive system locations.
54
+ */
55
+ const ALLOWED_ABSOLUTE_PREFIXES = ['/tmp/', '/var/tmp/']
56
+
51
57
  export function safePath(cwd: string, filePath: string): string {
52
58
  const path = require('path')
53
59
  const normalized = normalizeWorkspaceAlias(cwd, filePath)
54
60
  const resolvedRoot = path.resolve(cwd)
55
61
  const resolved = path.resolve(resolvedRoot, normalized)
56
- if (!resolved.startsWith(resolvedRoot)) {
57
- throw new Error('Path traversal not allowed')
62
+ // Allow workspace-relative paths
63
+ if (resolved.startsWith(resolvedRoot)) return resolved
64
+ // Allow explicitly safe absolute paths (e.g., /tmp/)
65
+ if (path.isAbsolute(normalized) && ALLOWED_ABSOLUTE_PREFIXES.some((p: string) => resolved.startsWith(p))) {
66
+ return resolved
58
67
  }
59
- return resolved
68
+ throw new Error('Path traversal not allowed')
60
69
  }
61
70
 
62
71
  export function truncate(text: string, max: number): string {