@swarmclawai/swarmclaw 0.7.2 → 0.7.4

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 (274) hide show
  1. package/README.md +116 -50
  2. package/bin/package-manager.js +157 -0
  3. package/bin/package-manager.test.js +90 -0
  4. package/bin/server-cmd.js +38 -7
  5. package/bin/swarmclaw.js +54 -4
  6. package/bin/update-cmd.js +48 -10
  7. package/bin/update-cmd.test.js +55 -0
  8. package/package.json +8 -3
  9. package/scripts/postinstall.mjs +26 -0
  10. package/src/app/api/agents/[id]/route.ts +43 -0
  11. package/src/app/api/agents/[id]/thread/route.ts +39 -8
  12. package/src/app/api/agents/route.ts +35 -2
  13. package/src/app/api/auth/route.ts +77 -8
  14. package/src/app/api/chatrooms/[id]/chat/route.ts +22 -6
  15. package/src/app/api/chatrooms/[id]/pins/route.ts +2 -1
  16. package/src/app/api/chatrooms/[id]/reactions/route.ts +2 -1
  17. package/src/app/api/chatrooms/[id]/route.ts +6 -0
  18. package/src/app/api/chats/[id]/browser/route.ts +5 -1
  19. package/src/app/api/chats/[id]/chat/route.ts +7 -3
  20. package/src/app/api/chats/[id]/messages/route.ts +19 -13
  21. package/src/app/api/chats/[id]/route.ts +30 -0
  22. package/src/app/api/chats/[id]/stop/route.ts +6 -1
  23. package/src/app/api/chats/heartbeat/route.ts +2 -1
  24. package/src/app/api/chats/route.ts +23 -1
  25. package/src/app/api/connectors/[id]/doctor/route.ts +26 -0
  26. package/src/app/api/connectors/doctor/route.ts +13 -0
  27. package/src/app/api/external-agents/[id]/heartbeat/route.ts +33 -0
  28. package/src/app/api/external-agents/[id]/route.ts +31 -0
  29. package/src/app/api/external-agents/register/route.ts +3 -0
  30. package/src/app/api/external-agents/route.ts +66 -0
  31. package/src/app/api/files/open/route.ts +16 -14
  32. package/src/app/api/gateways/[id]/health/route.ts +28 -0
  33. package/src/app/api/gateways/[id]/route.ts +79 -0
  34. package/src/app/api/gateways/route.ts +57 -0
  35. package/src/app/api/memory/maintenance/route.ts +11 -1
  36. package/src/app/api/openclaw/agent-files/route.ts +27 -4
  37. package/src/app/api/openclaw/gateway/route.ts +10 -7
  38. package/src/app/api/openclaw/skills/route.ts +12 -4
  39. package/src/app/api/plugins/dependencies/route.ts +24 -0
  40. package/src/app/api/plugins/install/route.ts +15 -92
  41. package/src/app/api/plugins/route.ts +3 -26
  42. package/src/app/api/plugins/settings/route.ts +17 -12
  43. package/src/app/api/plugins/ui/route.ts +1 -0
  44. package/src/app/api/providers/[id]/discover-models/route.ts +27 -0
  45. package/src/app/api/schedules/[id]/route.ts +38 -9
  46. package/src/app/api/schedules/route.ts +51 -28
  47. package/src/app/api/settings/route.ts +55 -17
  48. package/src/app/api/setup/doctor/route.ts +6 -4
  49. package/src/app/api/tasks/[id]/route.ts +16 -6
  50. package/src/app/api/tasks/bulk/route.ts +3 -3
  51. package/src/app/api/tasks/route.ts +9 -4
  52. package/src/app/api/webhooks/[id]/route.ts +8 -1
  53. package/src/app/page.tsx +135 -17
  54. package/src/cli/binary.test.js +142 -0
  55. package/src/cli/index.js +38 -11
  56. package/src/cli/index.test.js +195 -0
  57. package/src/cli/index.ts +21 -12
  58. package/src/cli/server-cmd.test.js +59 -0
  59. package/src/cli/spec.js +20 -2
  60. package/src/components/agents/agent-card.tsx +15 -12
  61. package/src/components/agents/agent-chat-list.tsx +101 -1
  62. package/src/components/agents/agent-list.tsx +46 -9
  63. package/src/components/agents/agent-sheet.tsx +456 -23
  64. package/src/components/agents/inspector-panel.tsx +110 -49
  65. package/src/components/agents/sandbox-env-panel.tsx +4 -1
  66. package/src/components/auth/access-key-gate.tsx +36 -97
  67. package/src/components/auth/setup-wizard.tsx +970 -275
  68. package/src/components/chat/chat-area.tsx +70 -27
  69. package/src/components/chat/chat-card.tsx +6 -21
  70. package/src/components/chat/chat-header.tsx +263 -366
  71. package/src/components/chat/chat-list.tsx +62 -26
  72. package/src/components/chat/checkpoint-timeline.tsx +1 -1
  73. package/src/components/chat/message-list.tsx +145 -19
  74. package/src/components/chatrooms/chatroom-input.tsx +96 -33
  75. package/src/components/chatrooms/chatroom-list.tsx +141 -72
  76. package/src/components/chatrooms/chatroom-message.tsx +7 -6
  77. package/src/components/chatrooms/chatroom-sheet.tsx +13 -1
  78. package/src/components/chatrooms/chatroom-tool-request-banner.tsx +5 -2
  79. package/src/components/chatrooms/chatroom-view.tsx +422 -209
  80. package/src/components/chatrooms/reaction-picker.tsx +38 -33
  81. package/src/components/connectors/connector-list.tsx +265 -127
  82. package/src/components/connectors/connector-sheet.tsx +217 -0
  83. package/src/components/gateways/gateway-sheet.tsx +567 -0
  84. package/src/components/home/home-view.tsx +128 -4
  85. package/src/components/input/chat-input.tsx +135 -86
  86. package/src/components/layout/app-layout.tsx +385 -194
  87. package/src/components/layout/mobile-header.tsx +26 -8
  88. package/src/components/memory/memory-browser.tsx +71 -6
  89. package/src/components/memory/memory-card.tsx +18 -0
  90. package/src/components/memory/memory-detail.tsx +58 -31
  91. package/src/components/memory/memory-sheet.tsx +32 -4
  92. package/src/components/plugins/plugin-list.tsx +15 -3
  93. package/src/components/plugins/plugin-sheet.tsx +118 -9
  94. package/src/components/projects/project-detail.tsx +189 -1
  95. package/src/components/providers/provider-list.tsx +158 -2
  96. package/src/components/providers/provider-sheet.tsx +81 -70
  97. package/src/components/shared/agent-picker-list.tsx +2 -2
  98. package/src/components/shared/bottom-sheet.tsx +31 -15
  99. package/src/components/shared/command-palette.tsx +111 -24
  100. package/src/components/shared/confirm-dialog.tsx +45 -30
  101. package/src/components/shared/model-combobox.tsx +90 -8
  102. package/src/components/shared/settings/plugin-manager.tsx +20 -4
  103. package/src/components/shared/settings/section-capability-policy.tsx +105 -0
  104. package/src/components/shared/settings/section-heartbeat.tsx +88 -6
  105. package/src/components/shared/settings/section-orchestrator.tsx +6 -3
  106. package/src/components/shared/settings/section-runtime-loop.tsx +5 -5
  107. package/src/components/shared/settings/section-secrets.tsx +6 -6
  108. package/src/components/shared/settings/section-user-preferences.tsx +1 -1
  109. package/src/components/shared/settings/section-voice.tsx +5 -1
  110. package/src/components/shared/settings/section-web-search.tsx +10 -2
  111. package/src/components/shared/settings/settings-page.tsx +248 -47
  112. package/src/components/tasks/approvals-panel.tsx +211 -18
  113. package/src/components/tasks/task-board.tsx +242 -46
  114. package/src/components/ui/dialog.tsx +2 -2
  115. package/src/components/usage/metrics-dashboard.tsx +74 -1
  116. package/src/components/wallets/wallet-approval-dialog.tsx +59 -54
  117. package/src/components/wallets/wallet-panel.tsx +17 -5
  118. package/src/components/webhooks/webhook-sheet.tsx +7 -7
  119. package/src/lib/auth.ts +17 -0
  120. package/src/lib/chat-streaming-state.test.ts +108 -0
  121. package/src/lib/chat-streaming-state.ts +108 -0
  122. package/src/lib/heartbeat-defaults.ts +48 -0
  123. package/src/lib/memory-presentation.ts +59 -0
  124. package/src/lib/openclaw-agent-id.test.ts +14 -0
  125. package/src/lib/openclaw-agent-id.ts +31 -0
  126. package/src/lib/provider-model-discovery-client.ts +29 -0
  127. package/src/lib/providers/index.ts +12 -5
  128. package/src/lib/runtime-loop.ts +105 -3
  129. package/src/lib/safe-storage.ts +6 -1
  130. package/src/lib/server/agent-assignment.test.ts +112 -0
  131. package/src/lib/server/agent-assignment.ts +169 -0
  132. package/src/lib/server/agent-runtime-config.test.ts +141 -0
  133. package/src/lib/server/agent-runtime-config.ts +277 -0
  134. package/src/lib/server/approval-connector-notify.test.ts +253 -0
  135. package/src/lib/server/approvals-auto-approve.test.ts +264 -0
  136. package/src/lib/server/approvals.ts +483 -75
  137. package/src/lib/server/autonomy-runtime.test.ts +341 -0
  138. package/src/lib/server/browser-state.test.ts +118 -0
  139. package/src/lib/server/browser-state.ts +123 -0
  140. package/src/lib/server/build-llm.test.ts +44 -0
  141. package/src/lib/server/build-llm.ts +11 -4
  142. package/src/lib/server/builtin-plugins.ts +34 -0
  143. package/src/lib/server/chat-execution-heartbeat.test.ts +40 -0
  144. package/src/lib/server/chat-execution-tool-events.test.ts +219 -0
  145. package/src/lib/server/chat-execution.ts +402 -125
  146. package/src/lib/server/chatroom-health.test.ts +26 -0
  147. package/src/lib/server/chatroom-health.ts +2 -3
  148. package/src/lib/server/chatroom-helpers.test.ts +74 -2
  149. package/src/lib/server/chatroom-helpers.ts +144 -11
  150. package/src/lib/server/chatroom-session-persistence.test.ts +87 -0
  151. package/src/lib/server/connectors/discord.ts +175 -11
  152. package/src/lib/server/connectors/doctor.test.ts +80 -0
  153. package/src/lib/server/connectors/doctor.ts +116 -0
  154. package/src/lib/server/connectors/manager.ts +994 -130
  155. package/src/lib/server/connectors/policy.test.ts +222 -0
  156. package/src/lib/server/connectors/policy.ts +452 -0
  157. package/src/lib/server/connectors/slack.ts +189 -10
  158. package/src/lib/server/connectors/telegram.ts +65 -15
  159. package/src/lib/server/connectors/thread-context.test.ts +44 -0
  160. package/src/lib/server/connectors/thread-context.ts +72 -0
  161. package/src/lib/server/connectors/types.ts +41 -11
  162. package/src/lib/server/daemon-state.ts +62 -3
  163. package/src/lib/server/data-dir.ts +13 -0
  164. package/src/lib/server/delegation-jobs.test.ts +140 -0
  165. package/src/lib/server/delegation-jobs.ts +248 -0
  166. package/src/lib/server/document-utils.test.ts +47 -0
  167. package/src/lib/server/document-utils.ts +397 -0
  168. package/src/lib/server/eval/agent-regression.test.ts +47 -0
  169. package/src/lib/server/eval/agent-regression.ts +1742 -0
  170. package/src/lib/server/eval/runner.ts +11 -1
  171. package/src/lib/server/eval/store.ts +2 -1
  172. package/src/lib/server/heartbeat-service.ts +23 -43
  173. package/src/lib/server/heartbeat-source.test.ts +22 -0
  174. package/src/lib/server/heartbeat-source.ts +7 -0
  175. package/src/lib/server/identity-continuity.test.ts +77 -0
  176. package/src/lib/server/identity-continuity.ts +127 -0
  177. package/src/lib/server/mailbox-utils.ts +347 -0
  178. package/src/lib/server/main-agent-loop.ts +31 -964
  179. package/src/lib/server/memory-db.ts +4 -6
  180. package/src/lib/server/memory-tiers.ts +40 -0
  181. package/src/lib/server/openclaw-agent-resolver.test.ts +70 -0
  182. package/src/lib/server/openclaw-agent-resolver.ts +128 -0
  183. package/src/lib/server/openclaw-exec-config.ts +6 -5
  184. package/src/lib/server/openclaw-gateway.ts +123 -36
  185. package/src/lib/server/openclaw-skills-normalize.test.ts +56 -0
  186. package/src/lib/server/openclaw-skills-normalize.ts +136 -0
  187. package/src/lib/server/openclaw-sync.ts +3 -2
  188. package/src/lib/server/orchestrator-lg.ts +18 -8
  189. package/src/lib/server/orchestrator.ts +5 -4
  190. package/src/lib/server/playwright-proxy.mjs +27 -3
  191. package/src/lib/server/plugins.test.ts +215 -0
  192. package/src/lib/server/plugins.ts +832 -69
  193. package/src/lib/server/provider-health.ts +33 -3
  194. package/src/lib/server/provider-model-discovery.ts +481 -0
  195. package/src/lib/server/queue.ts +4 -21
  196. package/src/lib/server/runtime-settings.test.ts +119 -0
  197. package/src/lib/server/runtime-settings.ts +12 -92
  198. package/src/lib/server/schedule-normalization.ts +187 -0
  199. package/src/lib/server/scheduler.ts +2 -0
  200. package/src/lib/server/session-archive-memory.test.ts +85 -0
  201. package/src/lib/server/session-archive-memory.ts +230 -0
  202. package/src/lib/server/session-mailbox.ts +8 -18
  203. package/src/lib/server/session-reset-policy.test.ts +99 -0
  204. package/src/lib/server/session-reset-policy.ts +311 -0
  205. package/src/lib/server/session-run-manager.ts +33 -80
  206. package/src/lib/server/session-tools/autonomy-tools.test.ts +128 -0
  207. package/src/lib/server/session-tools/calendar.ts +2 -12
  208. package/src/lib/server/session-tools/connector.ts +109 -8
  209. package/src/lib/server/session-tools/context.ts +14 -2
  210. package/src/lib/server/session-tools/crawl.ts +447 -0
  211. package/src/lib/server/session-tools/crud.ts +96 -34
  212. package/src/lib/server/session-tools/delegate-fallback.test.ts +219 -0
  213. package/src/lib/server/session-tools/delegate.ts +406 -20
  214. package/src/lib/server/session-tools/discovery-approvals.test.ts +170 -0
  215. package/src/lib/server/session-tools/discovery.ts +40 -12
  216. package/src/lib/server/session-tools/document.ts +283 -0
  217. package/src/lib/server/session-tools/email.ts +1 -3
  218. package/src/lib/server/session-tools/extract.ts +137 -0
  219. package/src/lib/server/session-tools/file-normalize.test.ts +98 -0
  220. package/src/lib/server/session-tools/file-send.test.ts +84 -1
  221. package/src/lib/server/session-tools/file.ts +243 -24
  222. package/src/lib/server/session-tools/http.ts +9 -3
  223. package/src/lib/server/session-tools/human-loop.ts +227 -0
  224. package/src/lib/server/session-tools/image-gen.ts +1 -3
  225. package/src/lib/server/session-tools/index.ts +87 -2
  226. package/src/lib/server/session-tools/mailbox.ts +276 -0
  227. package/src/lib/server/session-tools/manage-schedules.test.ts +137 -0
  228. package/src/lib/server/session-tools/memory.ts +35 -3
  229. package/src/lib/server/session-tools/monitor.ts +162 -12
  230. package/src/lib/server/session-tools/normalize-tool-args.ts +17 -14
  231. package/src/lib/server/session-tools/openclaw-nodes.test.ts +111 -0
  232. package/src/lib/server/session-tools/openclaw-nodes.ts +86 -20
  233. package/src/lib/server/session-tools/platform-normalize.test.ts +142 -0
  234. package/src/lib/server/session-tools/platform.ts +142 -4
  235. package/src/lib/server/session-tools/plugin-creator.ts +95 -25
  236. package/src/lib/server/session-tools/primitive-tools.test.ts +257 -0
  237. package/src/lib/server/session-tools/replicate.ts +1 -3
  238. package/src/lib/server/session-tools/sandbox.ts +51 -92
  239. package/src/lib/server/session-tools/schedule.ts +20 -10
  240. package/src/lib/server/session-tools/session-info.ts +58 -4
  241. package/src/lib/server/session-tools/session-tools-wiring.test.ts +54 -17
  242. package/src/lib/server/session-tools/shell.ts +2 -2
  243. package/src/lib/server/session-tools/subagent.ts +195 -27
  244. package/src/lib/server/session-tools/table.ts +587 -0
  245. package/src/lib/server/session-tools/wallet.ts +13 -10
  246. package/src/lib/server/session-tools/web-browser-config.test.ts +39 -0
  247. package/src/lib/server/session-tools/web.ts +947 -108
  248. package/src/lib/server/storage.ts +255 -10
  249. package/src/lib/server/stream-agent-chat.test.ts +61 -0
  250. package/src/lib/server/stream-agent-chat.ts +185 -25
  251. package/src/lib/server/structured-extract.test.ts +72 -0
  252. package/src/lib/server/structured-extract.ts +373 -0
  253. package/src/lib/server/task-mention.test.ts +16 -2
  254. package/src/lib/server/task-mention.ts +61 -11
  255. package/src/lib/server/tool-aliases.ts +80 -12
  256. package/src/lib/server/tool-capability-policy.ts +7 -1
  257. package/src/lib/server/tool-retry.ts +2 -0
  258. package/src/lib/server/watch-jobs.test.ts +173 -0
  259. package/src/lib/server/watch-jobs.ts +532 -0
  260. package/src/lib/server/ws-hub.ts +5 -3
  261. package/src/lib/setup-defaults.ts +352 -11
  262. package/src/lib/tool-definitions.ts +3 -4
  263. package/src/lib/validation/schemas.test.ts +26 -0
  264. package/src/lib/validation/schemas.ts +62 -1
  265. package/src/lib/ws-client.ts +14 -12
  266. package/src/proxy.ts +5 -5
  267. package/src/stores/use-app-store.ts +43 -7
  268. package/src/stores/use-chat-store.ts +31 -2
  269. package/src/stores/use-chatroom-store.ts +153 -26
  270. package/src/types/index.ts +470 -44
  271. package/src/app/api/chats/[id]/main-loop/route.ts +0 -94
  272. package/src/components/chat/new-chat-sheet.tsx +0 -253
  273. package/src/lib/server/main-session.ts +0 -17
  274. package/src/lib/server/session-run-manager.test.ts +0 -26
@@ -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,8 +22,15 @@ 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'
33
+ import { normalizeSchedulePayload } from '@/lib/server/schedule-normalization'
27
34
  import type { ToolBuildContext } from './context'
28
35
  import { safePath, findBinaryOnPath } from './context'
29
36
  import { normalizeToolInputArgs } from './normalize-tool-args'
@@ -137,7 +144,8 @@ const RESOURCE_DEFAULTS: Record<string, (parsed: any) => any> = {
137
144
  soul: p.soul || '',
138
145
  provider: p.provider || 'claude-cli',
139
146
  model: p.model || '',
140
- isOrchestrator: p.isOrchestrator || false,
147
+ platformAssignScope: p.platformAssignScope === 'all' ? 'all' : 'self',
148
+ isOrchestrator: p.platformAssignScope === 'all',
141
149
  tools: p.tools || [],
142
150
  skills: p.skills || [],
143
151
  skillIds: p.skillIds || [],
@@ -254,19 +262,22 @@ export function buildCrudTools(bctx: ToolBuildContext): StructuredToolInterface[
254
262
  if (!hasPlugin(toolKey)) continue
255
263
 
256
264
  let description = `Manage SwarmClaw ${res.label}. ${res.readOnly ? 'List and get only.' : 'List, get, create, update, or delete.'} Returns JSON.`
265
+ if (toolKey.startsWith('manage_') && toolKey !== 'manage_platform') {
266
+ description += `\n\nUse this direct tool name exactly as shown (\`${toolKey}\`). Do not swap it for \`manage_platform\` unless that umbrella tool is separately enabled in the current session.`
267
+ }
257
268
  if (toolKey === 'manage_tasks') {
258
269
  if (assignScope === 'self') {
259
- description += `\n\nDo NOT create tasks for yourself just do the work directly. Tasks are for delegating work to other agents or for user-created work items. You can only list, get, update status, or complete tasks assigned to you ("${ctx?.agentId || 'unknown'}"). Valid manual statuses: backlog, queued, completed, failed, archived. "running" is runtime-only and set automatically when execution starts.`
270
+ 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
271
  } else {
261
- description += `\n\nDo NOT create tasks for yourself just do the work directly. Only create tasks to delegate work to OTHER agents. Your agent ID is "${ctx?.agentId || 'unknown'}". Valid manual statuses: backlog, queued, completed, failed, archived. "running" is runtime-only and set automatically when execution starts.` + agentSummary
272
+ 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
273
  }
263
274
  } 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.`
275
+ 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
276
  } else if (toolKey === 'manage_schedules') {
266
277
  if (assignScope === 'self') {
267
- 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).`
278
+ description += `\n\nOmit "agentId" to assign a schedule to yourself ("${ctx?.agentId || 'unknown'}"), or set it explicitly to yourself. You can only assign schedules to yourself. Schedule types: interval (set intervalMs), cron (set cron), once (set runAt). Provide either taskPrompt, command, or action+path. Before create, call list/get to avoid duplicate schedules. If an equivalent active/paused schedule already exists, create returns that existing schedule (deduplicated=true).`
268
279
  } else {
269
- description += `\n\nSet "agentId" to assign a schedule to an agent (including yourself: "${ctx?.agentId || 'unknown'}"). 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).` + agentSummary
280
+ description += `\n\nOmit "agentId" to assign a schedule to yourself ("${ctx?.agentId || 'unknown'}"), or set "agentId" to another agent when needed. Schedule types: interval (set intervalMs), cron (set cron), once (set runAt). Provide either taskPrompt, command, or action+path. Before create, call list/get to avoid duplicate schedules. If an equivalent active/paused schedule already exists, create returns that existing schedule (deduplicated=true).` + agentSummary
270
281
  }
271
282
  } else if (toolKey === 'manage_webhooks') {
272
283
  description += '\n\nUse `source`, `events`, `agentId`, and `secret` when creating webhooks. Inbound calls should POST to `/api/webhooks/{id}` with header `x-webhook-secret` when a secret is configured.'
@@ -337,14 +348,39 @@ export function buildCrudTools(bctx: ToolBuildContext): StructuredToolInterface[
337
348
  if (parsed && typeof parsed === 'object' && 'id' in parsed) {
338
349
  delete (parsed as Record<string, unknown>).id
339
350
  }
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
351
  const now = Date.now()
352
+ if (toolKey === 'manage_tasks' || toolKey === 'manage_schedules') {
353
+ const agents = loadAgents()
354
+ const resolution = resolveManagedAgentAssignment(
355
+ parsed as Record<string, unknown>,
356
+ agents,
357
+ toolKey === 'manage_tasks' || toolKey === 'manage_schedules'
358
+ ? (parsed.agentId || ctx?.agentId || null)
359
+ : null,
360
+ { allowDescription: toolKey === 'manage_tasks' },
361
+ )
362
+ const assignmentError = validateManagedAgentAssignment({
363
+ resourceLabel: res.label,
364
+ agents,
365
+ assignScope,
366
+ currentAgentId: ctx?.agentId || null,
367
+ targetAgentId: resolution.agentId,
368
+ unresolvedReference: resolution.unresolvedReference,
369
+ isDelegation: toolKey === 'manage_tasks' ? isDelegationTaskPayload(parsed as Record<string, unknown>) : false,
370
+ delegatorAgentId: toolKey === 'manage_tasks'
371
+ ? resolveDelegatorAgentId(parsed as Record<string, unknown>, agents, ctx?.agentId || null)
372
+ : null,
373
+ })
374
+ if (assignmentError) return assignmentError
375
+ parsed.agentId = resolution.agentId
376
+ }
347
377
  if (toolKey === 'manage_schedules') {
378
+ const normalizedSchedule = normalizeSchedulePayload(parsed as Record<string, unknown>, {
379
+ cwd,
380
+ now,
381
+ })
382
+ if (!normalizedSchedule.ok) return normalizedSchedule.error
383
+ Object.assign(parsed, normalizedSchedule.value)
348
384
  const duplicate = findDuplicateSchedule(all as Record<string, ScheduleLike>, {
349
385
  agentId: parsed.agentId || null,
350
386
  taskPrompt: parsed.taskPrompt || '',
@@ -387,23 +423,6 @@ export function buildCrudTools(bctx: ToolBuildContext): StructuredToolInterface[
387
423
  })
388
424
  }
389
425
  }
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
- // Agents cannot create tasks for themselves — just do the work directly.
400
- // Tasks are for delegating to other agents or user-created work items.
401
- if (toolKey === 'manage_tasks' && ctx?.agentId) {
402
- const resolvedAgentId = parsed.agentId || ctx.agentId
403
- if (resolvedAgentId === ctx.agentId) {
404
- return 'Error: You cannot create tasks for yourself — just do the work directly. Tasks are for delegating work to other agents. If you need to track progress, use memory instead.'
405
- }
406
- }
407
426
  if (toolKey === 'manage_tasks') {
408
427
  parsed.title = deriveTaskTitle(parsed)
409
428
  if (!parsed.title || /^untitled task$/i.test(parsed.title)) {
@@ -507,13 +526,56 @@ export function buildCrudTools(bctx: ToolBuildContext): StructuredToolInterface[
507
526
  ? normalizeTaskQualityGate(parsed.qualityGate, settings)
508
527
  : null
509
528
  }
510
- // Enforce assignment scope for tasks and schedules
511
- if (assignScope === 'self' && (toolKey === 'manage_tasks' || toolKey === 'manage_schedules')) {
512
- if (parsed.agentId && parsed.agentId !== ctx?.agentId) {
513
- 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.`
529
+ if (toolKey === 'manage_tasks' || toolKey === 'manage_schedules') {
530
+ const agents = loadAgents()
531
+ const requestedClear = Object.prototype.hasOwnProperty.call(parsed, 'agentId') && parsed.agentId == null
532
+ const shouldResolveAssignment = requestedClear
533
+ || hasManagedAgentAssignmentInput(parsed as Record<string, unknown>)
534
+ if (shouldResolveAssignment) {
535
+ const resolution = resolveManagedAgentAssignment(
536
+ parsed as Record<string, unknown>,
537
+ agents,
538
+ null,
539
+ { allowDescription: false },
540
+ )
541
+ const assignmentError = validateManagedAgentAssignment({
542
+ resourceLabel: res.label,
543
+ agents,
544
+ assignScope,
545
+ currentAgentId: ctx?.agentId || null,
546
+ targetAgentId: requestedClear ? null : resolution.agentId,
547
+ unresolvedReference: requestedClear ? null : resolution.unresolvedReference,
548
+ isDelegation: toolKey === 'manage_tasks'
549
+ ? isDelegationTaskPayload({
550
+ ...all[id],
551
+ ...parsed,
552
+ agentId: requestedClear ? null : resolution.agentId,
553
+ } as Record<string, unknown>)
554
+ : false,
555
+ delegatorAgentId: toolKey === 'manage_tasks'
556
+ ? resolveDelegatorAgentId({
557
+ ...all[id],
558
+ ...parsed,
559
+ } as Record<string, unknown>, agents, ctx?.agentId || null)
560
+ : null,
561
+ })
562
+ if (assignmentError) return assignmentError
563
+ if (!requestedClear) parsed.agentId = resolution.agentId
514
564
  }
515
565
  }
516
566
  all[id] = { ...all[id], ...parsed, updatedAt: Date.now() }
567
+ if (toolKey === 'manage_schedules') {
568
+ const normalizedSchedule = normalizeSchedulePayload(all[id] as Record<string, unknown>, {
569
+ cwd,
570
+ now: Date.now(),
571
+ })
572
+ if (!normalizedSchedule.ok) return normalizedSchedule.error
573
+ all[id] = {
574
+ ...all[id],
575
+ ...normalizedSchedule.value,
576
+ updatedAt: Date.now(),
577
+ }
578
+ }
517
579
  if (toolKey === 'manage_secrets') {
518
580
  if (!canAccessSecret(all[id])) return 'Error: you do not have access to this secret.'
519
581
  const nextScope = parsed.scope === 'agent'