@swarmclawai/swarmclaw 0.7.7 → 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 (281) hide show
  1. package/README.md +12 -14
  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 +23 -2
  15. package/src/app/api/clawhub/install/route.ts +28 -8
  16. package/src/app/api/connectors/[id]/route.ts +46 -3
  17. package/src/app/api/connectors/route.ts +12 -8
  18. package/src/app/api/external-agents/route.test.ts +165 -0
  19. package/src/app/api/gateways/[id]/health/route.ts +27 -12
  20. package/src/app/api/gateways/[id]/route.ts +2 -0
  21. package/src/app/api/gateways/health-route.test.ts +135 -0
  22. package/src/app/api/gateways/route.ts +2 -0
  23. package/src/app/api/mcp-servers/route.test.ts +130 -0
  24. package/src/app/api/openclaw/deploy/route.ts +38 -5
  25. package/src/app/api/plugins/install/route.ts +46 -6
  26. package/src/app/api/plugins/marketplace/route.ts +48 -15
  27. package/src/app/api/preview-server/route.ts +26 -11
  28. package/src/app/api/projects/[id]/route.ts +6 -2
  29. package/src/app/api/projects/route.ts +4 -3
  30. package/src/app/api/schedules/[id]/run/route.ts +4 -0
  31. package/src/app/api/schedules/route.test.ts +86 -0
  32. package/src/app/api/schedules/route.ts +6 -1
  33. package/src/app/api/secrets/[id]/route.ts +1 -0
  34. package/src/app/api/secrets/route.ts +2 -1
  35. package/src/app/api/settings/route.ts +2 -0
  36. package/src/app/api/setup/check-provider/route.test.ts +19 -0
  37. package/src/app/api/setup/check-provider/route.ts +40 -10
  38. package/src/app/api/skills/[id]/route.ts +12 -0
  39. package/src/app/api/skills/import/route.ts +14 -12
  40. package/src/app/api/skills/route.ts +13 -1
  41. package/src/app/api/tasks/[id]/route.ts +10 -1
  42. package/src/app/api/tasks/import/github/route.test.ts +65 -0
  43. package/src/app/api/tasks/import/github/route.ts +337 -0
  44. package/src/app/api/wallets/[id]/approve/route.ts +17 -3
  45. package/src/app/api/wallets/[id]/route.ts +79 -33
  46. package/src/app/api/wallets/[id]/send/route.ts +19 -33
  47. package/src/app/api/wallets/route.ts +78 -61
  48. package/src/app/api/webhooks/[id]/route.ts +33 -6
  49. package/src/app/api/webhooks/route.test.ts +272 -0
  50. package/src/cli/index.js +1 -0
  51. package/src/cli/spec.js +1 -0
  52. package/src/components/agents/agent-card.tsx +9 -2
  53. package/src/components/agents/agent-chat-list.tsx +18 -2
  54. package/src/components/agents/agent-list.tsx +1 -0
  55. package/src/components/agents/agent-sheet.tsx +257 -38
  56. package/src/components/agents/inspector-panel.tsx +41 -0
  57. package/src/components/canvas/canvas-panel.tsx +236 -65
  58. package/src/components/chat/chat-area.tsx +36 -19
  59. package/src/components/chat/chat-card.tsx +36 -13
  60. package/src/components/chat/chat-header.tsx +48 -16
  61. package/src/components/chat/chat-list.tsx +28 -4
  62. package/src/components/chat/checkpoint-timeline.tsx +50 -34
  63. package/src/components/chat/delegation-banner.test.ts +14 -1
  64. package/src/components/chat/delegation-banner.tsx +1 -1
  65. package/src/components/chat/message-bubble.tsx +208 -145
  66. package/src/components/chat/message-list.tsx +48 -19
  67. package/src/components/chatrooms/chatroom-message.tsx +2 -2
  68. package/src/components/chatrooms/chatroom-sheet.tsx +16 -2
  69. package/src/components/connectors/connector-health.tsx +1 -1
  70. package/src/components/connectors/connector-list.tsx +7 -2
  71. package/src/components/connectors/connector-sheet.tsx +337 -148
  72. package/src/components/gateways/gateway-sheet.tsx +2 -2
  73. package/src/components/layout/app-layout.tsx +40 -23
  74. package/src/components/mcp-servers/mcp-server-list.tsx +26 -5
  75. package/src/components/mcp-servers/mcp-server-sheet.tsx +19 -2
  76. package/src/components/openclaw/openclaw-deploy-panel.tsx +269 -21
  77. package/src/components/plugins/plugin-list.tsx +45 -9
  78. package/src/components/plugins/plugin-sheet.tsx +55 -7
  79. package/src/components/projects/project-detail.tsx +217 -0
  80. package/src/components/projects/project-sheet.tsx +176 -4
  81. package/src/components/providers/provider-list.tsx +2 -1
  82. package/src/components/providers/provider-sheet.tsx +21 -2
  83. package/src/components/schedules/schedule-card.tsx +25 -1
  84. package/src/components/schedules/schedule-sheet.tsx +44 -2
  85. package/src/components/secrets/secret-sheet.tsx +21 -2
  86. package/src/components/shared/agent-switch-dialog.tsx +12 -1
  87. package/src/components/shared/bottom-sheet.tsx +13 -3
  88. package/src/components/shared/command-palette.tsx +8 -1
  89. package/src/components/shared/confirm-dialog.tsx +19 -4
  90. package/src/components/shared/connector-platform-icon.test.ts +28 -0
  91. package/src/components/shared/connector-platform-icon.tsx +39 -6
  92. package/src/components/shared/settings/plugin-manager.tsx +29 -6
  93. package/src/components/shared/settings/section-capability-policy.tsx +45 -3
  94. package/src/components/shared/settings/section-voice.tsx +11 -3
  95. package/src/components/skills/skill-list.tsx +25 -0
  96. package/src/components/skills/skill-sheet.tsx +84 -12
  97. package/src/components/tasks/approvals-panel.tsx +289 -34
  98. package/src/components/tasks/task-board.tsx +410 -25
  99. package/src/components/tasks/task-card.tsx +66 -8
  100. package/src/components/tasks/task-sheet.tsx +16 -4
  101. package/src/components/ui/dialog.tsx +2 -2
  102. package/src/components/wallets/wallet-approval-dialog.tsx +4 -2
  103. package/src/components/wallets/wallet-panel.tsx +435 -90
  104. package/src/components/wallets/wallet-section.tsx +198 -48
  105. package/src/components/webhooks/webhook-sheet.tsx +22 -2
  106. package/src/lib/approval-display.ts +20 -0
  107. package/src/lib/canvas-content.ts +198 -0
  108. package/src/lib/chat-artifact-summary.ts +165 -0
  109. package/src/lib/chat-display.test.ts +91 -0
  110. package/src/lib/chat-display.ts +58 -0
  111. package/src/lib/chat-streaming-state.test.ts +47 -1
  112. package/src/lib/chat-streaming-state.ts +42 -0
  113. package/src/lib/ollama-model.ts +10 -0
  114. package/src/lib/openclaw-endpoint.test.ts +8 -0
  115. package/src/lib/openclaw-endpoint.ts +6 -1
  116. package/src/lib/plugin-install-cors.ts +46 -0
  117. package/src/lib/plugin-sources.test.ts +43 -0
  118. package/src/lib/plugin-sources.ts +77 -0
  119. package/src/lib/providers/ollama.ts +16 -6
  120. package/src/lib/providers/openclaw.test.ts +54 -0
  121. package/src/lib/providers/openclaw.ts +127 -11
  122. package/src/lib/schedule-dedupe-advanced.test.ts +1335 -0
  123. package/src/lib/schedule-dedupe.test.ts +66 -1
  124. package/src/lib/schedule-dedupe.ts +169 -12
  125. package/src/lib/schedule-origin.test.ts +20 -0
  126. package/src/lib/schedule-origin.ts +15 -0
  127. package/src/lib/server/__fixtures__/fake-mcp-stdio-server.mjs +27 -0
  128. package/src/lib/server/agent-availability.ts +16 -0
  129. package/src/lib/server/agent-runtime-config.ts +12 -4
  130. package/src/lib/server/agent-thread-session.test.ts +51 -0
  131. package/src/lib/server/agent-thread-session.ts +7 -0
  132. package/src/lib/server/approval-match.ts +205 -0
  133. package/src/lib/server/approvals-auto-approve.test.ts +538 -1
  134. package/src/lib/server/approvals.ts +214 -1
  135. package/src/lib/server/assistant-control.test.ts +29 -0
  136. package/src/lib/server/assistant-control.ts +23 -0
  137. package/src/lib/server/build-llm.test.ts +79 -0
  138. package/src/lib/server/build-llm.ts +14 -4
  139. package/src/lib/server/canvas-content.test.ts +32 -0
  140. package/src/lib/server/canvas-content.ts +6 -0
  141. package/src/lib/server/capability-router.test.ts +33 -0
  142. package/src/lib/server/capability-router.ts +80 -19
  143. package/src/lib/server/chat-execution-advanced.test.ts +651 -0
  144. package/src/lib/server/chat-execution-disabled.test.ts +94 -0
  145. package/src/lib/server/chat-execution-tool-events.test.ts +157 -0
  146. package/src/lib/server/chat-execution.ts +378 -73
  147. package/src/lib/server/clawhub-client.test.ts +14 -8
  148. package/src/lib/server/connectors/manager-reconnect.test.ts +47 -0
  149. package/src/lib/server/connectors/manager.test.ts +1147 -0
  150. package/src/lib/server/connectors/manager.ts +461 -137
  151. package/src/lib/server/connectors/pairing.ts +26 -5
  152. package/src/lib/server/connectors/types.ts +2 -0
  153. package/src/lib/server/connectors/whatsapp.test.ts +134 -0
  154. package/src/lib/server/connectors/whatsapp.ts +271 -47
  155. package/src/lib/server/context-manager.ts +6 -1
  156. package/src/lib/server/daemon-state.ts +84 -47
  157. package/src/lib/server/data-dir.test.ts +37 -0
  158. package/src/lib/server/data-dir.ts +20 -1
  159. package/src/lib/server/delegation-jobs-advanced.test.ts +513 -0
  160. package/src/lib/server/devserver-launch.test.ts +60 -0
  161. package/src/lib/server/devserver-launch.ts +85 -0
  162. package/src/lib/server/elevenlabs.test.ts +247 -1
  163. package/src/lib/server/elevenlabs.ts +147 -43
  164. package/src/lib/server/ethereum.ts +590 -0
  165. package/src/lib/server/eval/agent-regression-advanced.test.ts +302 -0
  166. package/src/lib/server/eval/agent-regression.test.ts +18 -1
  167. package/src/lib/server/eval/agent-regression.ts +383 -11
  168. package/src/lib/server/evm-swap.ts +475 -0
  169. package/src/lib/server/execution-log.ts +1 -0
  170. package/src/lib/server/heartbeat-service-timer.test.ts +173 -0
  171. package/src/lib/server/heartbeat-service.ts +20 -11
  172. package/src/lib/server/heartbeat-wake.test.ts +112 -0
  173. package/src/lib/server/heartbeat-wake.ts +338 -57
  174. package/src/lib/server/main-agent-loop-advanced.test.ts +538 -0
  175. package/src/lib/server/main-agent-loop.test.ts +260 -0
  176. package/src/lib/server/main-agent-loop.ts +559 -14
  177. package/src/lib/server/mcp-client.test.ts +16 -0
  178. package/src/lib/server/mcp-client.ts +25 -0
  179. package/src/lib/server/memory-integration.test.ts +719 -0
  180. package/src/lib/server/memory-policy.test.ts +43 -0
  181. package/src/lib/server/memory-policy.ts +132 -0
  182. package/src/lib/server/memory-tiers.test.ts +60 -0
  183. package/src/lib/server/memory-tiers.ts +16 -0
  184. package/src/lib/server/ollama-runtime.ts +58 -0
  185. package/src/lib/server/openclaw-deploy.test.ts +109 -1
  186. package/src/lib/server/openclaw-deploy.ts +557 -81
  187. package/src/lib/server/openclaw-gateway.test.ts +131 -0
  188. package/src/lib/server/openclaw-gateway.ts +10 -4
  189. package/src/lib/server/openclaw-health.test.ts +35 -0
  190. package/src/lib/server/openclaw-health.ts +215 -47
  191. package/src/lib/server/orchestrator-lg.ts +3 -2
  192. package/src/lib/server/orchestrator.ts +2 -0
  193. package/src/lib/server/plugins-advanced.test.ts +351 -0
  194. package/src/lib/server/plugins.ts +211 -6
  195. package/src/lib/server/project-context.ts +162 -0
  196. package/src/lib/server/project-utils.ts +150 -0
  197. package/src/lib/server/queue-advanced.test.ts +528 -0
  198. package/src/lib/server/queue-followups.test.ts +409 -2
  199. package/src/lib/server/queue-reconcile.test.ts +128 -0
  200. package/src/lib/server/queue.ts +527 -68
  201. package/src/lib/server/scheduler.ts +29 -1
  202. package/src/lib/server/session-note.test.ts +36 -0
  203. package/src/lib/server/session-note.ts +42 -0
  204. package/src/lib/server/session-run-manager.ts +83 -4
  205. package/src/lib/server/session-tools/canvas.ts +14 -12
  206. package/src/lib/server/session-tools/connector-inputs.test.ts +37 -0
  207. package/src/lib/server/session-tools/connector.test.ts +138 -0
  208. package/src/lib/server/session-tools/connector.ts +366 -54
  209. package/src/lib/server/session-tools/context.ts +17 -3
  210. package/src/lib/server/session-tools/crud.ts +484 -84
  211. package/src/lib/server/session-tools/delegate-fallback.test.ts +103 -0
  212. package/src/lib/server/session-tools/delegate-resume.test.ts +50 -0
  213. package/src/lib/server/session-tools/delegate.ts +102 -10
  214. package/src/lib/server/session-tools/discovery-approvals.test.ts +142 -0
  215. package/src/lib/server/session-tools/discovery.ts +80 -12
  216. package/src/lib/server/session-tools/file-normalize.test.ts +36 -0
  217. package/src/lib/server/session-tools/file.ts +43 -4
  218. package/src/lib/server/session-tools/human-loop.ts +35 -5
  219. package/src/lib/server/session-tools/index.ts +44 -9
  220. package/src/lib/server/session-tools/manage-connectors.test.ts +139 -0
  221. package/src/lib/server/session-tools/manage-schedules-advanced.test.ts +564 -0
  222. package/src/lib/server/session-tools/manage-schedules.test.ts +283 -0
  223. package/src/lib/server/session-tools/manage-tasks-advanced.test.ts +852 -0
  224. package/src/lib/server/session-tools/manage-tasks.test.ts +114 -0
  225. package/src/lib/server/session-tools/memory.test.ts +93 -0
  226. package/src/lib/server/session-tools/memory.ts +554 -75
  227. package/src/lib/server/session-tools/normalize-tool-args.ts +1 -1
  228. package/src/lib/server/session-tools/platform-access.test.ts +58 -0
  229. package/src/lib/server/session-tools/platform.ts +60 -19
  230. package/src/lib/server/session-tools/plugin-creator.ts +57 -1
  231. package/src/lib/server/session-tools/primitive-tools.test.ts +6 -0
  232. package/src/lib/server/session-tools/schedule.ts +6 -1
  233. package/src/lib/server/session-tools/shell-normalize.test.ts +25 -1
  234. package/src/lib/server/session-tools/shell.ts +22 -3
  235. package/src/lib/server/session-tools/wallet-tool.test.ts +254 -0
  236. package/src/lib/server/session-tools/wallet.ts +1374 -139
  237. package/src/lib/server/session-tools/web-inputs.test.ts +178 -0
  238. package/src/lib/server/session-tools/web.ts +621 -70
  239. package/src/lib/server/skill-discovery.ts +128 -0
  240. package/src/lib/server/skill-eligibility.test.ts +84 -0
  241. package/src/lib/server/skill-eligibility.ts +95 -0
  242. package/src/lib/server/skill-prompt-budget.test.ts +102 -0
  243. package/src/lib/server/skill-prompt-budget.ts +125 -0
  244. package/src/lib/server/skills-normalize.test.ts +54 -0
  245. package/src/lib/server/skills-normalize.ts +372 -26
  246. package/src/lib/server/solana.ts +214 -29
  247. package/src/lib/server/storage.ts +65 -36
  248. package/src/lib/server/stream-agent-chat.test.ts +437 -2
  249. package/src/lib/server/stream-agent-chat.ts +957 -79
  250. package/src/lib/server/system-events.ts +1 -1
  251. package/src/lib/server/tool-aliases.ts +2 -0
  252. package/src/lib/server/tool-capability-policy-advanced.test.ts +502 -0
  253. package/src/lib/server/tool-capability-policy.test.ts +24 -0
  254. package/src/lib/server/tool-capability-policy.ts +29 -1
  255. package/src/lib/server/tool-loop-detection.test.ts +105 -0
  256. package/src/lib/server/tool-loop-detection.ts +260 -0
  257. package/src/lib/server/tool-planning.test.ts +44 -0
  258. package/src/lib/server/tool-planning.ts +271 -0
  259. package/src/lib/server/wallet-execution.test.ts +198 -0
  260. package/src/lib/server/wallet-portfolio.test.ts +98 -0
  261. package/src/lib/server/wallet-portfolio.ts +724 -0
  262. package/src/lib/server/wallet-service.test.ts +57 -0
  263. package/src/lib/server/wallet-service.ts +213 -0
  264. package/src/lib/server/watch-jobs-advanced.test.ts +594 -0
  265. package/src/lib/server/watch-jobs.ts +17 -2
  266. package/src/lib/server/workspace-context.ts +111 -0
  267. package/src/lib/skill-save-payload.test.ts +39 -0
  268. package/src/lib/skill-save-payload.ts +37 -0
  269. package/src/lib/tasks.ts +28 -0
  270. package/src/lib/tool-definitions.ts +2 -1
  271. package/src/lib/tool-event-summary.test.ts +30 -0
  272. package/src/lib/tool-event-summary.ts +37 -0
  273. package/src/lib/validation/schemas.ts +1 -0
  274. package/src/lib/wallet-transactions.test.ts +75 -0
  275. package/src/lib/wallet-transactions.ts +43 -0
  276. package/src/lib/wallet.test.ts +17 -0
  277. package/src/lib/wallet.ts +183 -0
  278. package/src/proxy.test.ts +31 -0
  279. package/src/proxy.ts +34 -2
  280. package/src/stores/use-chat-store.ts +15 -1
  281. package/src/types/index.ts +249 -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()
@@ -189,7 +456,7 @@ function pickChannelTarget(params: {
189
456
  return { channelId }
190
457
  }
191
458
 
192
- function resolveConnectorMediaInput(params: {
459
+ export function resolveConnectorMediaInput(params: {
193
460
  cwd: string
194
461
  mediaPath?: string
195
462
  imageUrl?: string
@@ -199,6 +466,23 @@ function resolveConnectorMediaInput(params: {
199
466
  let resolvedImageUrl = params.imageUrl?.trim() || undefined
200
467
  let resolvedFileUrl = params.fileUrl?.trim() || undefined
201
468
 
469
+ // Be forgiving when the model passes a served upload URL or remote URL in mediaPath.
470
+ if (resolvedMediaPath?.startsWith('/api/uploads/')) {
471
+ const fromUpload = resolveUploadUrl(resolvedMediaPath)
472
+ if (fromUpload) {
473
+ resolvedMediaPath = fromUpload.mediaPath
474
+ } else {
475
+ return { error: `Error: File not found: ${resolvedMediaPath}` }
476
+ }
477
+ } else if (resolvedMediaPath && /^https?:\/\//i.test(resolvedMediaPath)) {
478
+ if (/\.(png|jpe?g|webp|gif|svg)(?:[?#].*)?$/i.test(resolvedMediaPath)) {
479
+ resolvedImageUrl = resolvedMediaPath
480
+ } else {
481
+ resolvedFileUrl = resolvedMediaPath
482
+ }
483
+ resolvedMediaPath = undefined
484
+ }
485
+
202
486
  if (resolvedMediaPath && !path.isAbsolute(resolvedMediaPath) && !resolvedMediaPath.startsWith('/api/uploads/')) {
203
487
  const candidatePaths = [
204
488
  path.resolve(params.cwd, resolvedMediaPath),
@@ -273,33 +557,10 @@ interface ConnectorActionContext {
273
557
  }
274
558
 
275
559
  async function executeConnectorAction(input: ConnectorActionInput, bctx: ConnectorActionContext) {
276
- const normalized = normalizeToolInputArgs((input ?? {}) as Record<string, unknown>)
277
- const {
278
- action,
279
- connectorId,
280
- platform,
281
- to,
282
- message,
283
- voiceText,
284
- voiceId,
285
- imageUrl,
286
- fileUrl,
287
- mediaPath,
288
- mimeType,
289
- fileName,
290
- caption,
291
- messageId,
292
- targetMessage,
293
- emoji,
294
- replyToMessageId,
295
- threadId,
296
- dedupeKey,
297
- approved,
298
- ptt,
299
- } = normalized as ConnectorActionInput
560
+ const baseNormalized = normalizeToolInputArgs((input ?? {}) as Record<string, unknown>)
300
561
 
301
562
  try {
302
- const actionName = String(action)
563
+ const tentativePlatform = pickConnectorString(baseNormalized.platform)
303
564
  const {
304
565
  listRunningConnectors,
305
566
  sendConnectorMessage,
@@ -307,7 +568,34 @@ async function executeConnectorAction(input: ConnectorActionInput, bctx: Connect
307
568
  scheduleConnectorFollowUp,
308
569
  performConnectorMessageAction,
309
570
  } = await import('../connectors/manager')
310
- 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.'
311
599
 
312
600
  if (actionName === 'list_running' || actionName === 'list_targets') {
313
601
  return JSON.stringify(running)
@@ -374,8 +662,10 @@ async function executeConnectorAction(input: ConnectorActionInput, bctx: Connect
374
662
 
375
663
  const target = pickChannelTarget({
376
664
  connector,
665
+ connectorId: selected.id,
377
666
  to,
378
667
  recentChannelId: getConnectorRecentChannelId(selected.id),
668
+ currentSession,
379
669
  })
380
670
  if (target.error) return target.error
381
671
 
@@ -399,22 +689,61 @@ async function executeConnectorAction(input: ConnectorActionInput, bctx: Connect
399
689
  }
400
690
 
401
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
+ }
402
697
  const ttsText = (voiceText || message || '').trim()
403
- if (!ttsText) return 'Error: voiceText or message is required.'
404
- const audioBuffer = await synthesizeElevenLabsMp3({ text: ttsText, voiceId: voiceId?.trim() || undefined })
405
- const voiceFileName = `${Date.now()}-${genId()}-voicenote.mp3`
406
- const voicePath = path.join(UPLOAD_DIR, voiceFileName)
407
- 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
+ }
408
729
 
409
730
  const sent = await sendConnectorMessage({
410
- connectorId: selected.id, channelId, text: '', mediaPath: voicePath, mimeType: 'audio/mpeg',
731
+ connectorId: selected.id, channelId, text: '', mediaPath: voicePath, mimeType: outboundMimeType,
411
732
  fileName: fileName?.trim() || 'voicenote.mp3', caption: caption?.trim() || undefined, ptt: ptt ?? true,
412
733
  sessionId,
413
734
  replyToMessageId: replyToMessageId?.trim() || undefined,
414
735
  threadId: threadId?.trim() || undefined,
415
736
  })
416
- 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
+ })
417
745
  connectorTurnSendBudget.set(turnKey, { count: (existingBudget?.count || 0) + 1, at: now, lastResult: result })
746
+ recentConnectorActionCache.set(voiceActionKey, { at: now, result })
418
747
  return result
419
748
  }
420
749
 
@@ -486,8 +815,10 @@ async function executeConnectorAction(input: ConnectorActionInput, bctx: Connect
486
815
  const { selected } = resolved
487
816
  const target = pickChannelTarget({
488
817
  connector: resolved.connector,
818
+ connectorId: selected.id,
489
819
  to,
490
820
  recentChannelId: getConnectorRecentChannelId(selected.id),
821
+ currentSession,
491
822
  })
492
823
  if (target.error) return target.error
493
824
  const result = await performConnectorMessageAction({
@@ -529,26 +860,7 @@ const ConnectorPlugin: Plugin = {
529
860
  {
530
861
  name: 'connector_message_tool',
531
862
  description: 'Send and manage outbound messages across chat platforms.',
532
- parameters: {
533
- type: 'object',
534
- properties: {
535
- action: { type: 'string', enum: ['list_running', 'start', 'stop', 'send', 'send_voice_note', 'schedule_followup', 'react', 'edit', 'delete', 'pin'] },
536
- connectorId: { type: 'string' },
537
- platform: { type: 'string' },
538
- to: { type: 'string' },
539
- message: { type: 'string' },
540
- messageId: { type: 'string' },
541
- targetMessage: { type: 'string', enum: ['last_inbound', 'last_outbound'] },
542
- emoji: { type: 'string' },
543
- replyToMessageId: { type: 'string' },
544
- threadId: { type: 'string' },
545
- delaySec: { type: 'number' },
546
- followUpMessage: { type: 'string' },
547
- followUpDelaySec: { type: 'number' },
548
- dedupeKey: { type: 'string' },
549
- },
550
- required: ['action']
551
- },
863
+ parameters: CONNECTOR_MESSAGE_TOOL_PARAMETERS,
552
864
  execute: async (args, context) => executeConnectorAction(args as ConnectorActionInput, { ...context.session, cwd: context.session.cwd || process.cwd() })
553
865
  }
554
866
  ]
@@ -9,6 +9,11 @@ export interface ToolContext {
9
9
  platformAssignScope?: 'self' | 'all'
10
10
  mcpServerIds?: string[]
11
11
  mcpDisabledTools?: string[]
12
+ projectId?: string | null
13
+ projectRoot?: string | null
14
+ projectName?: string | null
15
+ projectDescription?: string | null
16
+ memoryScopeMode?: 'auto' | 'all' | 'global' | 'agent' | 'session' | 'project' | null
12
17
  }
13
18
 
14
19
  export interface SessionToolsResult {
@@ -43,15 +48,24 @@ function normalizeWorkspaceAlias(cwd: string, filePath: string): string {
43
48
  return trimmed
44
49
  }
45
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
+
46
57
  export function safePath(cwd: string, filePath: string): string {
47
58
  const path = require('path')
48
59
  const normalized = normalizeWorkspaceAlias(cwd, filePath)
49
60
  const resolvedRoot = path.resolve(cwd)
50
61
  const resolved = path.resolve(resolvedRoot, normalized)
51
- if (!resolved.startsWith(resolvedRoot)) {
52
- 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
53
67
  }
54
- return resolved
68
+ throw new Error('Path traversal not allowed')
55
69
  }
56
70
 
57
71
  export function truncate(text: string, max: number): string {