@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
@@ -1,14 +1,127 @@
1
1
  import { z } from 'zod'
2
2
  import { tool, type StructuredToolInterface } from '@langchain/core/tools'
3
3
  import fs from 'fs'
4
+ import os from 'os'
4
5
  import path from 'path'
5
6
  import { UPLOAD_DIR } from '../storage'
7
+ import { WORKSPACE_DIR } from '../data-dir'
6
8
  import type { ToolBuildContext } from './context'
7
9
  import { safePath, truncate, listDirRecursive, MAX_FILE } from './context'
8
10
  import type { Plugin, PluginHooks } from '@/types'
9
11
  import { getPluginManager } from '../plugins'
10
12
  import { normalizeToolInputArgs } from './normalize-tool-args'
11
13
 
14
+ function pickNonEmptyString(...values: unknown[]): string | undefined {
15
+ for (const value of values) {
16
+ if (typeof value !== 'string') continue
17
+ const trimmed = value.trim()
18
+ if (trimmed) return trimmed
19
+ }
20
+ return undefined
21
+ }
22
+
23
+ function pickStringValue(...values: unknown[]): string | undefined {
24
+ for (const value of values) {
25
+ if (typeof value === 'string') return value
26
+ }
27
+ return undefined
28
+ }
29
+
30
+ function getFileEntryPath(entry: Record<string, unknown> | undefined): string | undefined {
31
+ if (!entry) return undefined
32
+ return pickNonEmptyString(
33
+ entry.path,
34
+ entry.filePath,
35
+ entry.filename,
36
+ entry.fileName,
37
+ entry.name,
38
+ entry.targetPath,
39
+ entry.target,
40
+ )
41
+ }
42
+
43
+ function getFileEntryContent(entry: Record<string, unknown> | undefined): string | undefined {
44
+ if (!entry) return undefined
45
+ const raw = entry.content ?? entry.text ?? entry.contents ?? entry.value ?? entry.body
46
+ if (raw === undefined || raw === null) return undefined
47
+ return typeof raw === 'string' ? raw : JSON.stringify(raw)
48
+ }
49
+
50
+ function inferFileAction(
51
+ normalized: Record<string, unknown>,
52
+ files: Array<Record<string, unknown>> | undefined,
53
+ filePath: string | undefined,
54
+ dirPath: string | undefined,
55
+ ): string | undefined {
56
+ const fileHasContent = Array.isArray(files) && files.some((entry) => getFileEntryContent(entry) !== undefined)
57
+ if (fileHasContent) return 'write'
58
+ if (getFileEntryContent(normalized) !== undefined) return 'write'
59
+ if (dirPath) return 'list'
60
+ if (filePath) return 'read'
61
+ return 'list'
62
+ }
63
+
64
+ export function normalizeFileArgs(rawArgs: Record<string, unknown>): Record<string, unknown> {
65
+ const normalized = normalizeToolInputArgs(rawArgs)
66
+ const actionPayload = ['read', 'write', 'list', 'copy', 'move', 'delete']
67
+ .map((candidate) => {
68
+ const value = normalized[candidate]
69
+ return value && typeof value === 'object' && !Array.isArray(value)
70
+ ? { action: candidate, value: value as Record<string, unknown> }
71
+ : null
72
+ })
73
+ .find(Boolean)
74
+ const merged = {
75
+ ...normalized,
76
+ ...(actionPayload?.value || {}),
77
+ }
78
+ const files = Array.isArray(merged.files)
79
+ ? merged.files.filter((entry): entry is Record<string, unknown> => !!entry && typeof entry === 'object' && !Array.isArray(entry))
80
+ : undefined
81
+
82
+ let action = pickNonEmptyString(normalized.action, actionPayload?.action)
83
+ if (!action && Array.isArray(files) && files.length > 0) {
84
+ action = pickNonEmptyString(files[0].action)
85
+ }
86
+
87
+ const filePath = pickNonEmptyString(
88
+ merged.filePath,
89
+ merged.filepath,
90
+ merged.path,
91
+ merged.name,
92
+ merged.filename,
93
+ merged.fileName,
94
+ merged.file,
95
+ merged.targetPath,
96
+ merged.target,
97
+ )
98
+ const dirPath = pickNonEmptyString(
99
+ merged.dirPath,
100
+ merged.directory,
101
+ merged.directoryPath,
102
+ merged.dir,
103
+ merged.folder,
104
+ )
105
+
106
+ if (!action) {
107
+ action = inferFileAction(merged, files, filePath, dirPath)
108
+ }
109
+
110
+ return {
111
+ action,
112
+ files,
113
+ encoding: pickNonEmptyString(merged.encoding),
114
+ filePath,
115
+ content: pickStringValue(merged.content, merged.text, merged.contents, merged.value, merged.body),
116
+ dirPath,
117
+ sourcePath: pickNonEmptyString(merged.sourcePath, merged.source, merged.from, merged.src),
118
+ destinationPath: pickNonEmptyString(merged.destinationPath, merged.destination, merged.to, merged.dest),
119
+ overwrite: !!merged.overwrite,
120
+ recursive: !!merged.recursive,
121
+ force: !!merged.force,
122
+ }
123
+ }
124
+
12
125
  function resolveFileToolPath(cwd: string, target: string): string {
13
126
  try {
14
127
  return safePath(cwd, target)
@@ -21,23 +134,16 @@ function resolveFileToolPath(cwd: string, target: string): string {
21
134
  /**
22
135
  * Unified File Execution Logic
23
136
  */
24
- async function executeFileAction(args: Record<string, unknown>, bctx: { cwd: string }) {
25
- const normalized = normalizeToolInputArgs(args)
26
- // Normalize filePath/content for single-file mode
137
+ export async function executeFileAction(args: Record<string, unknown>, bctx: { cwd: string }) {
138
+ const normalized = normalizeFileArgs(args)
27
139
  const files = normalized.files as Array<Record<string, unknown>> | undefined
28
- let action = normalized.action as string | undefined
140
+ const action = normalized.action as string | undefined
29
141
  const encoding = normalized.encoding as string | undefined
30
-
31
- // Resiliency: check if action is buried in the files array
32
- if (!action && Array.isArray(files) && files.length > 0) {
33
- action = files[0].action as string
34
- }
35
-
36
- const filePath = (normalized.filePath || normalized.path) as string | undefined
142
+ const filePath = normalized.filePath as string | undefined
37
143
  const content = normalized.content as string | undefined
38
- const dirPath = (normalized.dirPath || normalized.directory || normalized.path) as string | undefined
39
- const sourcePath = (normalized.sourcePath || normalized.source || normalized.from) as string | undefined
40
- const destinationPath = (normalized.destinationPath || normalized.destination || normalized.to) as string | undefined
144
+ const dirPath = normalized.dirPath as string | undefined
145
+ const sourcePath = normalized.sourcePath as string | undefined
146
+ const destinationPath = normalized.destinationPath as string | undefined
41
147
  const overwrite = !!normalized.overwrite
42
148
  const recursive = !!normalized.recursive
43
149
  const force = !!normalized.force
@@ -45,7 +151,7 @@ async function executeFileAction(args: Record<string, unknown>, bctx: { cwd: str
45
151
  try {
46
152
  switch (action) {
47
153
  case 'read': {
48
- const target = filePath || (files?.[0]?.path as string | undefined)
154
+ const target = filePath || getFileEntryPath(files?.[0])
49
155
  if (!target) return 'Error: no filePath or path provided.'
50
156
  const resolved = resolveFileToolPath(bctx.cwd, target)
51
157
  return truncate(fs.readFileSync(resolved, 'utf-8'), MAX_FILE)
@@ -57,9 +163,15 @@ async function executeFileAction(args: Record<string, unknown>, bctx: { cwd: str
57
163
  const results: string[] = []
58
164
 
59
165
  for (const file of filesToWrite) {
60
- const targetPath = (file.path || file.filePath) as string | undefined
166
+ const targetPath = getFileEntryPath(file)
61
167
  if (!targetPath) continue
62
- const fileContent = (file.content ?? '') as string
168
+ const fileContent = getFileEntryContent(file) ?? ''
169
+ if (/[\\/]$/.test(targetPath)) {
170
+ const resolvedDir = resolveFileToolPath(bctx.cwd, targetPath)
171
+ fs.mkdirSync(resolvedDir, { recursive: true })
172
+ results.push(`Created directory ${targetPath}`)
173
+ continue
174
+ }
63
175
 
64
176
  const resolved = resolveFileToolPath(bctx.cwd, targetPath)
65
177
  fs.mkdirSync(path.dirname(resolved), { recursive: true })
@@ -106,7 +218,7 @@ async function executeFileAction(args: Record<string, unknown>, bctx: { cwd: str
106
218
  }
107
219
 
108
220
  case 'delete': {
109
- const target = filePath || (files?.[0]?.path as string | undefined)
221
+ const target = filePath || getFileEntryPath(files?.[0])
110
222
  if (!target) return 'Error: no filePath or path provided.'
111
223
  const resolved = resolveFileToolPath(bctx.cwd, target)
112
224
  if (resolved === path.resolve(bctx.cwd) || resolved === path.resolve(process.cwd())) return 'Error: cannot delete root'
@@ -126,7 +238,17 @@ function collectSendFilePaths(payload: unknown, into: string[]): void {
126
238
  if (!payload) return
127
239
  if (typeof payload === 'string') {
128
240
  const trimmed = payload.trim()
129
- if (trimmed) into.push(trimmed)
241
+ if (trimmed) {
242
+ const extracted = new Set<string>()
243
+ const uploadMatches = trimmed.match(/(?:sandbox:)?\/api\/uploads\/[^\s)\]]+/g) || []
244
+ for (const match of uploadMatches) extracted.add(match)
245
+ const markdownMatches = [...trimmed.matchAll(/\]\(((?:sandbox:)?\/api\/uploads\/[^\s)]+|(?:\.{1,2}\/|\/)?[^\s)]+\.(?:png|jpg|jpeg|gif|webp|pdf|md|txt|html|json|csv|yml|yaml))\)/gi)]
246
+ for (const match of markdownMatches) {
247
+ if (typeof match[1] === 'string' && match[1].trim()) extracted.add(match[1].trim())
248
+ }
249
+ if (extracted.size === 0) extracted.add(trimmed)
250
+ for (const candidate of extracted) into.push(candidate)
251
+ }
130
252
  return
131
253
  }
132
254
  if (Array.isArray(payload)) {
@@ -135,15 +257,29 @@ function collectSendFilePaths(payload: unknown, into: string[]): void {
135
257
  }
136
258
  if (typeof payload !== 'object') return
137
259
  const record = payload as Record<string, unknown>
260
+ if (record.filePaths !== undefined) collectSendFilePaths(record.filePaths, into)
138
261
  if (typeof record.filePath === 'string') into.push(record.filePath)
262
+ if (typeof record.filepath === 'string') into.push(record.filepath)
263
+ if (typeof record.fileId === 'string') into.push(record.fileId)
264
+ if (typeof record.id === 'string') into.push(record.id)
139
265
  if (typeof record.path === 'string') into.push(record.path)
266
+ if (typeof record.filename === 'string') into.push(record.filename)
267
+ if (typeof record.fileName === 'string') into.push(record.fileName)
268
+ if (typeof record.name === 'string') into.push(record.name)
269
+ if (typeof record.targetPath === 'string') into.push(record.targetPath)
270
+ if (typeof record.target === 'string') into.push(record.target)
140
271
  if (record.files !== undefined) collectSendFilePaths(record.files, into)
141
272
  }
142
273
 
143
274
  export function normalizeSendFilePaths(args: Record<string, unknown>): string[] {
144
275
  const candidates: string[] = []
276
+ collectSendFilePaths(args.filePaths, candidates)
145
277
  collectSendFilePaths(args.filePath, candidates)
278
+ collectSendFilePaths(args.filepath, candidates)
146
279
  collectSendFilePaths(args.path, candidates)
280
+ collectSendFilePaths(args.filename, candidates)
281
+ collectSendFilePaths(args.fileName, candidates)
282
+ collectSendFilePaths(args.name, candidates)
147
283
  collectSendFilePaths(args.file, candidates)
148
284
  collectSendFilePaths(args.files, candidates)
149
285
 
@@ -167,17 +303,94 @@ export function normalizeSendFilePaths(args: Record<string, unknown>): string[]
167
303
  return [...deduped]
168
304
  }
169
305
 
306
+ function collectRecentFiles(
307
+ root: string,
308
+ currentDir: string,
309
+ maxAgeMs: number,
310
+ into: string[],
311
+ depth: number,
312
+ ): void {
313
+ if (depth > 3) return
314
+ let entries: fs.Dirent[] = []
315
+ try {
316
+ entries = fs.readdirSync(currentDir, { withFileTypes: true })
317
+ } catch {
318
+ return
319
+ }
320
+
321
+ for (const entry of entries) {
322
+ if (entry.name.startsWith('.')) continue
323
+ if (entry.isDirectory()) {
324
+ if (entry.name === 'node_modules' || entry.name === '.next') continue
325
+ collectRecentFiles(root, path.join(currentDir, entry.name), maxAgeMs, into, depth + 1)
326
+ continue
327
+ }
328
+ if (!entry.isFile()) continue
329
+ const absolute = path.join(currentDir, entry.name)
330
+ let stat: fs.Stats | null = null
331
+ try {
332
+ stat = fs.statSync(absolute)
333
+ } catch {
334
+ stat = null
335
+ }
336
+ if (!stat) continue
337
+ if (Date.now() - stat.mtimeMs > maxAgeMs) continue
338
+ into.push(path.relative(root, absolute))
339
+ }
340
+ }
341
+
342
+ export function findRecentSendFileFallbackPaths(cwd: string, maxAgeMs = 10 * 60 * 1000): string[] {
343
+ const resolvedRoot = path.resolve(cwd)
344
+ const candidates: string[] = []
345
+ collectRecentFiles(resolvedRoot, resolvedRoot, maxAgeMs, candidates, 0)
346
+ return [...new Set(candidates)]
347
+ }
348
+
349
+ export function resolveSendFileSourcePath(cwd: string, rawPath: string): string {
350
+ const trimmed = rawPath.trim()
351
+ const uploadMatch = trimmed.match(/^(?:sandbox:)?\/api\/uploads\/(.+)$/)
352
+ if (uploadMatch) {
353
+ return path.join(UPLOAD_DIR, path.basename(uploadMatch[1]))
354
+ }
355
+ const browserProfileIdx = trimmed.lastIndexOf('.swarmclaw/browser-profiles/')
356
+ if (browserProfileIdx !== -1) {
357
+ const relative = trimmed.slice(browserProfileIdx)
358
+ return path.join(os.homedir(), relative)
359
+ }
360
+ if (trimmed.startsWith('browser-profiles/')) {
361
+ const candidate = path.join(os.homedir(), '.swarmclaw', trimmed)
362
+ if (fs.existsSync(candidate)) return candidate
363
+ }
364
+ if (trimmed === '/workspace' || trimmed === 'workspace') return cwd
365
+ if (trimmed.startsWith('/workspace/') || trimmed.startsWith('workspace/')) {
366
+ const relative = trimmed.replace(/^\/?workspace\/?/, '')
367
+ const sessionScoped = path.resolve(cwd, relative)
368
+ if (fs.existsSync(sessionScoped)) return sessionScoped
369
+ return path.resolve(WORKSPACE_DIR, relative)
370
+ }
371
+ try {
372
+ return safePath(cwd, trimmed)
373
+ } catch (err: unknown) {
374
+ if (path.isAbsolute(trimmed)) return trimmed
375
+ throw err
376
+ }
377
+ }
378
+
170
379
  async function executeSendFile(args: Record<string, unknown>, bctx: { cwd: string }) {
171
380
  try {
172
- const paths = normalizeSendFilePaths(args)
381
+ const explicitPaths = normalizeSendFilePaths(args)
382
+ const paths = explicitPaths.length > 0 ? explicitPaths : findRecentSendFileFallbackPaths(bctx.cwd)
173
383
  if (paths.length === 0) {
174
384
  return 'Error: filePath/path is required (or provide files[] / input.files[]).'
175
385
  }
386
+ if (explicitPaths.length === 0 && paths.length !== 1) {
387
+ return 'Error: filePath/path is required (or provide files[] / input.files[]).'
388
+ }
176
389
 
177
390
  const links: string[] = []
178
391
  const errors: string[] = []
179
392
  for (const rawPath of paths) {
180
- const resolved = path.isAbsolute(rawPath) ? rawPath : path.resolve(bctx.cwd, rawPath)
393
+ const resolved = resolveSendFileSourcePath(bctx.cwd, rawPath)
181
394
  if (!fs.existsSync(resolved)) {
182
395
  errors.push(`file not found: ${rawPath}`)
183
396
  continue
@@ -204,12 +417,18 @@ const FilePlugin: Plugin = {
204
417
  name: 'Core Files',
205
418
  description: 'Complete file management: read, write, list, move, copy, delete, and send.',
206
419
  hooks: {
207
- getCapabilityDescription: () => 'I can read, write, copy, move, and send files (`read_file`, `write_file`, `list_files`, `copy_file`, `move_file`, `send_file`). Deleting files is destructive, so that may need explicit permission.',
420
+ getCapabilityDescription: () => 'I can manage files with the unified `files` tool (actions: `read`, `write`, `list`, `copy`, `move`, `delete`) and deliver finished artifacts with `send_file`.',
421
+ getOperatingGuidance: () => [
422
+ 'The `files` tool always works best with an explicit action. Use `{"action":"list","dirPath":"."}` to inspect the workspace, `{"action":"read","filePath":"path/to/file.md"}` to inspect a file, and `{"action":"write","files":[{"path":"path/to/file.md","content":"..."}]}` to create or overwrite content.',
423
+ 'For follow-up revision requests, read the current file first, then overwrite it with the improved version or use `edit_file` for a surgical change.',
424
+ 'If a `files` call fails, correct the arguments and retry. Do not conclude that the workspace is inaccessible until an explicit read/list/write attempt with a path fails.',
425
+ 'When `send_file` returns a download link, copy that link exactly instead of rewriting it.',
426
+ ],
208
427
  } as PluginHooks,
209
428
  tools: [
210
429
  {
211
430
  name: 'files',
212
- description: 'Unified file management tool. Actions: read, write, list, copy, move, delete. Supports bulk writes via "files" array.',
431
+ description: 'Unified file management tool. Actions: read, write, list, copy, move, delete. For writes, include a target path (`filePath`, `path`, `filename`, or `name`) plus content (`content`, `text`, or `body`). Supports bulk writes via "files" array.',
213
432
  parameters: {
214
433
  type: 'object',
215
434
  properties: {
@@ -238,7 +457,7 @@ const FilePlugin: Plugin = {
238
457
  },
239
458
  {
240
459
  name: 'send_file',
241
- description: 'Send a file to the user in chat.',
460
+ description: 'Send a file to the user in chat. Use the returned /api/uploads/... links exactly as provided.',
242
461
  parameters: {
243
462
  type: 'object',
244
463
  properties: {
@@ -68,12 +68,18 @@ async function executeHttpAction(args: HttpRequestArgs) {
68
68
  */
69
69
  const HttpPlugin: Plugin = {
70
70
  name: 'Core HTTP',
71
- description: 'Make direct HTTP API calls with custom methods, headers, and bodies.',
72
- hooks: {} as PluginHooks,
71
+ description: 'Make direct HTTP API calls without generating throwaway code.',
72
+ hooks: {
73
+ getCapabilityDescription: () => 'I can make direct HTTP requests (`http_request`) without writing code. Use this for straightforward API calls or fetching JSON.',
74
+ getOperatingGuidance: () => [
75
+ 'Prefer `http_request` over `sandbox_exec` for straightforward REST or JSON API calls.',
76
+ 'Keep API keys in plugin settings or SwarmClaw secrets instead of hardcoding them in generated code.',
77
+ ],
78
+ } as PluginHooks,
73
79
  tools: [
74
80
  {
75
81
  name: 'http_request',
76
- description: 'Make an HTTP API request.',
82
+ description: 'Make an HTTP API request without generating code.',
77
83
  parameters: {
78
84
  type: 'object',
79
85
  properties: {
@@ -0,0 +1,227 @@
1
+ import { z } from 'zod'
2
+ import { tool, type StructuredToolInterface } from '@langchain/core/tools'
3
+ import type { Plugin, PluginHooks } from '@/types'
4
+ import type { ToolBuildContext } from './context'
5
+ import { getPluginManager } from '../plugins'
6
+ import { normalizeToolInputArgs } from './normalize-tool-args'
7
+ import { ackMailboxEnvelope, listMailbox, sendMailboxEnvelope } from '../session-mailbox'
8
+ import { loadApprovals } from '../storage'
9
+ import { requestApprovalMaybeAutoApprove } from '../approvals'
10
+ import { createWatchJob, getWatchJob } from '../watch-jobs'
11
+
12
+ async function executeHumanLoopAction(args: Record<string, unknown>, bctx: { sessionId?: string | null; agentId?: string | null }) {
13
+ const normalized = normalizeToolInputArgs(args)
14
+ const action = String(normalized.action || '').trim().toLowerCase()
15
+
16
+ try {
17
+ if (action === 'request_input') {
18
+ const toSessionId = typeof normalized.toSessionId === 'string' ? normalized.toSessionId : bctx.sessionId
19
+ if (!toSessionId) return 'Error: toSessionId or current session is required.'
20
+ const question = typeof normalized.question === 'string' ? normalized.question.trim() : ''
21
+ if (!question) return 'Error: question is required.'
22
+ const correlationId = typeof normalized.correlationId === 'string' ? normalized.correlationId.trim() : `human-${Date.now()}`
23
+ const options = Array.isArray(normalized.options)
24
+ ? normalized.options.filter((value): value is string => typeof value === 'string' && value.trim().length > 0)
25
+ : []
26
+ const envelope = sendMailboxEnvelope({
27
+ toSessionId,
28
+ type: typeof normalized.type === 'string' ? normalized.type : 'human_request',
29
+ payload: JSON.stringify({
30
+ question,
31
+ options,
32
+ expectedFormat: typeof normalized.expectedFormat === 'string' ? normalized.expectedFormat : null,
33
+ notes: typeof normalized.notes === 'string' ? normalized.notes : null,
34
+ }),
35
+ fromSessionId: bctx.sessionId || null,
36
+ fromAgentId: bctx.agentId || null,
37
+ correlationId,
38
+ ttlSec: typeof normalized.ttlSec === 'number' ? normalized.ttlSec : null,
39
+ })
40
+ return JSON.stringify({
41
+ ok: true,
42
+ envelope,
43
+ correlationId,
44
+ hint: `A human can answer via POST /api/chats/${toSessionId}/mailbox with action="send", type="human_reply", correlationId="${correlationId}", and payload set to the response.`,
45
+ })
46
+ }
47
+
48
+ if (action === 'list_mailbox') {
49
+ const sessionId = typeof normalized.sessionId === 'string' ? normalized.sessionId : bctx.sessionId
50
+ if (!sessionId) return 'Error: sessionId or current session is required.'
51
+ const includeAcked = normalized.includeAcked === true
52
+ return JSON.stringify(listMailbox(sessionId, {
53
+ includeAcked,
54
+ limit: typeof normalized.limit === 'number' ? normalized.limit : undefined,
55
+ }))
56
+ }
57
+
58
+ if (action === 'ack_mailbox') {
59
+ const sessionId = typeof normalized.sessionId === 'string' ? normalized.sessionId : bctx.sessionId
60
+ if (!sessionId) return 'Error: sessionId or current session is required.'
61
+ const envelopeId = typeof normalized.envelopeId === 'string' ? normalized.envelopeId.trim() : ''
62
+ if (!envelopeId) return 'Error: envelopeId is required.'
63
+ const envelope = ackMailboxEnvelope(sessionId, envelopeId)
64
+ return envelope ? JSON.stringify(envelope) : `Error: mailbox envelope "${envelopeId}" not found.`
65
+ }
66
+
67
+ if (action === 'request_approval') {
68
+ const title = typeof normalized.title === 'string' && normalized.title.trim()
69
+ ? normalized.title.trim()
70
+ : 'Human approval requested'
71
+ const approval = await requestApprovalMaybeAutoApprove({
72
+ category: 'human_loop',
73
+ title,
74
+ description: typeof normalized.description === 'string' ? normalized.description : undefined,
75
+ agentId: bctx.agentId || null,
76
+ sessionId: bctx.sessionId || null,
77
+ data: {
78
+ question: typeof normalized.question === 'string' ? normalized.question : title,
79
+ options: Array.isArray(normalized.options) ? normalized.options : undefined,
80
+ metadata: normalized.metadata,
81
+ },
82
+ })
83
+ return JSON.stringify(approval)
84
+ }
85
+
86
+ if (action === 'wait_for_reply') {
87
+ const sessionId = typeof normalized.sessionId === 'string' ? normalized.sessionId : bctx.sessionId
88
+ if (!sessionId) return 'Error: sessionId or current session is required.'
89
+ const job = await createWatchJob({
90
+ type: 'mailbox',
91
+ sessionId,
92
+ agentId: bctx.agentId || null,
93
+ createdByAgentId: bctx.agentId || null,
94
+ resumeMessage: typeof normalized.resumeMessage === 'string' && normalized.resumeMessage.trim()
95
+ ? normalized.resumeMessage.trim()
96
+ : 'A human reply arrived in the mailbox. Read it and continue the task.',
97
+ description: typeof normalized.description === 'string' ? normalized.description : 'Wait for mailbox reply',
98
+ timeoutAt: typeof normalized.timeoutMinutes === 'number'
99
+ ? Date.now() + Math.max(1, normalized.timeoutMinutes) * 60_000
100
+ : undefined,
101
+ target: {
102
+ sessionId,
103
+ },
104
+ condition: {
105
+ type: typeof normalized.type === 'string' ? normalized.type : 'human_reply',
106
+ correlationId: typeof normalized.correlationId === 'string' ? normalized.correlationId : undefined,
107
+ fromSessionId: typeof normalized.fromSessionId === 'string' ? normalized.fromSessionId : undefined,
108
+ containsText: typeof normalized.containsText === 'string' ? normalized.containsText : undefined,
109
+ },
110
+ })
111
+ return JSON.stringify(job)
112
+ }
113
+
114
+ if (action === 'wait_for_approval') {
115
+ const approvalId = typeof normalized.approvalId === 'string' ? normalized.approvalId.trim() : ''
116
+ if (!approvalId) return 'Error: approvalId is required.'
117
+ const job = await createWatchJob({
118
+ type: 'approval',
119
+ sessionId: bctx.sessionId || null,
120
+ agentId: bctx.agentId || null,
121
+ createdByAgentId: bctx.agentId || null,
122
+ resumeMessage: typeof normalized.resumeMessage === 'string' && normalized.resumeMessage.trim()
123
+ ? normalized.resumeMessage.trim()
124
+ : 'A human approval decision was made. Inspect it and continue the task.',
125
+ description: typeof normalized.description === 'string' ? normalized.description : 'Wait for approval decision',
126
+ timeoutAt: typeof normalized.timeoutMinutes === 'number'
127
+ ? Date.now() + Math.max(1, normalized.timeoutMinutes) * 60_000
128
+ : undefined,
129
+ target: {
130
+ approvalId,
131
+ },
132
+ condition: {
133
+ statusIn: Array.isArray(normalized.statusIn)
134
+ ? normalized.statusIn.filter((value): value is string => typeof value === 'string')
135
+ : ['approved', 'rejected'],
136
+ },
137
+ })
138
+ return JSON.stringify(job)
139
+ }
140
+
141
+ if (action === 'status') {
142
+ const approvalId = typeof normalized.approvalId === 'string' ? normalized.approvalId.trim() : ''
143
+ const watchJobId = typeof normalized.watchJobId === 'string' ? normalized.watchJobId.trim() : ''
144
+ if (approvalId) {
145
+ const approvals = loadApprovals()
146
+ const approval = approvals[approvalId]
147
+ return approval ? JSON.stringify(approval) : `Error: approval "${approvalId}" not found.`
148
+ }
149
+ if (watchJobId) {
150
+ const watch = getWatchJob(watchJobId)
151
+ return watch ? JSON.stringify(watch) : `Error: watch job "${watchJobId}" not found.`
152
+ }
153
+ return 'Error: approvalId or watchJobId is required for status.'
154
+ }
155
+
156
+ return `Error: Unknown action "${action}".`
157
+ } catch (err: unknown) {
158
+ return `Error: ${err instanceof Error ? err.message : String(err)}`
159
+ }
160
+ }
161
+
162
+ const HumanLoopPlugin: Plugin = {
163
+ name: 'Human Loop',
164
+ enabledByDefault: false,
165
+ description: 'Request structured human input or approvals, then wait durably for the response.',
166
+ hooks: {
167
+ getCapabilityDescription: () =>
168
+ 'I can request structured human input or explicit approvals with `ask_human`, then pause on durable wait handles until the response arrives.',
169
+ } as PluginHooks,
170
+ tools: [
171
+ {
172
+ name: 'ask_human',
173
+ description: 'Human-loop tool. Actions: request_input, request_approval, wait_for_reply, wait_for_approval, list_mailbox, ack_mailbox, status.',
174
+ parameters: {
175
+ type: 'object',
176
+ properties: {
177
+ action: { type: 'string', enum: ['request_input', 'request_approval', 'wait_for_reply', 'wait_for_approval', 'list_mailbox', 'ack_mailbox', 'status'] },
178
+ question: { type: 'string' },
179
+ title: { type: 'string' },
180
+ description: { type: 'string' },
181
+ options: { type: 'array', items: { type: 'string' } },
182
+ correlationId: { type: 'string' },
183
+ expectedFormat: { type: 'string' },
184
+ notes: { type: 'string' },
185
+ envelopeId: { type: 'string' },
186
+ sessionId: { type: 'string' },
187
+ toSessionId: { type: 'string' },
188
+ approvalId: { type: 'string' },
189
+ watchJobId: { type: 'string' },
190
+ statusIn: { type: 'array', items: { type: 'string' } },
191
+ type: { type: 'string' },
192
+ fromSessionId: { type: 'string' },
193
+ containsText: { type: 'string' },
194
+ ttlSec: { type: 'number' },
195
+ timeoutMinutes: { type: 'number' },
196
+ resumeMessage: { type: 'string' },
197
+ limit: { type: 'number' },
198
+ includeAcked: { type: 'boolean' },
199
+ },
200
+ required: ['action'],
201
+ },
202
+ execute: async (args, context) => executeHumanLoopAction(args, {
203
+ sessionId: context.session.id,
204
+ agentId: context.session.agentId || null,
205
+ }),
206
+ },
207
+ ],
208
+ }
209
+
210
+ getPluginManager().registerBuiltin('ask_human', HumanLoopPlugin)
211
+
212
+ export function buildHumanLoopTools(bctx: ToolBuildContext): StructuredToolInterface[] {
213
+ if (!bctx.hasPlugin('ask_human')) return []
214
+ return [
215
+ tool(
216
+ async (args) => executeHumanLoopAction(args, {
217
+ sessionId: bctx.ctx?.sessionId || null,
218
+ agentId: bctx.ctx?.agentId || null,
219
+ }),
220
+ {
221
+ name: 'ask_human',
222
+ description: HumanLoopPlugin.tools![0].description,
223
+ schema: z.object({}).passthrough(),
224
+ },
225
+ ),
226
+ ]
227
+ }
@@ -5,7 +5,6 @@ import path from 'path'
5
5
  import type { Plugin, PluginHooks } from '@/types'
6
6
  import { getPluginManager } from '../plugins'
7
7
  import { normalizeToolInputArgs } from './normalize-tool-args'
8
- import { loadSettings } from '../storage'
9
8
  import { UPLOAD_DIR } from '../storage'
10
9
  import type { ToolBuildContext } from './context'
11
10
 
@@ -20,8 +19,7 @@ interface PluginConfig {
20
19
  }
21
20
 
22
21
  function getConfig(): PluginConfig {
23
- const settings = loadSettings()
24
- const ps = (settings.pluginSettings as Record<string, Record<string, unknown>> | undefined)?.image_gen ?? {}
22
+ const ps = getPluginManager().getPluginSettings('image_gen')
25
23
  return {
26
24
  provider: (ps.provider as ImageProvider) || 'openai',
27
25
  apiKey: (ps.apiKey as string) || '',