@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,171 @@
1
+ import { z } from 'zod'
2
+ import { tool, type StructuredToolInterface } from '@langchain/core/tools'
3
+ import {
4
+ clearManagedProcess,
5
+ getManagedProcess,
6
+ killManagedProcess,
7
+ listManagedProcesses,
8
+ pollManagedProcess,
9
+ readManagedProcessLog,
10
+ removeManagedProcess,
11
+ startManagedProcess,
12
+ writeManagedProcessStdin,
13
+ } from '../process-manager'
14
+ import type { ToolBuildContext } from './context'
15
+ import { safePath, truncate, coerceEnvMap, MAX_OUTPUT } from './context'
16
+
17
+ export function buildShellTools(bctx: ToolBuildContext): StructuredToolInterface[] {
18
+ const tools: StructuredToolInterface[] = []
19
+ const { cwd, ctx, hasTool, commandTimeoutMs } = bctx
20
+
21
+ if (hasTool('shell')) {
22
+ tools.push(
23
+ tool(
24
+ async ({ command, background, yieldMs, timeoutSec, env, workdir }) => {
25
+ try {
26
+ const result = await startManagedProcess({
27
+ command,
28
+ cwd: workdir ? safePath(cwd, workdir) : cwd,
29
+ env: coerceEnvMap(env),
30
+ agentId: ctx?.agentId || null,
31
+ sessionId: ctx?.sessionId || null,
32
+ background: !!background,
33
+ yieldMs: typeof yieldMs === 'number' ? yieldMs : undefined,
34
+ timeoutMs: typeof timeoutSec === 'number'
35
+ ? Math.max(1, Math.trunc(timeoutSec)) * 1000
36
+ : commandTimeoutMs,
37
+ })
38
+ if (result.status === 'completed') {
39
+ return truncate(result.output || '(no output)', MAX_OUTPUT)
40
+ }
41
+ return JSON.stringify({
42
+ status: 'running',
43
+ processId: result.processId,
44
+ tail: result.tail || '',
45
+ }, null, 2)
46
+ } catch (err: any) {
47
+ return truncate(`Error: ${err.message || String(err)}`, MAX_OUTPUT)
48
+ }
49
+ },
50
+ {
51
+ name: 'execute_command',
52
+ description: 'Execute a shell command in the session working directory. Supports background mode and timeout/yield controls.',
53
+ schema: z.object({
54
+ command: z.string().describe('The shell command to execute'),
55
+ background: z.boolean().optional().describe('If true, start command in background immediately'),
56
+ yieldMs: z.number().optional().describe('If command runs longer than this, return a running process id instead of blocking'),
57
+ timeoutSec: z.number().optional().describe('Per-command timeout in seconds'),
58
+ workdir: z.string().optional().describe('Relative working directory override'),
59
+ env: z.record(z.string(), z.string()).optional().describe('Environment variable overrides'),
60
+ }),
61
+ },
62
+ ),
63
+ )
64
+ }
65
+
66
+ if (hasTool('process')) {
67
+ tools.push(
68
+ tool(
69
+ async ({ action, processId, offset, limit, data, eof, signal }) => {
70
+ try {
71
+ if (action === 'list') {
72
+ return JSON.stringify(listManagedProcesses(ctx?.agentId || null).map((p) => ({
73
+ id: p.id,
74
+ command: p.command,
75
+ status: p.status,
76
+ pid: p.pid,
77
+ startedAt: p.startedAt,
78
+ endedAt: p.endedAt,
79
+ exitCode: p.exitCode,
80
+ signal: p.signal,
81
+ })), null, 2)
82
+ }
83
+
84
+ if (!processId) return 'Error: processId is required for this action.'
85
+
86
+ const ownerCheck = getManagedProcess(processId)
87
+ if (ownerCheck && ctx?.sessionId && ownerCheck.sessionId && ownerCheck.sessionId !== ctx.sessionId) {
88
+ return `Error: process ${processId} belongs to a different session.`
89
+ }
90
+
91
+ if (action === 'poll') {
92
+ const res = pollManagedProcess(processId)
93
+ if (!res) return `Process not found: ${processId}`
94
+ return JSON.stringify({
95
+ id: res.process.id,
96
+ status: res.process.status,
97
+ exitCode: res.process.exitCode,
98
+ signal: res.process.signal,
99
+ chunk: res.chunk,
100
+ }, null, 2)
101
+ }
102
+
103
+ if (action === 'log') {
104
+ const res = readManagedProcessLog(processId, offset, limit)
105
+ if (!res) return `Process not found: ${processId}`
106
+ return JSON.stringify({
107
+ id: res.process.id,
108
+ status: res.process.status,
109
+ totalLines: res.totalLines,
110
+ text: res.text,
111
+ }, null, 2)
112
+ }
113
+
114
+ if (action === 'write') {
115
+ const out = writeManagedProcessStdin(processId, data || '', !!eof)
116
+ return out.ok ? `Wrote to process ${processId}` : `Error: ${out.error}`
117
+ }
118
+
119
+ if (action === 'kill') {
120
+ const out = killManagedProcess(processId, (signal as NodeJS.Signals) || 'SIGTERM')
121
+ return out.ok ? `Killed process ${processId}` : `Error: ${out.error}`
122
+ }
123
+
124
+ if (action === 'clear') {
125
+ const out = clearManagedProcess(processId)
126
+ return out.ok ? `Cleared process ${processId}` : `Error: ${out.error}`
127
+ }
128
+
129
+ if (action === 'remove') {
130
+ const out = removeManagedProcess(processId)
131
+ return out.ok ? `Removed process ${processId}` : `Error: ${out.error}`
132
+ }
133
+
134
+ if (action === 'status') {
135
+ const p = getManagedProcess(processId)
136
+ if (!p) return `Process not found: ${processId}`
137
+ return JSON.stringify({
138
+ id: p.id,
139
+ status: p.status,
140
+ pid: p.pid,
141
+ startedAt: p.startedAt,
142
+ endedAt: p.endedAt,
143
+ exitCode: p.exitCode,
144
+ signal: p.signal,
145
+ }, null, 2)
146
+ }
147
+
148
+ return `Unknown action "${action}".`
149
+ } catch (err: any) {
150
+ return `Error: ${err.message || String(err)}`
151
+ }
152
+ },
153
+ {
154
+ name: 'process_tool',
155
+ description: 'Manage long-running shell processes started by execute_command. Supports list, status, poll, log, write, kill, clear, and remove.',
156
+ schema: z.object({
157
+ action: z.enum(['list', 'status', 'poll', 'log', 'write', 'kill', 'clear', 'remove']),
158
+ processId: z.string().optional(),
159
+ offset: z.number().optional(),
160
+ limit: z.number().optional(),
161
+ data: z.string().optional(),
162
+ eof: z.boolean().optional(),
163
+ signal: z.string().optional().describe('Signal for kill action, e.g. SIGTERM or SIGKILL'),
164
+ }),
165
+ },
166
+ ),
167
+ )
168
+ }
169
+
170
+ return tools
171
+ }
@@ -0,0 +1,408 @@
1
+ import { z } from 'zod'
2
+ import { tool, type StructuredToolInterface } from '@langchain/core/tools'
3
+ import fs from 'fs'
4
+ import path from 'path'
5
+ import * as cheerio from 'cheerio'
6
+ import { UPLOAD_DIR } from '../storage'
7
+ import type { ToolBuildContext } from './context'
8
+ import { safePath, truncate, MAX_OUTPUT } from './context'
9
+
10
+ // ---------------------------------------------------------------------------
11
+ // DuckDuckGo redirect-URL decoder
12
+ // ---------------------------------------------------------------------------
13
+
14
+ function decodeDuckDuckGoUrl(rawUrl: string): string {
15
+ if (!rawUrl) return rawUrl
16
+ try {
17
+ const url = rawUrl.startsWith('http')
18
+ ? new URL(rawUrl)
19
+ : new URL(rawUrl, 'https://duckduckgo.com')
20
+ const uddg = url.searchParams.get('uddg')
21
+ if (uddg) return decodeURIComponent(uddg)
22
+ return url.toString()
23
+ } catch {
24
+ const fromQuery = rawUrl.match(/[?&]uddg=([^&]+)/)?.[1]
25
+ if (fromQuery) {
26
+ try { return decodeURIComponent(fromQuery) } catch { /* noop */ }
27
+ }
28
+ return rawUrl
29
+ }
30
+ }
31
+
32
+ // ---------------------------------------------------------------------------
33
+ // Global registry of active browser instances for cleanup sweeps
34
+ // ---------------------------------------------------------------------------
35
+
36
+ export const activeBrowsers = new Map<string, { client: any; server: any; createdAt: number }>()
37
+
38
+ /** Kill all browser instances that have been alive longer than maxAge (default 30 min) */
39
+ export function sweepOrphanedBrowsers(maxAgeMs = 30 * 60 * 1000): number {
40
+ const now = Date.now()
41
+ let cleaned = 0
42
+ for (const [key, entry] of activeBrowsers) {
43
+ if (now - entry.createdAt > maxAgeMs) {
44
+ try { entry.client?.close?.() } catch { /* ignore */ }
45
+ try { entry.server?.close?.() } catch { /* ignore */ }
46
+ activeBrowsers.delete(key)
47
+ cleaned++
48
+ }
49
+ }
50
+ return cleaned
51
+ }
52
+
53
+ /** Kill a specific session's browser instance */
54
+ export function cleanupSessionBrowser(sessionId: string): void {
55
+ const entry = activeBrowsers.get(sessionId)
56
+ if (entry) {
57
+ try { entry.client?.close?.() } catch { /* ignore */ }
58
+ try { entry.server?.close?.() } catch { /* ignore */ }
59
+ activeBrowsers.delete(sessionId)
60
+ }
61
+ }
62
+
63
+ /** Get count of active browser instances */
64
+ export function getActiveBrowserCount(): number {
65
+ return activeBrowsers.size
66
+ }
67
+
68
+ /** Check if a specific session has an active browser */
69
+ export function hasActiveBrowser(sessionId: string): boolean {
70
+ return activeBrowsers.has(sessionId)
71
+ }
72
+
73
+ // ---------------------------------------------------------------------------
74
+ // buildWebTools
75
+ // ---------------------------------------------------------------------------
76
+
77
+ export function buildWebTools(bctx: ToolBuildContext): StructuredToolInterface[] {
78
+ const tools: StructuredToolInterface[] = []
79
+ const { cwd, ctx, cleanupFns } = bctx
80
+
81
+ // ---- web_search --------------------------------------------------------
82
+
83
+ if (bctx.hasTool('web_search')) {
84
+ tools.push(
85
+ tool(
86
+ async ({ query, maxResults }) => {
87
+ try {
88
+ const limit = Math.min(maxResults || 5, 10)
89
+ const url = `https://duckduckgo.com/html/?q=${encodeURIComponent(query)}`
90
+ const res = await fetch(url, {
91
+ headers: { 'User-Agent': 'Mozilla/5.0 (compatible; SwarmClaw/1.0)' },
92
+ signal: AbortSignal.timeout(15000),
93
+ })
94
+ if (!res.ok) {
95
+ return `Error searching web: HTTP ${res.status} ${res.statusText}`
96
+ }
97
+ const html = await res.text()
98
+ const $ = cheerio.load(html)
99
+ const results: { title: string; url: string; snippet: string }[] = []
100
+
101
+ // Primary parser: DuckDuckGo result cards
102
+ $('.result').each((_i, el) => {
103
+ if (results.length >= limit) return false
104
+ const link = $(el).find('a.result__a').first()
105
+ const rawHref = link.attr('href') || ''
106
+ const title = link.text().replace(/\s+/g, ' ').trim()
107
+ if (!rawHref || !title) return
108
+ const snippet = $(el).find('.result__snippet').first().text().replace(/\s+/g, ' ').trim()
109
+ results.push({
110
+ title,
111
+ url: decodeDuckDuckGoUrl(rawHref),
112
+ snippet,
113
+ })
114
+ })
115
+
116
+ // Fallback parser: any result__a anchors
117
+ if (results.length === 0) {
118
+ $('a.result__a').each((_i, el) => {
119
+ if (results.length >= limit) return false
120
+ const rawHref = $(el).attr('href') || ''
121
+ const title = $(el).text().replace(/\s+/g, ' ').trim()
122
+ if (!rawHref || !title) return
123
+ results.push({
124
+ title,
125
+ url: decodeDuckDuckGoUrl(rawHref),
126
+ snippet: '',
127
+ })
128
+ })
129
+ }
130
+
131
+ return results.length > 0
132
+ ? JSON.stringify(results, null, 2)
133
+ : 'No results found.'
134
+ } catch (err: any) {
135
+ return `Error searching web: ${err.message}`
136
+ }
137
+ },
138
+ {
139
+ name: 'web_search',
140
+ description: 'Search the web using DuckDuckGo. Returns an array of results with title, url, and snippet.',
141
+ schema: z.object({
142
+ query: z.string().describe('Search query'),
143
+ maxResults: z.number().optional().describe('Maximum results to return (default 5, max 10)'),
144
+ }),
145
+ },
146
+ ),
147
+ )
148
+ }
149
+
150
+ // ---- web_fetch ---------------------------------------------------------
151
+
152
+ if (bctx.hasTool('web_fetch')) {
153
+ tools.push(
154
+ tool(
155
+ async ({ url }) => {
156
+ try {
157
+ const res = await fetch(url, {
158
+ headers: { 'User-Agent': 'Mozilla/5.0 (compatible; SwarmClaw/1.0)' },
159
+ signal: AbortSignal.timeout(15000),
160
+ })
161
+ if (!res.ok) return `HTTP ${res.status}: ${res.statusText}`
162
+ const html = await res.text()
163
+ // Use cheerio for robust HTML text extraction
164
+ const $ = cheerio.load(html)
165
+ $('script, style, noscript, nav, footer, header').remove()
166
+ // Prefer article/main content if available
167
+ const main = $('article, main, [role="main"]').first()
168
+ let text = (main.length ? main.text() : $('body').text())
169
+ .replace(/\s+/g, ' ')
170
+ .trim()
171
+ return truncate(text, MAX_OUTPUT)
172
+ } catch (err: any) {
173
+ return `Error fetching URL: ${err.message}`
174
+ }
175
+ },
176
+ {
177
+ name: 'web_fetch',
178
+ description: 'Fetch a URL and return its text content (HTML stripped). Useful for reading web pages.',
179
+ schema: z.object({
180
+ url: z.string().describe('The URL to fetch'),
181
+ }),
182
+ },
183
+ ),
184
+ )
185
+ }
186
+
187
+ // ---- browser -----------------------------------------------------------
188
+
189
+ if (bctx.hasTool('browser')) {
190
+ // In-process Playwright MCP client via @playwright/mcp programmatic API
191
+ const sessionKey = ctx?.sessionId || `anon-${Date.now()}`
192
+ let mcpClient: any = null
193
+ let mcpServer: any = null
194
+ let mcpInitializing: Promise<void> | null = null
195
+
196
+ const ensureMcp = (): Promise<void> => {
197
+ if (mcpClient) return Promise.resolve()
198
+ if (mcpInitializing) return mcpInitializing
199
+ mcpInitializing = (async () => {
200
+ const { createConnection } = await import('@playwright/mcp')
201
+ const { Client } = await import('@modelcontextprotocol/sdk/client/index.js')
202
+ const { InMemoryTransport } = await import('@modelcontextprotocol/sdk/inMemory.js')
203
+
204
+ const server = await createConnection({
205
+ browser: {
206
+ launchOptions: { headless: true },
207
+ isolated: true,
208
+ },
209
+ imageResponses: 'allow',
210
+ capabilities: ['core', 'pdf', 'vision', 'network'],
211
+ })
212
+ const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair()
213
+ const client = new Client({ name: 'swarmclaw', version: '1.0' })
214
+ await Promise.all([
215
+ client.connect(clientTransport),
216
+ server.connect(serverTransport),
217
+ ])
218
+ mcpClient = client
219
+ mcpServer = server
220
+ // Register in global tracker
221
+ activeBrowsers.set(sessionKey, { client, server, createdAt: Date.now() })
222
+ })()
223
+ return mcpInitializing
224
+ }
225
+
226
+ // Register cleanup for this session's browser
227
+ cleanupFns.push(async () => {
228
+ try { mcpClient?.close?.() } catch { /* ignore */ }
229
+ try { mcpServer?.close?.() } catch { /* ignore */ }
230
+ activeBrowsers.delete(sessionKey)
231
+ mcpClient = null
232
+ mcpServer = null
233
+ })
234
+
235
+ /** Strip Playwright debug noise — keep page context for the LLM */
236
+ const cleanPlaywrightOutput = (text: string): string => {
237
+ // Remove "### Ran Playwright code" blocks (internal debug)
238
+ text = text.replace(/### Ran Playwright code[\s\S]*?(?=###|$)/g, '')
239
+ // Truncate snapshot to first 40 lines so LLM has page context without flooding
240
+ text = text.replace(/### Snapshot\n([\s\S]*?)(?=###|$)/g, (_match, snapshot) => {
241
+ const lines = (snapshot as string).split('\n')
242
+ if (lines.length > 40) {
243
+ return 'Page elements:\n' + lines.slice(0, 40).join('\n') + '\n... (truncated)\n'
244
+ }
245
+ return 'Page elements:\n' + snapshot
246
+ })
247
+ // Clean headers
248
+ text = text.replace(/^### Result\n/gm, '')
249
+ text = text.replace(/^### Page\n/gm, '')
250
+ return text.replace(/\n{3,}/g, '\n').trim()
251
+ }
252
+
253
+ const callMcpTool = async (
254
+ toolName: string,
255
+ args: Record<string, any>,
256
+ options?: { saveTo?: string },
257
+ ): Promise<string> => {
258
+ await ensureMcp()
259
+ const result = await mcpClient.callTool({ name: toolName, arguments: args })
260
+ const isError = result?.isError === true
261
+ const content = result?.content
262
+ const savedPaths: string[] = []
263
+
264
+ const saveArtifact = (buffer: Buffer, suggestedExt: string): void => {
265
+ const rawSaveTo = options?.saveTo?.trim()
266
+ if (!rawSaveTo) return
267
+ let resolved = safePath(cwd, rawSaveTo)
268
+ if (!path.extname(resolved) && suggestedExt) {
269
+ resolved = `${resolved}.${suggestedExt}`
270
+ }
271
+ fs.mkdirSync(path.dirname(resolved), { recursive: true })
272
+ fs.writeFileSync(resolved, buffer)
273
+ savedPaths.push(resolved)
274
+ }
275
+
276
+ if (Array.isArray(content)) {
277
+ const parts: string[] = []
278
+ let hasBinaryImage = false
279
+ for (const c of content) {
280
+ if (c.type === 'image' && c.data) {
281
+ hasBinaryImage = true
282
+ const imageBuffer = Buffer.from(c.data, 'base64')
283
+ const filename = `screenshot-${Date.now()}.png`
284
+ const filepath = path.join(UPLOAD_DIR, filename)
285
+ fs.writeFileSync(filepath, imageBuffer)
286
+ saveArtifact(imageBuffer, 'png')
287
+ parts.push(`![Screenshot](/api/uploads/${filename})`)
288
+ } else if (c.type === 'resource' && c.resource?.blob) {
289
+ const ext = c.resource.mimeType?.includes('pdf') ? 'pdf' : 'bin'
290
+ const resourceBuffer = Buffer.from(c.resource.blob, 'base64')
291
+ const filename = `browser-${Date.now()}.${ext}`
292
+ const filepath = path.join(UPLOAD_DIR, filename)
293
+ fs.writeFileSync(filepath, resourceBuffer)
294
+ saveArtifact(resourceBuffer, ext)
295
+ parts.push(`[Download ${filename}](/api/uploads/${filename})`)
296
+ } else {
297
+ let text = c.text || ''
298
+ // Detect file paths in output (e.g. PDF save returns a local path)
299
+ const fileMatch = text.match(/\]\((\.\.\/[^\s)]+|\/[^\s)]+\.(pdf|png|jpg|jpeg|gif|webp|html|mp4|webm))\)/)
300
+ if (fileMatch) {
301
+ const rawPath = fileMatch[1]
302
+ const srcPath = rawPath.startsWith('/') ? rawPath : path.resolve(process.cwd(), rawPath)
303
+ if (fs.existsSync(srcPath)) {
304
+ const ext = path.extname(srcPath).slice(1).toLowerCase()
305
+ const IMAGE_EXTS = ['png', 'jpg', 'jpeg', 'gif', 'webp']
306
+ // Skip file-path images if we already have a binary image (avoids duplicates)
307
+ if (IMAGE_EXTS.includes(ext) && hasBinaryImage) {
308
+ parts.push(isError ? text : cleanPlaywrightOutput(text))
309
+ } else {
310
+ const filename = `browser-${Date.now()}.${ext}`
311
+ const destPath = path.join(UPLOAD_DIR, filename)
312
+ fs.copyFileSync(srcPath, destPath)
313
+ if (options?.saveTo?.trim()) {
314
+ const raw = options.saveTo.trim()
315
+ let targetPath = safePath(cwd, raw)
316
+ if (!path.extname(targetPath)) targetPath = `${targetPath}.${ext}`
317
+ fs.mkdirSync(path.dirname(targetPath), { recursive: true })
318
+ fs.copyFileSync(srcPath, targetPath)
319
+ savedPaths.push(targetPath)
320
+ }
321
+ if (IMAGE_EXTS.includes(ext)) {
322
+ parts.push(`![Screenshot](/api/uploads/${filename})`)
323
+ } else {
324
+ parts.push(`[Download ${filename}](/api/uploads/${filename})`)
325
+ }
326
+ }
327
+ } else {
328
+ parts.push(isError ? text : cleanPlaywrightOutput(text))
329
+ }
330
+ } else {
331
+ parts.push(isError ? text : cleanPlaywrightOutput(text))
332
+ }
333
+ }
334
+ }
335
+ if (savedPaths.length > 0) {
336
+ const unique = Array.from(new Set(savedPaths))
337
+ const rendered = unique.map((p) => path.relative(cwd, p) || '.').join(', ')
338
+ parts.push(`Saved to: ${rendered}`)
339
+ }
340
+ return parts.join('\n')
341
+ }
342
+ return JSON.stringify(result)
343
+ }
344
+
345
+ // Action-to-MCP tool mapping
346
+ const MCP_TOOL_MAP: Record<string, string> = {
347
+ navigate: 'browser_navigate',
348
+ screenshot: 'browser_take_screenshot',
349
+ snapshot: 'browser_snapshot',
350
+ click: 'browser_click',
351
+ type: 'browser_type',
352
+ press_key: 'browser_press_key',
353
+ select: 'browser_select_option',
354
+ evaluate: 'browser_evaluate',
355
+ pdf: 'browser_pdf_save',
356
+ upload: 'browser_file_upload',
357
+ wait: 'browser_wait_for',
358
+ }
359
+
360
+ tools.push(
361
+ tool(
362
+ async (params) => {
363
+ try {
364
+ const { action, ...rest } = params
365
+ // Build MCP args based on action
366
+ const mcpTool = MCP_TOOL_MAP[action]
367
+ if (!mcpTool) return `Unknown browser action: "${action}". Valid: ${Object.keys(MCP_TOOL_MAP).join(', ')}`
368
+ // Pass only defined (non-undefined) params to MCP
369
+ const args: Record<string, any> = {}
370
+ for (const [k, v] of Object.entries(rest)) {
371
+ if (v !== undefined && v !== null && v !== '') args[k] = v
372
+ }
373
+ const saveTo = typeof params.saveTo === 'string' && params.saveTo.trim()
374
+ ? params.saveTo.trim()
375
+ : undefined
376
+ return await callMcpTool(mcpTool, args, { saveTo })
377
+ } catch (err: any) {
378
+ return `Error: ${err.message}`
379
+ }
380
+ },
381
+ {
382
+ name: 'browser',
383
+ description: [
384
+ 'Control the browser. Use action to specify what to do.',
385
+ 'Actions: navigate (url), screenshot, snapshot (get page elements), click (element/ref), type (element/ref, text), press_key (key), select (element/ref, option), evaluate (expression), pdf, upload (paths, ref), wait (text/timeout).',
386
+ 'Workflow: use snapshot to see the page and get element refs, then use click/type/select with those refs.',
387
+ 'Screenshots are returned as images visible to the user. Use saveTo to persist screenshot/PDF artifacts to disk.',
388
+ ].join(' '),
389
+ schema: z.object({
390
+ action: z.enum(['navigate', 'screenshot', 'snapshot', 'click', 'type', 'press_key', 'select', 'evaluate', 'pdf', 'upload', 'wait']).describe('The browser action to perform'),
391
+ url: z.string().optional().describe('URL to navigate to (for navigate action)'),
392
+ element: z.string().optional().describe('CSS selector or description of an element (for click/type/select)'),
393
+ ref: z.string().optional().describe('Element reference from a previous snapshot (for click/type/select/upload)'),
394
+ text: z.string().optional().describe('Text to type (for type action) or text to wait for (for wait action)'),
395
+ key: z.string().optional().describe('Key to press, e.g. Enter, Tab, Escape (for press_key action)'),
396
+ option: z.string().optional().describe('Option value or label to select (for select action)'),
397
+ expression: z.string().optional().describe('JavaScript expression to evaluate (for evaluate action)'),
398
+ paths: z.array(z.string()).optional().describe('File paths to upload (for upload action)'),
399
+ timeout: z.number().optional().describe('Timeout in milliseconds (for wait action, default 30000)'),
400
+ saveTo: z.string().optional().describe('Optional output path for screenshot/pdf artifacts (relative to working directory).'),
401
+ }),
402
+ },
403
+ ),
404
+ )
405
+ }
406
+
407
+ return tools
408
+ }
@@ -0,0 +1,9 @@
1
+ export {
2
+ buildSessionTools,
3
+ sweepOrphanedBrowsers,
4
+ cleanupSessionBrowser,
5
+ getActiveBrowserCount,
6
+ hasActiveBrowser,
7
+ type SessionToolsResult,
8
+ type ToolContext,
9
+ } from './session-tools/index'