@swarmclawai/swarmclaw 0.7.1 → 0.7.3

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 (237) hide show
  1. package/README.md +155 -150
  2. package/package.json +1 -1
  3. package/src/app/api/agents/[id]/route.ts +26 -0
  4. package/src/app/api/agents/[id]/thread/route.ts +37 -9
  5. package/src/app/api/agents/route.ts +13 -2
  6. package/src/app/api/auth/route.ts +76 -7
  7. package/src/app/api/chatrooms/[id]/chat/route.ts +7 -2
  8. package/src/app/api/{sessions → chats}/[id]/browser/route.ts +5 -1
  9. package/src/app/api/{sessions → chats}/[id]/chat/route.ts +7 -3
  10. package/src/app/api/{sessions → chats}/[id]/checkpoints/route.ts +1 -1
  11. package/src/app/api/chats/[id]/main-loop/route.ts +13 -0
  12. package/src/app/api/{sessions → chats}/[id]/messages/route.ts +19 -13
  13. package/src/app/api/{sessions → chats}/[id]/restore/route.ts +1 -1
  14. package/src/app/api/{sessions → chats}/[id]/route.ts +22 -52
  15. package/src/app/api/{sessions → chats}/[id]/stop/route.ts +6 -1
  16. package/src/app/api/{sessions → chats}/route.ts +21 -7
  17. package/src/app/api/connectors/[id]/doctor/route.ts +26 -0
  18. package/src/app/api/connectors/doctor/route.ts +13 -0
  19. package/src/app/api/files/open/route.ts +16 -14
  20. package/src/app/api/memory/maintenance/route.ts +11 -1
  21. package/src/app/api/openclaw/agent-files/route.ts +27 -4
  22. package/src/app/api/openclaw/skills/route.ts +11 -3
  23. package/src/app/api/plugins/dependencies/route.ts +24 -0
  24. package/src/app/api/plugins/install/route.ts +15 -92
  25. package/src/app/api/plugins/route.ts +6 -26
  26. package/src/app/api/plugins/settings/route.ts +40 -0
  27. package/src/app/api/plugins/ui/route.ts +1 -0
  28. package/src/app/api/settings/route.ts +49 -7
  29. package/src/app/api/tasks/[id]/route.ts +15 -6
  30. package/src/app/api/tasks/bulk/route.ts +2 -2
  31. package/src/app/api/tasks/route.ts +9 -4
  32. package/src/app/api/usage/route.ts +30 -0
  33. package/src/app/api/webhooks/[id]/route.ts +8 -1
  34. package/src/app/page.tsx +9 -2
  35. package/src/cli/index.js +39 -33
  36. package/src/cli/index.ts +43 -49
  37. package/src/cli/spec.js +29 -27
  38. package/src/components/agents/agent-card.tsx +16 -13
  39. package/src/components/agents/agent-chat-list.tsx +104 -4
  40. package/src/components/agents/agent-list.tsx +54 -22
  41. package/src/components/agents/agent-sheet.tsx +209 -18
  42. package/src/components/agents/cron-job-form.tsx +3 -3
  43. package/src/components/agents/inspector-panel.tsx +110 -50
  44. package/src/components/auth/access-key-gate.tsx +36 -97
  45. package/src/components/auth/setup-wizard.tsx +5 -38
  46. package/src/components/chat/chat-area.tsx +39 -27
  47. package/src/components/{sessions/session-card.tsx → chat/chat-card.tsx} +7 -23
  48. package/src/components/chat/chat-header.tsx +299 -314
  49. package/src/components/{sessions/session-list.tsx → chat/chat-list.tsx} +11 -14
  50. package/src/components/chat/chat-tool-toggles.tsx +26 -17
  51. package/src/components/chat/checkpoint-timeline.tsx +4 -4
  52. package/src/components/chat/message-bubble.tsx +4 -1
  53. package/src/components/chat/message-list.tsx +5 -3
  54. package/src/components/chat/session-debug-panel.tsx +1 -1
  55. package/src/components/chat/tool-request-banner.tsx +3 -3
  56. package/src/components/chatrooms/agent-hover-card.tsx +3 -3
  57. package/src/components/chatrooms/chatroom-tool-request-banner.tsx +2 -2
  58. package/src/components/chatrooms/chatroom-view.tsx +347 -205
  59. package/src/components/connectors/connector-list.tsx +265 -127
  60. package/src/components/connectors/connector-sheet.tsx +218 -1
  61. package/src/components/home/home-view.tsx +129 -5
  62. package/src/components/layout/app-layout.tsx +392 -182
  63. package/src/components/layout/mobile-header.tsx +26 -8
  64. package/src/components/plugins/plugin-list.tsx +487 -254
  65. package/src/components/plugins/plugin-sheet.tsx +236 -13
  66. package/src/components/projects/project-detail.tsx +183 -0
  67. package/src/components/settings/gateway-connection-panel.tsx +1 -1
  68. package/src/components/shared/agent-picker-list.tsx +2 -2
  69. package/src/components/shared/command-palette.tsx +111 -25
  70. package/src/components/shared/settings/plugin-manager.tsx +20 -4
  71. package/src/components/shared/settings/section-capability-policy.tsx +105 -0
  72. package/src/components/shared/settings/section-heartbeat.tsx +78 -1
  73. package/src/components/shared/settings/section-orchestrator.tsx +3 -3
  74. package/src/components/shared/settings/section-providers.tsx +1 -1
  75. package/src/components/shared/settings/section-runtime-loop.tsx +5 -5
  76. package/src/components/shared/settings/section-secrets.tsx +6 -6
  77. package/src/components/shared/settings/section-user-preferences.tsx +1 -1
  78. package/src/components/shared/settings/section-voice.tsx +5 -1
  79. package/src/components/shared/settings/section-web-search.tsx +10 -2
  80. package/src/components/shared/settings/settings-page.tsx +244 -56
  81. package/src/components/tasks/approvals-panel.tsx +205 -18
  82. package/src/components/tasks/task-board.tsx +242 -46
  83. package/src/components/usage/metrics-dashboard.tsx +147 -1
  84. package/src/components/wallets/wallet-panel.tsx +17 -5
  85. package/src/components/webhooks/webhook-sheet.tsx +8 -8
  86. package/src/lib/auth.ts +17 -0
  87. package/src/lib/chat-streaming-state.test.ts +108 -0
  88. package/src/lib/chat-streaming-state.ts +108 -0
  89. package/src/lib/chat.ts +1 -1
  90. package/src/lib/{sessions.ts → chats.ts} +28 -18
  91. package/src/lib/openclaw-agent-id.test.ts +14 -0
  92. package/src/lib/openclaw-agent-id.ts +31 -0
  93. package/src/lib/providers/claude-cli.ts +1 -1
  94. package/src/lib/server/agent-assignment.test.ts +112 -0
  95. package/src/lib/server/agent-assignment.ts +169 -0
  96. package/src/lib/server/approval-connector-notify.test.ts +253 -0
  97. package/src/lib/server/approvals-auto-approve.test.ts +205 -0
  98. package/src/lib/server/approvals.ts +483 -75
  99. package/src/lib/server/autonomy-runtime.test.ts +341 -0
  100. package/src/lib/server/browser-state.test.ts +118 -0
  101. package/src/lib/server/browser-state.ts +123 -0
  102. package/src/lib/server/build-llm.test.ts +36 -0
  103. package/src/lib/server/build-llm.ts +11 -4
  104. package/src/lib/server/builtin-plugins.ts +34 -0
  105. package/src/lib/server/capability-router.ts +10 -8
  106. package/src/lib/server/chat-execution-heartbeat.test.ts +40 -0
  107. package/src/lib/server/chat-execution-tool-events.test.ts +134 -0
  108. package/src/lib/server/chat-execution.ts +285 -165
  109. package/src/lib/server/chatroom-health.test.ts +26 -0
  110. package/src/lib/server/chatroom-health.ts +2 -3
  111. package/src/lib/server/chatroom-helpers.test.ts +67 -2
  112. package/src/lib/server/chatroom-helpers.ts +48 -8
  113. package/src/lib/server/connectors/discord.ts +175 -11
  114. package/src/lib/server/connectors/doctor.test.ts +80 -0
  115. package/src/lib/server/connectors/doctor.ts +116 -0
  116. package/src/lib/server/connectors/manager.ts +948 -112
  117. package/src/lib/server/connectors/policy.test.ts +222 -0
  118. package/src/lib/server/connectors/policy.ts +452 -0
  119. package/src/lib/server/connectors/slack.ts +188 -9
  120. package/src/lib/server/connectors/telegram.ts +65 -15
  121. package/src/lib/server/connectors/thread-context.test.ts +44 -0
  122. package/src/lib/server/connectors/thread-context.ts +72 -0
  123. package/src/lib/server/connectors/types.ts +41 -11
  124. package/src/lib/server/cost.ts +34 -1
  125. package/src/lib/server/daemon-state.ts +61 -3
  126. package/src/lib/server/data-dir.ts +13 -0
  127. package/src/lib/server/delegation-jobs.test.ts +140 -0
  128. package/src/lib/server/delegation-jobs.ts +248 -0
  129. package/src/lib/server/document-utils.test.ts +47 -0
  130. package/src/lib/server/document-utils.ts +397 -0
  131. package/src/lib/server/heartbeat-service.ts +14 -40
  132. package/src/lib/server/heartbeat-source.test.ts +22 -0
  133. package/src/lib/server/heartbeat-source.ts +7 -0
  134. package/src/lib/server/identity-continuity.test.ts +77 -0
  135. package/src/lib/server/identity-continuity.ts +127 -0
  136. package/src/lib/server/mailbox-utils.ts +347 -0
  137. package/src/lib/server/main-agent-loop.ts +28 -1103
  138. package/src/lib/server/memory-db.ts +4 -6
  139. package/src/lib/server/memory-tiers.ts +40 -0
  140. package/src/lib/server/openclaw-agent-resolver.test.ts +70 -0
  141. package/src/lib/server/openclaw-agent-resolver.ts +128 -0
  142. package/src/lib/server/openclaw-exec-config.ts +5 -6
  143. package/src/lib/server/openclaw-skills-normalize.test.ts +56 -0
  144. package/src/lib/server/openclaw-skills-normalize.ts +136 -0
  145. package/src/lib/server/openclaw-sync.ts +3 -2
  146. package/src/lib/server/orchestrator-lg.ts +20 -9
  147. package/src/lib/server/orchestrator.ts +7 -7
  148. package/src/lib/server/playwright-proxy.mjs +27 -3
  149. package/src/lib/server/plugins.test.ts +207 -0
  150. package/src/lib/server/plugins.ts +927 -66
  151. package/src/lib/server/provider-health.ts +38 -6
  152. package/src/lib/server/queue.ts +13 -28
  153. package/src/lib/server/scheduler.ts +2 -0
  154. package/src/lib/server/session-archive-memory.test.ts +85 -0
  155. package/src/lib/server/session-archive-memory.ts +230 -0
  156. package/src/lib/server/session-mailbox.ts +8 -18
  157. package/src/lib/server/session-reset-policy.test.ts +99 -0
  158. package/src/lib/server/session-reset-policy.ts +311 -0
  159. package/src/lib/server/session-run-manager.ts +33 -82
  160. package/src/lib/server/session-tools/autonomy-tools.test.ts +105 -0
  161. package/src/lib/server/session-tools/calendar.ts +366 -0
  162. package/src/lib/server/session-tools/canvas.ts +1 -1
  163. package/src/lib/server/session-tools/chatroom.ts +4 -2
  164. package/src/lib/server/session-tools/connector.ts +114 -10
  165. package/src/lib/server/session-tools/context.ts +21 -5
  166. package/src/lib/server/session-tools/crawl.ts +447 -0
  167. package/src/lib/server/session-tools/crud.ts +74 -28
  168. package/src/lib/server/session-tools/delegate-fallback.test.ts +219 -0
  169. package/src/lib/server/session-tools/delegate.ts +497 -24
  170. package/src/lib/server/session-tools/discovery.ts +24 -6
  171. package/src/lib/server/session-tools/document.ts +283 -0
  172. package/src/lib/server/session-tools/edit_file.ts +4 -2
  173. package/src/lib/server/session-tools/email.ts +320 -0
  174. package/src/lib/server/session-tools/extract.ts +137 -0
  175. package/src/lib/server/session-tools/file-normalize.test.ts +93 -0
  176. package/src/lib/server/session-tools/file-send.test.ts +84 -1
  177. package/src/lib/server/session-tools/file.ts +241 -25
  178. package/src/lib/server/session-tools/git.ts +1 -1
  179. package/src/lib/server/session-tools/http.ts +1 -1
  180. package/src/lib/server/session-tools/human-loop.ts +227 -0
  181. package/src/lib/server/session-tools/image-gen.ts +380 -0
  182. package/src/lib/server/session-tools/index.ts +130 -50
  183. package/src/lib/server/session-tools/mailbox.ts +276 -0
  184. package/src/lib/server/session-tools/memory.ts +172 -3
  185. package/src/lib/server/session-tools/monitor.ts +151 -8
  186. package/src/lib/server/session-tools/normalize-tool-args.ts +17 -14
  187. package/src/lib/server/session-tools/openclaw-nodes.ts +1 -1
  188. package/src/lib/server/session-tools/openclaw-workspace.ts +1 -1
  189. package/src/lib/server/session-tools/platform-normalize.test.ts +142 -0
  190. package/src/lib/server/session-tools/platform.ts +148 -7
  191. package/src/lib/server/session-tools/plugin-creator.ts +89 -26
  192. package/src/lib/server/session-tools/primitive-tools.test.ts +257 -0
  193. package/src/lib/server/session-tools/replicate.ts +301 -0
  194. package/src/lib/server/session-tools/sample-ui.ts +1 -1
  195. package/src/lib/server/session-tools/sandbox.ts +4 -2
  196. package/src/lib/server/session-tools/schedule.ts +24 -12
  197. package/src/lib/server/session-tools/session-info.ts +43 -7
  198. package/src/lib/server/session-tools/session-tools-wiring.test.ts +31 -17
  199. package/src/lib/server/session-tools/shell.ts +5 -2
  200. package/src/lib/server/session-tools/subagent.ts +194 -28
  201. package/src/lib/server/session-tools/table.ts +587 -0
  202. package/src/lib/server/session-tools/wallet.ts +42 -12
  203. package/src/lib/server/session-tools/web-browser-config.test.ts +39 -0
  204. package/src/lib/server/session-tools/web.ts +926 -91
  205. package/src/lib/server/storage.ts +255 -16
  206. package/src/lib/server/stream-agent-chat.ts +116 -268
  207. package/src/lib/server/structured-extract.test.ts +72 -0
  208. package/src/lib/server/structured-extract.ts +373 -0
  209. package/src/lib/server/task-mention.test.ts +16 -2
  210. package/src/lib/server/task-mention.ts +61 -10
  211. package/src/lib/server/tool-aliases.ts +66 -18
  212. package/src/lib/server/tool-capability-policy.test.ts +9 -9
  213. package/src/lib/server/tool-capability-policy.ts +38 -27
  214. package/src/lib/server/tool-retry.ts +2 -0
  215. package/src/lib/server/watch-jobs.test.ts +173 -0
  216. package/src/lib/server/watch-jobs.ts +532 -0
  217. package/src/lib/server/ws-hub.ts +5 -3
  218. package/src/lib/tool-definitions.ts +4 -0
  219. package/src/lib/validation/schemas.test.ts +26 -0
  220. package/src/lib/validation/schemas.ts +10 -1
  221. package/src/lib/ws-client.ts +14 -12
  222. package/src/proxy.ts +5 -5
  223. package/src/stores/use-app-store.ts +5 -11
  224. package/src/stores/use-chat-store.ts +38 -9
  225. package/src/types/index.ts +352 -47
  226. package/src/app/api/sessions/[id]/main-loop/route.ts +0 -94
  227. package/src/components/sessions/new-session-sheet.tsx +0 -253
  228. package/src/lib/server/main-session.ts +0 -24
  229. package/src/lib/server/session-run-manager.test.ts +0 -23
  230. /package/src/app/api/{sessions → chats}/[id]/clear/route.ts +0 -0
  231. /package/src/app/api/{sessions → chats}/[id]/deploy/route.ts +0 -0
  232. /package/src/app/api/{sessions → chats}/[id]/devserver/route.ts +0 -0
  233. /package/src/app/api/{sessions → chats}/[id]/edit-resend/route.ts +0 -0
  234. /package/src/app/api/{sessions → chats}/[id]/fork/route.ts +0 -0
  235. /package/src/app/api/{sessions → chats}/[id]/mailbox/route.ts +0 -0
  236. /package/src/app/api/{sessions → chats}/[id]/retry/route.ts +0 -0
  237. /package/src/app/api/{sessions → chats}/heartbeat/route.ts +0 -0
@@ -0,0 +1,447 @@
1
+ import crypto from 'crypto'
2
+ import { URL } from 'url'
3
+ import { z } from 'zod'
4
+ import { tool, type StructuredToolInterface } from '@langchain/core/tools'
5
+ import * as cheerio from 'cheerio'
6
+ import type { Plugin, PluginHooks } from '@/types'
7
+ import { getPluginManager } from '../plugins'
8
+ import { runStructuredExtraction } from '../structured-extract'
9
+ import type { ToolBuildContext } from './context'
10
+ import { normalizeToolInputArgs } from './normalize-tool-args'
11
+
12
+ interface CrawledPage {
13
+ url: string
14
+ status: number
15
+ title: string | null
16
+ depth: number
17
+ textPreview: string
18
+ headings: string[]
19
+ links: string[]
20
+ hash: string
21
+ }
22
+
23
+ function cleanText(value: string, max = 1200): string {
24
+ const normalized = value.replace(/\s+/g, ' ').trim()
25
+ return normalized.length <= max ? normalized : `${normalized.slice(0, max)}...`
26
+ }
27
+
28
+ function normalizeUrl(input: string, base?: string): string {
29
+ const resolved = base ? new URL(input, base) : new URL(input)
30
+ resolved.hash = ''
31
+ if (resolved.pathname.endsWith('/') && resolved.pathname !== '/') {
32
+ resolved.pathname = resolved.pathname.replace(/\/+$/, '')
33
+ }
34
+ return resolved.toString()
35
+ }
36
+
37
+ function shouldIncludeUrl(url: string, params: { includePattern?: string | null; excludePattern?: string | null }) {
38
+ if (params.includePattern) {
39
+ try {
40
+ if (!new RegExp(params.includePattern, 'i').test(url)) return false
41
+ } catch {
42
+ return false
43
+ }
44
+ }
45
+ if (params.excludePattern) {
46
+ try {
47
+ if (new RegExp(params.excludePattern, 'i').test(url)) return false
48
+ } catch {
49
+ return false
50
+ }
51
+ }
52
+ return true
53
+ }
54
+
55
+ function pageHash(text: string): string {
56
+ return crypto.createHash('sha1').update(text).digest('hex')
57
+ }
58
+
59
+ async function fetchCrawlPage(url: string, depth: number): Promise<CrawledPage> {
60
+ const res = await fetch(url, {
61
+ headers: { 'User-Agent': 'Mozilla/5.0 (compatible; SwarmClaw/1.0)' },
62
+ signal: AbortSignal.timeout(15_000),
63
+ })
64
+ const html = await res.text()
65
+ const $ = cheerio.load(html)
66
+ $('script, style, noscript').remove()
67
+
68
+ const title = cleanText($('title').first().text(), 200) || null
69
+ const headings = $('h1, h2, h3')
70
+ .toArray()
71
+ .map((node) => cleanText($(node).text(), 200))
72
+ .filter(Boolean)
73
+ .slice(0, 12)
74
+ const textPreview = cleanText($('body').text() || $.text(), 1600)
75
+ const links = $('a[href]')
76
+ .toArray()
77
+ .map((node) => $(node).attr('href') || '')
78
+ .filter(Boolean)
79
+ .map((href) => {
80
+ try {
81
+ return normalizeUrl(href, url)
82
+ } catch {
83
+ return null
84
+ }
85
+ })
86
+ .filter((href): href is string => !!href)
87
+ .slice(0, 200)
88
+
89
+ return {
90
+ url,
91
+ status: res.status,
92
+ title,
93
+ depth,
94
+ textPreview,
95
+ headings,
96
+ links: Array.from(new Set(links)),
97
+ hash: pageHash(`${title || ''}\n${textPreview}`),
98
+ }
99
+ }
100
+
101
+ async function crawlSite(params: {
102
+ startUrl: string
103
+ limit: number
104
+ maxDepth: number
105
+ sameOrigin: boolean
106
+ includePattern?: string | null
107
+ excludePattern?: string | null
108
+ }): Promise<CrawledPage[]> {
109
+ const startUrl = normalizeUrl(params.startUrl)
110
+ const startOrigin = new URL(startUrl).origin
111
+ const queue: Array<{ url: string; depth: number }> = [{ url: startUrl, depth: 0 }]
112
+ const visited = new Set<string>()
113
+ const pages: CrawledPage[] = []
114
+
115
+ while (queue.length > 0 && pages.length < params.limit) {
116
+ const current = queue.shift()!
117
+ if (visited.has(current.url)) continue
118
+ visited.add(current.url)
119
+ if (!shouldIncludeUrl(current.url, params)) continue
120
+ if (params.sameOrigin && new URL(current.url).origin !== startOrigin) continue
121
+
122
+ try {
123
+ const page = await fetchCrawlPage(current.url, current.depth)
124
+ pages.push(page)
125
+ if (current.depth >= params.maxDepth) continue
126
+ for (const link of page.links) {
127
+ if (visited.has(link)) continue
128
+ if (params.sameOrigin && new URL(link).origin !== startOrigin) continue
129
+ queue.push({ url: link, depth: current.depth + 1 })
130
+ }
131
+ } catch {
132
+ // skip failed pages and continue crawling
133
+ }
134
+ }
135
+
136
+ return pages
137
+ }
138
+
139
+ async function followPagination(params: {
140
+ startUrl: string
141
+ limit: number
142
+ }): Promise<CrawledPage[]> {
143
+ const pages: CrawledPage[] = []
144
+ const visited = new Set<string>()
145
+ let currentUrl = normalizeUrl(params.startUrl)
146
+ let depth = 0
147
+
148
+ while (currentUrl && pages.length < params.limit && !visited.has(currentUrl)) {
149
+ visited.add(currentUrl)
150
+ const page = await fetchCrawlPage(currentUrl, depth)
151
+ pages.push(page)
152
+
153
+ const res = await fetch(currentUrl, {
154
+ headers: { 'User-Agent': 'Mozilla/5.0 (compatible; SwarmClaw/1.0)' },
155
+ signal: AbortSignal.timeout(15_000),
156
+ })
157
+ const html = await res.text()
158
+ const $ = cheerio.load(html)
159
+ const nextHref = $('link[rel="next"]').attr('href')
160
+ || $('a[rel="next"]').attr('href')
161
+ || $('a').toArray().map((node) => ({
162
+ href: $(node).attr('href') || '',
163
+ text: cleanText($(node).text(), 80).toLowerCase(),
164
+ })).find((candidate) => /^(next|next page|older|more|continue)/i.test(candidate.text))?.href
165
+
166
+ if (!nextHref) break
167
+ try {
168
+ currentUrl = normalizeUrl(nextHref, currentUrl)
169
+ } catch {
170
+ break
171
+ }
172
+ depth += 1
173
+ }
174
+
175
+ return pages
176
+ }
177
+
178
+ function dedupePages(input: CrawledPage[]): CrawledPage[] {
179
+ const seen = new Set<string>()
180
+ const out: CrawledPage[] = []
181
+ for (const page of input) {
182
+ const key = `${page.url}|${page.hash}`
183
+ if (seen.has(key)) continue
184
+ seen.add(key)
185
+ out.push(page)
186
+ }
187
+ return out
188
+ }
189
+
190
+ async function fetchSitemapUrls(sitemapUrl: string): Promise<string[]> {
191
+ const res = await fetch(sitemapUrl, {
192
+ headers: { 'User-Agent': 'Mozilla/5.0 (compatible; SwarmClaw/1.0)' },
193
+ signal: AbortSignal.timeout(15_000),
194
+ })
195
+ const xml = await res.text()
196
+ const matches = Array.from(xml.matchAll(/<loc>\s*([^<]+)\s*<\/loc>/gi))
197
+ return Array.from(new Set(matches.map((match) => match[1]?.trim()).filter((value): value is string => !!value)))
198
+ }
199
+
200
+ function normalizeSelectorMap(value: unknown): Record<string, string> {
201
+ if (!value || typeof value !== 'object' || Array.isArray(value)) return {}
202
+ const entries: Array<readonly [string, string]> = []
203
+ for (const [key, selector] of Object.entries(value as Record<string, unknown>)) {
204
+ if (typeof selector !== 'string') continue
205
+ const trimmed = selector.trim()
206
+ if (!trimmed) continue
207
+ entries.push([key, trimmed] as const)
208
+ }
209
+ return Object.fromEntries(entries)
210
+ }
211
+
212
+ async function extractSelectorRows(urls: string[], selectors: Record<string, string>) {
213
+ const rows: Array<Record<string, unknown>> = []
214
+ for (const url of urls) {
215
+ const res = await fetch(url, {
216
+ headers: { 'User-Agent': 'Mozilla/5.0 (compatible; SwarmClaw/1.0)' },
217
+ signal: AbortSignal.timeout(15_000),
218
+ })
219
+ const html = await res.text()
220
+ const $ = cheerio.load(html)
221
+ $('script, style, noscript').remove()
222
+ const row: Record<string, unknown> = { url }
223
+ for (const [key, selector] of Object.entries(selectors)) {
224
+ row[key] = cleanText($(selector).first().text(), 800)
225
+ }
226
+ rows.push(row)
227
+ }
228
+ return rows
229
+ }
230
+
231
+ function normalizePagesInput(value: unknown): CrawledPage[] {
232
+ if (typeof value === 'string' && value.trim()) {
233
+ try {
234
+ return JSON.parse(value) as CrawledPage[]
235
+ } catch {
236
+ return []
237
+ }
238
+ }
239
+ if (Array.isArray(value)) return value as CrawledPage[]
240
+ return []
241
+ }
242
+
243
+ function resolveExtractionSession(bctx: ToolBuildContext) {
244
+ const session = bctx.resolveCurrentSession?.()
245
+ if (!session) throw new Error('crawl batch_extract requires an active session context.')
246
+ return session
247
+ }
248
+
249
+ async function executeCrawlAction(args: Record<string, unknown>, bctx: ToolBuildContext) {
250
+ const normalized = normalizeToolInputArgs(args)
251
+ const action = String(normalized.action || 'crawl_site').trim().toLowerCase()
252
+
253
+ try {
254
+ if (action === 'status') {
255
+ return JSON.stringify({
256
+ supports: ['crawl_site', 'follow_pagination', 'extract_sitemap', 'dedupe_pages', 'batch_extract'],
257
+ })
258
+ }
259
+
260
+ if (action === 'dedupe_pages') {
261
+ const pages = dedupePages(normalizePagesInput(normalized.pages))
262
+ return JSON.stringify({ count: pages.length, pages })
263
+ }
264
+
265
+ const startUrl = typeof normalized.url === 'string'
266
+ ? normalized.url
267
+ : typeof normalized.startUrl === 'string'
268
+ ? normalized.startUrl
269
+ : ''
270
+
271
+ const limit = typeof normalized.limit === 'number' ? Math.max(1, Math.min(normalized.limit, 100)) : 20
272
+ const maxDepth = typeof normalized.maxDepth === 'number' ? Math.max(0, Math.min(normalized.maxDepth, 5)) : 2
273
+ const sameOrigin = normalized.sameOrigin !== false
274
+
275
+ if (action === 'crawl_site' || action === 'extract_sitemap') {
276
+ const sitemapUrl = typeof normalized.sitemapUrl === 'string' && normalized.sitemapUrl.trim()
277
+ ? normalized.sitemapUrl.trim()
278
+ : null
279
+ const pages = action === 'extract_sitemap' && sitemapUrl
280
+ ? dedupePages(await Promise.all(
281
+ (await fetchSitemapUrls(sitemapUrl))
282
+ .slice(0, limit)
283
+ .map((url) => fetchCrawlPage(normalizeUrl(url), 0)),
284
+ ))
285
+ : dedupePages(await crawlSite({
286
+ startUrl,
287
+ limit,
288
+ maxDepth,
289
+ sameOrigin,
290
+ includePattern: typeof normalized.includePattern === 'string' ? normalized.includePattern : null,
291
+ excludePattern: typeof normalized.excludePattern === 'string' ? normalized.excludePattern : null,
292
+ }))
293
+ if (action === 'extract_sitemap') {
294
+ return JSON.stringify({
295
+ startUrl: normalizeUrl(startUrl),
296
+ count: pages.length,
297
+ urlCount: pages.length,
298
+ urls: pages.map((page) => page.url),
299
+ })
300
+ }
301
+ return JSON.stringify({
302
+ startUrl: normalizeUrl(startUrl),
303
+ count: pages.length,
304
+ pageCount: pages.length,
305
+ pages,
306
+ })
307
+ }
308
+
309
+ if (action === 'follow_pagination') {
310
+ const pages = dedupePages(await followPagination({ startUrl, limit }))
311
+ return JSON.stringify({
312
+ startUrl: normalizeUrl(startUrl),
313
+ count: pages.length,
314
+ pageCount: pages.length,
315
+ pages,
316
+ })
317
+ }
318
+
319
+ if (action === 'batch_extract') {
320
+ const seededPages = normalizePagesInput(normalized.pages)
321
+ if (seededPages.length === 0 && !startUrl) return 'Error: url/startUrl or pages is required.'
322
+ const pages = seededPages.length > 0
323
+ ? dedupePages(seededPages)
324
+ : dedupePages(await crawlSite({
325
+ startUrl,
326
+ limit,
327
+ maxDepth,
328
+ sameOrigin,
329
+ includePattern: typeof normalized.includePattern === 'string' ? normalized.includePattern : null,
330
+ excludePattern: typeof normalized.excludePattern === 'string' ? normalized.excludePattern : null,
331
+ }))
332
+ const selectors = normalizeSelectorMap(normalized.selectors)
333
+ if (Object.keys(selectors).length > 0) {
334
+ const rows = await extractSelectorRows(pages.map((page) => page.url), selectors)
335
+ return JSON.stringify({
336
+ count: pages.length,
337
+ pageCount: pages.length,
338
+ rowCount: rows.length,
339
+ urls: pages.map((page) => page.url),
340
+ rows,
341
+ })
342
+ }
343
+ const session = resolveExtractionSession(bctx)
344
+ const sourceText = pages
345
+ .map((page) => `URL: ${page.url}\nTitle: ${page.title || ''}\nHeadings: ${page.headings.join(' | ')}\nText: ${page.textPreview}`)
346
+ .join('\n\n---\n\n')
347
+ const extracted = await runStructuredExtraction({
348
+ session,
349
+ text: sourceText,
350
+ schema: normalized.schema,
351
+ instruction: typeof normalized.instruction === 'string'
352
+ ? normalized.instruction
353
+ : 'Aggregate the crawled pages and extract the requested structured information.',
354
+ maxChars: typeof normalized.maxChars === 'number' ? Math.max(10_000, normalized.maxChars) : 120_000,
355
+ })
356
+ return JSON.stringify({
357
+ count: pages.length,
358
+ pageCount: pages.length,
359
+ urls: pages.map((page) => page.url),
360
+ object: extracted.object,
361
+ validationErrors: extracted.validationErrors,
362
+ provider: extracted.provider,
363
+ model: extracted.model,
364
+ raw: normalized.includeRaw === true ? extracted.raw : undefined,
365
+ })
366
+ }
367
+
368
+ if (!startUrl) return 'Error: url or startUrl is required.'
369
+
370
+ return `Error: Unknown action "${action}".`
371
+ } catch (err: unknown) {
372
+ return `Error: ${err instanceof Error ? err.message : String(err)}`
373
+ }
374
+ }
375
+
376
+ const CrawlPlugin: Plugin = {
377
+ name: 'Crawl',
378
+ enabledByDefault: false,
379
+ description: 'Research whole sites by crawling pages, following pagination, deduping results, and batch-extracting structure.',
380
+ hooks: {
381
+ getCapabilityDescription: () =>
382
+ 'I can crawl websites with `crawl`, including sitemap extraction, pagination following, page deduping, and batch structured extraction over many pages.',
383
+ } as PluginHooks,
384
+ tools: [
385
+ {
386
+ name: 'crawl',
387
+ description: 'Site crawler. Actions: crawl_site, follow_pagination, extract_sitemap, dedupe_pages, batch_extract, status.',
388
+ parameters: {
389
+ type: 'object',
390
+ properties: {
391
+ action: {
392
+ type: 'string',
393
+ enum: ['crawl_site', 'follow_pagination', 'extract_sitemap', 'dedupe_pages', 'batch_extract', 'status'],
394
+ },
395
+ url: { type: 'string' },
396
+ startUrl: { type: 'string' },
397
+ sitemapUrl: { type: 'string' },
398
+ pages: {},
399
+ limit: { type: 'number' },
400
+ maxDepth: { type: 'number' },
401
+ sameOrigin: { type: 'boolean' },
402
+ includePattern: { type: 'string' },
403
+ excludePattern: { type: 'string' },
404
+ selectors: {},
405
+ schema: {},
406
+ instruction: { type: 'string' },
407
+ maxChars: { type: 'number' },
408
+ includeRaw: { type: 'boolean' },
409
+ },
410
+ required: ['action'],
411
+ },
412
+ execute: async (args, context) => {
413
+ const syntheticBuildContext = {
414
+ cwd: context.session.cwd || process.cwd(),
415
+ ctx: { sessionId: context.session.id, agentId: context.session.agentId || null },
416
+ hasPlugin: () => true,
417
+ hasTool: () => true,
418
+ cleanupFns: [],
419
+ commandTimeoutMs: 0,
420
+ claudeTimeoutMs: 0,
421
+ cliProcessTimeoutMs: 0,
422
+ persistDelegateResumeId: () => undefined,
423
+ readStoredDelegateResumeId: () => null,
424
+ resolveCurrentSession: () => context.session,
425
+ activePlugins: context.session.plugins || [],
426
+ } as ToolBuildContext
427
+ return executeCrawlAction(args, syntheticBuildContext)
428
+ },
429
+ },
430
+ ],
431
+ }
432
+
433
+ getPluginManager().registerBuiltin('crawl', CrawlPlugin)
434
+
435
+ export function buildCrawlTools(bctx: ToolBuildContext): StructuredToolInterface[] {
436
+ if (!bctx.hasPlugin('crawl')) return []
437
+ return [
438
+ tool(
439
+ async (args) => executeCrawlAction(args, bctx),
440
+ {
441
+ name: 'crawl',
442
+ description: CrawlPlugin.tools![0].description,
443
+ schema: z.object({}).passthrough(),
444
+ },
445
+ ),
446
+ ]
447
+ }
@@ -22,7 +22,13 @@ import {
22
22
  import { resolveScheduleName } from '@/lib/schedule-name'
23
23
  import { findDuplicateSchedule, type ScheduleLike } from '@/lib/schedule-dedupe'
24
24
  import { computeTaskFingerprint, findDuplicateTask } from '@/lib/task-dedupe'
25
- import { resolveTaskAgentFromDescription } from '@/lib/server/task-mention'
25
+ import {
26
+ hasManagedAgentAssignmentInput,
27
+ isDelegationTaskPayload,
28
+ resolveDelegatorAgentId,
29
+ resolveManagedAgentAssignment,
30
+ validateManagedAgentAssignment,
31
+ } from '@/lib/server/agent-assignment'
26
32
  import { normalizeTaskQualityGate } from '@/lib/server/task-quality-gate'
27
33
  import type { ToolBuildContext } from './context'
28
34
  import { safePath, findBinaryOnPath } from './context'
@@ -137,7 +143,8 @@ const RESOURCE_DEFAULTS: Record<string, (parsed: any) => any> = {
137
143
  soul: p.soul || '',
138
144
  provider: p.provider || 'claude-cli',
139
145
  model: p.model || '',
140
- isOrchestrator: p.isOrchestrator || false,
146
+ platformAssignScope: p.platformAssignScope === 'all' ? 'all' : 'self',
147
+ isOrchestrator: p.platformAssignScope === 'all',
141
148
  tools: p.tools || [],
142
149
  skills: p.skills || [],
143
150
  skillIds: p.skillIds || [],
@@ -233,12 +240,12 @@ const PLATFORM_RESOURCES: Record<string, {
233
240
 
234
241
  export function buildCrudTools(bctx: ToolBuildContext): StructuredToolInterface[] {
235
242
  const tools: StructuredToolInterface[] = []
236
- const { cwd, ctx, hasTool } = bctx
243
+ const { cwd, ctx, hasPlugin } = bctx
237
244
 
238
245
  // Build dynamic agent summary for tools that need agent awareness
239
246
  const assignScope = ctx?.platformAssignScope || 'self'
240
247
  let agentSummary = ''
241
- if (hasTool('manage_tasks') || hasTool('manage_schedules')) {
248
+ if (hasPlugin('manage_tasks') || hasPlugin('manage_schedules')) {
242
249
  if (assignScope === 'all') {
243
250
  try {
244
251
  const agents = loadAgents()
@@ -251,17 +258,17 @@ export function buildCrudTools(bctx: ToolBuildContext): StructuredToolInterface[
251
258
  }
252
259
 
253
260
  for (const [toolKey, res] of Object.entries(PLATFORM_RESOURCES)) {
254
- if (!hasTool(toolKey)) continue
261
+ if (!hasPlugin(toolKey)) continue
255
262
 
256
263
  let description = `Manage SwarmClaw ${res.label}. ${res.readOnly ? 'List and get only.' : 'List, get, create, update, or delete.'} Returns JSON.`
257
264
  if (toolKey === 'manage_tasks') {
258
265
  if (assignScope === 'self') {
259
- description += `\n\nSet "agentId" to assign a task to yourself ("${ctx?.agentId || 'unknown'}") or leave it null. You can only assign tasks to yourself. Valid manual statuses: backlog, queued, completed, failed, archived. "running" is runtime-only and set automatically when execution starts.`
266
+ description += `\n\nYou may create tasks for yourself ("${ctx?.agentId || 'unknown'}") or leave them unassigned to track multi-step work. You cannot assign tasks to other agents unless a user enables "Assign to Other Agents" in your agent settings. Valid manual statuses: backlog, queued, completed, failed, archived. "running" is runtime-only and set automatically when execution starts.`
260
267
  } else {
261
- description += `\n\nSet "agentId" to assign a task to an agent (including yourself: "${ctx?.agentId || 'unknown'}"). Valid manual statuses: backlog, queued, completed, failed, archived. "running" is runtime-only and set automatically when execution starts.` + agentSummary
268
+ description += `\n\nYou may create tasks for yourself, leave them unassigned, or delegate them to other agents. Your agent ID is "${ctx?.agentId || 'unknown'}". When delegating, set a target agent using "agentId", "assignee", "agent", "assignedAgentId", or "assigned_agent_id". Use the target agent's exact ID when possible. Valid manual statuses: backlog, queued, completed, failed, archived. "running" is runtime-only and set automatically when execution starts.` + agentSummary
262
269
  }
263
270
  } else if (toolKey === 'manage_agents') {
264
- description += `\n\nAgents may self-edit their own soul. To update your soul, use action="update", id="${ctx?.agentId || 'your-agent-id'}", and include data with the "soul" field.`
271
+ description += `\n\nAgents may self-edit their own soul. To update your soul, use action="update", id="${ctx?.agentId || 'your-agent-id'}", and include data with the "soul" field. Set "platformAssignScope":"all" to let an agent delegate work across the fleet; use "self" for solo execution.`
265
272
  } else if (toolKey === 'manage_schedules') {
266
273
  if (assignScope === 'self') {
267
274
  description += `\n\nSet "agentId" to assign a schedule to yourself ("${ctx?.agentId || 'unknown'}") or leave it null. You can only assign schedules to yourself. Schedule types: interval (set intervalMs), cron (set cron), once (set runAt). Set taskPrompt for what the agent should do. Before create, call list/get to avoid duplicate schedules. If an equivalent active/paused schedule already exists, create returns that existing schedule (deduplicated=true).`
@@ -337,13 +344,30 @@ export function buildCrudTools(bctx: ToolBuildContext): StructuredToolInterface[
337
344
  if (parsed && typeof parsed === 'object' && 'id' in parsed) {
338
345
  delete (parsed as Record<string, unknown>).id
339
346
  }
340
- // Enforce assignment scope for tasks and schedules
341
- if (assignScope === 'self' && (toolKey === 'manage_tasks' || toolKey === 'manage_schedules')) {
342
- if (parsed.agentId && parsed.agentId !== ctx?.agentId) {
343
- return `Error: You can only assign ${res.label} to yourself ("${ctx?.agentId}"). To assign to other agents, ask a user to enable "Assign to Other Agents" in your agent settings.`
344
- }
345
- }
346
347
  const now = Date.now()
348
+ if (toolKey === 'manage_tasks' || toolKey === 'manage_schedules') {
349
+ const agents = loadAgents()
350
+ const resolution = resolveManagedAgentAssignment(
351
+ parsed as Record<string, unknown>,
352
+ agents,
353
+ toolKey === 'manage_tasks' ? (parsed.agentId || ctx?.agentId || null) : null,
354
+ { allowDescription: toolKey === 'manage_tasks' },
355
+ )
356
+ const assignmentError = validateManagedAgentAssignment({
357
+ resourceLabel: res.label,
358
+ agents,
359
+ assignScope,
360
+ currentAgentId: ctx?.agentId || null,
361
+ targetAgentId: resolution.agentId,
362
+ unresolvedReference: resolution.unresolvedReference,
363
+ isDelegation: toolKey === 'manage_tasks' ? isDelegationTaskPayload(parsed as Record<string, unknown>) : false,
364
+ delegatorAgentId: toolKey === 'manage_tasks'
365
+ ? resolveDelegatorAgentId(parsed as Record<string, unknown>, agents, ctx?.agentId || null)
366
+ : null,
367
+ })
368
+ if (assignmentError) return assignmentError
369
+ parsed.agentId = resolution.agentId
370
+ }
347
371
  if (toolKey === 'manage_schedules') {
348
372
  const duplicate = findDuplicateSchedule(all as Record<string, ScheduleLike>, {
349
373
  agentId: parsed.agentId || null,
@@ -387,15 +411,6 @@ export function buildCrudTools(bctx: ToolBuildContext): StructuredToolInterface[
387
411
  })
388
412
  }
389
413
  }
390
- // @mention agent resolution for tasks
391
- if (toolKey === 'manage_tasks' && parsed.description) {
392
- const agents = loadAgents()
393
- parsed.agentId = resolveTaskAgentFromDescription(
394
- parsed.description,
395
- parsed.agentId || ctx?.agentId || '',
396
- agents,
397
- )
398
- }
399
414
  if (toolKey === 'manage_tasks') {
400
415
  parsed.title = deriveTaskTitle(parsed)
401
416
  if (!parsed.title || /^untitled task$/i.test(parsed.title)) {
@@ -499,10 +514,41 @@ export function buildCrudTools(bctx: ToolBuildContext): StructuredToolInterface[
499
514
  ? normalizeTaskQualityGate(parsed.qualityGate, settings)
500
515
  : null
501
516
  }
502
- // Enforce assignment scope for tasks and schedules
503
- if (assignScope === 'self' && (toolKey === 'manage_tasks' || toolKey === 'manage_schedules')) {
504
- if (parsed.agentId && parsed.agentId !== ctx?.agentId) {
505
- return `Error: You can only assign ${res.label} to yourself ("${ctx?.agentId}"). To assign to other agents, ask a user to enable "Assign to Other Agents" in your agent settings.`
517
+ if (toolKey === 'manage_tasks' || toolKey === 'manage_schedules') {
518
+ const agents = loadAgents()
519
+ const requestedClear = Object.prototype.hasOwnProperty.call(parsed, 'agentId') && parsed.agentId == null
520
+ const shouldResolveAssignment = requestedClear
521
+ || hasManagedAgentAssignmentInput(parsed as Record<string, unknown>)
522
+ if (shouldResolveAssignment) {
523
+ const resolution = resolveManagedAgentAssignment(
524
+ parsed as Record<string, unknown>,
525
+ agents,
526
+ null,
527
+ { allowDescription: false },
528
+ )
529
+ const assignmentError = validateManagedAgentAssignment({
530
+ resourceLabel: res.label,
531
+ agents,
532
+ assignScope,
533
+ currentAgentId: ctx?.agentId || null,
534
+ targetAgentId: requestedClear ? null : resolution.agentId,
535
+ unresolvedReference: requestedClear ? null : resolution.unresolvedReference,
536
+ isDelegation: toolKey === 'manage_tasks'
537
+ ? isDelegationTaskPayload({
538
+ ...all[id],
539
+ ...parsed,
540
+ agentId: requestedClear ? null : resolution.agentId,
541
+ } as Record<string, unknown>)
542
+ : false,
543
+ delegatorAgentId: toolKey === 'manage_tasks'
544
+ ? resolveDelegatorAgentId({
545
+ ...all[id],
546
+ ...parsed,
547
+ } as Record<string, unknown>, agents, ctx?.agentId || null)
548
+ : null,
549
+ })
550
+ if (assignmentError) return assignmentError
551
+ if (!requestedClear) parsed.agentId = resolution.agentId
506
552
  }
507
553
  }
508
554
  all[id] = { ...all[id], ...parsed, updatedAt: Date.now() }
@@ -598,7 +644,7 @@ export function buildCrudTools(bctx: ToolBuildContext): StructuredToolInterface[
598
644
  )
599
645
  }
600
646
 
601
- if (hasTool('manage_documents')) {
647
+ if (hasPlugin('manage_documents')) {
602
648
  tools.push(
603
649
  tool(
604
650
  async (rawArgs) => {