@swarmclawai/swarmclaw 0.5.3 → 0.6.2

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 (224) hide show
  1. package/README.md +53 -9
  2. package/bin/server-cmd.js +1 -0
  3. package/bin/swarmclaw.js +76 -16
  4. package/next.config.ts +11 -1
  5. package/package.json +5 -2
  6. package/scripts/postinstall.mjs +18 -0
  7. package/src/app/api/canvas/[sessionId]/route.ts +31 -0
  8. package/src/app/api/chatrooms/[id]/chat/route.ts +284 -0
  9. package/src/app/api/chatrooms/[id]/members/route.ts +82 -0
  10. package/src/app/api/chatrooms/[id]/pins/route.ts +39 -0
  11. package/src/app/api/chatrooms/[id]/reactions/route.ts +42 -0
  12. package/src/app/api/chatrooms/[id]/route.ts +84 -0
  13. package/src/app/api/chatrooms/route.ts +50 -0
  14. package/src/app/api/connectors/[id]/route.ts +1 -0
  15. package/src/app/api/connectors/route.ts +2 -1
  16. package/src/app/api/credentials/route.ts +2 -3
  17. package/src/app/api/files/open/route.ts +43 -0
  18. package/src/app/api/knowledge/[id]/route.ts +13 -2
  19. package/src/app/api/knowledge/route.ts +8 -1
  20. package/src/app/api/memory/route.ts +8 -0
  21. package/src/app/api/notifications/route.ts +4 -0
  22. package/src/app/api/orchestrator/run/route.ts +1 -1
  23. package/src/app/api/plugins/install/route.ts +2 -2
  24. package/src/app/api/search/route.ts +53 -1
  25. package/src/app/api/sessions/[id]/chat/route.ts +2 -0
  26. package/src/app/api/sessions/[id]/edit-resend/route.ts +1 -1
  27. package/src/app/api/sessions/[id]/fork/route.ts +1 -1
  28. package/src/app/api/sessions/[id]/messages/route.ts +70 -2
  29. package/src/app/api/sessions/[id]/route.ts +4 -0
  30. package/src/app/api/sessions/route.ts +3 -3
  31. package/src/app/api/settings/route.ts +9 -0
  32. package/src/app/api/setup/check-provider/route.ts +3 -16
  33. package/src/app/api/skills/[id]/route.ts +6 -0
  34. package/src/app/api/skills/route.ts +6 -0
  35. package/src/app/api/tasks/[id]/route.ts +12 -0
  36. package/src/app/api/tasks/bulk/route.ts +100 -0
  37. package/src/app/api/tasks/metrics/route.ts +101 -0
  38. package/src/app/api/tasks/route.ts +18 -2
  39. package/src/app/api/tts/route.ts +3 -2
  40. package/src/app/api/tts/stream/route.ts +3 -2
  41. package/src/app/api/uploads/[filename]/route.ts +19 -34
  42. package/src/app/api/uploads/route.ts +94 -0
  43. package/src/app/api/webhooks/[id]/route.ts +15 -1
  44. package/src/app/globals.css +63 -15
  45. package/src/app/page.tsx +142 -13
  46. package/src/cli/index.js +40 -1
  47. package/src/cli/index.test.js +30 -0
  48. package/src/cli/spec.js +42 -0
  49. package/src/components/agents/agent-avatar.tsx +57 -10
  50. package/src/components/agents/agent-card.tsx +50 -17
  51. package/src/components/agents/agent-chat-list.tsx +148 -12
  52. package/src/components/agents/agent-list.tsx +50 -19
  53. package/src/components/agents/agent-sheet.tsx +120 -65
  54. package/src/components/agents/inspector-panel.tsx +81 -6
  55. package/src/components/agents/openclaw-skills-panel.tsx +32 -3
  56. package/src/components/agents/personality-builder.tsx +42 -14
  57. package/src/components/agents/soul-library-picker.tsx +89 -0
  58. package/src/components/auth/access-key-gate.tsx +10 -3
  59. package/src/components/auth/setup-wizard.tsx +2 -2
  60. package/src/components/auth/user-picker.tsx +31 -3
  61. package/src/components/canvas/canvas-panel.tsx +96 -0
  62. package/src/components/chat/activity-moment.tsx +173 -0
  63. package/src/components/chat/chat-area.tsx +46 -22
  64. package/src/components/chat/chat-header.tsx +457 -286
  65. package/src/components/chat/chat-preview-panel.tsx +1 -2
  66. package/src/components/chat/chat-tool-toggles.tsx +1 -1
  67. package/src/components/chat/delegation-banner.tsx +371 -0
  68. package/src/components/chat/file-path-chip.tsx +146 -0
  69. package/src/components/chat/heartbeat-history-panel.tsx +269 -0
  70. package/src/components/chat/markdown-utils.ts +9 -0
  71. package/src/components/chat/message-bubble.tsx +356 -315
  72. package/src/components/chat/message-list.tsx +230 -8
  73. package/src/components/chat/streaming-bubble.tsx +104 -47
  74. package/src/components/chat/suggestions-bar.tsx +1 -1
  75. package/src/components/chat/thinking-indicator.tsx +72 -10
  76. package/src/components/chat/tool-call-bubble.tsx +111 -73
  77. package/src/components/chat/tool-request-banner.tsx +31 -7
  78. package/src/components/chat/transfer-agent-picker.tsx +63 -0
  79. package/src/components/chatrooms/agent-hover-card.tsx +124 -0
  80. package/src/components/chatrooms/chatroom-input.tsx +320 -0
  81. package/src/components/chatrooms/chatroom-list.tsx +130 -0
  82. package/src/components/chatrooms/chatroom-message.tsx +432 -0
  83. package/src/components/chatrooms/chatroom-sheet.tsx +215 -0
  84. package/src/components/chatrooms/chatroom-tool-request-banner.tsx +134 -0
  85. package/src/components/chatrooms/chatroom-typing-bar.tsx +88 -0
  86. package/src/components/chatrooms/chatroom-view.tsx +344 -0
  87. package/src/components/chatrooms/reaction-picker.tsx +273 -0
  88. package/src/components/connectors/connector-list.tsx +168 -90
  89. package/src/components/connectors/connector-sheet.tsx +95 -56
  90. package/src/components/home/home-view.tsx +501 -0
  91. package/src/components/input/chat-input.tsx +107 -43
  92. package/src/components/knowledge/knowledge-list.tsx +31 -1
  93. package/src/components/knowledge/knowledge-sheet.tsx +83 -2
  94. package/src/components/layout/app-layout.tsx +194 -97
  95. package/src/components/layout/update-banner.tsx +2 -2
  96. package/src/components/logs/log-list.tsx +2 -2
  97. package/src/components/mcp-servers/mcp-server-sheet.tsx +1 -1
  98. package/src/components/memory/memory-agent-list.tsx +143 -0
  99. package/src/components/memory/memory-browser.tsx +205 -0
  100. package/src/components/memory/memory-card.tsx +34 -7
  101. package/src/components/memory/memory-detail.tsx +359 -120
  102. package/src/components/memory/memory-sheet.tsx +157 -23
  103. package/src/components/plugins/plugin-list.tsx +1 -1
  104. package/src/components/plugins/plugin-sheet.tsx +1 -1
  105. package/src/components/projects/project-detail.tsx +509 -0
  106. package/src/components/projects/project-list.tsx +195 -59
  107. package/src/components/providers/provider-list.tsx +2 -2
  108. package/src/components/providers/provider-sheet.tsx +3 -3
  109. package/src/components/schedules/schedule-card.tsx +1 -1
  110. package/src/components/schedules/schedule-list.tsx +1 -1
  111. package/src/components/schedules/schedule-sheet.tsx +259 -126
  112. package/src/components/secrets/secret-sheet.tsx +47 -24
  113. package/src/components/secrets/secrets-list.tsx +18 -8
  114. package/src/components/sessions/new-session-sheet.tsx +33 -65
  115. package/src/components/sessions/session-card.tsx +45 -14
  116. package/src/components/sessions/session-list.tsx +35 -18
  117. package/src/components/settings/gateway-disconnect-overlay.tsx +80 -0
  118. package/src/components/shared/agent-picker-list.tsx +90 -0
  119. package/src/components/shared/agent-switch-dialog.tsx +156 -0
  120. package/src/components/shared/attachment-chip.tsx +165 -0
  121. package/src/components/shared/avatar.tsx +10 -1
  122. package/src/components/shared/chatroom-picker-list.tsx +61 -0
  123. package/src/components/shared/check-icon.tsx +12 -0
  124. package/src/components/shared/confirm-dialog.tsx +1 -1
  125. package/src/components/shared/connector-platform-icon.tsx +51 -4
  126. package/src/components/shared/empty-state.tsx +32 -0
  127. package/src/components/shared/file-preview.tsx +34 -0
  128. package/src/components/shared/form-styles.ts +2 -0
  129. package/src/components/shared/icon-button.tsx +16 -2
  130. package/src/components/shared/keyboard-shortcuts-dialog.tsx +116 -0
  131. package/src/components/shared/notification-center.tsx +44 -6
  132. package/src/components/shared/profile-sheet.tsx +115 -0
  133. package/src/components/shared/reply-quote.tsx +26 -0
  134. package/src/components/shared/search-dialog.tsx +31 -15
  135. package/src/components/shared/section-label.tsx +12 -0
  136. package/src/components/shared/settings/plugin-manager.tsx +1 -1
  137. package/src/components/shared/settings/section-embedding.tsx +48 -13
  138. package/src/components/shared/settings/section-orchestrator.tsx +46 -15
  139. package/src/components/shared/settings/section-providers.tsx +1 -1
  140. package/src/components/shared/settings/section-secrets.tsx +1 -1
  141. package/src/components/shared/settings/section-storage.tsx +206 -0
  142. package/src/components/shared/settings/section-theme.tsx +95 -0
  143. package/src/components/shared/settings/section-user-preferences.tsx +57 -0
  144. package/src/components/shared/settings/section-voice.tsx +42 -21
  145. package/src/components/shared/settings/section-web-search.tsx +30 -6
  146. package/src/components/shared/settings/settings-page.tsx +182 -27
  147. package/src/components/shared/settings/settings-sheet.tsx +9 -73
  148. package/src/components/shared/settings/storage-browser.tsx +259 -0
  149. package/src/components/shared/sheet-footer.tsx +33 -0
  150. package/src/components/skills/skill-list.tsx +61 -30
  151. package/src/components/skills/skill-sheet.tsx +81 -2
  152. package/src/components/tasks/task-board.tsx +448 -26
  153. package/src/components/tasks/task-card.tsx +59 -9
  154. package/src/components/tasks/task-column.tsx +62 -3
  155. package/src/components/tasks/task-list.tsx +12 -4
  156. package/src/components/tasks/task-sheet.tsx +416 -74
  157. package/src/components/ui/hover-card.tsx +52 -0
  158. package/src/components/usage/metrics-dashboard.tsx +90 -6
  159. package/src/components/usage/usage-list.tsx +1 -1
  160. package/src/components/webhooks/webhook-sheet.tsx +1 -1
  161. package/src/hooks/use-continuous-speech.ts +10 -4
  162. package/src/hooks/use-view-router.ts +69 -19
  163. package/src/hooks/use-voice-conversation.ts +53 -10
  164. package/src/hooks/use-ws.ts +4 -2
  165. package/src/instrumentation.ts +15 -1
  166. package/src/lib/chat.ts +2 -0
  167. package/src/lib/memory.ts +3 -0
  168. package/src/lib/providers/anthropic.ts +13 -7
  169. package/src/lib/providers/index.ts +1 -0
  170. package/src/lib/providers/openai.ts +13 -7
  171. package/src/lib/server/chat-execution.ts +75 -15
  172. package/src/lib/server/chatroom-helpers.ts +146 -0
  173. package/src/lib/server/connectors/manager.ts +229 -7
  174. package/src/lib/server/context-manager.ts +225 -13
  175. package/src/lib/server/create-notification.ts +14 -2
  176. package/src/lib/server/daemon-state.ts +157 -10
  177. package/src/lib/server/execution-log.ts +1 -0
  178. package/src/lib/server/heartbeat-service.ts +48 -6
  179. package/src/lib/server/heartbeat-wake.ts +110 -0
  180. package/src/lib/server/langgraph-checkpoint.ts +1 -0
  181. package/src/lib/server/main-agent-loop.ts +1 -1
  182. package/src/lib/server/memory-consolidation.ts +105 -0
  183. package/src/lib/server/memory-db.ts +183 -10
  184. package/src/lib/server/mime.ts +51 -0
  185. package/src/lib/server/openclaw-gateway.ts +9 -1
  186. package/src/lib/server/orchestrator-lg.ts +2 -0
  187. package/src/lib/server/orchestrator.ts +5 -2
  188. package/src/lib/server/playwright-proxy.mjs +2 -3
  189. package/src/lib/server/prompt-runtime-context.ts +53 -0
  190. package/src/lib/server/provider-health.ts +125 -0
  191. package/src/lib/server/queue.ts +56 -10
  192. package/src/lib/server/scheduler.ts +8 -0
  193. package/src/lib/server/session-run-manager.ts +4 -0
  194. package/src/lib/server/session-tools/canvas.ts +67 -0
  195. package/src/lib/server/session-tools/chatroom.ts +136 -0
  196. package/src/lib/server/session-tools/connector.ts +83 -9
  197. package/src/lib/server/session-tools/context-mgmt.ts +36 -18
  198. package/src/lib/server/session-tools/crud.ts +21 -0
  199. package/src/lib/server/session-tools/delegate.ts +68 -4
  200. package/src/lib/server/session-tools/git.ts +71 -0
  201. package/src/lib/server/session-tools/http.ts +57 -0
  202. package/src/lib/server/session-tools/index.ts +10 -0
  203. package/src/lib/server/session-tools/memory.ts +7 -1
  204. package/src/lib/server/session-tools/search-providers.ts +16 -8
  205. package/src/lib/server/session-tools/subagent.ts +106 -0
  206. package/src/lib/server/session-tools/web.ts +115 -4
  207. package/src/lib/server/storage.ts +53 -29
  208. package/src/lib/server/stream-agent-chat.ts +185 -57
  209. package/src/lib/server/system-events.ts +49 -0
  210. package/src/lib/server/task-mention.ts +41 -0
  211. package/src/lib/server/ws-hub.ts +11 -0
  212. package/src/lib/sessions.ts +10 -0
  213. package/src/lib/soul-library.ts +103 -0
  214. package/src/lib/soul-suggestions.ts +109 -0
  215. package/src/lib/task-dedupe.ts +26 -0
  216. package/src/lib/tasks.ts +4 -1
  217. package/src/lib/tool-definitions.ts +2 -0
  218. package/src/lib/tts.ts +2 -2
  219. package/src/lib/view-routes.ts +36 -1
  220. package/src/lib/ws-client.ts +14 -4
  221. package/src/stores/use-app-store.ts +41 -3
  222. package/src/stores/use-chat-store.ts +113 -5
  223. package/src/stores/use-chatroom-store.ts +276 -0
  224. package/src/types/index.ts +88 -4
@@ -111,3 +111,128 @@ export function getProviderHealthSnapshot(): Record<string, ProviderHealthState
111
111
  return out
112
112
  }
113
113
 
114
+ // ---------------------------------------------------------------------------
115
+ // Lightweight provider ping functions (extracted from check-provider/route.ts)
116
+ // ---------------------------------------------------------------------------
117
+
118
+ const PING_TIMEOUT_MS = 8_000
119
+
120
+ async function parseErrorMessage(res: Response, fallback: string): Promise<string> {
121
+ const text = await res.text().catch(() => '')
122
+ if (!text) return fallback
123
+ try {
124
+ const parsed = JSON.parse(text)
125
+ if (typeof parsed?.error?.message === 'string' && parsed.error.message.trim()) return parsed.error.message.trim()
126
+ if (typeof parsed?.error === 'string' && parsed.error.trim()) return parsed.error.trim()
127
+ if (typeof parsed?.message === 'string' && parsed.message.trim()) return parsed.message.trim()
128
+ if (typeof parsed?.detail === 'string' && parsed.detail.trim()) return parsed.detail.trim()
129
+ } catch { /* non-JSON */ }
130
+ return text.slice(0, 300).trim() || fallback
131
+ }
132
+
133
+ export const OPENAI_COMPATIBLE_DEFAULTS: Record<string, { name: string; defaultEndpoint: string }> = {
134
+ openai: { name: 'OpenAI', defaultEndpoint: 'https://api.openai.com/v1' },
135
+ google: { name: 'Google Gemini', defaultEndpoint: 'https://generativelanguage.googleapis.com/v1beta/openai' },
136
+ deepseek: { name: 'DeepSeek', defaultEndpoint: 'https://api.deepseek.com/v1' },
137
+ groq: { name: 'Groq', defaultEndpoint: 'https://api.groq.com/openai/v1' },
138
+ together: { name: 'Together AI', defaultEndpoint: 'https://api.together.xyz/v1' },
139
+ mistral: { name: 'Mistral AI', defaultEndpoint: 'https://api.mistral.ai/v1' },
140
+ xai: { name: 'xAI (Grok)', defaultEndpoint: 'https://api.x.ai/v1' },
141
+ fireworks: { name: 'Fireworks AI', defaultEndpoint: 'https://api.fireworks.ai/inference/v1' },
142
+ }
143
+
144
+ export async function pingOpenAiCompatible(
145
+ apiKey: string,
146
+ endpoint: string,
147
+ ): Promise<{ ok: boolean; message: string }> {
148
+ const normalizedEndpoint = endpoint.replace(/\/+$/, '')
149
+ const res = await fetch(`${normalizedEndpoint}/models`, {
150
+ headers: { authorization: `Bearer ${apiKey}` },
151
+ signal: AbortSignal.timeout(PING_TIMEOUT_MS),
152
+ cache: 'no-store',
153
+ })
154
+ if (!res.ok) {
155
+ const detail = await parseErrorMessage(res, `Provider returned ${res.status}.`)
156
+ return { ok: false, message: detail }
157
+ }
158
+ return { ok: true, message: 'Connected.' }
159
+ }
160
+
161
+ export async function pingAnthropic(apiKey: string): Promise<{ ok: boolean; message: string }> {
162
+ const res = await fetch('https://api.anthropic.com/v1/models', {
163
+ headers: {
164
+ 'x-api-key': apiKey,
165
+ 'anthropic-version': '2023-06-01',
166
+ },
167
+ signal: AbortSignal.timeout(PING_TIMEOUT_MS),
168
+ cache: 'no-store',
169
+ })
170
+ if (!res.ok) {
171
+ const detail = await parseErrorMessage(res, `Anthropic returned ${res.status}.`)
172
+ return { ok: false, message: detail }
173
+ }
174
+ return { ok: true, message: 'Connected to Anthropic.' }
175
+ }
176
+
177
+ export async function pingOllama(endpoint: string): Promise<{ ok: boolean; message: string }> {
178
+ const normalizedEndpoint = (endpoint || 'http://localhost:11434').replace(/\/+$/, '')
179
+ const res = await fetch(`${normalizedEndpoint}/api/tags`, {
180
+ signal: AbortSignal.timeout(PING_TIMEOUT_MS),
181
+ cache: 'no-store',
182
+ })
183
+ if (!res.ok) {
184
+ const detail = await parseErrorMessage(res, `Ollama returned ${res.status}.`)
185
+ return { ok: false, message: detail }
186
+ }
187
+ return { ok: true, message: 'Connected to Ollama.' }
188
+ }
189
+
190
+ export async function pingOpenClaw(
191
+ apiKey: string | undefined,
192
+ endpoint: string,
193
+ ): Promise<{ ok: boolean; message: string }> {
194
+ const { wsConnect } = await import('@/lib/providers/openclaw')
195
+ let url = (endpoint || 'http://localhost:18789').replace(/\/+$/, '')
196
+ if (!/^(https?|wss?):\/\//i.test(url)) url = `http://${url}`
197
+ const wsUrl = url.replace(/^http:/i, 'ws:').replace(/^https:/i, 'wss:')
198
+ const result = await wsConnect(wsUrl, apiKey || undefined, true, PING_TIMEOUT_MS)
199
+ if (result.ws) try { result.ws.close() } catch { /* ignore */ }
200
+ return { ok: result.ok, message: result.ok ? 'Connected to OpenClaw.' : result.message }
201
+ }
202
+
203
+ /**
204
+ * Ping a provider to check reachability. Returns `{ ok, message }`.
205
+ * Skips CLI-based providers (claude-cli, codex-cli, opencode-cli) — returns ok.
206
+ */
207
+ export async function pingProvider(
208
+ provider: string,
209
+ apiKey: string | undefined,
210
+ endpoint: string | undefined,
211
+ ): Promise<{ ok: boolean; message: string }> {
212
+ const CLI_PROVIDERS = ['claude-cli', 'codex-cli', 'opencode-cli']
213
+ if (CLI_PROVIDERS.includes(provider)) return { ok: true, message: 'CLI provider — skipped.' }
214
+
215
+ try {
216
+ if (provider === 'anthropic') {
217
+ if (!apiKey) return { ok: false, message: 'No API key configured.' }
218
+ return await pingAnthropic(apiKey)
219
+ }
220
+ if (provider === 'ollama') {
221
+ return await pingOllama(endpoint || 'http://localhost:11434')
222
+ }
223
+ if (provider === 'openclaw') {
224
+ return await pingOpenClaw(apiKey, endpoint || 'http://localhost:18789')
225
+ }
226
+ // OpenAI-compatible providers (openai, google, deepseek, groq, together, mistral, xai, fireworks, custom)
227
+ const defaults = OPENAI_COMPATIBLE_DEFAULTS[provider]
228
+ const resolvedEndpoint = endpoint || defaults?.defaultEndpoint
229
+ if (!resolvedEndpoint) return { ok: false, message: `No endpoint for provider "${provider}".` }
230
+ if (!apiKey) return { ok: false, message: 'No API key configured.' }
231
+ return await pingOpenAiCompatible(apiKey, resolvedEndpoint)
232
+ } catch (err: unknown) {
233
+ const msg = err instanceof Error && err.name === 'TimeoutError'
234
+ ? 'Connection timed out.'
235
+ : (err instanceof Error ? err.message : String(err))
236
+ return { ok: false, message: msg }
237
+ }
238
+ }
@@ -12,13 +12,14 @@ import { getCheckpointSaver } from './langgraph-checkpoint'
12
12
  import { isProtectedMainSession } from './main-session'
13
13
  import type { Agent, BoardTask, Message } from '@/types'
14
14
 
15
- let processing = false
15
+ // HMR-safe: pin processing flag to globalThis so hot reloads don't reset it
16
+ const _queueState = ((globalThis as Record<string, unknown>).__swarmclaw_queue__ ??= { processing: false, pendingKick: false }) as { processing: boolean; pendingKick: boolean }
16
17
 
17
18
  interface SessionMessageLike {
18
19
  role?: string
19
20
  text?: string
20
21
  time?: number
21
- kind?: 'chat' | 'heartbeat' | 'system'
22
+ kind?: 'chat' | 'heartbeat' | 'system' | 'context-clear'
22
23
  toolEvents?: Array<{ name?: string; output?: string }>
23
24
  }
24
25
 
@@ -279,19 +280,44 @@ function notifyAgentThreadTaskResult(task: BoardTask): void {
279
280
  changed = true
280
281
  }
281
282
 
282
- // 2. If delegated, push to delegating agent's thread
283
+ // 2. If delegated, push to delegating agent's thread AND active chat sessions
283
284
  const delegatedBy = (task as unknown as Record<string, unknown>).delegatedByAgentId
284
285
  if (typeof delegatedBy === 'string' && delegatedBy !== task.agentId) {
285
286
  const delegator = agents[delegatedBy]
287
+ const agentName = agent?.name || task.agentId
288
+ const delegationBody = buildResultBlock(`Delegated task ${statusLabel}: **${taskLink}** (by ${agentName})`)
289
+
290
+ // Push to delegating agent's thread
286
291
  if (delegator?.threadSessionId && sessions[delegator.threadSessionId]) {
287
292
  const thread = sessions[delegator.threadSessionId]
288
293
  if (!Array.isArray(thread.messages)) thread.messages = []
289
- const agentName = agent?.name || task.agentId
290
- const body = buildResultBlock(`Delegated task ${statusLabel}: **${taskLink}** (by ${agentName})`)
291
- thread.messages.push(buildMsg(body))
294
+ thread.messages.push(buildMsg(delegationBody))
292
295
  thread.lastActiveAt = now
293
296
  changed = true
294
297
  }
298
+
299
+ // Push to delegating agent's active user-facing chat sessions
300
+ // so the result is visible in the chat the user is looking at
301
+ if (delegator) {
302
+ for (const session of Object.values(sessions)) {
303
+ if (!session || session.agentId !== delegatedBy) continue
304
+ // Skip thread sessions and orchestrated/subagent sessions
305
+ if (session.id === delegator.threadSessionId) continue
306
+ if (session.sessionType === 'orchestrated') continue
307
+ // Only push to recently-active sessions (within last 30 minutes)
308
+ const lastActive = typeof session.lastActiveAt === 'number' ? session.lastActiveAt : 0
309
+ if (now - lastActive > 30 * 60_000) continue
310
+ if (!Array.isArray(session.messages)) session.messages = []
311
+ // Avoid duplicate push
312
+ const lastMsg = session.messages.at(-1)
313
+ if (lastMsg?.text === delegationBody && typeof lastMsg?.time === 'number' && now - lastMsg.time < 30_000) continue
314
+ session.messages.push(buildMsg(delegationBody))
315
+ session.lastActiveAt = now
316
+ changed = true
317
+ // Notify the specific session's message topic for real-time UI update
318
+ notify(`messages:${session.id}`)
319
+ }
320
+ }
295
321
  }
296
322
 
297
323
  if (changed) saveSessions(sessions)
@@ -330,6 +356,10 @@ export function enqueueTask(taskId: string) {
330
356
  text: `Task queued: "${task.title}" (${task.id})`,
331
357
  })
332
358
 
359
+ // If processNext is already running, mark a pending kick so it re-enters after finishing
360
+ if (_queueState.processing) {
361
+ _queueState.pendingKick = true
362
+ }
333
363
  // Delay before kicking worker so UI shows the queued state
334
364
  setTimeout(() => processNext(), 2000)
335
365
  }
@@ -467,8 +497,8 @@ function dequeueNextRunnableTask(queue: string[], tasks: Record<string, BoardTas
467
497
  }
468
498
 
469
499
  export async function processNext() {
470
- if (processing) return
471
- processing = true
500
+ if (_queueState.processing) return
501
+ _queueState.processing = true
472
502
 
473
503
  try {
474
504
  // Recover orphaned tasks: status is 'queued' but missing from the queue array
@@ -618,10 +648,21 @@ export async function processNext() {
618
648
  // Save initial assistant message so user sees context when opening the session
619
649
  const sessions = loadSessions()
620
650
  if (sessions[sessionId]) {
651
+ const isDelegation = (task as unknown as Record<string, unknown>).sourceType === 'delegation'
652
+ let initialText: string
653
+ if (isDelegation) {
654
+ const delegatorId = (task as unknown as Record<string, unknown>).delegatedByAgentId as string | undefined
655
+ const delegator = delegatorId ? agents[delegatorId] : null
656
+ const prefix = `[delegation-source:${delegatorId || ''}:${delegator?.name || 'Agent'}:${delegator?.avatarSeed || ''}]`
657
+ initialText = `${prefix}\nDelegated by **${delegator?.name || 'another agent'}** | [${task.title}](#task:${task.id})\n\n${task.description || ''}\n\nWorking directory: \`${taskCwd}\`\n\nI'll begin working on this now.`
658
+ } else {
659
+ initialText = `Starting task: **${task.title}**\n\n${task.description || ''}\n\nWorking directory: \`${taskCwd}\`\n\nI'll begin working on this now.`
660
+ }
621
661
  sessions[sessionId].messages.push({
622
662
  role: 'assistant',
623
- text: `Starting task: **${task.title}**\n\n${task.description || ''}\n\nWorking directory: \`${taskCwd}\`\n\nI'll begin working on this now.`,
663
+ text: initialText,
624
664
  time: Date.now(),
665
+ ...(isDelegation ? { kind: 'system' as const } : {}),
625
666
  })
626
667
  saveSessions(sessions)
627
668
  }
@@ -805,7 +846,12 @@ export async function processNext() {
805
846
  }
806
847
  }
807
848
  } finally {
808
- processing = false
849
+ _queueState.processing = false
850
+ // If tasks were enqueued while we were processing, kick another round
851
+ if (_queueState.pendingKick) {
852
+ _queueState.pendingKick = false
853
+ setTimeout(() => processNext(), 500)
854
+ }
809
855
  }
810
856
  }
811
857
 
@@ -4,6 +4,8 @@ import { enqueueTask } from './queue'
4
4
  import { CronExpressionParser } from 'cron-parser'
5
5
  import { pushMainLoopEventToMainSessions } from './main-agent-loop'
6
6
  import { getScheduleSignatureKey } from '@/lib/schedule-dedupe'
7
+ import { enqueueSystemEvent } from './system-events'
8
+ import { requestHeartbeatNow } from './heartbeat-wake'
7
9
 
8
10
  const TICK_INTERVAL = 60_000 // 60 seconds
9
11
  let intervalId: ReturnType<typeof setInterval> | null = null
@@ -192,5 +194,11 @@ async function tick() {
192
194
  type: 'schedule_fired',
193
195
  text: `Schedule fired: "${schedule.name}" (${schedule.id}) run #${schedule.runNumber} — task ${taskId}`,
194
196
  })
197
+
198
+ // Enqueue system event + heartbeat wake for the schedule's agent
199
+ if (schedule.createdInSessionId) {
200
+ enqueueSystemEvent(schedule.createdInSessionId, `Schedule triggered: ${schedule.name}`)
201
+ }
202
+ requestHeartbeatNow({ agentId: schedule.agentId, reason: 'schedule' })
195
203
  }
196
204
  }
@@ -37,6 +37,7 @@ interface QueueEntry {
37
37
  maxRuntimeMs?: number
38
38
  modelOverride?: string
39
39
  heartbeatConfig?: { ackMaxChars: number; showOk: boolean; showAlerts: boolean; target: string | null }
40
+ replyToId?: string
40
41
  resolve: (value: ExecuteChatTurnResult) => void
41
42
  reject: (error: Error) => void
42
43
  promise: Promise<ExecuteChatTurnResult>
@@ -245,6 +246,7 @@ async function drainExecution(executionKey: string): Promise<void> {
245
246
  onEvent: (event) => emitToSubscribers(next, event),
246
247
  modelOverride: next.modelOverride,
247
248
  heartbeatConfig: next.heartbeatConfig,
249
+ replyToId: next.replyToId,
248
250
  })
249
251
 
250
252
  const failed = !!result.error
@@ -344,6 +346,7 @@ export interface EnqueueSessionRunInput {
344
346
  maxRuntimeMs?: number
345
347
  modelOverride?: string
346
348
  heartbeatConfig?: { ackMaxChars: number; showOk: boolean; showAlerts: boolean; target: string | null }
349
+ replyToId?: string
347
350
  }
348
351
 
349
352
  export interface EnqueueSessionRunResult {
@@ -454,6 +457,7 @@ export function enqueueSessionRun(input: EnqueueSessionRunInput): EnqueueSession
454
457
  maxRuntimeMs: effectiveMaxRuntimeMs > 0 ? effectiveMaxRuntimeMs : undefined,
455
458
  modelOverride: input.modelOverride,
456
459
  heartbeatConfig: input.heartbeatConfig,
460
+ replyToId: input.replyToId,
457
461
  resolve,
458
462
  reject,
459
463
  promise,
@@ -0,0 +1,67 @@
1
+ import { z } from 'zod'
2
+ import { tool, type StructuredToolInterface } from '@langchain/core/tools'
3
+ import { loadSessions, saveSessions } from '../storage'
4
+ import { notify } from '../ws-hub'
5
+ import type { ToolBuildContext } from './context'
6
+
7
+ export function buildCanvasTools(bctx: ToolBuildContext): StructuredToolInterface[] {
8
+ const { ctx, hasTool } = bctx
9
+ if (!hasTool('canvas')) return []
10
+
11
+ return [
12
+ tool(
13
+ async ({ action, content }) => {
14
+ try {
15
+ const sessionId = ctx?.sessionId
16
+ if (!sessionId) return 'Error: no active session for canvas.'
17
+
18
+ const sessions = loadSessions()
19
+ const session = sessions[sessionId]
20
+ if (!session) return 'Error: session not found.'
21
+
22
+ if (action === 'present') {
23
+ if (!content) return 'Error: content is required for present action.'
24
+ ;(session as Record<string, unknown>).canvasContent = content
25
+ session.lastActiveAt = Date.now()
26
+ sessions[sessionId] = session
27
+ saveSessions(sessions)
28
+ notify(`canvas:${sessionId}`)
29
+ return JSON.stringify({ ok: true, action: 'present', contentLength: content.length })
30
+ }
31
+
32
+ if (action === 'hide') {
33
+ ;(session as Record<string, unknown>).canvasContent = null
34
+ session.lastActiveAt = Date.now()
35
+ sessions[sessionId] = session
36
+ saveSessions(sessions)
37
+ notify(`canvas:${sessionId}`)
38
+ return JSON.stringify({ ok: true, action: 'hide' })
39
+ }
40
+
41
+ if (action === 'snapshot') {
42
+ const current = (session as Record<string, unknown>).canvasContent
43
+ return JSON.stringify({
44
+ ok: true,
45
+ action: 'snapshot',
46
+ hasContent: !!current,
47
+ contentLength: typeof current === 'string' ? current.length : 0,
48
+ preview: typeof current === 'string' ? current.slice(0, 500) : null,
49
+ })
50
+ }
51
+
52
+ return `Unknown canvas action "${action}". Valid: present, hide, snapshot`
53
+ } catch (err: unknown) {
54
+ return `Error: ${err instanceof Error ? err.message : String(err)}`
55
+ }
56
+ },
57
+ {
58
+ name: 'canvas',
59
+ description: 'Present live HTML/CSS/JS content to the user in an interactive canvas panel. Use "present" to show content, "hide" to dismiss, "snapshot" to check current state. The canvas renders in a sandboxed iframe alongside the chat.',
60
+ schema: z.object({
61
+ action: z.enum(['present', 'hide', 'snapshot']).describe('Canvas action to perform'),
62
+ content: z.string().optional().describe('HTML content to render (required for "present"). Can include inline CSS and JS.'),
63
+ }),
64
+ },
65
+ ),
66
+ ]
67
+ }
@@ -0,0 +1,136 @@
1
+ import { z } from 'zod'
2
+ import { tool, type StructuredToolInterface } from '@langchain/core/tools'
3
+ import { loadChatrooms, saveChatrooms, loadAgents } from '../storage'
4
+ import { genId } from '@/lib/id'
5
+ import { notify } from '../ws-hub'
6
+ import type { ToolBuildContext } from './context'
7
+ import type { Chatroom } from '@/types'
8
+
9
+ export function buildChatroomTools(bctx: ToolBuildContext): StructuredToolInterface[] {
10
+ const tools: StructuredToolInterface[] = []
11
+ const { hasTool } = bctx
12
+
13
+ if (hasTool('manage_chatrooms')) {
14
+ tools.push(
15
+ tool(
16
+ async ({ action, chatroomId, name, description, agentIds, agentId, message }) => {
17
+ try {
18
+ const chatrooms = loadChatrooms() as Record<string, Chatroom>
19
+
20
+ if (action === 'list_chatrooms') {
21
+ const list = Object.values(chatrooms).map((cr) => ({
22
+ id: cr.id,
23
+ name: cr.name,
24
+ description: cr.description,
25
+ memberCount: cr.agentIds.length,
26
+ messageCount: cr.messages.length,
27
+ }))
28
+ return JSON.stringify(list)
29
+ }
30
+
31
+ if (action === 'create_chatroom') {
32
+ const id = genId()
33
+ const agents = loadAgents()
34
+ const validAgentIds = (agentIds || []).filter((aid: string) => agents[aid])
35
+ const chatroom: Chatroom = {
36
+ id,
37
+ name: name || 'New Chatroom',
38
+ description: description || '',
39
+ agentIds: validAgentIds,
40
+ messages: [],
41
+ createdAt: Date.now(),
42
+ updatedAt: Date.now(),
43
+ }
44
+ chatrooms[id] = chatroom
45
+ saveChatrooms(chatrooms)
46
+ notify('chatrooms')
47
+ return JSON.stringify({ ok: true, chatroom: { id, name: chatroom.name, agentIds: validAgentIds } })
48
+ }
49
+
50
+ if (!chatroomId) return 'Error: chatroomId is required for this action.'
51
+ const chatroom = chatrooms[chatroomId]
52
+ if (!chatroom) return `Error: chatroom not found: ${chatroomId}`
53
+
54
+ if (action === 'add_agent') {
55
+ if (!agentId) return 'Error: agentId is required.'
56
+ const agents = loadAgents()
57
+ if (!agents[agentId]) return `Error: agent not found: ${agentId}`
58
+ if (!chatroom.agentIds.includes(agentId)) {
59
+ chatroom.agentIds.push(agentId)
60
+ chatroom.updatedAt = Date.now()
61
+ chatrooms[chatroomId] = chatroom
62
+ saveChatrooms(chatrooms)
63
+ notify('chatrooms')
64
+ notify(`chatroom:${chatroomId}`)
65
+ }
66
+ return JSON.stringify({ ok: true, agentIds: chatroom.agentIds })
67
+ }
68
+
69
+ if (action === 'remove_agent') {
70
+ if (!agentId) return 'Error: agentId is required.'
71
+ chatroom.agentIds = chatroom.agentIds.filter((id: string) => id !== agentId)
72
+ chatroom.updatedAt = Date.now()
73
+ chatrooms[chatroomId] = chatroom
74
+ saveChatrooms(chatrooms)
75
+ notify('chatrooms')
76
+ notify(`chatroom:${chatroomId}`)
77
+ return JSON.stringify({ ok: true, agentIds: chatroom.agentIds })
78
+ }
79
+
80
+ if (action === 'list_members') {
81
+ const agents = loadAgents()
82
+ const members = chatroom.agentIds.map((id: string) => {
83
+ const agent = agents[id]
84
+ return agent ? { id, name: agent.name, description: agent.description } : { id, name: 'Unknown' }
85
+ })
86
+ return JSON.stringify(members)
87
+ }
88
+
89
+ if (action === 'send_message') {
90
+ if (!message) return 'Error: message is required.'
91
+ const msgId = genId()
92
+ const senderName = bctx.ctx?.agentId
93
+ ? (loadAgents()[bctx.ctx.agentId]?.name || 'Agent')
94
+ : 'Agent'
95
+ chatroom.messages.push({
96
+ id: msgId,
97
+ senderId: bctx.ctx?.agentId || 'agent',
98
+ senderName,
99
+ role: 'assistant' as const,
100
+ text: message,
101
+ mentions: [],
102
+ reactions: [],
103
+ time: Date.now(),
104
+ })
105
+ chatroom.updatedAt = Date.now()
106
+ chatrooms[chatroomId] = chatroom
107
+ saveChatrooms(chatrooms)
108
+ notify(`chatroom:${chatroomId}`)
109
+ return JSON.stringify({ ok: true, messageId: msgId })
110
+ }
111
+
112
+ return `Error: unknown action "${action}". Valid actions: list_chatrooms, create_chatroom, add_agent, remove_agent, list_members, send_message`
113
+ } catch (err: unknown) {
114
+ return `Error: ${err instanceof Error ? err.message : String(err)}`
115
+ }
116
+ },
117
+ {
118
+ name: 'manage_chatrooms',
119
+ description: 'Manage chatrooms for multi-agent collaboration. Actions: list_chatrooms, create_chatroom, add_agent, remove_agent, list_members, send_message.',
120
+ schema: z.object({
121
+ action: z.enum(['list_chatrooms', 'create_chatroom', 'add_agent', 'remove_agent', 'list_members', 'send_message'])
122
+ .describe('The action to perform'),
123
+ chatroomId: z.string().optional().describe('Chatroom ID (required for most actions except list/create)'),
124
+ name: z.string().optional().describe('Chatroom name (for create_chatroom)'),
125
+ description: z.string().optional().describe('Chatroom description (for create_chatroom)'),
126
+ agentIds: z.array(z.string()).optional().describe('Initial agent IDs (for create_chatroom)'),
127
+ agentId: z.string().optional().describe('Agent ID (for add_agent/remove_agent)'),
128
+ message: z.string().optional().describe('Message text (for send_message)'),
129
+ }),
130
+ },
131
+ ),
132
+ )
133
+ }
134
+
135
+ return tools
136
+ }
@@ -1,8 +1,21 @@
1
1
  import { z } from 'zod'
2
2
  import { tool, type StructuredToolInterface } from '@langchain/core/tools'
3
- import { loadConnectors, loadSettings } from '../storage'
3
+ import path from 'path'
4
+ import fs from 'fs'
5
+ import { loadConnectors, loadSettings, UPLOAD_DIR } from '../storage'
4
6
  import type { ToolBuildContext } from './context'
5
7
 
8
+ /** Resolve /api/uploads/filename URLs to actual disk paths */
9
+ function resolveUploadUrl(url: string | undefined): { mediaPath: string; mimeType?: string } | null {
10
+ if (!url) return null
11
+ const match = url.match(/^\/api\/uploads\/([^?#]+)/)
12
+ if (!match) return null
13
+ const safeName = match[1].replace(/[^a-zA-Z0-9._-]/g, '')
14
+ const filePath = path.join(UPLOAD_DIR, safeName)
15
+ if (!fs.existsSync(filePath)) return null
16
+ return { mediaPath: filePath }
17
+ }
18
+
6
19
  export function buildConnectorTools(bctx: ToolBuildContext): StructuredToolInterface[] {
7
20
  const tools: StructuredToolInterface[] = []
8
21
  const { ctx, hasTool } = bctx
@@ -32,6 +45,29 @@ export function buildConnectorTools(bctx: ToolBuildContext): StructuredToolInter
32
45
  return JSON.stringify(running)
33
46
  }
34
47
 
48
+ if (action === 'start') {
49
+ if (!connectorId) {
50
+ // If no ID given, list available connectors to start
51
+ const allConnectors = loadConnectors()
52
+ const stopped = Object.values(allConnectors)
53
+ .filter((c) => !platform || c.platform === platform)
54
+ .filter((c) => !running.find((r) => r.id === c.id))
55
+ .map((c) => ({ id: c.id, name: c.name, platform: c.platform }))
56
+ if (!stopped.length) return 'All connectors are already running.'
57
+ return `Error: connectorId is required. Stopped connectors available to start: ${JSON.stringify(stopped)}`
58
+ }
59
+ const { startConnector: doStart } = await import('../connectors/manager')
60
+ await doStart(connectorId)
61
+ return JSON.stringify({ status: 'started', connectorId })
62
+ }
63
+
64
+ if (action === 'stop') {
65
+ if (!connectorId) return 'Error: connectorId is required for stop action.'
66
+ const { stopConnector: doStop } = await import('../connectors/manager')
67
+ await doStop(connectorId)
68
+ return JSON.stringify({ status: 'stopped', connectorId })
69
+ }
70
+
35
71
  if (action === 'send') {
36
72
  const settings = loadSettings()
37
73
  if (settings.safetyRequireApprovalForOutbound === true && approved !== true) {
@@ -41,7 +77,15 @@ export function buildConnectorTools(bctx: ToolBuildContext): StructuredToolInter
41
77
  const hasMedia = !!imageUrl?.trim() || !!fileUrl?.trim()
42
78
  if (!hasText && !hasMedia) return 'Error: message or media URL is required for send action.'
43
79
  if (!running.length) {
44
- return `Error: no running connectors${platform ? ` for platform "${platform}"` : ''}.`
80
+ // Check for configured-but-not-running connectors to give actionable feedback
81
+ const allConnectors = loadConnectors()
82
+ const configured = Object.values(allConnectors)
83
+ .filter((c) => !platform || c.platform === platform)
84
+ .map((c) => ({ id: c.id, name: c.name, platform: c.platform, agentId: c.agentId || null }))
85
+ if (configured.length) {
86
+ return `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.`
87
+ }
88
+ return `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.`
45
89
  }
46
90
 
47
91
  const selected = connectorId
@@ -75,19 +119,49 @@ export function buildConnectorTools(bctx: ToolBuildContext): StructuredToolInter
75
119
  if (allowed.length) channelId = allowed[0]
76
120
  }
77
121
  if (!channelId) {
78
- return `Error: no target recipient configured. Provide "to", or set connector config "outboundJid"/"allowedJids"/"outboundTarget"/"allowFrom".`
122
+ // Collect any known numbers/targets from config to help the agent suggest them
123
+ const knownTargets: string[] = []
124
+ const jids = connector.config?.allowedJids?.split(',').map((s: string) => s.trim()).filter(Boolean) || []
125
+ const from = connector.config?.allowFrom?.split(',').map((s: string) => s.trim()).filter(Boolean) || []
126
+ const outJid = connector.config?.outboundJid?.trim()
127
+ const outTarget = connector.config?.outboundTarget?.trim()
128
+ if (outJid) knownTargets.push(outJid)
129
+ if (outTarget) knownTargets.push(outTarget)
130
+ knownTargets.push(...jids, ...from)
131
+ const unique = [...new Set(knownTargets)]
132
+ if (unique.length) {
133
+ return `Error: no default outbound target is set, but the connector has ${unique.length} configured number(s)/target(s): ${JSON.stringify(unique)}. Ask the user which one to send to, then re-call with the "to" parameter set to their choice.`
134
+ }
135
+ return `Error: no target recipient configured and no known contacts on this connector. Ask the user for the recipient number/ID, then re-call with the "to" parameter. They can also configure "allowedJids" or "outboundJid" in the connector settings.`
79
136
  }
80
137
  if (connector.platform === 'whatsapp') {
81
138
  channelId = normalizeWhatsAppTarget(channelId)
82
139
  }
83
140
 
141
+ // Resolve /api/uploads/ URLs to actual disk paths so connectors can read the files
142
+ let resolvedMediaPath = mediaPath?.trim() || undefined
143
+ let resolvedImageUrl = imageUrl?.trim() || undefined
144
+ let resolvedFileUrl = fileUrl?.trim() || undefined
145
+ if (!resolvedMediaPath) {
146
+ const fromImage = resolveUploadUrl(resolvedImageUrl)
147
+ if (fromImage) {
148
+ resolvedMediaPath = fromImage.mediaPath
149
+ resolvedImageUrl = undefined
150
+ }
151
+ const fromFile = resolveUploadUrl(resolvedFileUrl)
152
+ if (fromFile) {
153
+ resolvedMediaPath = fromFile.mediaPath
154
+ resolvedFileUrl = undefined
155
+ }
156
+ }
157
+
84
158
  const sent = await sendConnectorMessage({
85
159
  connectorId: selected.id,
86
160
  channelId,
87
161
  text: message?.trim() || '',
88
- imageUrl: imageUrl?.trim() || undefined,
89
- fileUrl: fileUrl?.trim() || undefined,
90
- mediaPath: mediaPath?.trim() || undefined,
162
+ imageUrl: resolvedImageUrl,
163
+ fileUrl: resolvedFileUrl,
164
+ mediaPath: resolvedMediaPath,
91
165
  mimeType: mimeType?.trim() || undefined,
92
166
  fileName: fileName?.trim() || undefined,
93
167
  caption: caption?.trim() || undefined,
@@ -140,16 +214,16 @@ export function buildConnectorTools(bctx: ToolBuildContext): StructuredToolInter
140
214
  }
141
215
  }
142
216
 
143
- return 'Unknown action. Use list_running, list_targets, or send.'
217
+ return 'Unknown action. Use list_running, list_targets, start, stop, or send.'
144
218
  } catch (err: unknown) {
145
219
  return `Error: ${err instanceof Error ? err.message : String(err)}`
146
220
  }
147
221
  },
148
222
  {
149
223
  name: 'connector_message_tool',
150
- description: 'Send proactive outbound messages and perform rich messaging actions through running connectors. Supports listing running connectors/targets, sending text/media, and rich messaging (react, edit, delete, pin). For rich actions: connectorId + message (as messageId) required; caption carries emoji for react or new text for edit.',
224
+ description: '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 messages, 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.',
151
225
  schema: z.object({
152
- action: z.enum(['list_running', 'list_targets', 'send', 'message_react', 'message_edit', 'message_delete', 'message_pin']).describe('connector messaging action'),
226
+ action: z.enum(['list_running', 'list_targets', 'start', 'stop', 'send', 'message_react', 'message_edit', 'message_delete', 'message_pin']).describe('connector messaging action'),
153
227
  connectorId: z.string().optional().describe('Optional connector id. Defaults to the first running connector (or first for selected platform).'),
154
228
  platform: z.string().optional().describe('Optional platform filter (whatsapp, telegram, slack, discord, bluebubbles, etc.).'),
155
229
  to: z.string().optional().describe('Target channel id / recipient. For WhatsApp, phone number or full JID.'),