@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
@@ -6,12 +6,150 @@ import type { Plugin, PluginHooks } from '@/types'
6
6
  import { getPluginManager } from '../plugins'
7
7
  import { normalizeToolInputArgs } from './normalize-tool-args'
8
8
 
9
+ function parsePlatformData(value: unknown): Record<string, unknown> | null {
10
+ if (!value) return null
11
+ if (typeof value === 'object' && !Array.isArray(value)) {
12
+ return value as Record<string, unknown>
13
+ }
14
+ if (typeof value !== 'string') return null
15
+ const trimmed = value.trim()
16
+ if (!trimmed) return null
17
+ try {
18
+ const parsed = JSON.parse(trimmed)
19
+ if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
20
+ return parsed as Record<string, unknown>
21
+ }
22
+ } catch {
23
+ // Preserve non-JSON data strings as-is in the caller.
24
+ }
25
+ return null
26
+ }
27
+
28
+ function firstPlatformResource(value: unknown): Record<string, unknown> | null {
29
+ if (!Array.isArray(value)) return null
30
+ const first = value.find((entry) => entry && typeof entry === 'object' && !Array.isArray(entry))
31
+ return first && typeof first === 'object' && !Array.isArray(first)
32
+ ? first as Record<string, unknown>
33
+ : null
34
+ }
35
+
36
+ function normalizePlatformResourceName(value: unknown): string | undefined {
37
+ if (typeof value !== 'string') return undefined
38
+ const normalized = value.trim().toLowerCase()
39
+ if (!normalized) return undefined
40
+ const singularMap: Record<string, string> = {
41
+ agent: 'agents',
42
+ task: 'tasks',
43
+ backlog_task: 'tasks',
44
+ 'backlog-task': 'tasks',
45
+ backlogtask: 'tasks',
46
+ task_backlog: 'tasks',
47
+ 'task-backlog': 'tasks',
48
+ work_item: 'tasks',
49
+ 'work-item': 'tasks',
50
+ schedule: 'schedules',
51
+ skill: 'skills',
52
+ document: 'documents',
53
+ secret: 'secrets',
54
+ connector: 'connectors',
55
+ session: 'sessions',
56
+ }
57
+ return singularMap[normalized] || normalized
58
+ }
59
+
60
+ function inferPlatformResourceFromAction(value: unknown): { resource?: string; action?: string } {
61
+ if (typeof value !== 'string') return {}
62
+ const normalized = value.trim().toLowerCase().replace(/-/g, '_')
63
+ if (!normalized) return {}
64
+ const match = normalized.match(/^(list|get|create|update|delete)_([a-z_]+)$/)
65
+ if (!match) return {}
66
+ const [, action, rawResource] = match
67
+ const resource = normalizePlatformResourceName(rawResource)
68
+ if (!resource) return {}
69
+ return { resource, action }
70
+ }
71
+
72
+ function extractPlatformFields(value: Record<string, unknown>): Record<string, unknown> {
73
+ const fields: Record<string, unknown> = {}
74
+ for (const [key, fieldValue] of Object.entries(value)) {
75
+ if (fieldValue === undefined || fieldValue === null) continue
76
+ if (['input', 'args', 'arguments', 'payload', 'resources', 'parameters', 'resource', 'type', 'action', 'id'].includes(key)) continue
77
+ fields[key] = fieldValue
78
+ }
79
+ return fields
80
+ }
81
+
82
+ export function normalizePlatformActionArgs(rawArgs: Record<string, unknown>): Record<string, unknown> {
83
+ const normalized = normalizeToolInputArgs(rawArgs)
84
+ const resourceEntry = firstPlatformResource(normalized.resources)
85
+ const { resource, action, id, data, ...rest } = normalized
86
+ const payload: Record<string, unknown> = {}
87
+ const resourceValue = resource ?? resourceEntry?.resource ?? resourceEntry?.type
88
+ const rawResourceName = typeof resourceValue === 'string'
89
+ ? String(resourceValue).trim()
90
+ : undefined
91
+
92
+ const rawAction = action ?? resourceEntry?.action
93
+ const inferredFromAction = resourceValue === undefined
94
+ ? inferPlatformResourceFromAction(rawAction)
95
+ : {}
96
+ const effectiveResource = normalizePlatformResourceName(resourceValue) ?? inferredFromAction.resource
97
+ const effectiveAction = inferredFromAction.action && resourceValue === undefined
98
+ ? inferredFromAction.action
99
+ : rawAction
100
+ const effectiveId = id ?? resourceEntry?.id
101
+
102
+ if (effectiveResource !== undefined) payload.resource = effectiveResource
103
+ if (effectiveAction !== undefined) payload.action = effectiveAction
104
+ if (effectiveId !== undefined) payload.id = effectiveId
105
+
106
+ const directFields = extractPlatformFields(rest)
107
+ const resourcePayloadCandidates = effectiveResource
108
+ ? uniqueStrings([
109
+ rawResourceName,
110
+ effectiveResource,
111
+ effectiveResource.replace(/s$/, ''),
112
+ ])
113
+ : []
114
+ const directResourcePayload = resourcePayloadCandidates
115
+ .map((candidate) => parsePlatformData(normalized[candidate]))
116
+ .find(Boolean)
117
+ || null
118
+ if (effectiveResource) {
119
+ for (const candidate of resourcePayloadCandidates) delete directFields[candidate]
120
+ }
121
+ const parameterFields = {
122
+ ...(parsePlatformData(resourceEntry?.parameters) || {}),
123
+ ...(parsePlatformData(resourceEntry?.params) || {}),
124
+ ...(parsePlatformData(normalized.parameters) || {}),
125
+ ...(directResourcePayload || {}),
126
+ }
127
+ const parsedData = parsePlatformData(data)
128
+ const mergedData = {
129
+ ...(parsedData || {}),
130
+ ...parameterFields,
131
+ ...directFields,
132
+ }
133
+
134
+ if (Object.keys(mergedData).length > 0) {
135
+ payload.data = JSON.stringify(mergedData)
136
+ } else if (typeof data === 'string' && data.trim()) {
137
+ payload.data = data
138
+ }
139
+
140
+ return payload
141
+ }
142
+
143
+ function uniqueStrings(values: Array<string | undefined>): string[] {
144
+ return [...new Set(values.filter((value): value is string => Boolean(value)))]
145
+ }
146
+
9
147
  /**
10
148
  * Unified Platform Execution Logic
11
149
  */
12
150
  async function executePlatformAction(args: any, bctx: any) {
13
- const normalized = normalizeToolInputArgs((args ?? {}) as Record<string, unknown>)
14
- const { resource, action, id, data, ...rest } = normalized
151
+ const normalized = normalizePlatformActionArgs((args ?? {}) as Record<string, unknown>)
152
+ const { resource, action, id, data } = normalized
15
153
 
16
154
  // We reuse the existing CRUD tool logic but expose it via a single tool
17
155
  const crudTools = buildCrudTools({
@@ -36,7 +174,7 @@ async function executePlatformAction(args: any, bctx: any) {
36
174
  }
37
175
 
38
176
  // Forward to the specific CRUD tool implementation
39
- return targetTool.invoke({ action, id, data, ...rest })
177
+ return targetTool.invoke({ action, id, data })
40
178
  }
41
179
 
42
180
  /**
@@ -52,7 +190,7 @@ const PlatformPlugin: Plugin = {
52
190
  tools: [
53
191
  {
54
192
  name: 'manage_platform',
55
- description: 'Unified tool for managing all SwarmClaw resources.',
193
+ description: 'Unified fallback tool for managing SwarmClaw resources when a more specific `manage_*` tool is not available. For create/update, pass resource + action, then either put fields inside data, pass them as top-level fields, or use a single resources[0].parameters envelope.',
56
194
  parameters: {
57
195
  type: 'object',
58
196
  properties: {
@@ -27,6 +27,8 @@ async function executePluginCreatorAction(args: Record<string, unknown>, ctxOrBc
27
27
  const action = normalized.action as string | undefined
28
28
  const filename = (normalized.filename ?? normalized.fileName) as string | undefined
29
29
  const code = (normalized.code ?? normalized.content) as string | undefined
30
+ const packageJson = normalized.packageJson ?? normalized.package_json ?? normalized.manifest
31
+ const packageManager = typeof normalized.packageManager === 'string' ? normalized.packageManager : undefined
30
32
  const approved = normalized.approved as boolean | undefined
31
33
 
32
34
  try {
@@ -40,15 +42,23 @@ async function executePluginCreatorAction(args: Record<string, unknown>, ctxOrBc
40
42
 
41
43
  // REQUIRE USER APPROVAL
42
44
  if (approved !== true) {
43
- const { requestApproval } = await import('../approvals')
44
- requestApproval({
45
+ const { requestApprovalMaybeAutoApprove } = await import('../approvals')
46
+ const approval = await requestApprovalMaybeAutoApprove({
45
47
  category: 'plugin_scaffold',
46
48
  title: `Scaffold Plugin: ${filename}`,
47
49
  description: `Create new plugin file with ${code.length} chars of code.`,
48
- data: { filename, code, createdByAgentId: pctx.agentId || null },
50
+ data: { filename, code, packageJson, packageManager, createdByAgentId: pctx.agentId || null },
49
51
  agentId: pctx.agentId,
50
52
  sessionId: pctx.sessionId,
51
53
  })
54
+ if (approval.status === 'approved') {
55
+ return JSON.stringify({
56
+ type: 'plugin_scaffold_request',
57
+ filename,
58
+ autoApproved: true,
59
+ message: `Plugin "${filename}" was auto-approved and scaffolded. It is now available in this chat.`,
60
+ })
61
+ }
52
62
  return JSON.stringify({
53
63
  type: 'plugin_scaffold_request',
54
64
  filename,
@@ -56,12 +66,13 @@ async function executePluginCreatorAction(args: Record<string, unknown>, ctxOrBc
56
66
  })
57
67
  }
58
68
 
59
- const filePath = path.join(PLUGINS_DIR, filename)
60
- fs.writeFileSync(filePath, code, 'utf8')
61
-
62
- // Reload the plugin manager so the new plugin is discovered
63
69
  const manager = getPluginManager()
64
- manager.reload()
70
+ await manager.savePluginSource(filename, code, {
71
+ packageJson,
72
+ packageManager,
73
+ installDependencies: packageJson !== undefined,
74
+ })
75
+ const filePath = path.join(PLUGINS_DIR, filename)
65
76
 
66
77
  // Auto-enable the plugin for the agent that created it
67
78
  if (pctx.agentId && pctx.sessionId) {
@@ -82,10 +93,53 @@ async function executePluginCreatorAction(args: Record<string, unknown>, ctxOrBc
82
93
  return `Plugin saved to ${filePath} and PluginManager reloaded. It is now enabled for this chat.`
83
94
  }
84
95
 
96
+ if (action === 'install_dependencies') {
97
+ if (!filename) return 'Error: filename is required for install_dependencies.'
98
+
99
+ if (approved !== true) {
100
+ const { requestApprovalMaybeAutoApprove } = await import('../approvals')
101
+ const approval = await requestApprovalMaybeAutoApprove({
102
+ category: 'plugin_install',
103
+ title: `Install Plugin Dependencies: ${filename}`,
104
+ description: `Install package dependencies for plugin ${filename}${packageManager ? ` using ${packageManager}` : ''}.`,
105
+ data: { filename, packageJson, packageManager, createdByAgentId: pctx.agentId || null },
106
+ agentId: pctx.agentId,
107
+ sessionId: pctx.sessionId,
108
+ })
109
+ if (approval.status === 'approved') {
110
+ return JSON.stringify({
111
+ type: 'plugin_install_request',
112
+ filename,
113
+ autoApproved: true,
114
+ message: `Dependencies for "${filename}" were auto-approved and are being installed.`,
115
+ })
116
+ }
117
+ return JSON.stringify({
118
+ type: 'plugin_install_request',
119
+ filename,
120
+ message: `I've requested approval to install dependencies for "${filename}". Once approved, the plugin manager will run the selected package manager automatically.`,
121
+ })
122
+ }
123
+
124
+ const manager = getPluginManager()
125
+ if (packageJson !== undefined) {
126
+ const source = manager.readPluginSource(filename)
127
+ await manager.savePluginSource(filename, source, {
128
+ packageJson,
129
+ packageManager,
130
+ installDependencies: false,
131
+ })
132
+ }
133
+ const result = await manager.installPluginDependencies(filename, {
134
+ packageManager: packageManager as import('@/types').PluginPackageManager | undefined,
135
+ })
136
+ return `Dependencies installed for ${filename} using ${result.packageManager || packageManager || 'npm'}.`
137
+ }
138
+
85
139
  if (action === 'get_spec') {
86
140
  return `
87
141
  SwarmClaw Plugin Specification:
88
- A plugin is a CommonJS module (.js) that must be DUAL-COMPATIBLE with both SwarmClaw and OpenClaw platforms.
142
+ A plugin is a JavaScript module (.js or .mjs) that can be dual-compatible with both SwarmClaw and OpenClaw platforms.
89
143
 
90
144
  \`\`\`js
91
145
  module.exports = {
@@ -100,10 +154,11 @@ module.exports = {
100
154
  hooks: {
101
155
  beforeAgentStart: async ({ session, message }) => {},
102
156
  afterAgentComplete: async ({ session, response }) => {},
103
- beforeToolExec: async ({ session, toolName, args }) => {},
104
- afterToolExec: async ({ session, toolName, result }) => {},
157
+ beforeToolExec: async ({ toolName, input }) => input,
158
+ afterToolExec: async ({ session, toolName, input, output }) => {},
105
159
  transformInboundMessage: async ({ session, text }) => { return text; },
106
160
  transformOutboundMessage: async ({ session, text }) => { return text; },
161
+ afterChatTurn: async ({ session, message, response, source, internal }) => {},
107
162
  },
108
163
 
109
164
  tools: [
@@ -140,27 +195,31 @@ module.exports = {
140
195
  \`\`\`
141
196
 
142
197
  Key rules:
143
- - Export BOTH SwarmClaw hooks/tools AND a register(api) method for cross-platform compatibility
144
- - SwarmClaw checks for hooks/tools first; OpenClaw checks for register()
198
+ - Export SwarmClaw hooks/tools. Add register(api) too if you want OpenClaw compatibility.
199
+ - SwarmClaw checks hooks/tools first; OpenClaw checks register()
145
200
  - Tools must have name, description, parameters (JSON Schema), and execute function
146
201
  - Hooks are optional — only include the ones you need
202
+ - If your plugin needs npm/pnpm/yarn/bun packages, include a packageJson object during scaffold or call install_dependencies later.
203
+ - Dependency installs are run by the plugin manager inside a per-plugin workspace using the selected package manager with scripts disabled.
204
+ - Plugin settings are declared through ui.settingsFields and stored per plugin ID
147
205
  - Keep plugins focused: one clear purpose per plugin
148
206
  `
149
207
  }
150
208
 
151
209
  if (action === 'read') {
152
210
  if (!filename) return 'Error: filename required.'
153
- const filePath = path.join(PLUGINS_DIR, filename)
154
- if (!fs.existsSync(filePath)) return `File not found: ${filename}`
155
- return fs.readFileSync(filePath, 'utf8')
211
+ return getPluginManager().readPluginSource(filename)
156
212
  }
157
213
 
158
214
  if (action === 'edit') {
159
215
  if (!filename || !code) return 'Error: filename and code are required for edit.'
160
- const filePath = path.join(PLUGINS_DIR, filename)
161
- if (!fs.existsSync(filePath)) return `File not found: ${filename}. Use scaffold to create new plugins.`
162
- fs.writeFileSync(filePath, code, 'utf8')
163
- getPluginManager().reload()
216
+ const manager = getPluginManager()
217
+ try {
218
+ manager.readPluginSource(filename)
219
+ } catch {
220
+ return `File not found: ${filename}. Use scaffold to create new plugins.`
221
+ }
222
+ await manager.savePluginSource(filename, code)
164
223
  return `Updated ${filename} and reloaded plugin manager.`
165
224
  }
166
225
 
@@ -175,7 +234,7 @@ Key rules:
175
234
  return `File not found: ${filename}`
176
235
  }
177
236
 
178
- return `Unknown action "${action}". Valid actions: get_spec, scaffold, read, edit, delete`
237
+ return `Unknown action "${action}". Valid actions: get_spec, scaffold, read, edit, delete, install_dependencies`
179
238
  } catch (err: unknown) {
180
239
  return `Error: ${err instanceof Error ? err.message : String(err)}`
181
240
  }
@@ -186,8 +245,15 @@ Key rules:
186
245
  */
187
246
  const PluginCreatorPlugin: Plugin = {
188
247
  name: 'Plugin Creator',
189
- description: 'Design, write, and test custom SwarmClaw plugins dynamically.',
190
- hooks: {} as PluginHooks,
248
+ description: 'Design focused SwarmClaw plugins for durable capabilities and recurring automations.',
249
+ hooks: {
250
+ getCapabilityDescription: () => 'I can scaffold focused plugins (`plugin_creator_tool`) when a capability should become durable instead of living in a one-off sandbox script.',
251
+ getOperatingGuidance: () => [
252
+ 'For recurring or scheduled automations, prefer a focused plugin plus `manage_schedules` over repeated sandbox runs.',
253
+ 'Put API keys in plugin settings or SwarmClaw secrets instead of hardcoding them in plugin source.',
254
+ 'Call `get_spec` before scaffolding so the plugin follows the current contract.',
255
+ ],
256
+ } as PluginHooks,
191
257
  tools: [
192
258
  {
193
259
  name: 'plugin_creator_tool',
@@ -195,9 +261,11 @@ const PluginCreatorPlugin: Plugin = {
195
261
  parameters: {
196
262
  type: 'object',
197
263
  properties: {
198
- action: { type: 'string', enum: ['get_spec', 'scaffold', 'read', 'edit', 'delete'], description: 'get_spec: learn format. scaffold: create (needs approval). read: view code. edit: update existing. delete: remove.' },
264
+ action: { type: 'string', enum: ['get_spec', 'scaffold', 'read', 'edit', 'delete', 'install_dependencies'], description: 'get_spec: learn format. scaffold: create (needs approval). read: view code. edit: update existing. delete: remove. install_dependencies: write/read package.json and install runtime deps.' },
199
265
  filename: { type: 'string', description: 'Plugin filename, e.g. my-plugin.js. Required for scaffold and delete.' },
200
266
  code: { type: 'string', description: 'The raw JavaScript code for the plugin. Required for scaffold.' },
267
+ packageJson: { type: 'object', description: 'Optional package.json object for dependency-aware plugins. Use with scaffold or install_dependencies.' },
268
+ packageManager: { type: 'string', enum: ['npm', 'pnpm', 'yarn', 'bun'], description: 'Optional package manager to use for dependency installs.' },
201
269
  approved: { type: 'boolean', description: 'Internal flag — do NOT set this. The approval system handles it automatically.' }
202
270
  },
203
271
  required: ['action']
@@ -228,9 +296,11 @@ export function buildPluginCreatorTools(bctx: ToolBuildContext): StructuredToolI
228
296
  name: 'plugin_creator_tool',
229
297
  description: PluginCreatorPlugin.tools![0].description,
230
298
  schema: z.object({
231
- action: z.enum(['get_spec', 'scaffold', 'read', 'edit', 'delete']),
299
+ action: z.enum(['get_spec', 'scaffold', 'read', 'edit', 'delete', 'install_dependencies']),
232
300
  filename: z.string().optional(),
233
301
  code: z.string().optional(),
302
+ packageJson: z.record(z.string(), z.any()).optional(),
303
+ packageManager: z.enum(['npm', 'pnpm', 'yarn', 'bun']).optional(),
234
304
  approved: z.boolean().optional()
235
305
  })
236
306
  }
@@ -0,0 +1,257 @@
1
+ import assert from 'node:assert/strict'
2
+ import fs from 'node:fs'
3
+ import os from 'node:os'
4
+ import path from 'node:path'
5
+ import { after, before, describe, it } from 'node:test'
6
+ import type { Session } from '@/types'
7
+
8
+ const originalEnv = {
9
+ DATA_DIR: process.env.DATA_DIR,
10
+ WORKSPACE_DIR: process.env.WORKSPACE_DIR,
11
+ SWARMCLAW_BUILD_MODE: process.env.SWARMCLAW_BUILD_MODE,
12
+ }
13
+
14
+ let tempDir = ''
15
+ let workspaceDir = ''
16
+ let buildDocumentTools: typeof import('./document').buildDocumentTools
17
+ let buildExtractTools: typeof import('./extract').buildExtractTools
18
+ let buildTableTools: typeof import('./table').buildTableTools
19
+ let buildMailboxTools: typeof import('./mailbox').buildMailboxTools
20
+ let buildHumanLoopTools: typeof import('./human-loop').buildHumanLoopTools
21
+ let buildCrawlTools: typeof import('./crawl').buildCrawlTools
22
+ let sessionMailbox: typeof import('../session-mailbox')
23
+ let watchJobs: typeof import('../watch-jobs')
24
+ let storage: typeof import('../storage')
25
+
26
+ function makeSession(overrides?: Partial<Session>): Session {
27
+ return {
28
+ id: 'session_1',
29
+ name: 'Test Session',
30
+ cwd: workspaceDir,
31
+ user: 'tester',
32
+ provider: 'ollama',
33
+ model: 'qwen3.5',
34
+ apiEndpoint: 'http://localhost:11434',
35
+ claudeSessionId: null,
36
+ messages: [],
37
+ createdAt: Date.now(),
38
+ lastActiveAt: Date.now(),
39
+ plugins: [],
40
+ ...overrides,
41
+ }
42
+ }
43
+
44
+ function makeBuildContext(overrides?: {
45
+ cwd?: string
46
+ session?: Session
47
+ }) {
48
+ const session = overrides?.session || makeSession()
49
+ return {
50
+ cwd: overrides?.cwd || workspaceDir,
51
+ ctx: {
52
+ sessionId: session.id,
53
+ agentId: session.agentId || 'agent_1',
54
+ },
55
+ hasPlugin: () => true,
56
+ hasTool: () => true,
57
+ cleanupFns: [],
58
+ commandTimeoutMs: 5000,
59
+ claudeTimeoutMs: 5000,
60
+ cliProcessTimeoutMs: 5000,
61
+ persistDelegateResumeId: () => {},
62
+ readStoredDelegateResumeId: () => null,
63
+ resolveCurrentSession: () => session,
64
+ activePlugins: [],
65
+ }
66
+ }
67
+
68
+ before(async () => {
69
+ tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'swarmclaw-primitive-tools-'))
70
+ workspaceDir = path.join(tempDir, 'workspace')
71
+ fs.mkdirSync(workspaceDir, { recursive: true })
72
+ process.env.DATA_DIR = path.join(tempDir, 'data')
73
+ process.env.WORKSPACE_DIR = workspaceDir
74
+ process.env.SWARMCLAW_BUILD_MODE = '1'
75
+ fs.mkdirSync(process.env.DATA_DIR, { recursive: true })
76
+
77
+ ;({ buildDocumentTools } = await import('./document'))
78
+ ;({ buildExtractTools } = await import('./extract'))
79
+ ;({ buildTableTools } = await import('./table'))
80
+ ;({ buildMailboxTools } = await import('./mailbox'))
81
+ ;({ buildHumanLoopTools } = await import('./human-loop'))
82
+ ;({ buildCrawlTools } = await import('./crawl'))
83
+ sessionMailbox = await import('../session-mailbox')
84
+ watchJobs = await import('../watch-jobs')
85
+ storage = await import('../storage')
86
+ })
87
+
88
+ after(() => {
89
+ if (originalEnv.DATA_DIR === undefined) delete process.env.DATA_DIR
90
+ else process.env.DATA_DIR = originalEnv.DATA_DIR
91
+ if (originalEnv.WORKSPACE_DIR === undefined) delete process.env.WORKSPACE_DIR
92
+ else process.env.WORKSPACE_DIR = originalEnv.WORKSPACE_DIR
93
+ if (originalEnv.SWARMCLAW_BUILD_MODE === undefined) delete process.env.SWARMCLAW_BUILD_MODE
94
+ else process.env.SWARMCLAW_BUILD_MODE = originalEnv.SWARMCLAW_BUILD_MODE
95
+ fs.rmSync(tempDir, { recursive: true, force: true })
96
+ })
97
+
98
+ describe('primitive tools', () => {
99
+ it('document tool reads, stores, and searches extracted text', async () => {
100
+ const sourcePath = path.join(workspaceDir, 'note.txt')
101
+ fs.writeFileSync(sourcePath, 'Invoice 42 for ACME\nTotal: $120.50\n')
102
+
103
+ const [documentTool] = buildDocumentTools(makeBuildContext())
104
+ const read = JSON.parse(String(await documentTool.invoke({ action: 'read', filePath: 'note.txt' })))
105
+ assert.match(read.text, /Invoice 42/)
106
+
107
+ const stored = JSON.parse(String(await documentTool.invoke({ action: 'store', filePath: 'note.txt', title: 'Invoice Note' })))
108
+ const search = JSON.parse(String(await documentTool.invoke({ action: 'search', query: 'ACME' })))
109
+ const fetched = JSON.parse(String(await documentTool.invoke({ action: 'get', id: stored.id })))
110
+
111
+ assert.equal(search.matches[0].id, stored.id)
112
+ assert.equal(fetched.title, 'Invoice Note')
113
+ })
114
+
115
+ it('table tool transforms inline data and writes results', async () => {
116
+ const [tableTool] = buildTableTools(makeBuildContext())
117
+ const rows = [
118
+ { id: '1', name: 'Ada', score: 10 },
119
+ { id: '2', name: 'Grace', score: 25 },
120
+ { id: '2', name: 'Grace', score: 25 },
121
+ ]
122
+
123
+ const filtered = JSON.parse(String(await tableTool.invoke({
124
+ action: 'filter',
125
+ rows,
126
+ where: [{ column: 'score', op: 'gt', value: 15 }],
127
+ })))
128
+ assert.equal(filtered.rowCount, 2)
129
+
130
+ const deduped = JSON.parse(String(await tableTool.invoke({
131
+ action: 'dedupe',
132
+ rows,
133
+ on: ['id'],
134
+ })))
135
+ assert.equal(deduped.rowCount, 2)
136
+
137
+ const joined = JSON.parse(String(await tableTool.invoke({
138
+ action: 'join',
139
+ leftRows: [{ id: '1', team: 'red' }],
140
+ rightRows: [{ id: '1', email: 'ada@example.com' }],
141
+ on: 'id',
142
+ })))
143
+ assert.equal(joined.rows[0].email, 'ada@example.com')
144
+
145
+ const writeResult = JSON.parse(String(await tableTool.invoke({
146
+ action: 'write',
147
+ rows,
148
+ outputPath: 'exports/report.csv',
149
+ })))
150
+ assert.equal(fs.existsSync(writeResult.output.filePath), true)
151
+ })
152
+
153
+ it('human-loop tool creates durable mailbox and approval waits', async () => {
154
+ const [humanTool] = buildHumanLoopTools(makeBuildContext())
155
+ const sessions = storage.loadSessions()
156
+ sessions.session_1 = makeSession({ id: 'session_1', agentId: 'agent_1' })
157
+ storage.saveSessions(sessions)
158
+
159
+ const requestInput = JSON.parse(String(await humanTool.invoke({
160
+ action: 'request_input',
161
+ question: 'Ship it?',
162
+ correlationId: 'corr_123',
163
+ })))
164
+ assert.equal(requestInput.ok, true)
165
+
166
+ const replyWatch = JSON.parse(String(await humanTool.invoke({
167
+ action: 'wait_for_reply',
168
+ correlationId: 'corr_123',
169
+ })))
170
+ const replyEnvelope = sessionMailbox.sendMailboxEnvelope({
171
+ toSessionId: 'session_1',
172
+ type: 'human_reply',
173
+ payload: 'yes',
174
+ correlationId: 'corr_123',
175
+ })
176
+ watchJobs.triggerMailboxWatchJobs({ sessionId: 'session_1', envelope: replyEnvelope })
177
+ assert.equal(watchJobs.getWatchJob(replyWatch.id)?.status, 'triggered')
178
+
179
+ const approval = JSON.parse(String(await humanTool.invoke({
180
+ action: 'request_approval',
181
+ title: 'Need signoff',
182
+ question: 'Allow publish?',
183
+ })))
184
+ const approvalWatch = JSON.parse(String(await humanTool.invoke({
185
+ action: 'wait_for_approval',
186
+ approvalId: approval.id,
187
+ })))
188
+ watchJobs.triggerApprovalWatchJobs({ approvalId: approval.id, status: 'approved' })
189
+ assert.equal(watchJobs.getWatchJob(approvalWatch.id)?.status, 'triggered')
190
+ })
191
+
192
+ it('mailbox tool reports configuration status without requiring network', async () => {
193
+ const [mailboxTool] = buildMailboxTools(makeBuildContext())
194
+ const status = JSON.parse(String(await mailboxTool.invoke({ action: 'status' })))
195
+ assert.equal(status.configured, false)
196
+ assert.equal(status.folder, 'INBOX')
197
+ })
198
+
199
+ it('extract tool reports active model context', async () => {
200
+ const [extractTool] = buildExtractTools(makeBuildContext({
201
+ session: makeSession({
202
+ provider: 'ollama',
203
+ model: 'qwen3.5',
204
+ apiEndpoint: 'http://localhost:11434',
205
+ }),
206
+ }))
207
+ const status = JSON.parse(String(await extractTool.invoke({ action: 'status' })))
208
+ assert.equal(status.provider, 'ollama')
209
+ assert.equal(Array.isArray(status.supports), true)
210
+ })
211
+
212
+ it('crawl tool crawls and dedupes fetched pages without a live server', async () => {
213
+ const originalFetch = global.fetch
214
+ global.fetch = (async (input: RequestInfo | URL) => {
215
+ const url = typeof input === 'string' ? input : input.toString()
216
+ if (url.endsWith('/page-2')) {
217
+ return new Response('<html><head><title>Page Two</title></head><body><article><h1>Second</h1><p>Next content</p></article></body></html>', {
218
+ status: 200,
219
+ headers: { 'content-type': 'text/html' },
220
+ })
221
+ }
222
+ return new Response('<html><head><title>Root</title></head><body><article><h1>Home</h1><p>Welcome</p></article><a href="/page-2" rel="next">Next</a></body></html>', {
223
+ status: 200,
224
+ headers: { 'content-type': 'text/html' },
225
+ })
226
+ }) as typeof fetch
227
+
228
+ try {
229
+ const [crawlTool] = buildCrawlTools(makeBuildContext())
230
+ const baseUrl = 'https://example.test/'
231
+
232
+ const crawled = JSON.parse(String(await crawlTool.invoke({
233
+ action: 'crawl_site',
234
+ url: baseUrl,
235
+ limit: 2,
236
+ })))
237
+ assert.equal(crawled.count, 2)
238
+ assert.equal(crawled.pages[0].title, 'Root')
239
+
240
+ const extracted = JSON.parse(String(await crawlTool.invoke({
241
+ action: 'extract_sitemap',
242
+ url: baseUrl,
243
+ limit: 2,
244
+ })))
245
+ assert.equal(extracted.count, 2)
246
+ assert.equal(extracted.urls.includes('https://example.test/page-2'), true)
247
+
248
+ const deduped = JSON.parse(String(await crawlTool.invoke({
249
+ action: 'dedupe_pages',
250
+ pages: [crawled.pages[0], crawled.pages[0], crawled.pages[1]],
251
+ })))
252
+ assert.equal(deduped.count, 2)
253
+ } finally {
254
+ global.fetch = originalFetch
255
+ }
256
+ })
257
+ })
@@ -3,7 +3,6 @@ import { tool, type StructuredToolInterface } from '@langchain/core/tools'
3
3
  import type { Plugin, PluginHooks } from '@/types'
4
4
  import { getPluginManager } from '../plugins'
5
5
  import { normalizeToolInputArgs } from './normalize-tool-args'
6
- import { loadSettings } from '../storage'
7
6
  import type { ToolBuildContext } from './context'
8
7
 
9
8
  interface ReplicateConfig {
@@ -14,8 +13,7 @@ interface ReplicateConfig {
14
13
  }
15
14
 
16
15
  function getConfig(): ReplicateConfig {
17
- const settings = loadSettings()
18
- const ps = (settings.pluginSettings as Record<string, Record<string, unknown>> | undefined)?.replicate ?? {}
16
+ const ps = getPluginManager().getPluginSettings('replicate')
19
17
  return {
20
18
  apiToken: (ps.apiToken as string) || '',
21
19
  defaultModel: (ps.defaultModel as string) || '',