@swarmclawai/swarmclaw 0.2.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 (319) hide show
  1. package/README.md +577 -0
  2. package/bin/server-cmd.js +359 -0
  3. package/bin/swarmclaw.js +29 -0
  4. package/bin/swarmclaw.mjs +1504 -0
  5. package/next.config.ts +33 -0
  6. package/package.json +112 -0
  7. package/postcss.config.mjs +7 -0
  8. package/public/branding/swarmclaw-org-avatar.png +0 -0
  9. package/public/branding/swarmclaw-org-avatar.svg +58 -0
  10. package/public/file.svg +1 -0
  11. package/public/globe.svg +1 -0
  12. package/public/next.svg +1 -0
  13. package/public/screenshots/agents.png +0 -0
  14. package/public/screenshots/connectors.png +0 -0
  15. package/public/screenshots/dashboard.png +0 -0
  16. package/public/screenshots/new-session-openclaw.png +0 -0
  17. package/public/screenshots/providers.png +0 -0
  18. package/public/screenshots/schedules.png +0 -0
  19. package/public/screenshots/tasks.png +0 -0
  20. package/public/vercel.svg +1 -0
  21. package/public/window.svg +1 -0
  22. package/src/app/api/agents/[id]/route.ts +30 -0
  23. package/src/app/api/agents/[id]/thread/route.ts +66 -0
  24. package/src/app/api/agents/generate/route.ts +42 -0
  25. package/src/app/api/agents/route.ts +33 -0
  26. package/src/app/api/auth/route.ts +25 -0
  27. package/src/app/api/claude-skills/route.ts +42 -0
  28. package/src/app/api/clawhub/install/route.ts +39 -0
  29. package/src/app/api/clawhub/search/route.ts +11 -0
  30. package/src/app/api/connectors/[id]/route.ts +79 -0
  31. package/src/app/api/connectors/route.ts +60 -0
  32. package/src/app/api/credentials/[id]/route.ts +14 -0
  33. package/src/app/api/credentials/route.ts +31 -0
  34. package/src/app/api/daemon/health-check/route.ts +11 -0
  35. package/src/app/api/daemon/route.ts +22 -0
  36. package/src/app/api/dirs/pick/route.ts +60 -0
  37. package/src/app/api/dirs/route.ts +29 -0
  38. package/src/app/api/documents/[id]/route.ts +47 -0
  39. package/src/app/api/documents/route.ts +93 -0
  40. package/src/app/api/files/serve/route.ts +69 -0
  41. package/src/app/api/generate/info/route.ts +12 -0
  42. package/src/app/api/generate/route.ts +106 -0
  43. package/src/app/api/ip/route.ts +6 -0
  44. package/src/app/api/knowledge/[id]/route.ts +61 -0
  45. package/src/app/api/knowledge/route.ts +48 -0
  46. package/src/app/api/knowledge/upload/route.ts +86 -0
  47. package/src/app/api/logs/route.ts +65 -0
  48. package/src/app/api/mcp-servers/[id]/route.ts +32 -0
  49. package/src/app/api/mcp-servers/[id]/test/route.ts +23 -0
  50. package/src/app/api/mcp-servers/[id]/tools/route.ts +32 -0
  51. package/src/app/api/mcp-servers/route.ts +27 -0
  52. package/src/app/api/memory/[id]/route.ts +126 -0
  53. package/src/app/api/memory/maintenance/route.ts +63 -0
  54. package/src/app/api/memory/route.ts +111 -0
  55. package/src/app/api/memory-images/[filename]/route.ts +36 -0
  56. package/src/app/api/orchestrator/run/route.ts +43 -0
  57. package/src/app/api/plugins/install/route.ts +58 -0
  58. package/src/app/api/plugins/marketplace/route.ts +33 -0
  59. package/src/app/api/plugins/route.ts +21 -0
  60. package/src/app/api/preview-server/route.ts +339 -0
  61. package/src/app/api/providers/[id]/models/route.ts +29 -0
  62. package/src/app/api/providers/[id]/route.ts +34 -0
  63. package/src/app/api/providers/configs/route.ts +7 -0
  64. package/src/app/api/providers/ollama/route.ts +30 -0
  65. package/src/app/api/providers/openclaw/health/route.ts +23 -0
  66. package/src/app/api/providers/route.ts +28 -0
  67. package/src/app/api/runs/[id]/route.ts +9 -0
  68. package/src/app/api/runs/route.ts +13 -0
  69. package/src/app/api/schedules/[id]/route.ts +28 -0
  70. package/src/app/api/schedules/[id]/run/route.ts +104 -0
  71. package/src/app/api/schedules/route.ts +78 -0
  72. package/src/app/api/secrets/[id]/route.ts +29 -0
  73. package/src/app/api/secrets/route.ts +42 -0
  74. package/src/app/api/sessions/[id]/browser/route.ts +13 -0
  75. package/src/app/api/sessions/[id]/chat/route.ts +96 -0
  76. package/src/app/api/sessions/[id]/clear/route.ts +19 -0
  77. package/src/app/api/sessions/[id]/deploy/route.ts +34 -0
  78. package/src/app/api/sessions/[id]/devserver/route.ts +69 -0
  79. package/src/app/api/sessions/[id]/mailbox/route.ts +70 -0
  80. package/src/app/api/sessions/[id]/main-loop/route.ts +94 -0
  81. package/src/app/api/sessions/[id]/messages/route.ts +9 -0
  82. package/src/app/api/sessions/[id]/retry/route.ts +28 -0
  83. package/src/app/api/sessions/[id]/route.ts +103 -0
  84. package/src/app/api/sessions/[id]/stop/route.ts +13 -0
  85. package/src/app/api/sessions/heartbeat/route.ts +26 -0
  86. package/src/app/api/sessions/route.ts +85 -0
  87. package/src/app/api/settings/route.ts +58 -0
  88. package/src/app/api/setup/check-provider/route.ts +326 -0
  89. package/src/app/api/setup/doctor/route.ts +250 -0
  90. package/src/app/api/skills/[id]/route.ts +40 -0
  91. package/src/app/api/skills/import/route.ts +69 -0
  92. package/src/app/api/skills/route.ts +28 -0
  93. package/src/app/api/tasks/[id]/route.ts +102 -0
  94. package/src/app/api/tasks/route.ts +115 -0
  95. package/src/app/api/tts/route.ts +40 -0
  96. package/src/app/api/upload/route.ts +18 -0
  97. package/src/app/api/uploads/[filename]/route.ts +59 -0
  98. package/src/app/api/usage/route.ts +35 -0
  99. package/src/app/api/version/route.ts +81 -0
  100. package/src/app/api/version/update/route.ts +95 -0
  101. package/src/app/api/webhooks/[id]/history/route.ts +13 -0
  102. package/src/app/api/webhooks/[id]/route.ts +204 -0
  103. package/src/app/api/webhooks/route.ts +37 -0
  104. package/src/app/favicon.ico +0 -0
  105. package/src/app/globals.css +370 -0
  106. package/src/app/layout.tsx +52 -0
  107. package/src/app/page.tsx +172 -0
  108. package/src/cli/index.js +1232 -0
  109. package/src/cli/index.test.js +281 -0
  110. package/src/cli/index.ts +1158 -0
  111. package/src/cli/spec.js +284 -0
  112. package/src/components/agents/agent-card.tsx +219 -0
  113. package/src/components/agents/agent-chat-list.tsx +165 -0
  114. package/src/components/agents/agent-list.tsx +110 -0
  115. package/src/components/agents/agent-sheet.tsx +1220 -0
  116. package/src/components/auth/access-key-gate.tsx +248 -0
  117. package/src/components/auth/setup-wizard.tsx +940 -0
  118. package/src/components/auth/user-picker.tsx +88 -0
  119. package/src/components/chat/chat-area.tsx +406 -0
  120. package/src/components/chat/chat-header.tsx +491 -0
  121. package/src/components/chat/chat-tool-toggles.tsx +161 -0
  122. package/src/components/chat/code-block.tsx +146 -0
  123. package/src/components/chat/dev-server-bar.tsx +39 -0
  124. package/src/components/chat/message-bubble.tsx +486 -0
  125. package/src/components/chat/message-list.tsx +299 -0
  126. package/src/components/chat/session-debug-panel.tsx +196 -0
  127. package/src/components/chat/streaming-bubble.tsx +85 -0
  128. package/src/components/chat/thinking-indicator.tsx +26 -0
  129. package/src/components/chat/tool-call-bubble.tsx +438 -0
  130. package/src/components/chat/tool-request-banner.tsx +103 -0
  131. package/src/components/connectors/connector-list.tsx +196 -0
  132. package/src/components/connectors/connector-sheet.tsx +804 -0
  133. package/src/components/input/chat-input.tsx +235 -0
  134. package/src/components/knowledge/knowledge-list.tsx +206 -0
  135. package/src/components/knowledge/knowledge-sheet.tsx +316 -0
  136. package/src/components/layout/app-layout.tsx +1016 -0
  137. package/src/components/layout/daemon-indicator.tsx +56 -0
  138. package/src/components/layout/mobile-header.tsx +31 -0
  139. package/src/components/layout/network-banner.tsx +17 -0
  140. package/src/components/layout/update-banner.tsx +130 -0
  141. package/src/components/logs/log-list.tsx +358 -0
  142. package/src/components/mcp-servers/mcp-server-list.tsx +122 -0
  143. package/src/components/mcp-servers/mcp-server-sheet.tsx +243 -0
  144. package/src/components/memory/memory-card.tsx +63 -0
  145. package/src/components/memory/memory-detail.tsx +339 -0
  146. package/src/components/memory/memory-list.tsx +198 -0
  147. package/src/components/memory/memory-sheet.tsx +70 -0
  148. package/src/components/plugins/plugin-list.tsx +60 -0
  149. package/src/components/plugins/plugin-sheet.tsx +311 -0
  150. package/src/components/providers/provider-list.tsx +96 -0
  151. package/src/components/providers/provider-sheet.tsx +542 -0
  152. package/src/components/runs/run-list.tsx +231 -0
  153. package/src/components/schedules/schedule-card.tsx +63 -0
  154. package/src/components/schedules/schedule-list.tsx +76 -0
  155. package/src/components/schedules/schedule-sheet.tsx +336 -0
  156. package/src/components/secrets/secret-sheet.tsx +180 -0
  157. package/src/components/secrets/secrets-list.tsx +91 -0
  158. package/src/components/sessions/new-session-sheet.tsx +478 -0
  159. package/src/components/sessions/session-card.tsx +144 -0
  160. package/src/components/sessions/session-list.tsx +202 -0
  161. package/src/components/shared/ai-gen-block.tsx +77 -0
  162. package/src/components/shared/avatar.tsx +48 -0
  163. package/src/components/shared/bottom-sheet.tsx +30 -0
  164. package/src/components/shared/confirm-dialog.tsx +47 -0
  165. package/src/components/shared/connector-platform-icon.tsx +113 -0
  166. package/src/components/shared/dir-browser.tsx +285 -0
  167. package/src/components/shared/dropdown.tsx +55 -0
  168. package/src/components/shared/icon-button.tsx +25 -0
  169. package/src/components/shared/settings/plugin-manager.tsx +207 -0
  170. package/src/components/shared/settings/section-capability-policy.tsx +93 -0
  171. package/src/components/shared/settings/section-embedding.tsx +99 -0
  172. package/src/components/shared/settings/section-heartbeat.tsx +168 -0
  173. package/src/components/shared/settings/section-memory.tsx +77 -0
  174. package/src/components/shared/settings/section-orchestrator.tsx +108 -0
  175. package/src/components/shared/settings/section-providers.tsx +181 -0
  176. package/src/components/shared/settings/section-runtime-loop.tsx +183 -0
  177. package/src/components/shared/settings/section-secrets.tsx +132 -0
  178. package/src/components/shared/settings/section-user-preferences.tsx +24 -0
  179. package/src/components/shared/settings/section-voice.tsx +53 -0
  180. package/src/components/shared/settings/settings-sheet.tsx +88 -0
  181. package/src/components/shared/settings/types.ts +7 -0
  182. package/src/components/shared/settings/utils.ts +13 -0
  183. package/src/components/shared/settings-sheet.tsx +1 -0
  184. package/src/components/shared/skeleton.tsx +19 -0
  185. package/src/components/shared/usage-badge.tsx +28 -0
  186. package/src/components/skills/clawhub-browser.tsx +225 -0
  187. package/src/components/skills/skill-list.tsx +70 -0
  188. package/src/components/skills/skill-sheet.tsx +254 -0
  189. package/src/components/tasks/task-board.tsx +96 -0
  190. package/src/components/tasks/task-card.tsx +179 -0
  191. package/src/components/tasks/task-column.tsx +73 -0
  192. package/src/components/tasks/task-list.tsx +118 -0
  193. package/src/components/tasks/task-sheet.tsx +415 -0
  194. package/src/components/ui/avatar.tsx +109 -0
  195. package/src/components/ui/badge.tsx +48 -0
  196. package/src/components/ui/button.tsx +64 -0
  197. package/src/components/ui/card.tsx +92 -0
  198. package/src/components/ui/dialog.tsx +158 -0
  199. package/src/components/ui/dropdown-menu.tsx +257 -0
  200. package/src/components/ui/input.tsx +21 -0
  201. package/src/components/ui/scroll-area.tsx +58 -0
  202. package/src/components/ui/select.tsx +190 -0
  203. package/src/components/ui/separator.tsx +28 -0
  204. package/src/components/ui/sheet.tsx +143 -0
  205. package/src/components/ui/sonner.tsx +22 -0
  206. package/src/components/ui/textarea.tsx +18 -0
  207. package/src/components/ui/tooltip.tsx +56 -0
  208. package/src/components/usage/usage-list.tsx +105 -0
  209. package/src/components/webhooks/webhook-list.tsx +166 -0
  210. package/src/components/webhooks/webhook-sheet.tsx +402 -0
  211. package/src/hooks/use-auto-resize.ts +20 -0
  212. package/src/hooks/use-media-query.ts +21 -0
  213. package/src/hooks/use-speech-recognition.ts +83 -0
  214. package/src/instrumentation.ts +8 -0
  215. package/src/lib/agents.ts +13 -0
  216. package/src/lib/api-client.ts +100 -0
  217. package/src/lib/chat.ts +60 -0
  218. package/src/lib/memory.ts +42 -0
  219. package/src/lib/openclaw-endpoint.test.ts +48 -0
  220. package/src/lib/openclaw-endpoint.ts +67 -0
  221. package/src/lib/provider-config.ts +13 -0
  222. package/src/lib/providers/anthropic.ts +135 -0
  223. package/src/lib/providers/claude-cli.ts +202 -0
  224. package/src/lib/providers/codex-cli.ts +260 -0
  225. package/src/lib/providers/index.ts +351 -0
  226. package/src/lib/providers/ollama.ts +131 -0
  227. package/src/lib/providers/openai.ts +164 -0
  228. package/src/lib/providers/openclaw.ts +330 -0
  229. package/src/lib/providers/opencode-cli.ts +164 -0
  230. package/src/lib/runtime-loop.ts +15 -0
  231. package/src/lib/schedule-dedupe.test.ts +84 -0
  232. package/src/lib/schedule-dedupe.ts +174 -0
  233. package/src/lib/schedule-name.ts +62 -0
  234. package/src/lib/schedules.ts +16 -0
  235. package/src/lib/server/agent-registry.ts +70 -0
  236. package/src/lib/server/api-routes.test.ts +362 -0
  237. package/src/lib/server/autonomy-contract.ts +200 -0
  238. package/src/lib/server/build-llm.ts +155 -0
  239. package/src/lib/server/capability-router.test.ts +21 -0
  240. package/src/lib/server/capability-router.ts +172 -0
  241. package/src/lib/server/chat-execution.ts +894 -0
  242. package/src/lib/server/clawhub-client.test.ts +161 -0
  243. package/src/lib/server/clawhub-client.ts +26 -0
  244. package/src/lib/server/connectors/connector-routing.test.ts +243 -0
  245. package/src/lib/server/connectors/discord.ts +116 -0
  246. package/src/lib/server/connectors/googlechat.ts +66 -0
  247. package/src/lib/server/connectors/manager.ts +559 -0
  248. package/src/lib/server/connectors/matrix.ts +78 -0
  249. package/src/lib/server/connectors/media.ts +149 -0
  250. package/src/lib/server/connectors/openclaw.test.ts +375 -0
  251. package/src/lib/server/connectors/openclaw.ts +1132 -0
  252. package/src/lib/server/connectors/signal.ts +183 -0
  253. package/src/lib/server/connectors/slack.ts +258 -0
  254. package/src/lib/server/connectors/teams.ts +94 -0
  255. package/src/lib/server/connectors/telegram.ts +221 -0
  256. package/src/lib/server/connectors/types.ts +62 -0
  257. package/src/lib/server/connectors/whatsapp.ts +349 -0
  258. package/src/lib/server/context-manager.ts +232 -0
  259. package/src/lib/server/cost.ts +31 -0
  260. package/src/lib/server/daemon-state.ts +354 -0
  261. package/src/lib/server/data-dir.ts +3 -0
  262. package/src/lib/server/embeddings.ts +111 -0
  263. package/src/lib/server/execution-log.ts +257 -0
  264. package/src/lib/server/gateway/protocol.test.ts +54 -0
  265. package/src/lib/server/gateway/protocol.ts +114 -0
  266. package/src/lib/server/heartbeat-service.ts +366 -0
  267. package/src/lib/server/knowledge-db.test.ts +441 -0
  268. package/src/lib/server/logger.ts +47 -0
  269. package/src/lib/server/main-agent-loop.ts +1017 -0
  270. package/src/lib/server/mcp-client.test.ts +342 -0
  271. package/src/lib/server/mcp-client.ts +130 -0
  272. package/src/lib/server/memory-db.ts +1078 -0
  273. package/src/lib/server/memory-graph.test.ts +153 -0
  274. package/src/lib/server/memory-graph.ts +138 -0
  275. package/src/lib/server/openclaw-health.ts +245 -0
  276. package/src/lib/server/orchestrator-lg.ts +431 -0
  277. package/src/lib/server/orchestrator.ts +364 -0
  278. package/src/lib/server/playwright-proxy.mjs +70 -0
  279. package/src/lib/server/plugins.ts +229 -0
  280. package/src/lib/server/process-manager.ts +327 -0
  281. package/src/lib/server/provider-health.ts +113 -0
  282. package/src/lib/server/queue.ts +859 -0
  283. package/src/lib/server/runtime-settings.ts +119 -0
  284. package/src/lib/server/scheduler.ts +196 -0
  285. package/src/lib/server/session-mailbox.ts +129 -0
  286. package/src/lib/server/session-run-manager.ts +512 -0
  287. package/src/lib/server/session-tools/connector.ts +124 -0
  288. package/src/lib/server/session-tools/context-mgmt.ts +103 -0
  289. package/src/lib/server/session-tools/context.ts +114 -0
  290. package/src/lib/server/session-tools/crud.ts +673 -0
  291. package/src/lib/server/session-tools/delegate.ts +708 -0
  292. package/src/lib/server/session-tools/file.ts +264 -0
  293. package/src/lib/server/session-tools/index.ts +164 -0
  294. package/src/lib/server/session-tools/memory.ts +230 -0
  295. package/src/lib/server/session-tools/session-info.ts +422 -0
  296. package/src/lib/server/session-tools/session-tools-wiring.test.ts +166 -0
  297. package/src/lib/server/session-tools/shell.ts +171 -0
  298. package/src/lib/server/session-tools/web.ts +408 -0
  299. package/src/lib/server/session-tools.ts +9 -0
  300. package/src/lib/server/skills-normalize.ts +130 -0
  301. package/src/lib/server/storage-mcp.test.ts +161 -0
  302. package/src/lib/server/storage.ts +670 -0
  303. package/src/lib/server/stream-agent-chat.ts +571 -0
  304. package/src/lib/server/task-reports.ts +122 -0
  305. package/src/lib/server/task-result.ts +161 -0
  306. package/src/lib/server/task-validation.test.ts +27 -0
  307. package/src/lib/server/task-validation.ts +90 -0
  308. package/src/lib/server/tool-capability-policy.test.ts +58 -0
  309. package/src/lib/server/tool-capability-policy.ts +262 -0
  310. package/src/lib/sessions.ts +68 -0
  311. package/src/lib/tasks.ts +20 -0
  312. package/src/lib/tts.ts +42 -0
  313. package/src/lib/upload.ts +10 -0
  314. package/src/lib/utils.ts +6 -0
  315. package/src/proxy.ts +43 -0
  316. package/src/stores/use-app-store.ts +468 -0
  317. package/src/stores/use-chat-store.ts +323 -0
  318. package/src/types/index.ts +621 -0
  319. package/tsconfig.json +34 -0
@@ -0,0 +1,364 @@
1
+ import crypto from 'crypto'
2
+ import {
3
+ loadSessions, saveSessions, loadAgents,
4
+ loadCredentials, decryptKey, loadSettings, loadSkills,
5
+ } from './storage'
6
+ import { loadRuntimeSettings, getLegacyOrchestratorMaxTurns } from './runtime-settings'
7
+ import { getMemoryDb } from './memory-db'
8
+ import { getProvider } from '../providers'
9
+ import type { Agent } from '@/types'
10
+
11
+ /**
12
+ * Creates the orchestrator session and returns its ID immediately.
13
+ * Call executeOrchestrator() separately to run the loop in the background.
14
+ */
15
+ export function createOrchestratorSession(
16
+ orchestrator: Agent,
17
+ task: string,
18
+ parentSessionId?: string,
19
+ cwd?: string,
20
+ ): string {
21
+ const sessions = loadSessions()
22
+ const sessionId = crypto.randomBytes(4).toString('hex')
23
+ sessions[sessionId] = {
24
+ id: sessionId,
25
+ name: `[Orch] ${orchestrator.name}: ${task.slice(0, 40)}`,
26
+ cwd: cwd || process.cwd(),
27
+ user: 'system',
28
+ provider: orchestrator.provider,
29
+ model: orchestrator.model,
30
+ credentialId: orchestrator.credentialId || null,
31
+ apiEndpoint: orchestrator.apiEndpoint || null,
32
+ claudeSessionId: null,
33
+ codexThreadId: null,
34
+ opencodeSessionId: null,
35
+ delegateResumeIds: {
36
+ claudeCode: null,
37
+ codex: null,
38
+ opencode: null,
39
+ },
40
+ messages: [] as any[],
41
+ createdAt: Date.now(),
42
+ lastActiveAt: Date.now(),
43
+ sessionType: 'orchestrated' as const,
44
+ agentId: orchestrator.id,
45
+ parentSessionId: parentSessionId || null,
46
+ tools: Array.isArray(orchestrator.tools) ? [...orchestrator.tools] : [],
47
+ heartbeatEnabled: false,
48
+ }
49
+ saveSessions(sessions)
50
+ return sessionId
51
+ }
52
+
53
+ export async function runOrchestrator(
54
+ orchestrator: Agent,
55
+ task: string,
56
+ parentSessionId?: string,
57
+ ): Promise<string> {
58
+ const sessionId = createOrchestratorSession(orchestrator, task, parentSessionId)
59
+ return executeOrchestrator(orchestrator, task, sessionId)
60
+ }
61
+
62
+ export async function executeOrchestrator(
63
+ orchestrator: Agent,
64
+ task: string,
65
+ sessionId: string,
66
+ ): Promise<string> {
67
+ // Use LangGraph for all non-CLI providers (including OpenAI-compatible custom providers)
68
+ const isCliProvider = orchestrator.provider === 'claude-cli' || orchestrator.provider === 'codex-cli' || orchestrator.provider === 'opencode-cli'
69
+ if (!isCliProvider) {
70
+ console.log(`[orchestrator] Using LangGraph engine for ${orchestrator.name} (${orchestrator.provider})`)
71
+ const { executeLangGraphOrchestrator } = await import('./orchestrator-lg')
72
+ return executeLangGraphOrchestrator(orchestrator, task, sessionId)
73
+ }
74
+
75
+ // claude-cli fallback (no structured tool calling)
76
+ console.warn(`[orchestrator] Using legacy regex-based engine for ${orchestrator.name} (claude-cli)`)
77
+ return executeOrchestratorLegacy(orchestrator, task, sessionId)
78
+ }
79
+
80
+ async function executeOrchestratorLegacy(
81
+ orchestrator: Agent,
82
+ task: string,
83
+ sessionId: string,
84
+ ): Promise<string> {
85
+ const allAgents = loadAgents()
86
+ const sessions = loadSessions()
87
+ const session = sessions[sessionId]
88
+ if (!session) throw new Error('Orchestrator session not found')
89
+
90
+ // Build available agents list
91
+ const agentIds = orchestrator.subAgentIds || []
92
+ const agents = agentIds.map((id) => allAgents[id]).filter(Boolean)
93
+ const agentList = agents.map((a) => {
94
+ const tools = a.tools?.length ? ` [tools: ${a.tools.join(', ')}]` : ''
95
+ const skills = a.skills?.length ? ` [skills: ${a.skills.join(', ')}]` : ''
96
+ return `- ${a.name}: ${a.description}${tools}${skills}`
97
+ }).join('\n')
98
+
99
+ // Load relevant memories
100
+ const db = getMemoryDb()
101
+ const memories = db.getByAgent(orchestrator.id)
102
+ const memoryContext = memories.length
103
+ ? '\n\nRelevant memories:\n' + memories.slice(0, 10).map((m) => `[${m.category}] ${m.title}: ${m.content.slice(0, 200)}`).join('\n')
104
+ : ''
105
+
106
+ // Build system prompt: [userPrompt] \n\n [soul] \n\n [systemPrompt] \n\n [orchestrator context]
107
+ const settings = loadSettings()
108
+ const promptParts: string[] = []
109
+ if (settings.userPrompt) promptParts.push(settings.userPrompt)
110
+ if (orchestrator.soul) promptParts.push(orchestrator.soul)
111
+ if (orchestrator.systemPrompt) promptParts.push(orchestrator.systemPrompt)
112
+ if (orchestrator.skillIds?.length) {
113
+ const allSkills = loadSkills()
114
+ for (const skillId of orchestrator.skillIds) {
115
+ const skill = allSkills[skillId]
116
+ if (skill?.content) promptParts.push(`## Skill: ${skill.name}\n${skill.content}`)
117
+ }
118
+ }
119
+ const basePrompt = promptParts.join('\n\n')
120
+
121
+ const systemPrompt = [
122
+ basePrompt,
123
+ '\n\nYou are an orchestrator agent. You can delegate tasks to these agents:',
124
+ agentList || '(no agents available)',
125
+ '\n\nTo delegate a task, output a JSON block on its own line:',
126
+ '{"delegate": {"agent": "agent-name", "task": "what to do"}}',
127
+ '\n\nTo store something in memory:',
128
+ '{"memory_store": {"category": "keyword", "title": "...", "content": "..."}}',
129
+ '\n\nTo read memories:',
130
+ '{"memory_read": {"query": "search terms"}}',
131
+ '\n\nAgents with [tools: browser] have access to a Playwright browser and can navigate websites, scrape data, fill forms, take screenshots, and interact with web pages.',
132
+ '\n\nWhen you are done, output: {"done": true, "summary": "what was accomplished"}',
133
+ memoryContext,
134
+ ].join('\n')
135
+
136
+ // Conversation loop
137
+ const conversationHistory: { role: string; text: string }[] = []
138
+ conversationHistory.push({ role: 'user', text: task })
139
+
140
+ let result = ''
141
+ const runtime = loadRuntimeSettings()
142
+ const maxTurns = getLegacyOrchestratorMaxTurns(runtime)
143
+ const loopStart = Date.now()
144
+
145
+ for (let turn = 0; turn < maxTurns; turn++) {
146
+ if (runtime.loopMode === 'ongoing' && runtime.ongoingLoopMaxRuntimeMs) {
147
+ const elapsed = Date.now() - loopStart
148
+ if (elapsed >= runtime.ongoingLoopMaxRuntimeMs) {
149
+ const timeoutMsg = 'Ongoing loop stopped after reaching the configured runtime limit.'
150
+ session.messages.push({ role: 'assistant' as const, text: timeoutMsg, time: Date.now() })
151
+ session.lastActiveAt = Date.now()
152
+ const s = loadSessions()
153
+ s[sessionId] = session
154
+ saveSessions(s)
155
+ return timeoutMsg
156
+ }
157
+ }
158
+
159
+ const fullText = await callProvider(orchestrator, systemPrompt, conversationHistory)
160
+ conversationHistory.push({ role: 'assistant', text: fullText })
161
+
162
+ // Save to session
163
+ session.messages.push({ role: 'user' as const, text: turn === 0 ? task : '[system response]', time: Date.now() })
164
+ session.messages.push({ role: 'assistant' as const, text: fullText, time: Date.now() })
165
+ session.lastActiveAt = Date.now()
166
+ const s = loadSessions()
167
+ s[sessionId] = session
168
+ saveSessions(s)
169
+
170
+ // Parse JSON commands from the response
171
+ const commands = extractJsonCommands(fullText)
172
+ let hasDelegate = false
173
+
174
+ for (const cmd of commands) {
175
+ if (cmd.delegate) {
176
+ hasDelegate = true
177
+ const agent = agents.find((a) => a.name.toLowerCase() === cmd.delegate.agent.toLowerCase())
178
+ if (!agent) {
179
+ conversationHistory.push({
180
+ role: 'user',
181
+ text: `[System] Agent "${cmd.delegate.agent}" not found. Available agents: ${agents.map((a) => a.name).join(', ')}`,
182
+ })
183
+ continue
184
+ }
185
+
186
+ // Execute sub-task
187
+ const subResult = await executeSubTask(agent, cmd.delegate.task, sessionId)
188
+ conversationHistory.push({
189
+ role: 'user',
190
+ text: `[Agent ${agent.name} result]:\n${subResult}`,
191
+ })
192
+ }
193
+
194
+ if (cmd.memory_store) {
195
+ db.add({
196
+ agentId: orchestrator.id,
197
+ sessionId,
198
+ category: cmd.memory_store.category || 'note',
199
+ title: cmd.memory_store.title || 'Untitled',
200
+ content: cmd.memory_store.content || '',
201
+ })
202
+ conversationHistory.push({
203
+ role: 'user',
204
+ text: '[System] Memory stored successfully.',
205
+ })
206
+ }
207
+
208
+ if (cmd.memory_read) {
209
+ const results = db.search(cmd.memory_read.query, orchestrator.id)
210
+ const memText = results.length
211
+ ? results.map((m) => `[${m.category}] ${m.title}: ${m.content.slice(0, 300)}`).join('\n')
212
+ : 'No matching memories found.'
213
+ conversationHistory.push({
214
+ role: 'user',
215
+ text: `[Memory search results]:\n${memText}`,
216
+ })
217
+ }
218
+
219
+ if (cmd.done) {
220
+ result = cmd.summary || fullText
221
+ return result
222
+ }
223
+ }
224
+
225
+ if (!hasDelegate && commands.length === 0) {
226
+ // No commands found, treat as final response
227
+ result = fullText
228
+ break
229
+ }
230
+ }
231
+
232
+ if (!result) {
233
+ result = `Loop stopped after reaching max turns (${maxTurns}).`
234
+ }
235
+
236
+ return result
237
+ }
238
+
239
+ async function executeSubTask(
240
+ agent: Agent,
241
+ task: string,
242
+ parentSessionId: string,
243
+ ): Promise<string> {
244
+ // Look up parent session cwd to inherit
245
+ const sessions = loadSessions()
246
+ const parentSession = sessions[parentSessionId]
247
+ const childId = crypto.randomBytes(4).toString('hex')
248
+ const childSession = {
249
+ id: childId,
250
+ name: `[Agent] ${agent.name}: ${task.slice(0, 40)}`,
251
+ cwd: parentSession?.cwd || process.cwd(),
252
+ user: 'system',
253
+ provider: agent.provider,
254
+ model: agent.model,
255
+ credentialId: agent.credentialId || null,
256
+ apiEndpoint: agent.apiEndpoint || null,
257
+ claudeSessionId: null,
258
+ codexThreadId: null,
259
+ opencodeSessionId: null,
260
+ delegateResumeIds: {
261
+ claudeCode: null,
262
+ codex: null,
263
+ opencode: null,
264
+ },
265
+ messages: [] as any[],
266
+ createdAt: Date.now(),
267
+ lastActiveAt: Date.now(),
268
+ sessionType: 'orchestrated' as const,
269
+ agentId: agent.id,
270
+ parentSessionId,
271
+ tools: agent.tools || [],
272
+ }
273
+ sessions[childId] = childSession
274
+ saveSessions(sessions)
275
+
276
+ const history = [{ role: 'user', text: task }]
277
+ const result = await callProvider(agent, agent.systemPrompt, history)
278
+
279
+ childSession.messages.push({ role: 'user', text: task, time: Date.now() })
280
+ childSession.messages.push({ role: 'assistant', text: result, time: Date.now() })
281
+ childSession.lastActiveAt = Date.now()
282
+ const s = loadSessions()
283
+ s[childId] = childSession
284
+ saveSessions(s)
285
+
286
+ return result
287
+ }
288
+
289
+ export async function callProvider(
290
+ agent: Agent,
291
+ systemPrompt: string,
292
+ history: { role: string; text: string }[],
293
+ ): Promise<string> {
294
+ const provider = getProvider(agent.provider)
295
+ if (!provider) throw new Error(`Unknown provider: ${agent.provider}`)
296
+
297
+ let apiKey: string | null = null
298
+ if (agent.credentialId) {
299
+ const creds = loadCredentials()
300
+ const cred = creds[agent.credentialId]
301
+ if (cred?.encryptedKey) {
302
+ try { apiKey = decryptKey(cred.encryptedKey) } catch { /* ignore */ }
303
+ }
304
+ }
305
+
306
+ // Build a mock session for the provider
307
+ const mockSession = {
308
+ id: 'orch-' + crypto.randomBytes(2).toString('hex'),
309
+ provider: agent.provider,
310
+ model: agent.model,
311
+ credentialId: agent.credentialId,
312
+ apiEndpoint: agent.apiEndpoint,
313
+ cwd: process.cwd(),
314
+ tools: agent.tools || [],
315
+ messages: history.map((h) => ({
316
+ role: h.role as 'user' | 'assistant',
317
+ text: h.text,
318
+ time: Date.now(),
319
+ })),
320
+ }
321
+
322
+ let fullText = ''
323
+ const { active } = await import('./storage')
324
+
325
+ await provider.handler.streamChat({
326
+ session: mockSession,
327
+ message: history[history.length - 1].text,
328
+ apiKey,
329
+ write: (data: string) => {
330
+ // Parse SSE data to extract text
331
+ if (data.startsWith('data: ')) {
332
+ try {
333
+ const event = JSON.parse(data.slice(6))
334
+ if (event.t === 'd' || event.t === 'md' || event.t === 'r') {
335
+ if (event.t === 'd') fullText += event.text || ''
336
+ else fullText = event.text || ''
337
+ }
338
+ } catch { /* ignore */ }
339
+ }
340
+ },
341
+ active,
342
+ loadHistory: () => mockSession.messages,
343
+ })
344
+
345
+ return fullText
346
+ }
347
+
348
+ function extractJsonCommands(text: string): any[] {
349
+ const commands: any[] = []
350
+ // Match JSON blocks on their own lines
351
+ const regex = /^\s*(\{[^{}]*(?:\{[^{}]*\}[^{}]*)*\})\s*$/gm
352
+ let match
353
+ while ((match = regex.exec(text)) !== null) {
354
+ try {
355
+ const parsed = JSON.parse(match[1])
356
+ if (parsed.delegate || parsed.memory_store || parsed.memory_read || parsed.done) {
357
+ commands.push(parsed)
358
+ }
359
+ } catch {
360
+ // Not valid JSON, skip
361
+ }
362
+ }
363
+ return commands
364
+ }
@@ -0,0 +1,70 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * MCP proxy for Playwright that intercepts browser_screenshot responses,
4
+ * saves images to the uploads directory, and tells Claude the image URL
5
+ * so it can reference it in its response.
6
+ */
7
+ import { spawn } from 'child_process'
8
+ import fs from 'fs'
9
+ import path from 'path'
10
+ import os from 'os'
11
+
12
+ const UPLOAD_DIR = process.env.SWARMCLAW_UPLOAD_DIR || path.join(os.tmpdir(), 'swarmclaw-uploads')
13
+ if (!fs.existsSync(UPLOAD_DIR)) fs.mkdirSync(UPLOAD_DIR, { recursive: true })
14
+
15
+ const child = spawn('npx', ['@playwright/mcp@latest'], {
16
+ stdio: ['pipe', 'pipe', 'pipe'],
17
+ })
18
+
19
+ // Forward stdin → child
20
+ process.stdin.on('data', (chunk) => child.stdin.write(chunk))
21
+ process.stdin.on('end', () => child.stdin.end())
22
+
23
+ // Parse MCP Content-Length framed messages from child stdout, intercept screenshots
24
+ let buf = ''
25
+ child.stdout.on('data', (chunk) => {
26
+ buf += chunk.toString()
27
+ while (true) {
28
+ const headerEnd = buf.indexOf('\r\n\r\n')
29
+ if (headerEnd === -1) break
30
+ const header = buf.slice(0, headerEnd)
31
+ const match = header.match(/Content-Length:\s*(\d+)/i)
32
+ if (!match) { buf = buf.slice(headerEnd + 4); continue }
33
+ const contentLength = parseInt(match[1])
34
+ const bodyStart = headerEnd + 4
35
+ if (buf.length < bodyStart + contentLength) break
36
+ const body = buf.slice(bodyStart, bodyStart + contentLength)
37
+ buf = buf.slice(bodyStart + contentLength)
38
+
39
+ let output
40
+ try {
41
+ const msg = JSON.parse(body)
42
+ if (msg.result?.content && Array.isArray(msg.result.content)) {
43
+ const newContent = []
44
+ for (const block of msg.result.content) {
45
+ if (block.type === 'image' && block.data) {
46
+ const filename = `screenshot-${Date.now()}.png`
47
+ fs.writeFileSync(path.join(UPLOAD_DIR, filename), Buffer.from(block.data, 'base64'))
48
+ newContent.push({
49
+ type: 'text',
50
+ text: `Screenshot saved. Show it to the user with this markdown: ![Screenshot](/api/uploads/${filename})`,
51
+ })
52
+ newContent.push(block) // keep image so Claude can see it
53
+ } else {
54
+ newContent.push(block)
55
+ }
56
+ }
57
+ msg.result.content = newContent
58
+ }
59
+ output = JSON.stringify(msg)
60
+ } catch {
61
+ output = body
62
+ }
63
+ const frame = `Content-Length: ${Buffer.byteLength(output)}\r\n\r\n${output}`
64
+ process.stdout.write(frame)
65
+ }
66
+ })
67
+
68
+ child.stderr.on('data', (chunk) => process.stderr.write(chunk))
69
+ child.on('close', (code) => process.exit(code || 0))
70
+ child.on('error', (err) => { process.stderr.write(`Proxy error: ${err.message}\n`); process.exit(1) })
@@ -0,0 +1,229 @@
1
+ import fs from 'fs'
2
+ import path from 'path'
3
+ import { createRequire } from 'module'
4
+ import type { Plugin, PluginHooks, PluginMeta } from '@/types'
5
+
6
+ import { DATA_DIR } from './data-dir'
7
+
8
+ const PLUGINS_DIR = path.join(DATA_DIR, 'plugins')
9
+ const PLUGINS_CONFIG = path.join(DATA_DIR, 'plugins.json')
10
+
11
+ // OpenClaw plugin format: { name, version, activate(ctx), deactivate() }
12
+ interface OpenClawPlugin {
13
+ name: string
14
+ version?: string
15
+ activate: (ctx: Record<string, (fn: (...args: any[]) => any) => void>) => void
16
+ deactivate?: () => void
17
+ }
18
+
19
+ /**
20
+ * Normalize a module export into SwarmClaw's Plugin interface.
21
+ * Supports both SwarmClaw format ({ name, hooks }) and OpenClaw format
22
+ * ({ name, activate(ctx) }) where activate receives event hook registrars.
23
+ */
24
+ function normalizePlugin(mod: any): Plugin | null {
25
+ const raw = mod.default || mod
26
+
27
+ // SwarmClaw native format
28
+ if (raw.name && raw.hooks) {
29
+ return raw as Plugin
30
+ }
31
+
32
+ // OpenClaw format: { name, activate(ctx), deactivate() }
33
+ if (raw.name && typeof raw.activate === 'function') {
34
+ const oc = raw as OpenClawPlugin
35
+ const hooks: PluginHooks = {}
36
+
37
+ // OpenClaw's activate receives an object of hook registrars.
38
+ // Map OpenClaw lifecycle names to SwarmClaw hook names.
39
+ const registrar: Record<string, (fn: (...args: any[]) => any) => void> = {
40
+ onAgentStart: (fn) => { hooks.beforeAgentStart = fn },
41
+ onAgentComplete: (fn) => { hooks.afterAgentComplete = fn },
42
+ onToolCall: (fn) => { hooks.beforeToolExec = fn },
43
+ onToolResult: (fn) => { hooks.afterToolExec = fn },
44
+ onMessage: (fn) => { hooks.onMessage = fn },
45
+ }
46
+
47
+ try {
48
+ oc.activate(registrar)
49
+ } catch (err: any) {
50
+ console.error(`[plugins] OpenClaw activate() failed for ${oc.name}:`, err.message)
51
+ return null
52
+ }
53
+
54
+ return {
55
+ name: oc.name,
56
+ description: `OpenClaw plugin (v${oc.version || '0.0.0'})`,
57
+ hooks,
58
+ }
59
+ }
60
+
61
+ return null
62
+ }
63
+
64
+ // Ensure directories exist
65
+ if (!fs.existsSync(PLUGINS_DIR)) fs.mkdirSync(PLUGINS_DIR, { recursive: true })
66
+ if (!fs.existsSync(PLUGINS_CONFIG)) fs.writeFileSync(PLUGINS_CONFIG, '{}')
67
+
68
+ // Use createRequire to avoid Turbopack static analysis of require()
69
+ const dynamicRequire = createRequire(import.meta.url || __filename)
70
+
71
+ interface LoadedPlugin {
72
+ meta: PluginMeta
73
+ hooks: PluginHooks
74
+ }
75
+
76
+ class PluginManager {
77
+ private plugins: LoadedPlugin[] = []
78
+ private loaded = false
79
+
80
+ load() {
81
+ if (this.loaded) return
82
+ this.plugins = []
83
+
84
+ const config = this.loadConfig()
85
+
86
+ try {
87
+ const files = fs.readdirSync(PLUGINS_DIR).filter(
88
+ (f) => f.endsWith('.js') || f.endsWith('.mjs'),
89
+ )
90
+
91
+ for (const file of files) {
92
+ try {
93
+ const fullPath = path.join(PLUGINS_DIR, file)
94
+ // Clear require cache to allow reloads
95
+ delete dynamicRequire.cache[fullPath]
96
+ const mod = dynamicRequire(fullPath)
97
+ const plugin = normalizePlugin(mod)
98
+
99
+ if (!plugin) {
100
+ console.warn(`[plugins] Skipping ${file}: unrecognized plugin format`)
101
+ continue
102
+ }
103
+
104
+ const isEnabled = config[file]?.enabled !== false // enabled by default
105
+
106
+ if (isEnabled) {
107
+ this.plugins.push({
108
+ meta: {
109
+ name: plugin.name,
110
+ description: plugin.description,
111
+ filename: file,
112
+ enabled: true,
113
+ },
114
+ hooks: plugin.hooks,
115
+ })
116
+ console.log(`[plugins] Loaded: ${plugin.name} (${file})`)
117
+ }
118
+ } catch (err: any) {
119
+ console.error(`[plugins] Failed to load ${file}:`, err.message)
120
+ }
121
+ }
122
+ } catch {
123
+ // plugins dir doesn't exist or can't be read
124
+ }
125
+
126
+ this.loaded = true
127
+ }
128
+
129
+ async runHook<K extends keyof PluginHooks>(
130
+ hookName: K,
131
+ ctx: Parameters<NonNullable<PluginHooks[K]>>[0],
132
+ ): Promise<void> {
133
+ this.load()
134
+ for (const plugin of this.plugins) {
135
+ const hook = plugin.hooks[hookName]
136
+ if (hook) {
137
+ try {
138
+ await (hook as any)(ctx)
139
+ } catch (err: any) {
140
+ console.error(`[plugins] Error in ${plugin.meta.name}.${hookName}:`, err.message)
141
+ }
142
+ }
143
+ }
144
+ }
145
+
146
+ listPlugins(): PluginMeta[] {
147
+ this.load()
148
+ const config = this.loadConfig()
149
+
150
+ // Include both loaded and disabled plugins
151
+ const metas: PluginMeta[] = this.plugins.map((p) => p.meta)
152
+
153
+ try {
154
+ const files = fs.readdirSync(PLUGINS_DIR).filter(
155
+ (f) => f.endsWith('.js') || f.endsWith('.mjs'),
156
+ )
157
+ for (const file of files) {
158
+ if (!metas.find((m) => m.filename === file)) {
159
+ metas.push({
160
+ name: file.replace(/\.(js|mjs)$/, ''),
161
+ filename: file,
162
+ enabled: config[file]?.enabled !== false,
163
+ })
164
+ }
165
+ }
166
+ } catch { /* ignore */ }
167
+
168
+ return metas
169
+ }
170
+
171
+ setEnabled(filename: string, enabled: boolean) {
172
+ const config = this.loadConfig()
173
+ config[filename] = { ...config[filename], enabled }
174
+ fs.writeFileSync(PLUGINS_CONFIG, JSON.stringify(config, null, 2))
175
+ // Force reload on next hook call
176
+ this.loaded = false
177
+ this.plugins = []
178
+ }
179
+
180
+ async installPlugin(url: string, filename: string): Promise<{ ok: boolean; error?: string }> {
181
+ if (!url.startsWith('https://')) {
182
+ return { ok: false, error: 'URL must be HTTPS' }
183
+ }
184
+ const sanitized = path.basename(filename)
185
+ if (sanitized !== filename || !filename.endsWith('.js')) {
186
+ return { ok: false, error: 'Invalid filename' }
187
+ }
188
+
189
+ try {
190
+ const res = await fetch(url)
191
+ if (!res.ok) throw new Error(`Download failed: ${res.status}`)
192
+ const code = await res.text()
193
+
194
+ if (!fs.existsSync(PLUGINS_DIR)) {
195
+ fs.mkdirSync(PLUGINS_DIR, { recursive: true })
196
+ }
197
+
198
+ fs.writeFileSync(path.join(PLUGINS_DIR, sanitized), code, 'utf8')
199
+ this.reload()
200
+ return { ok: true }
201
+ } catch (err: any) {
202
+ return { ok: false, error: err.message }
203
+ }
204
+ }
205
+
206
+ private loadConfig(): Record<string, { enabled: boolean }> {
207
+ try {
208
+ return JSON.parse(fs.readFileSync(PLUGINS_CONFIG, 'utf8'))
209
+ } catch {
210
+ return {}
211
+ }
212
+ }
213
+
214
+ reload() {
215
+ this.loaded = false
216
+ this.plugins = []
217
+ this.load()
218
+ }
219
+ }
220
+
221
+ let _manager: PluginManager | null = null
222
+
223
+ export function getPluginManager(): PluginManager {
224
+ if (!_manager) {
225
+ _manager = new PluginManager()
226
+ _manager.load()
227
+ }
228
+ return _manager
229
+ }